原文:
annas-archive.org/md5/96ae7a0eb0ade91db24b62767897b433译者:飞龙
前言
我们生活在一个非常激动人心的时代。每一天你都可以发现一个全新的应用或数字设备,它可以为你完成某些任务,娱乐你,让你与家人和朋友保持联系,以图片或视频的形式记录你的记忆,等等。这个列表可能永远也说不完。其中大部分应用或设备——无论是你口袋里的小型智能手机,还是你手腕上的小巧智能手表,或者是你的智能汽车——都得益于生产更便宜、更快的处理器的大幅发展,这些处理器拥有出色的框架和库,反过来又包含了高效执行人类曾经只能依赖自然传感器(如眼睛和耳朵)才能完成的任务的算法和技术。
计算机视觉是计算机科学领域的一个分支,它因硬件和软件技术的最新进步而发生了革命性的变化,作为回应,它也对这些硬件和软件技术产生了同样甚至更大的影响。如今,计算机视觉被用于执行从使用数码相机拍照这样简单的任务到理解自动驾驶汽车的环境以避免事故这样复杂的任务。它还被用于在视频录制中将你的脸变成兔子的形象,以及检测显微镜图像中的癌细胞和组织,这可以挽救生命。对于基于计算机视觉的应用或数字设备,其可能性几乎是无限的,尤其是在考虑到库和框架的巨大进步,这些库和框架是企业和技术人员将他们的想法变为现实的关键。
在过去的几年里,OpenCV,即开源计算机视觉库,已经变成了计算机视觉开发者的一整套工具。它几乎包含了实现你所能想到的大多数计算机视觉问题所需的一切,从对合适的计算机视觉库所期望的最基本操作,如调整图像大小和过滤图像,到使用机器学习训练模型,这些模型可用于准确和快速的对象检测。OpenCV 库包含了开发计算机视觉应用所需的所有内容,它还支持一些最流行的编程语言,如 C++和 Python。你甚至可以在.NET 框架等中找到 OpenCV 的绑定。OpenCV 几乎可以在你所能想到的任何主要操作系统上运行,无论是在移动平台还是桌面平台上。
本书的目标是通过一系列动手示例和样例项目,教会开发者使用 OpenCV 库开发计算机视觉应用。本书的每一章都包含与该章节涵盖主题相对应的多个计算机视觉算法,通过按顺序阅读所有章节,您将了解可用于您应用的广泛计算机视觉算法。本书中涵盖的大多数计算机视觉算法都是相互独立的;然而,强烈建议您从开头开始,并尝试根据章节顺序构建您的计算机视觉知识。之所以称之为“动手”书,是因为确保尝试本书中提供的每个示例——所有这些示例都足够有趣和令人兴奋,足以让您继续前进,并在学习过程中建立信心。
本书是数月辛勤工作的结果,没有 Tiksha Sarang 的无价帮助是不可能的,感谢她的耐心和出色的编辑;Adhithya Haridas,感谢他精确和有洞察力的技术审查和评论;Sandeep Mishra,感谢这个美好的机会;非常有帮助的技术审查员朱清亮先生;以及 Packt Publishing 的每一位帮助我创建和交付本书,并为开源和计算机视觉社区服务的人。
本书面向的对象
任何对 C++编程语言有扎实理解,并了解所选操作系统第三方库使用的开发者,会发现本书的主题非常容易理解和操作。另一方面,熟悉 Python 编程语言的开发者也可以使用本书来学习 OpenCV 库的使用;然而,他们需要自己将 C++示例转换为 Python,因为本书的主要关注点将是展示算法的 C++版本。
本书涵盖的内容
第一章,《计算机视觉简介》,概述了计算机视觉科学的基础——它是什么;在哪里使用;图像的定义及其基本属性,如像素、深度和通道等。这是一章完全的入门章节,旨在为那些对计算机视觉世界一无所知的人。
第二章,《OpenCV 入门》,介绍了 OpenCV 库并详细介绍了其核心,通过介绍 OpenCV 开发的最重要构建块。您还将了解到如何获取它以及如何使用它。本章还将简要介绍 CMake 的使用以及如何创建和构建 OpenCV 项目,之后您将学习到 Mat 类及其变体、读取和写入图像和视频以及访问相机(以及其他输入源类型)。
第三章,数组和矩阵运算,涵盖了用于创建或修改矩阵的基本算法。在本章中,你将学习如何执行矩阵运算,例如叉积、点积和求逆。本章还将介绍许多所谓的逐元素矩阵运算,以及如均值、总和和傅里叶变换等数学运算。
第四章,绘制、过滤和变换,尽可能在本书的范围内涵盖广泛的图像处理算法。本章将教你如何在图像上绘制形状和文本。你将学习如何绘制线条、箭头、矩形等。本章还将向你展示一系列用于图像过滤操作的算法,例如平滑滤波、膨胀、腐蚀和图像的形态学操作。到本章结束时,你将熟悉强大的重映射算法以及在计算机视觉中颜色图的用法。
第五章,反向投影和直方图,介绍了直方图的概念,并教你如何从单通道和多通道图像中计算它们。你将了解灰度图像和彩色图像的直方图可视化,或者换句话说,从像素的色调值计算出的直方图。在本章中,你还将学习关于反向投影图像的内容;即直方图提取的逆操作。本章还涵盖了直方图比较和直方图均衡化等主题。
第六章,视频分析 – 运动检测与跟踪,解释了如何使用计算机视觉中最受欢迎的跟踪算法来处理视频,特别是实时目标检测和跟踪操作。在简要介绍了一般如何处理视频之后,你将了解均值漂移和 CAM 漂移算法,以及卡尔曼滤波,通过实际例子和目标跟踪场景进行学习。到本章结束时,你还将了解背景和前景提取算法及其在实际中的应用。
第七章,目标检测 – 特征与描述符,首先简要介绍了使用模板匹配进行目标检测,然后继续教你关于广泛算法的内容,这些算法可用于形状分析。本章涵盖的主题还包括关键点检测链、描述符提取和描述符匹配,这些用于基于特征而不是简单的像素颜色或强度值进行目标检测。
第八章,计算机视觉中的机器学习,涵盖了 OpenCV 的机器学习(ML)和深度神经网络(DNN)模块以及一些最重要的算法、类和函数。从 SVM 算法开始,您将学习如何根据相似的训练组训练模型,然后使用该模型对输入数据进行分类。您将学习如何使用 HOG 描述符与 SVM 对图像进行分类。本章还涵盖了 OpenCV 中人工神经网络的实现,然后继续讲解级联分类。本章的最后部分将教授您如何使用来自第三方库(如 TensorFlow)的预训练模型实时检测不同类型的多个对象。
为了充分利用本书
虽然每个章节的初始部分都提到了每个章节所需的所有工具和软件,但以下是一个可以作为简单快速参考的列表:
-
安装了最新版本的 Windows、macOS 或 Linux 操作系统(如 Ubuntu)的普通计算机
-
Microsoft Visual Studio(在 Windows 上)
-
Xcode(在 macOS 上)
-
CMake
-
OpenCV
首先,为了了解现在的普通计算机是什么样子,您可以在网上搜索或询问当地商店;然而,您现有的计算机很可能已经足够您开始使用了。
此外,您选择的集成开发环境(IDE)或您使用的构建系统(在这种情况下为 CMake)与本书中提供的示例几乎无关。例如,只要您熟悉该 IDE 和构建系统 OpenCV 库的配置,您就可以使用本书中的确切相同的示例与任何代码编辑器或构建系统一起使用。
下载示例代码文件
您可以从 www.packtpub.com 的账户下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
您可以通过以下步骤下载代码文件:
-
在 www.packtpub.com 登录或注册。
-
选择 SUPPORT 选项卡。
-
点击 Code Downloads & Errata。
-
在搜索框中输入本书的名称,并遵循屏幕上的说明。
下载文件后,请确保使用最新版本解压缩或提取文件夹。
-
WinRAR/7-Zip for Windows
-
Zipeg/iZip/UnRarX for Mac
-
7-Zip/PeaZip for Linux
该书的代码包也托管在 GitHub 上,地址为 github.com/PacktPublishing/Hands-On-Algorithms-for-Computer-Vision。如果代码有更新,它将在现有的 GitHub 仓库中更新。
我们还有其他来自我们丰富图书和视频目录的代码包可供选择,请访问 github.com/PacktPublishing/。查看它们吧!
下载彩色图像
我们还提供了一份包含本书中使用的截图/图表彩色图像的 PDF 文件。您可以从这里下载:www.packtpub.com/sites/default/files/downloads/HandsOnAlgorithmsforComputerVision_ColorImages.pdf。
使用的约定
本书中使用了多种文本约定。
CodeInText:表示文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称。以下是一个示例:“另一方面,要旋转图像,您可以使用rotate函数。”
代码块设置如下:
HistCompMethods method = HISTCMP_CORREL;
double result = compareHist(histogram1, histogram2, method);
任何命令行输入或输出都按以下方式编写:
pip install opencv-python
粗体:表示新术语、重要单词或屏幕上看到的单词。例如,菜单或对话框中的单词在文本中显示如下。以下是一个示例:“在我们开始形状分析和特征分析算法之前,我们将学习一种易于使用、功能强大的目标检测方法,称为模板匹配。”
警告或重要注意事项看起来像这样。
小技巧和窍门看起来像这样。
联系我们
我们始终欢迎读者的反馈。
一般反馈:请发送电子邮件至feedback@packtpub.com,并在邮件主题中提及书籍标题。如果您对本书的任何方面有疑问,请发送电子邮件至questions@packtpub.com。
勘误:尽管我们已经尽一切努力确保内容的准确性,但错误仍然可能发生。如果您在这本书中发现了错误,我们将非常感激您能向我们报告。请访问www.packtpub.com/submit-errata,选择您的书籍,点击勘误提交表单链接,并输入详细信息。
盗版:如果您在互联网上发现我们作品的任何非法副本,我们将非常感激您能提供位置地址或网站名称。请通过链接至材料的方式与我们联系至copyright@packtpub.com。
如果您有兴趣成为作者:如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请访问authors.packtpub.com。
评论
请留下评论。一旦您阅读并使用了这本书,为何不在您购买它的网站上留下评论呢?潜在读者可以查看并使用您的客观意见来做出购买决定,Packt 公司可以了解您对我们产品的看法,我们的作者也可以看到他们对书籍的反馈。谢谢!
有关 Packt 的更多信息,请访问 packtpub.com。
目录
-
标题页
-
版权和致谢
- 计算机视觉的动手算法
-
献词
-
Packt 升级销售
-
为什么订阅?
-
PacktPub.com
-
-
贡献者
-
关于作者
-
关于审稿人
-
Packt 正在寻找像你这样的作者
-
-
前言
-
本书面向对象
-
本书涵盖内容
-
充分利用本书
-
下载示例代码文件
-
下载彩色图像
-
使用的约定
-
-
取得联系
- 评论
-
-
计算机视觉简介
-
技术要求
-
理解计算机视觉
-
深入了解图像
-
颜色空间
-
输入、处理和输出
-
-
计算机视觉框架和库
-
摘要
-
问题
-
-
开始使用 OpenCV
-
技术要求
-
OpenCV 简介
- OpenCV 中的主要模块
-
下载和构建/安装 OpenCV
-
使用 C++ 或 Python 与 OpenCV
-
理解 Mat 类
-
构建 Mat 对象
-
删除 Mat 对象
-
访问像素
-
-
读取和写入图像
-
读取和写入视频
-
访问相机
-
访问 RTSP 和网络流
-
-
Mat-like 类
-
总结
-
问题
-
进一步阅读
-
-
数组和矩阵运算
-
技术要求
-
Mat 类中包含的操作
-
克隆矩阵
-
计算叉积
-
提取对角线
-
计算点积
-
了解单位矩阵
-
矩阵求逆
-
逐元素矩阵乘法
-
全一和全零矩阵
-
转置矩阵
-
重塑 Mat 对象
-
-
逐元素矩阵运算
-
基本操作
-
加法操作
-
加权加法
-
减法操作
-
乘法和除法操作
-
-
位运算逻辑操作
-
比较操作
-
数学运算
-
-
矩阵和数组运算
-
为外推制作边界
-
翻转(镜像)和旋转图像
-
处理通道
-
数学函数
-
矩阵求逆
-
元素的平均值和总和
-
离散傅里叶变换
-
生成随机数
-
-
搜索和定位功能
-
定位非零元素
-
定位最小和最大元素
-
查找表转换
-
-
-
总结
-
问题
-
-
绘制、过滤和转换
-
技术要求
-
在图像上绘制
-
在图像上打印文本
-
绘制形状
-
-
过滤图像
-
模糊/平滑滤波器
-
形态学滤波器
-
基于导数的滤波器
-
任意滤波
-
-
转换图像
-
阈值算法
-
颜色空间和类型转换
-
-
几何变换
-
应用色图
-
总结
-
问题
-
-
逆投影和直方图
-
技术要求
-
理解直方图
- 显示直方图
-
直方图的逆投影
- 了解更多关于逆投影的信息
-
比较直方图
-
直方图均衡化
-
总结
-
问题
-
进一步阅读
-
-
视频分析 - 运动检测和跟踪
-
技术要求
-
处理视频
-
理解均值漂移算法
-
使用连续自适应均值(CAM)变换
-
使用卡尔曼滤波进行跟踪和噪声降低
-
如何提取背景/前景
- 背景分割的示例
-
总结
-
问题
-
-
目标检测 - 特征和描述符
-
技术要求
-
目标检测中的模板匹配
-
检测角点和边缘
-
学习哈里斯角点检测算法
-
边缘检测算法
-
-
轮廓计算和分析
-
检测、描述和匹配特征
-
总结
-
问题
-
-
计算机视觉中的机器学习
-
技术要求
-
支持向量机
- 使用 SVM 和 HOG 进行图像分类
-
使用人工神经网络训练模型
-
级联分类算法
-
使用级联分类器进行目标检测
-
训练级联分类器
-
创建样本
-
创建分类器
-
-
-
使用深度学习模型
-
总结
-
问题
-
-
评估
-
第一章,计算机视觉简介
-
开始使用 OpenCV
-
第三章,数组和矩阵运算
-
第四章,绘图、过滤和变换
-
第五章,反向投影和直方图
-
第六章,视频分析 - 运动检测和跟踪
-
第七章,目标检测 - 特征和描述符
-
第八章,计算机视觉中的机器学习
-
-
您可能喜欢的其他书籍
- 留下评论 - 让其他读者了解您的想法
第一章:计算机视觉简介
毫无疑问,计算机科学,尤其是实现算法的方法,在近年来发展迅速。这得益于你的个人电脑甚至你口袋里的智能手机比它们的 predecessors 快得多,也便宜得多。受这种变化影响的最重要计算机科学领域之一是计算机视觉领域。近年来,计算机视觉算法的实现和使用方式发生了巨大的变化。本书,从这一章开始,旨在使用最新和最现代的技术来教授计算机视觉算法。
这旨在作为简要的入门章节,概述了将在许多(如果不是所有)计算机视觉算法中使用的概念基础。即使你已经熟悉计算机视觉和基础知识,如图像、像素、通道等,简要地浏览这一章也是一个好主意,以确保你理解计算机视觉的基本概念,并刷新你的记忆。
在本章中,我们将从计算机视觉领域的简要介绍开始。我们将探讨一些计算机视觉被广泛应用的最重要的行业,并举例说明。之后,我们将直接深入一些基本的计算机视觉概念,从图像开始。我们将学习在计算机视觉中图像是什么,以及它们的构建块是什么。在这个过程中,我们将涵盖像素、深度和通道等概念,这些对于理解和成功操作计算机视觉算法至关重要。
在本章结束时,你将了解以下内容:
-
什么是计算机视觉以及它在哪里被使用?
-
在计算机视觉中,图像是什么?
-
像素、深度和通道及其关系
技术要求
由于这是一个入门章节,我们只关注理论。因此,没有技术要求。
理解计算机视觉
定义计算机视觉不是一件容易的事情,计算机视觉专家在提供教科书定义时往往意见不一。这样做完全超出了本书的范围和兴趣,因此我们将专注于一个简单实用的定义,以满足我们的目的。从历史上看,计算机视觉与图像处理是同义的,本质上是指那些以图像为输入并基于该输入图像产生输出图像或一系列输出值(或测量值)的方法和技术,这些都是在执行一系列过程之后完成的。快进到现在,你会发现,当计算机视觉工程师谈论计算机视觉时,他们大多数情况下指的是与能够模仿人类视觉的概念相关的算法,例如在图像中看到(检测)物体或人。
那么,我们应该接受哪种定义呢?答案是相当简单的——两者都要。用简短的话来说,计算机视觉指的是以任何可想象的方式处理数字视觉数据(或任何可以可视化的数据)的算法、方法和技术。请注意,这里的视觉数据并不意味着只是使用传统相机拍摄的照片,但它们可能是地图上的图形表示或高程,热强度图,或任何可以无论其现实世界意义如何可视化的数据。
根据这个定义,以下所有问题——以及更多问题——都可以通过计算机视觉来解决:
-
我们如何使图像变柔和或变锐利?
-
我们如何减小图像的大小?
-
我们如何增加或减少图像的亮度?
-
我们如何检测图像中最亮的区域?
-
我们如何在视频中(或一系列连续的图像中)检测和跟踪人脸?
-
我们如何在安全摄像头的视频流中识别人脸?
-
我们如何在视频中检测运动?
在现代计算机视觉科学中,图像处理通常是计算机视觉方法和算法的一个子类别,涉及图像滤波、转换等。尽管如此,许多人还是将计算机视觉和图像处理这两个术语互换使用。
在这个时代,计算机视觉是计算机科学和软件行业中最热门的话题之一。其原因是它被用于各种方式,无论是使应用、数字设备或工业机器中的想法栩栩如生,还是处理或简化通常期望由人眼完成的广泛任务。我们提到的这些例子有很多实际应用,它们跨越了广泛的行业,包括汽车、电影、生物医学设备、国防、照片编辑和分享工具以及视频游戏行业。我们将讨论其中的一些例子,其余的留给你们去研究。
计算机视觉在汽车行业中持续使用,以提高现代车辆的安全性和功能性。车辆能够检测交通标志,警告驾驶员超速或甚至检测道路上的车道和障碍物,并通知驾驶员可能的危险。我们可以提供的关于计算机视觉如何使汽车行业现代化的实际例子是无穷无尽的——这还不包括自动驾驶汽车。主要科技公司正在投入大量资源,甚至与开源社区分享他们的一些成果。正如你在本书的最后一章中看到的,我们将利用其中的一些成果,特别是用于实时检测多种类型的多重对象。
下面的图像展示了汽车行业的一些常见物体、符号和感兴趣的区域,这些图像是通过安装在车辆上的摄像头看到的:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00005.jpeg
另一个即将迎来技术革命的行业是生物医学行业。不仅人体器官和身体部位的成像方法得到了极大的改进,而且这些图像的解释和可视化也通过计算机视觉算法得到了改善。计算机被用来在显微镜拍摄的图像中检测癌细胞组织,具有极高的精确度。还有来自能够进行手术的机器人的有希望和新兴的结果。
下面的图像是使用计算机视觉在组织扫描区域中计数特定类型的生物对象(在这种情况下是细胞)的示例,这些组织是通过数字显微镜扫描的:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00006.jpeg
除了汽车和生物医学行业,计算机视觉也被用于成千上万的移动和桌面应用程序中,以执行许多不同的任务。在你的智能手机上浏览在线应用商店,查看一些计算机视觉相关应用示例是个不错的主意。这样做,你将立即意识到,你与你的潜在计算机视觉应用想法之间,几乎只有想象力。
学习所有关于图像的知识
现在,是时候介绍计算机视觉的基础知识了,从图像开始。那么,究竟什么是图像呢?在计算机视觉中,图像只是一个矩阵,或者说是一个二维向量,具有有效的行数、列数等等。这种看待图像的方式不仅简化了对图像本身的描述,还简化了其所有组件的描述,这些组件如下:
-
图像的宽度对应于矩阵中的列数。
-
图像的高度是矩阵中的行数。
-
矩阵的每个元素代表一个像素,这是图像最基本的部分。图像是一系列像素的集合。
-
每个像素,或者矩阵中的每个元素,可以包含一个或多个与它的视觉表示(颜色、亮度等)相对应的数值。我们将在讨论计算机视觉中的颜色空间时了解更多这方面的内容。然而,重要的是要注意,与每个像素相关联的每个数值代表一个通道。例如,灰度图像中的像素通常使用一个介于 0 到 255 之间的单无符号 8 位整数值来表示;因此,灰度图像是单通道图像。在这种表示形式中,0 代表黑色,255 代表白色,而所有其他数字对应于灰度值。另一个例子是标准的 RGB 图像表示,其中每个像素由三个介于 0 到 255 之间的无符号 8 位整数值表示。RGB 图像中代表每个像素的三个通道对应于红色、蓝色和绿色的强度值,这三个值结合可以形成任何可能的颜色。这种图像被称为三通道图像。
以下图像展示了同一图像中同一区域的两个放大版本,一个是灰度格式,另一个是彩色(RGB)格式。注意灰度图像(左侧)中的较高值对应于较亮的值,反之亦然。同样,在彩色图像(右侧)中,你可以看到红色通道的值相当高,这与该区域的红色色调一致,以及白色通道:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00007.jpeg
除了我们之前提到的内容之外,图像还有一些额外的规格,具体如下:
-
每个像素,或者矩阵中的每个元素,可以是一个整数或浮点数。它可以是一个 8 位数字,16 位,等等。代表每个像素的数值类型以及通道数类似于图像的深度。例如,一个使用 16 位整数值来表示每个通道的四通道图像将具有 16 乘以 4 位的深度,或 64 位(或 4 字节)。
-
图像的分辨率指的是其中的像素数量。例如,宽度为 1920 像素、高度为 1080 像素(正如全高清图像的情况)的图像,其分辨率为 1920 乘以 1080,这略多于 200 万像素,或者说大约是 200 万像素。
正是因为这种图像表示形式,它才能被轻易地视为一个数学实体,这意味着可以设计出许多不同类型的算法来作用于图像。如果我们回到图像最简单的表示形式(灰度图像),通过几个简单的例子,我们可以看到大多数图片编辑软件(以及计算机视觉算法)都使用这种表示形式,以及相当简单的算法和矩阵运算来轻松地修改图像。在以下图像中,一个常数(在我们的例子中是 80)简单地加到输入图像(中间图像)的每个像素上,这使得结果图像变得更亮(右侧图像)。也可以从每个像素中减去一个数字,使结果图像变暗(左侧图像):
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00008.gif
现在,我们将只关注计算机视觉的基本概念,而不会深入探讨前面图像修改示例的实现细节。我们将在接下来的章节中学习关于这一点以及许多其他图像处理技术和算法。
本节中提到的图像属性(宽度、高度、分辨率、深度和通道)在计算机视觉中得到广泛使用。例如,在几种情况下,如果一个图像处理算法过于复杂且耗时,那么可以将图像调整大小以使其更小,从而减少处理所需的时间。一旦处理完毕,结果可以映射回原始图像大小并显示给用户。同样的过程也适用于深度和通道。如果一个算法只需要图像的特定通道,你可以提取并单独处理它,或者使用图像的灰度转换版本。请注意,一旦对象检测算法完成其工作,你将希望将结果显示在原始彩色图像上。对这些类型的图像属性有正确的理解将极大地帮助你在面对各种计算机视觉问题和与计算机视觉算法一起工作时。不再赘述,让我们继续讨论色彩空间。
色彩空间
尽管其定义可能有所不同,但通常来说,色彩空间(有时也称为色彩模型)是一种用于解释、存储和重现一组色彩的方法。让我们用一个例子来分解这一点——灰度色彩空间。在灰度色彩空间中,每个像素用一个单一的 8 位无符号整数值表示,该值对应于该像素的亮度或灰度强度。这使得存储 256 种不同的灰度级别成为可能,其中 0 对应绝对黑色,255 对应绝对白色。换句话说,像素的值越高,它就越亮,反之亦然。以下图像显示了灰度色彩空间中存在的所有可能的颜色:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00009.gif
另一个常用的颜色空间是 RGB,其中每个像素由三个不同的 8 位整数值表示,这些值对应于该像素的红色、绿色和蓝色强度。这种颜色空间特别因其被用于电视、LCD 和类似显示器而闻名。你可以通过放大镜观察你的显示器表面来验证这一点。它依赖于这样一个简单的事实:所有颜色都可以通过组合不同量的红色、绿色和蓝色来表示。以下图像展示了三种主要颜色(如黄色或粉色)之间所有其他颜色是如何形成的:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00010.jpeg
一个在其每个单独像素中都具有相同的 R、G 和 B 值的 RGB 图像将产生一个灰度图像。换句话说,相同的红、绿、蓝强度将产生一种灰色。
另一个在计算机视觉中广泛使用的颜色空间是HSV(色调、饱和度和亮度)颜色空间。在这个颜色空间中,每个像素由三个值表示:色调(颜色)、饱和度(颜色的强度)和亮度(它是多亮或多暗)。如以下图像所示,色调可以是 0 到 360(度)之间的值,它代表该像素的颜色。例如,0 度和附近的度数对应于红色和其他类似颜色:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00011.jpeg
这种颜色空间在基于物体颜色的计算机视觉检测和跟踪算法中特别受欢迎,正如你将在本书后面的内容中看到的。原因在于 HSV 颜色空间允许我们无论颜色有多暗或多亮都能处理颜色。使用 RGB 和类似颜色空间则难以实现这一点,因为查看单个像素通道的值并不能告诉我们它的颜色。
以下图像是 HSV 颜色空间的另一种表示,它展示了在同一图像中色调(从左到右)、饱和度和亮度值的变化,从而产生所有可能的颜色:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00012.jpeg
除了本节中提到的颜色空间外,还有很多其他的颜色空间,每个都有其特定的应用场景。例如,四通道CMYK颜色空间(青色、朱红色、黄色和关键色/黑色)在印刷系统中已被证明是最有效的。
请确保从互联网上了解其他流行的颜色空间以及它们可能对任何特定的计算机视觉问题有何用途。
输入、处理和输出
因此,既然我们知道图像基本上是具有宽度、高度、元素类型、通道、深度等基本属性的矩阵式实体,那么唯一剩下的大问题就是它们从何而来,发生了什么,又将去向何方?
让我们用一个简单的相册应用程序为例来进一步分解这个问题。很可能,你的智能手机上默认就有这样一个应用程序。相册应用程序通常允许你使用智能手机内置的相机拍摄新照片或视频,使用之前录制的文件,对图像应用过滤器,甚至通过社交媒体、电子邮件或与你的朋友和家人分享。虽然当你使用它时这个例子可能看起来很简单,但它包含了正确计算机视觉应用程序的所有关键部分。
以这个例子为前提,我们可以这样说,图像是由各种不同的输入设备根据使用案例提供的。以下是一些最常见的图像输入设备:
-
存储在磁盘、内存、网络或任何其他可访问位置的图像文件。请注意,存储的图像文件可以是原始的(包含确切的图像数据)或编码的(如 JPG);然而,它们仍然被认为是图像文件。
-
由相机捕获的图像。请注意,这里的相机指的是个人电脑上的网络摄像头、智能手机上的相机或任何其他专业摄影设备、数字显微镜、望远镜等等。
-
存储在磁盘、内存、网络等位置的连续或非连续的视频帧。与图像文件类似,视频文件可以是编码的,在这种情况下,需要一种特殊的软件(称为编解码器)来在它们可以使用之前对它们进行编码。
-
来自实时视频摄像头流的连续帧。
使用输入设备读取图像后,实际的图像处理过程就开始了。这可能是你在本书中寻找的计算机视觉过程周期的一部分——而且有很好的理由。这是实际使用计算机视觉算法从图像中提取值、以某种方式修改它,或者执行任何类型的计算机视觉任务的地方。这部分通常由特定设备上的软件完成。
现在,整个过程的输出需要被创建。这部分完全取决于计算机视觉算法和计算机视觉过程运行的设备类型,但一般来说,计算机视觉算法期望以下类型的输出:
-
从处理后的图像中派生出的数字、形状、图表或其他非图像类型的输出。例如,一个计算图像中人数的算法只需要输出一个整数或一个表示从安全摄像头连续视频帧中找到的人数图表。
-
存储在磁盘、内存和类似设备上的图像或视频文件。一个典型的例子是你手机或个人电脑上的照片编辑软件,它允许你将修改后的图像保存为 JPG 或 PNG 文件。
-
在显示屏幕上绘制和渲染图像和视频帧。显示器通常由固件(可能位于操作系统上)控制,该固件控制显示在其上的内容。
与输入设备类似,对图像输出设备的略微不同解释将产生更多结果和条目(例如打印机、绘图仪、视频投影仪等)。然而,前面的列表仍然足够,因为它涵盖了我们在处理计算机视觉算法时将遇到的最基本和最重要的输出类型。
计算机视觉框架和库
为了构建计算机视觉应用程序,我们需要一套工具、一个框架或一个支持图像输入、输出和处理的库。选择计算机视觉库是一个非常重要的决定,因为您可能会发现自己需要完全自己“重新发明轮子”。您也可能编写占用大量资源和时间的函数和代码,例如以您所需的格式读取或写入图像。
通常,在开发计算机视觉应用程序时,您可以从两种主要的计算机视觉库中选择;它们如下:
-
专有: 专有计算机视觉库通常由提供它的公司提供良好的文档和支持,但它们需要付费,并且通常针对一组特定的计算机视觉问题
-
开源: 相反,开源库通常涵盖更广泛的与计算机视觉相关的问题,并且可以免费使用和探索
您可以在网上找到许多专有和开源计算机视觉库的示例,以便您自己进行比较。
在本书中我们将使用的库是开源计算机视觉库(OpenCV)。OpenCV 是一个具有以下特性的计算机视觉库:
-
它是开源的,并且可以免费用于学术或商业项目
-
它支持 C++、Python 和 Java 语言
-
它是跨平台的,这意味着它可以用于开发 Windows、macOS、Linux、Android 和 iOS 的应用程序
-
它以模块化方式构建,速度快,文档齐全,支持良好
值得注意的是,OpenCV 还使用一些第三方库来处理各种计算机视觉任务。例如,FFmpeg 库在 OpenCV 中被用于处理读取某些视频文件格式。
摘要
在本章中,我们介绍了计算机视觉科学的最基本概念。我们首先学习了计算机视觉作为一个术语及其用例,然后查看了一些广泛使用它的行业。然后,我们继续学习图像及其最重要的属性,即像素、分辨率、通道、深度等。然后,我们讨论了一些最广泛使用的颜色空间,并学习了它们如何影响图像的通道数和其他属性。之后,我们介绍了计算机视觉中常用的输入和输出设备,以及计算机视觉算法和过程如何在这两者之间进行。我们以对计算机视觉库的非常简短的讨论结束本章,并介绍了我们选择的计算机视觉库,即 OpenCV。
在下一章中,我们将介绍 OpenCV 框架,并开始一些动手的计算机视觉课程。我们将学习如何使用 OpenCV 访问输入设备,执行计算机视觉算法,以及访问输出设备以显示或记录结果。下一章将是本书中的第一个真正的动手章节,并为后续更实用的章节奠定基础。
问题
-
除了本章中提到的行业外,还有哪些行业可以从计算机视觉中获得显著的好处?
-
举例说明一个用于安全目的的计算机视觉应用?(考虑一个你可能没有遇到的应用想法。)
-
举例说明一个用于提高生产力的计算机视觉应用?(再次,考虑一个你可能没有遇到,但怀疑可能存在的应用想法。)
-
存储一个 1920 x 1080 像素、四通道、32 位深度的图像需要多少兆字节?
-
超高清图像,也称为 4K 或 8K 图像,在当今相当普遍,但超高清图像包含多少百万像素?
-
除了本章中提到的颜色空间外,还有哪些常用的颜色空间?
-
将 OpenCV 库与 MATLAB 中的计算机视觉工具进行比较。每个工具的优缺点是什么?
第二章:开始使用 OpenCV
在上一章中,我们介绍了计算机视觉的基础知识,并展示了某些行业如何广泛使用它来改善他们的服务和产品。然后,我们学习了该领域最基本的概念,例如图像和像素。我们了解了色彩空间,并在本章结束时简要讨论了计算机视觉库和框架。我们将从上次结束的地方继续,即向您介绍最强大和最广泛使用的计算机视觉库之一,称为OpenCV。
OpenCV 是一个庞大的类、函数、模块和其他相关资源的集合,用于构建跨平台的计算机视觉应用程序。在本章中,我们将了解 OpenCV 的结构,它包含的模块及其用途,以及它支持的编程语言。我们将学习如何获取 OpenCV,并简要介绍您可以使用它来构建应用程序的可能工具。然后,我们将学习如何使用 CMake 的强大功能轻松创建使用 OpenCV 的项目。尽管这意味着我们的主要焦点将是 C++类和函数,但 wherever it makes sense,我们也会涵盖它们的 Python 等价物,以便熟悉这两种语言的开发者可以跟随本章中介绍的主题。
在了解了使用 OpenCV 库的初始阶段之后,我们将继续学习Mat类。我们将看到上一章中关于图像的所有概念是如何嵌入到 OpenCV 中Mat类的结构中的。我们还将讨论与Mat类兼容(或与之密切相关)的其他各种类。OpenCV 在函数中处理输入和输出参数的方法是本章后面部分将要讨论的一个重要主题。最后,我们将学习如何使用 OpenCV 在计算机视觉应用程序中应用输入、处理和输出的三个步骤。这需要学习使用 OpenCV 访问(并将数据写入)图像和视频文件。
本章作为对上一章入门章节的直接衔接,将阐述使用动手和实践示例学习计算机视觉算法的基础。
在本章中,我们将探讨以下内容:
-
OpenCV 是什么,在哪里可以获取它,以及如何使用它?
-
如何使用 CMake 创建 OpenCV 项目?
-
理解
Mat类及其如何用于访问像素 -
如何使用
Mat_、Matx和UMat类? -
如何使用
imread和imwrite函数读取和写入图像? -
如何使用
VideoCapture和VideoWriter类读取和写入视频? -
如何通过网络(使用实时流协议(RTSP))访问摄像头和视频流?
技术要求
-
Microsoft Visual Studio、Xcode 或任何可以用于开发 C++程序的 IDE
-
Visual Studio Code 或任何其他可以用来编辑 CMake 文件、Python 源文件等的代码编辑器
-
Python 3.X
-
CMake 3.X
-
OpenCV 3.X
尝试使用您正在尝试学习的最新版本的技术和软件总是最好的。本书涵盖的主题,以及计算机视觉总体来说,也不例外,所以请确保下载并安装所提及软件的最新版本。
在必要时,提供了一组简短的安装和配置说明。您可以使用以下 URL 下载本章的源代码和示例:
github.com/PacktPublishing/Hands-On-Algorithms-for-Computer-Vision/tree/master/Chapter02
OpenCV 简介
OpenCV,或开源计算机视觉,是一组包含构建计算机视觉应用程序所需的类和函数的库、工具和模块。OpenCV 库被全球的计算机视觉开发者下载了数百万次,它速度快,经过优化,适用于现实生活中的项目(包括商业项目)。截至本书编写时,OpenCV 的最新版本是 3.4.1,这也是本书所有示例中使用的版本。OpenCV 支持 C/C++、Python 和 Java 语言,并且可以用于构建适用于桌面和移动操作系统的计算机视觉应用程序,包括 Windows、Linux、macOS、Android 和 iOS。
需要注意的是,OpenCV 库和 OpenCV 框架都用来指代 OpenCV,在计算机视觉社区中,这两个术语大多数时候是互换使用的。出于同样的原因,我们在这本书中也将互换使用这些术语。然而,严格来说,框架通常是指用于实现共同目标的关联库和工具集的术语,例如 OpenCV。
OpenCV 由以下两种类型的模块组成:
-
主要模块:这些模块默认包含在 OpenCV 发布版本中,它们包含了所有核心 OpenCV 功能以及用于图像处理任务、过滤、转换等更多功能的模块,我们将在本节中讨论这些功能。
-
额外模块:这些模块包括所有默认不包含在 OpenCV 库中的 OpenCV 功能,它们主要包含额外的计算机视觉相关功能。例如,额外模块包括用于文本识别和非自由特征检测器的库。请注意,我们的重点是主要模块,并涵盖其中的功能,但 wherever 可能有所帮助,我们也会尝试提及额外模块中的可能选项,供您自行研究。
OpenCV 中的主要模块
如前所述,OpenCV 包含多个主模块,其中包含其所有核心和默认功能。以下是这些模块的列表:
-
core: 此模块包含所有核心 OpenCV 功能。例如,所有基本结构,包括Mat类(我们将在后面详细学习)和矩阵运算,都是嵌入到这个模块中的功能之一。 -
imgproc: 此模块包含所有图像处理功能,如滤波、变换和直方图。 -
imgcodecs: 此模块包括用于读取和写入图像的函数。 -
videoio: 此模块与imgcodecs模块类似,但根据其名称,它用于处理视频。 -
highgui: 本书将广泛使用此模块,它包含用于显示结果和创建 GUI 的所有功能。请注意,尽管highgui模块对于本书的目的以及在学习计算机视觉算法的同时可视化结果来说已经足够,但它并不适用于全面的应用。请参阅本章末尾的进一步阅读部分,以获取更多关于用于全面计算机视觉应用的正确 GUI 创建工具的参考资料。 -
video: 包含 OpenCV 的视频分析功能,如运动检测和跟踪、卡尔曼滤波以及臭名昭著的 CAM Shift 算法(用于对象跟踪)。 -
calib3d: 此模块包括校准和 3D 重建功能。此模块能力的知名示例是两个图像之间变换的估计。 -
features2d: 此模块包含支持的关键点检测和描述符提取算法。正如我们将在即将到来的章节中学习的那样,此模块包含一些最广泛使用的对象检测和分类算法。 -
objdetect: 如其名所示,此模块用于使用 OpenCV 进行对象检测。我们将在本书的最后一章学习这个模块包含的功能。 -
dnn: 与objdetect模块类似,此模块也用于对象检测和分类等目的。dnn模块在 OpenCV 主模块列表中相对较新,它包含了与深度学习相关的所有功能。 -
ml: 此机器学习模块包含用于处理分类和回归的类和函数。简单来说,所有严格相关的机器学习功能都包含在这个模块中。 -
flann: 这是 OpenCV 对快速近似最近邻库(FLANN)的接口。FLANN 包含一套广泛的优化算法,用于处理大型数据集中高维特征的最近邻搜索。这里提到的算法大多与其他模块中的算法结合使用,例如features2d。 -
photo:这是一个有趣的模块,用于处理与摄影相关的计算机视觉任务,它包含用于处理去噪、HDR 成像以及使用其邻域恢复照片区域的类和函数。 -
stitching:此模块包含用于图像拼接的类和函数。请注意,拼接本身是一个非常复杂的任务,它需要旋转估计和图像扭曲等功能,所有这些也都是这个非常有趣的 OpenCV 模块的一部分。 -
shape:此模块用于处理形状变换、匹配和距离相关主题。 -
superres:属于分辨率增强类别的算法包含在超分辨率模块中。 -
videostab:此模块包含用于视频稳定的算法。 -
viz:也称为 3D 可视化模块,它包含用于在 3D 可视化窗口上显示小部件的类和函数。此模块不会成为本书讨论的主题之一,但我们只是提一下。
除了我们刚刚提到的模块之外,OpenCV 还包含一些基于 CUDA(由 Nvidia 创建的 API)的主模块。这些模块很容易通过其名称区分,名称以单词 cuda 开头。由于这些模块的可用性完全取决于特定类型的硬件,并且几乎所有这些模块中的功能都由其他模块以某种方式覆盖,我们现在将跳过它们。但值得注意的是,如果您需要的算法已经实现在这些模块中,并且您的硬件满足它们的最小要求,那么使用 OpenCV 的 cuda 模块可以显著提高您应用程序的性能。
下载和构建/安装 OpenCV
OpenCV 大部分没有预构建和可直接使用的版本(本节中我们将讨论一些例外),类似于大多数开源库,它需要从源代码进行配置和构建。在本节中,我们将简要描述如何在计算机上构建(和安装)OpenCV。但首先,您需要将 OpenCV 源代码获取到您的计算机上。您可以使用以下链接进行此操作:
在本页面上,您可以找到 OpenCV 的发布版本。截至本书编写时,最新版本是 3.4.1,因此您应该下载它,或者如果有更高版本,则直接使用那个版本。
如以下截图所示,对于 OpenCV 的每个发布版本,都有各种可下载条目,例如 Win、iOS 和 Android 套件,但您应该下载源代码并根据自己的平台自行构建 OpenCV:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00013.jpeg
OpenCV 3.4.1 默认提供 Android、iOS 和 64 位 MSVC14 和 MSVC15(与 Microsoft Visual C++ 2015 和 Microsoft Visual C++ 2017 相同)库的预构建版本。因此,如果您想为这些平台中的任何一个构建应用程序,您可以下载相关的包并完全跳过 OpenCV 的构建过程。
要从源代码构建 OpenCV,您需要在您的计算机上安装以下工具:
-
支持 C++11 的 C/C++ 编译器:在 Windows 上,这意味着任何较新的 Microsoft Visual C++ 编译器,例如 MSVC15(2017)或 MSVC14(2015)。在 Linux 操作系统上,您可以使用最新的 GCC,而在 macOS 上,您可以使用包含所有必需工具的 Xcode 命令行工具。
-
CMake:确保您使用最新版本的 CMake,例如 3.10,以确保与较新版本的 OpenCV 安全兼容,尽管您可以使用 CMake 3.1 及以后的版本。
-
Python:如果您打算使用 Python 编程语言,这一点尤为重要。
OpenCV 包含大量工具和库,您可以通过多种方式自定义您的构建。例如,您可以使用 Qt 框架、Intel 线程构建块(TBB)、Intel 集成性能原语(IPP)和其他第三方库来进一步增强和自定义您的 OpenCV 构建,但由于我们将使用默认设置和工具集使用 OpenCV,所以我们忽略了上述第三方工具的要求列表。
在获取我们刚才提到的所有先决条件后,您可以通过使用 CMake 和相应的编译器配置和构建 OpenCV,具体取决于您的操作系统和所需的平台。
以下截图显示了具有默认配置集的 CMake 工具。通常,除非您想对 OpenCV 的构建应用自己的自定义设置,否则您不需要对配置进行任何更改:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00014.jpeg
注意,当首次打开 CMake 时,您需要设置源代码文件夹和构建文件夹,分别如前述截图所示,即“源代码在哪里:”和“在哪里构建二进制文件:”。点击“配置”按钮后,您需要设置一个生成器并应用设置,然后按“生成”。
生成后,您只需使用终端或命令提示符实例切换到 CMake 输出文件夹,并执行以下命令:
make
make install
请注意,运行这些命令中的每一个可能需要一些时间,具体取决于您的计算机速度和配置。另外,请注意,make 命令可能因您打算使用的工具集而异。例如,如果您使用 Microsoft Visual Studio,那么您需要将 make 替换为 nmake,或者如果您使用 MinGW,那么您必须将 make 替换为 mingw32-make。
在构建过程完成后,你可以开始使用 OpenCV。你需要注意的是配置你的 C++ 项目,以便它们可以使用你的 OpenCV 库和安装。
在 Windows 操作系统上,你需要确保你正在构建的应用程序可以访问 OpenCV DLL 文件。这可以通过将所有必需的 DLL 文件复制到与你的应用程序构建相同的文件夹中,或者简单地通过将 OpenCV DLL 文件的路径添加到 PATH 环境变量中来实现。在继续之前,请务必注意这一点,否则即使你的应用程序构建成功且在编译时没有报告任何问题,它们在执行时也可能会崩溃。
如果你打算使用 Python 来构建计算机视觉应用程序,那么事情对你来说将会非常简单,因为你可以使用 pip(包管理器)来安装 Python 的 OpenCV,使用以下命令:
pip install opencv-python
这将自动获取最新的 OpenCV 版本及其所有依赖项(如 numpy),或者如果你已经安装了 OpenCV,你可以使用以下命令来确保它升级到最新版本:
pip install --upgrade opencv-python
不言而喻,你需要一个正常工作的互联网连接才能使这些命令生效。
使用 OpenCV 与 C++ 或 Python
在本节中,我们将通过一个非常简单的示例来展示你如何在 C++ 或 Python 项目中使用 OpenCV,我们将这个示例称为 HelloOpenCV。你可能已经知道,这样的项目的目的是以下之一:
-
要开始使用一个全新的库,例如你之前从未使用过的 OpenCV
-
为了确保你的 OpenCV 安装是功能性的并且运行良好
所以,即使你不是 OpenCV 的初学者,仍然值得阅读以下说明并运行本节中的简单示例来测试你的 OpenCV 编译或安装。
我们将开始使用 OpenCV 的必要步骤,在 C++ 项目中:
-
创建一个名为
HelloOpenCV的新文件夹 -
在这个文件夹内创建两个新的文本文件,并将它们命名为
CMakeLists.txt和main.cpp -
确保文件
CMakeLists.txt包含以下内容:
cmake_minimum_required(VERSION 3.1)
project(HelloOpenCV)
set(OpenCV_DIR "path_to_opencv")
find_package(OpenCV REQUIRED)
include_directories(${OpenCV_INCLUDE_DIRS})
add_executable(${PROJECT_NAME} "main.cpp")
target_link_libraries(${PROJECT_NAME} ${OpenCV_LIBS})
在前面的代码中,你需要将 "path_to_opencv" 替换为包含 OpenCVConfig.cmake 和 OpenCVConfig-version.cmake 文件的文件夹路径,这个文件夹是你安装 OpenCV 库的地方。如果你使用的是 Linux 操作系统并且使用了预构建的 OpenCV 库,你可能不需要 OpenCV 文件夹的精确路径。
- 至于
main.cpp文件,请确保它包含以下内容,这是我们将会运行的实际的 C++ 代码:
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
int main()
{
Mat image = imread("MyImage.png");
if(!image.empty())
{
imshow("image", image);
waitKey();
}
else
{
cout << "Empty image!" << endl;
}
return 0;
}
我们将在本节和即将到来的章节中逐一介绍前面代码中使用的函数,然而,目前值得注意的是,这个程序正在尝试打开并显示存储在磁盘上的图像。如果成功,图像将一直显示,直到按下任意键,否则将显示Empty image!消息。请注意,在正常情况下,这个程序不应该崩溃,并且应该能够成功构建。所以,如果你遇到相反的情况,那么你需要回顾本章前面讨论的主题。
- 我们的项目已经准备好了。现在,我们可以使用 CMake 来生成 Visual Studio 或其他任何我们想要的类型的项目(取决于我们将要使用的平台、编译器和 IDE),然后构建并运行它。请注意,CMake 只是用来确保创建了一个跨平台且与 IDE 无关的 C++项目。
通过运行此示例项目,你的输入图像(在本例中为MyImage.png)将被读取并显示,直到按下键盘上的任意键。如果在读取图像的过程中出现任何问题,则将显示Empty image!消息。
我们可以通过以下代码在 Python 中创建和运行相同的项目:
import cv2
image = cv2.imread("MyImage.png")
if image is not None :
cv2.imshow("image", image)
cv2.waitKey()
else:
print("Empty image!")
在这里,相似之处非常明显。与 Python 版本中的相同代码完全相同的imshow和waitKey函数也被使用。正如之前提到的,现在不要担心任何函数的确切使用方式,只需确保你能够运行这些程序,无论是 C++还是 Python,或者两者都可以,并且能够看到显示的图像。
如果你能够成功运行本节中的HelloOpenCV示例项目,那么你就可以毫无问题地继续学习本章的下一节以及本书的下一章。如果你在讨论的主题上仍然遇到问题,或者你觉得你需要对这些主题有更深入的理解,你可以从章节的开始重新学习它们,或者更好的是,你可以参考本章末尾的“进一步阅读”部分中提到的附加书籍。
理解Mat类
请参考上一章中提供的计算机视觉中图像的描述,以及任何图像实际上都是一个具有给定宽度、高度、通道数和深度的矩阵。带着这个描述,我们可以说 OpenCV 的Mat类可以用来处理图像数据,并且它支持图像所需的所有属性,如宽度和高度。实际上,Mat类是一个 n 维数组,可以用来存储具有任何给定数据类型的单通道或多通道数据,并且它包含许多成员和方法,可以以多种方式创建、修改或操作它。
在本节中,我们将通过示例用例和代码示例来学习Mat类的一些最重要的成员和方法。
OpenCV C++ Mat 类在 Python 中的等效物最初不是 OpenCV 类,它由 numpy.ndarray 类型表示。NumPy 是一个 Python 库,包含广泛的数值算法和数学运算,并支持处理大型多维数组和矩阵。Python 中的 numpy.ndarray 类型被用作 Mat 的原因是因为它提供了最佳(如果不是相同的)成员和方法集,这些成员和方法是 OpenCV Mat 类在 C++ 中所需的。有关 numpy.ndarray 支持的成员和方法的完整列表,请参阅 NumPy 文档。
构建 Mat 对象
Mat 包含大约 20 个不同的构造函数,可以根据所需的初始化方式创建其实例。让我们看看一些最常用的构造函数和一些示例。
创建一个宽度为 1920 和高度为 1080,包含三个通道且包含 32 位浮点值的 Mat 对象(或类实例)如下所示:
Mat image(1080, 1920, CV_32FC3);
注意,Mat 构造函数中的 type 参数接受一种特殊类型的参数,即包含深度、类型和通道数的常量值。模式如下所示:
CV_<depth><type>C<channels>
<depth> 可以替换为 8、16、32 或 64,这代表用于存储每个像素中每个元素的位数。每个像素实际需要的位数可以通过将此数字乘以通道数来计算,或者换句话说,<channels>。最后,<type> 需要替换为 U、S 或 F,分别代表无符号整数、有符号整数和浮点值。例如,你可以使用以下方式创建宽度为 800 和高度为 600 像素的标准灰度和彩色图像。请注意,只有通道数不同,深度和类型参数代表 8 位无符号整数:
Mat grayscaleImage(600, 800, CV_8UC1);
Mat colorImage(600, 800, CV_8UC3);
你可以使用以下构造函数创建一个宽度为 W、高度为 H、包含 8 位无符号整数元素的三个通道 RGB 图像,并初始化所有元素为 R、G 和 B 颜色值:
int W = 800, H = 600, R = 50, G = 150, B = 200;
Mat image(H, W, CV_8UC3, Scalar(R, G, B));
重要的是要注意,OpenCV 中颜色的默认顺序是 BGR(而不是 RGB),这意味着交换了 B 和 R 值。如果我们希望在应用程序运行时某点显示处理后的图像,这一点尤为重要。
因此,前述代码中标量初始化器的正确方式如下:
Scalar(B, G, R)
如果我们需要更高维度的 Mat 对象,可以使用以下方法。注意,在以下示例中,创建了一个七维度的 Mat 对象。每个维度的尺寸由 sizes 数组提供,并且高维 Mat 中的每个元素,称为 hdm,包含两个 32 位浮点值通道:
const int dimensions = 7;
const int sizes[dimensions] = {800, 600, 3, 2, 1, 1, 1};
Mat hdm(7, sizes, CV_32FC2);
另一种实现相同功能的方法是使用 C++ 向量,如下所示:
vector<int> sizes = {800, 600, 3, 2, 1, 1, 1};
Mat hdm(sizes, CV_32FC2);
同样,你可以提供一个额外的Scalar参数来初始化Mat中的所有值。请注意,Scalar中的值的数量必须与通道数相匹配。例如,为了初始化前面提到的七维Mat中的所有元素,我们可以使用以下构造函数:
Mat hdm(sizes, CV_32FC2, Scalar(1.25, 3.5));
Mat类允许我们使用已存储的图像数据来初始化它。使用此构造函数,你可以使你的Mat类包含data指针指向的数据。请注意,此构造函数不会创建原始数据的完整副本,它只使新创建的Mat对象指向它。这允许非常高效地初始化和构建Mat类,但显然的缺点是在不需要时没有处理内存清理,因此在使用此构造函数时需要格外小心:
Mat image(1080, 1920, CV_8UC3, data);
注意,与之前的构造函数及其初始化器不同,这里的data不是一个Scalar,而是一个指向包含1920 x 1080像素,三通道图像数据的内存块的指针。使用指向内存空间的指针初始化Mat对象的方法也可以用于Mat类的更高维度。
最后一种构造函数类型,也是Mat类最重要的构造函数之一,是感兴趣区域(ROI)构造函数。此构造函数用于使用另一个Mat对象内的区域初始化Mat对象。让我们用一个例子来分解这一点。想象你有一张图片,你想要对该图片中的特定区域进行一些修改,换句话说,就是对 ROI 进行修改。你可以使用以下构造函数创建一个可以访问 ROI 的Mat类,并且对其所做的任何更改都将影响原始图像的相同区域。以下是你可以这样做的步骤:
Mat roi(image, Rect(240, 140, 300, 300));
如果在image(它本身是一个Mat对象)包含以下图像左侧的图片时使用前面的构造函数,那么roi将能够访问该图像中突出显示的区域,并且它将包含右侧看到的图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00015.jpeg
OpenCV 中的Rect类用于表示具有左上角点、宽度和高度的矩形。例如,前面代码示例中使用的Rect类具有左上角点为240和140,宽度为300像素,高度为300像素,如下所示:
Rect(240, 140, 300, 300)
如前所述,以任何方式修改 ROI 将导致原始图像被修改。例如,我们可以将以下类似图像处理算法应用于roi(现在不必担心以下算法的性质,因为我们将在接下来的章节中了解更多关于它的内容,只需关注 ROI 的概念):
dilate(roi, roi, Mat(), Point(-1,-1), 5);
如果我们尝试显示图像,结果将与以下类似。注意,在先前的图像中突出显示的区域在以下图像中已修改(膨胀),尽管我们是在roi上应用了更改,而不是图像本身:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00016.jpeg
与使用对应于图像矩形区域的Rect类构造 ROI Mat对象类似,你也可以创建一个对应于原始Mat对象中列和行的 ROI。例如,先前的例子中的相同区域也可以使用以下构造函数中看到的范围进行访问:
Mat roi(image, Range(140, 440), Range(240, 540));
OpenCV 中的Range类表示一个具有start和end的区间。根据start和end的值,可以检查Range类是否为空。在先前的构造函数中,第一个Range类对应于原始图像的行,从第140行到第440行。第二个Range对应于原始图像的列,从第240列到第540列。提供的两个区间的交集被认为是最终的 ROI。
删除 Mat 对象
可以通过使用其release函数来清理Mat对象,然而,由于release函数在Mat类的析构函数中被调用,通常没有必要调用此函数。需要注意的是,Mat类在多个指向它的对象之间共享相同的数据。这具有减少数据复制和内存使用的优势,并且由于所有引用计数都是自动完成的,你通常不需要关心任何事情。
在需要特别注意如何以及何时清理你的对象和数据的情况下,你需要格外小心,这种情况发生在你使用数据指针构造Mat对象时,如前所述。在这种情况下,调用Mat类的release函数或其析构函数将与用于构造它的外部数据无关,清理将完全由你负责。
访问像素
除了使用 ROI 访问图像矩形区域的像素,如前几节所述,还有一些其他方法可以实现相同的目标,甚至可以访问图像的个别像素。要能够访问图像中的任何单个像素(换句话说,一个Mat对象),你可以使用at函数,如下例所示:
image.at<TYPE>(R, C)
在前面的示例中,使用 at 函数时,TYPE 必须替换为一个有效的类型名,该类型名必须符合图像的通道数和深度。R 必须替换为像素的行号,而 C 替换为像素的列号,我们想要访问的像素。请注意,这与许多库中常用的像素访问方法略有不同,其中第一个参数是 X(或左),第二个参数是 Y(或顶)。所以基本上,参数在这里是颠倒的。以下是一些访问不同类型 Mat 对象中单个像素的示例。
按如下方式访问具有 8 位整数元素的单一通道 Mat 对象中的像素(灰度图像):
image.at<uchar>(R, C)
按如下方式访问具有浮点元素的单一通道 Mat 对象中的像素:
image.at<float>(R, C)
按如下方式访问具有 8 位整数元素的三个通道 Mat 对象中的像素:
image.at<Vec3b>(R, C)
在前面的代码中,使用了 Vec3b(3 字节向量)类型。OpenCV 定义了各种类似的向量类型以方便使用。以下是您可以使用 at 函数或用于其他目的的 OpenCV Vec 类型模式:
Vec<N><Type>
<N> 可以替换为 2、3、4、6 或 8(在 1 的情况下可以省略)并且它对应于 Mat 对象中的通道数。另一方面,<Type> 可以是以下之一,它们代表了每个像素每个通道中存储的数据类型:
-
b代表uchar(无符号字符) -
s代表short(有符号字) -
w代表ushort(无符号字) -
i代表int -
f代表float -
d代表double
例如,Vec4b 可以用来访问具有 uchar 元素的四个通道 Mat 对象的像素,而 Vec6f 可以用来访问具有 float 元素的六个通道 Mat 对象的像素。重要的是要注意,Vec 类型可以像数组一样处理以访问单个通道。以下是一个如何使用 uchar 元素访问三个通道 Mat 对象的第二个通道的示例:
image.at<Vec3b>(R, C)[1]
重要的是要注意,我们所说的“访问”既包括读取和写入像素及其各个通道。例如,以下示例是应用棕褐色滤镜到图像的一种方法:
for(int i=0; i<image.rows; i++)
{
for(int j=0; j<image.cols; j++)
{
int inputBlue = image.at<Vec3b>(i,j)[0];
int inputGreen = image.at<Vec3b>(i,j)[1];
int inputRed = image.at<Vec3b>(i,j)[2];
int red =
inputRed * 0.393 +
inputGreen * 0.769 +
inputBlue * 0.189;
if(red > 255 ) red = 255;
int green =
inputRed * 0.349 +
inputGreen * 0.686 +
inputBlue * 0.168;
if(green > 255) green = 255;
int blue =
inputRed * 0.272 +
inputGreen * 0.534 +
inputBlue * 0.131;
if(blue > 255) blue = 255;
image.at<Vec3b>(i,j)[0] = blue;
image.at<Vec3b>(i,j)[1] = green;
image.at<Vec3b>(i,j)[2] = red;
}
}
首先,这里需要注意的几点是图像的 rows 和 cols 成员,它们基本上代表了图像中的行数(或高度)和列数(或宽度)。同时注意 at 函数是如何被用来提取通道值和将更新后的值写入其中的。关于示例中用于乘法以获得正确棕褐色调的值,不必担心,因为它们是针对色调本身特定的,并且基本上任何类型的操作都可以应用于单个像素以改变它们。
以下图像展示了将前面的示例代码应用于三通道彩色图像的结果(左——原始图像,右——过滤后的图像):
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00017.jpeg
访问图像中的像素的另一种方法是使用 Mat 类的 forEach 函数。forEach 可以用来并行地对所有像素应用操作,而不是逐个遍历它们。以下是一个简单的示例,展示了如何使用 forEach 将所有像素的值除以 5,如果它在灰度图像上执行,这将导致图像变暗:
image.forEach<uchar>([](uchar &p, const int *)
{
p /= 5;
});
在前面的代码中,第二个参数,或位置参数(这里不需要,因此省略)是像素位置的指针。
使用之前的 for 循环,我们需要编写以下代码:
for(int i=0; i<image.rows; i++)
for(int j=0; j<image.cols; j++)
image.at<uchar>(i,j) /= 5;
OpenCV 还允许使用类似于 STL 的迭代器来访问或修改图像中的单个像素。以下是一个使用类似于 STL 的迭代器编写的相同示例:
MatIterator_<uchar> it_begin = image.begin<uchar>();
MatIterator_<uchar> it_end = image.end<uchar>();
for( ; it_begin != it_end; it_begin++)
{
*it_begin /= 5;
}
值得注意的是,在前三个示例中的相同操作也可以通过以下简单的语句来完成:
image /= 5;
这是因为 OpenCV 中的 Mat 对象将此语句视为逐元素除法操作,我们将在接下来的章节中了解更多关于它的内容。以下图像展示了将前面的示例应用于灰度图像的结果(左——原始图像,右——修改后的图像):
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00018.gif
显然,forEach、C++ 的 for 循环和类似于 STL 的迭代器都可以用来访问和修改 Mat 对象内的像素。我们将满足本节讨论的 Mat 类的函数和成员,但请确保探索它提供的用于以高效方式处理图像及其底层属性的庞大功能集。
读取和写入图像
OpenCV 允许使用 imread 函数从磁盘读取图像到 Mat 对象,我们在本章的前一个示例中简要使用过该函数。imread 函数接受一个输入图像文件名和一个 flag 参数,并返回一个填充有输入图像的 Mat 对象。输入图像文件必须具有 OpenCV 支持的图像格式之一。以下是一些最受欢迎的支持格式:
-
Windows 位图:
*.bmp,*.dib -
JPEG 文件:
*.jpeg,*.jpg,*.jpe -
便携式网络图形:
*.png -
便携式图像格式:
*.pbm,*.pgm,*.ppm,*.pxm,*.pnm -
TIFF 文件:
*.tiff,*.tif
请确保始终检查 OpenCV 文档以获取完整和更新的列表,特别是对于可能适用于某些操作系统上某些格式的异常情况和注意事项。至于
flag 参数,它可以是 ImreadModes 枚举中的一个值或其组合,该枚举在 OpenCV 中定义。以下是一些最广泛使用且易于理解的条目:
-
IMREAD_UNCHANGED -
IMREAD_GRAYSCALE -
IMREAD_COLOR -
IMREAD_IGNORE_ORIENTATION
例如,以下代码可以用来从磁盘读取图像,而不读取图像 EXIF 数据中存储的朝向值,并将其转换为灰度:
Mat image = imread("MyImage.png",
IMREAD_GRAYSCALE | IMREAD_IGNORE_ORIENTATION);
可交换图像文件格式 (EXIF) 是一种标准,用于为数字相机拍摄的照片添加标签和附加数据(或元数据)。这些标签可能包括制造商和相机型号以及拍照时相机的方向。OpenCV 能够读取某些标签(如方向)并对其进行解释,或者在前面示例代码的情况下,忽略它们。
在读取图像后,你可以调用 empty 来查看它是否成功读取。你也可以使用 channels 来获取通道数,depth 来获取深度,type 来获取图像类型,等等。或者,你可以调用 imshow 函数来显示它,就像我们在本章前面看到的那样。
类似地,imreadmulti 函数可以用来将多页图像读取到 Mat 对象的向量中。这里明显的区别是 imreadmulti 返回一个 bool 值,可以用来检查页面的成功读取,并通过引用填充传递给它的 vector<Mat> 对象。
要将图像写入磁盘上的文件,你可以使用 imwrite 函数。imwrite 函数接受要写入的文件名、一个 Mat 对象以及包含写入参数的 int 值 vector,在默认参数的情况下可以忽略。请参阅 OpenCV 中的以下枚举,以获取使用 imwrite 函数可以使用的完整参数列表,以改变写入过程的行为:
-
ImwriteFlags -
ImwriteEXRTypeFlags -
ImwritePNGFlags -
ImwritePAMFlags
以下是一个示例代码,展示了如何使用 imwrite 函数将 Mat 对象写入磁盘上的图像文件。请注意,图像的格式是从提供的扩展名派生出来的,在这种情况下是 png:
bool success = imwrite("c:/my_images/image1.png", image);
cout << (success ?
"Image was saved successfully!"
:
"Image could not be saved!")
<< endl;
除了 imread 和 imwrite 函数,这些函数用于从磁盘上的图像文件中读取和写入图像外,你还可以使用 imdecode 和 imencode 函数来读取存储在内存缓冲区中的图像或向其写入。我们将这两个函数留给你去发现,并继续下一个主题,即使用 OpenCV 访问视频。
读取和写入视频
OpenCV,使用其 videoio 模块或更确切地说,使用 VideoCapture 和 VideoWriter 类,允许我们读取和写入视频文件。在视频的情况下,明显的区别是它们包含一系列连续的图像(或者更好的说法,帧),而不是单个图像。因此,它们通常在一个循环中读取和处理或写入,该循环覆盖视频中的全部或任何所需数量的帧。让我们从以下示例代码开始,展示如何使用 OpenCV 的 VideoCapture 类读取和播放视频:
VideoCapture vid("MyVideo.mov");
// check if video file was opened correctly
if(!vid.isOpened())
{
cout << "Can't read the video file";
return -1;
}
// get frame rate per second of the video file
double fps = vid.get(CAP_PROP_FPS);
if(fps == 0)
{
cout << "Can't get video FPS";
return -1;
}
// required delay between frames in milliseconds
int delay_ms = 1000.0 / fps;
// infinite loop
while(true)
{
Mat frame;
vid >> frame;
if(frame.empty())
break;
// process the frame if necessary ...
// display the frame
imshow("Video", frame);
// stop playing if space is pressed
if(waitKey(delay_ms) == ' ')
break;
}
// release the video file
vid.release();
如前述代码所示,视频文件名在构造 VideoCapture 类时传递。如果文件存在且您的计算机(和 OpenCV)支持该格式,则会自动打开视频文件。因此,您可以使用 isOpened 函数检查视频文件是否成功打开。之后,使用 VideoCapture 类的 get 函数检索已打开的视频文件的 每秒帧率(FPS)。get 是 VideoCapture 的一个极其重要的函数,它允许我们检索打开的视频文件的广泛属性。以下是 get 函数可以提供的示例参数,以获取所需的结果:
-
CAP_PROP_POS_FRAMES: 要解码或捕获的下一个帧的基于 0 的索引 -
CAP_PROP_FRAME_WIDTH: 视频流中帧的宽度 -
CAP_PROP_FRAME_HEIGHT: 视频流中帧的高度 -
CAP_PROP_FPS: 视频的帧率 -
CAP_PROP_FRAME_COUNT: 视频文件中的帧数
要获取完整的列表,您可以参考 OpenCV 中 VideoCaptureProperties 枚举文档。回到前面的示例代码,在通过 get 函数检索到帧率之后,它被用来计算两个帧之间所需的延迟,以便在播放时不会太快或太慢。然后,在一个无限循环中,使用 >> 操作符读取帧并显示。请注意,这个操作符实际上是使用 VideoCapture 函数(如 read、grab 和 retrieve)的简化和便捷方式。我们已经熟悉了 imshow 函数及其用法。另一方面,waitKey 函数的使用方式与之前略有不同,它可以用来插入延迟并等待按键。在这种情况下,之前计算出的所需延迟(以毫秒为单位)被插入到显示的帧之间,如果按下空格键,循环将中断。最后的 release 函数基本上是自我解释的。
除了我们使用 VideoCapture 类及其方法的方式之外,我们还可以调用其 open 函数来打开视频文件,如果我们不想将文件名传递给构造函数,或者如果视频文件在 VideoCapture 构造时不存在。VideoCapture 的另一个重要功能是 set 函数。将 set 视为 get 函数的完全相反,因为它允许设置 VideoCapture 和打开的视频文件的参数。尝试使用之前在 VideoCaptureProperties 枚举中提到的不同参数进行实验。
要能够写入视频文件,您可以使用与 VideoCapture 类非常类似的方式使用 VideoWriter 类。以下是一个示例,展示了如何创建 VideoWriter 对象:
VideoWriter wrt("C:/output.avi",
VideoWriter::fourcc('M','J','P','G'),
30, Size(1920, 1080));
这将在"C:/output.avi"创建一个视频文件,分辨率为1920 x 1080像素,每秒30帧,准备好填充帧。但什么是fourcc?四字符代码(FourCC)简单地说是一个四字节的代码,表示(或更准确地说,是编解码器)将要用于记录视频文件的格式。在这个例子中,我们使用了一个最常见的 FourCC 值,但你可以在网上查找更全面的 FourCC 值及其规格列表。
在创建VideoWriter对象之后,你可以使用<<运算符或write函数将图像(与视频大小完全相同)写入视频文件:
wrt << image;
或者你也可以使用以下代码:
vid.write(frame);
最后,你可以调用release函数以确保视频文件被释放,并且所有更改都写入其中。
除了上述使用VideoCapture和VideoWriter类的方法之外,你还可以设置它们所使用的首选后端。有关更多信息,请参阅 OpenCV 文档中的VideoCaptureAPIs枚举。如果省略,如我们示例中所示,则使用计算机支持的默认后端。
访问摄像头
OpenCV 支持通过使用与访问视频文件相同的VideoCapture类来访问系统上可用的摄像头。唯一的区别是,你不需要将文件名传递给VideoCapture类的构造函数或其 open 函数,你必须提供一个对应于每个可用摄像头的基于 0 的索引号。例如,计算机上的默认摄像头可以通过以下示例代码访问和显示:
VideoCapture cam(0);
// check if camera was opened correctly
if(!cam.isOpened())
return -1;
// infinite loop
while(true)
{
Mat frame;
cam >> frame;
if(frame.empty())
break;
// process the frame if necessary ...
// display the frame
imshow("Camera", frame);
// stop camera if space is pressed
if(waitKey(10) == ' ')
break;
}
cam.release();
如你所见,唯一的区别在于构造函数。这个VideoCapture类的实现允许用户以相同的方式处理任何类型的视频源,因此处理摄像头而不是视频文件时几乎可以编写相同的代码。这与下一节中描述的网络流的情况相同。
访问 RTSP 和网络流
OpenCV 允许用户从网络流中读取视频帧,或者更确切地说,从位于网络上的 RTSP 流中读取,例如本地网络或甚至互联网。要能够做到这一点,你需要将 RTSP 流的 URL 传递给VideoCapture构造函数或其 open 函数,就像它是一个本地硬盘上的文件一样。以下是最常见的模式和可以使用的示例 URL:
rtsp://user:password@website.com/somevideo
在这个 URL 中,user被替换为实际的用户名,password为该用户的密码,依此类推。如果网络流不需要用户名和密码,它们可以被省略。
类似于 Mat 的类
除了Mat类之外,OpenCV 还提供了一些其他与Mat非常相似的类,但它们的使用方式和时机不同。以下是可以替代或与Mat类一起使用的最重要的Mat类似类:
-
Mat_:这是Mat类的一个子类,但它提供了比at函数更好的访问方法,即使用().Mat_是一个模板类,显然需要在编译时提供元素的类型,这是Mat类本身可以避免的。 -
Matx:这最适合用于尺寸较小的矩阵,或者更准确地说,在编译时已知且尺寸较小的矩阵。 -
UMat:这是Mat类的一个较新实现,它允许我们使用 OpenCL 进行更快的矩阵操作。
使用UMat可以显著提高您计算机视觉应用程序的性能,但由于它与Mat类使用方式完全相同,因此在本书的章节中我们将忽略它;然而,在实际应用中,尤其是在实时计算机视觉应用中,您必须始终确保使用更优化、性能更好的类和函数,例如UMat。
摘要
我们已经涵盖了所有关键主题,这些主题使我们能够通过实际示例和真实场景轻松地掌握计算机视觉算法。我们本章从学习 OpenCV 及其整体结构开始,包括其模块和主要构建块。这帮助我们获得了我们将要工作的计算机视觉库的视角,但更重要的是,这让我们对处理计算机视觉算法时可能发生的情况有了概述。然后,我们学习了在哪里以及如何获取 OpenCV,以及如何在我们的系统上安装或构建它。我们还学习了如何创建、构建和运行使用 OpenCV 库的 C++和 Python 项目。然后,通过学习所有关于Mat类以及处理图像中的像素,我们学习了如何修改和显示图像。本章的最后部分包括了我们需要了解的所有关于从磁盘上的文件读取和写入图像的知识,无论是单页(或多页)图像还是视频文件,以及摄像头和网络流。我们通过学习 OpenCV Mat 家族中的一些其他类型来结束本章,这些类型可以帮助提高我们的应用程序。
现在我们已经了解了图像的真实本质(即它本质上是一个矩阵),我们可以从矩阵及其类似实体的可能操作开始。在下一章中,我们将学习所有属于计算机视觉领域的矩阵和数组操作。到下一章结束时,我们将能够使用 OpenCV 执行大量的像素级和图像级操作和转换。
问题
-
列出三个额外的 OpenCV 模块及其用途。
-
启用
BUILD_opencv_world标志构建 OpenCV 3 会有什么影响? -
使用本章中描述的 ROI 像素访问方法,我们如何构建一个
Mat类,使其能够访问中间像素,以及另一个图像中所有相邻的像素(即中间的九个像素)? -
除了本章中提到的之外,请说出
Mat类的另一种像素访问方法。 -
仅使用
at方法和for循环编写一个程序,该程序创建三个单独的颜色图像,每个图像只包含从磁盘读取的 RGB 图像的一个通道。 -
使用类似 STL 的迭代器,计算灰度图像的平均像素值。
-
编写一个使用
VideoCapture、waitKey和imwrite的程序,当按下 S 键时显示您的网络摄像头并保存可见图像。如果按下空格键,此程序将停止网络摄像头并退出。
进一步阅读
-
使用 OpenCV 3 和 Qt5 进行计算机视觉:
www.packtpub.com/application-development/computer-vision-opencv-3-and-qt5 -
Qt5 项目:
www.packtpub.com/application-development/qt-5-projects
第三章:数组和矩阵操作
现在我们已经完成了本书的第一部分,以及计算机视觉的介绍和基本概念,我们可以开始学习 OpenCV 库提供的计算机视觉算法和函数,这些算法和函数几乎涵盖了所有可以想到的计算机视觉主题,并且有优化的实现。正如我们在前面的章节中学到的,OpenCV 使用模块化结构来分类其中包含的计算机视觉功能。我们将带着类似的思路继续本书的主题,这样学到的技能在每一章中都是相互关联的,不仅从理论角度来看,而且从实践角度来看。
在上一章中,我们学习了图像和矩阵之间的关系,并介绍了Mat类最关键的功能,例如使用给定的宽度、高度和类型来构建它。我们还学习了如何从磁盘、网络流、视频文件或摄像头中读取图像。在这个过程中,我们学习了如何使用各种方法访问图像中的像素。现在,我们可以开始实际进行图像和像素的修改和操作功能了。
在这个章节中,我们将学习大量用于处理图像的函数和算法,这些算法要么用于计算可能在其他过程中有用的值,要么用于直接修改图像中像素的值。本章中展示的几乎所有算法都是基于这样一个事实:图像本质上是由矩阵组成的,而且矩阵是通过数据数组实现的,因此本章的名称就是这样!
我们将从这个章节开始,介绍Mat类本身的功能,这些功能虽然不多,但在创建初始矩阵等方面却非常重要。然后,我们将继续学习大量的按元素(或逐元素)算法。通过许多实际案例的学习,我们将了解到这些算法对矩阵的每个单独元素执行特定操作,并且它们不关心任何其他元素(或像素)。最后,我们将学习那些不是逐元素操作的矩阵和数组操作,其结果可能取决于整个图像或元素组。随着我们在这个章节中继续学习算法,这一切都将变得清晰。需要注意的是,这个章节中所有的算法和函数都包含在 OpenCV 库的核心模块中。
到这个章节结束时,你将更好地理解以下内容:
-
Mat类中包含的操作 -
逐元素矩阵操作
-
矩阵和数组操作
技术要求
-
用于开发 C++或 Python 应用程序的 IDE
-
OpenCV 库
有关如何设置个人计算机并使其准备好使用 OpenCV 库开发计算机视觉应用程序的更多信息,请参阅第二章,OpenCV 入门。
您可以使用以下网址下载本章的源代码和示例:github.com/PacktPublishing/Hands-On-Algorithms-for-Computer-Vision/tree/master/Chapter03
Mat 类包含的操作
在本节中,我们将介绍 Mat 类本身包含的数学和其他操作集。尽管 Mat 类中的函数没有通用的使用模式,但它们大多数都与创建新矩阵有关,无论是使用现有的矩阵还是从头开始创建。所以,让我们开始吧。
在本书的整个过程中,单词图像、矩阵、Mat 类等将可以互换使用,并且它们都表示相同的意思,除非有明确的说明。利用这个机会,习惯于像计算机视觉专家那样从矩阵的角度思考图像。
克隆矩阵
您可以使用 Mat::clone 来创建一个完全独立的 Mat 对象的克隆。请注意,此函数创建了一个完整的图像副本,并在内存中为其分配了空间。以下是它的用法:
Mat clone = image.clone();
您也可以使用 copyTo 函数来完成相同的功能,如下所示:
Mat clone;
image.copyTo(clone);
在前面的两个代码示例中,image 是在执行克隆操作之前从图像、摄像头或以任何可能的方式产生的原始矩阵(或图像)。从现在开始,在本章和即将到来的所有章节中,除非另有说明,image 简单地是一个 Mat 对象,它是我们操作的数据源。
计算叉积
您可以使用 Mat::cross 来计算具有三个浮点元素的两个 Mat 对象的叉积,如下面的示例所示:
Mat A(1, 1, CV_32FC3),
B(1, 1, CV_32FC3);
A.at<Vec3f>(0, 0)[0] = 0;
A.at<Vec3f>(0, 0)[1] = 1;
A.at<Vec3f>(0, 0)[2] = 2;
B.at<Vec3f>(0, 0)[0] = 3;
B.at<Vec3f>(0, 0)[1] = 4;
B.at<Vec3f>(0, 0)[2] = 5;
Mat AxB = A.cross(B);
Mat BxA = B.cross(A);
显然,在两个向量的叉积中,AxB 与 BxA 是不同的。
提取对角线
Mat::diag 可以用来从一个 Mat 对象中提取对角线,如下面的示例所示:
int D = 0; // or +1, +2, -1, -2 and so on
Mat dg = image.diag(D);
此函数接受一个索引参数,可以用来提取主对角线以外的其他对角线,如下面的图所示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00019.gif
如果 D=0,则提取的对角线将包含 1、6、11 和 16,这是主对角线。但根据 D 的值,提取的对角线将位于主对角线之上或之下,如前图所示。
计算点积
要计算两个矩阵的点积、标量积或内积,您可以使用 Mat::dot 函数,如下所示:
double result = A.dot(B);
在这里,A 和 B 都是 OpenCV 的 Mat 对象。
学习单位矩阵
使用 OpenCV 中的 Mat::eye 函数创建单位矩阵。以下是一个示例:
Mat id = Mat::eye(10, 10, CV_32F);
如果你需要一个不同于单位矩阵对角线上的值,你可以使用一个 scale 参数:
double scale = 0.25;
Mat id = Mat::eye(10, 10, CV_32F) * scale;
创建单位矩阵的另一种方法是使用 setIdentity 函数。请确保查看 OpenCV 文档以获取有关此函数的更多信息。
矩阵求逆
你可以使用 Mat::inv 函数来求逆一个矩阵:
Mat inverted = m.inv();
注意,你可以向 inv 函数提供一个矩阵分解类型,这可以是 cv::DecompTypes 枚举中的一个条目。
元素级矩阵乘法
Mat::mul 可以用来执行两个 Mat 对象的元素级乘法。不用说,此函数也可以用于元素级除法。以下是一个示例:
Mat result = A.mul(B);
你还可以提供一个额外的 scale 参数,该参数将用于缩放结果。以下是一个另一个示例:
double scale = 0.75;
Mat result = A.mul(B, scale);
全 1 和全 0 矩阵
Mat::ones 和 Mat::zeroes 可以用来创建一个给定大小的矩阵,其中所有元素分别设置为 1 或 0。这些矩阵通常用于创建初始化矩阵。以下是一些示例:
Mat m1 = Mat::zeroes(240, 320, CV_8UC1);
Mat m2 = Mat::ones(240, 320, CV_8UC1);
如果你需要创建一个填充了除了 1 以外的值的矩阵,你可以使用以下类似的方法:
Mat white = Mat::ones(240, 320, CV_8UC1) * 255;
转置矩阵
你可以使用 Mat::t 来转置一个矩阵。以下是一个示例:
Mat transpose = image.t();
以下是一个演示图像转置的示例:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00020.jpeg
左侧的图像是原始图像,右侧的图像是原始图像的转置。正如你将在本章后面学到的那样,transpose 或 Mat::t 函数可以与 flip 函数结合使用,以在所有可能的方向上旋转或翻转/镜像图像。
转置一个转置矩阵与原始矩阵相同,运行以下代码将得到原始图像本身:
Mat org = image.t().t();
计算矩阵转置的另一种方法是使用 transpose 函数。以下是如何使用此函数的示例:
transpose(mat, trp);
在这里,mat 和 trp 都是 Mat 对象。
调整 Mat 对象的形状
可以使用 Mat::reshape 函数调整 Mat 对象的形状。请注意,在这个意义上,调整形状意味着改变图像的通道数和行数。以下是一个示例:
int ch = 1;
int rows = 200;
Mat rshpd = image.reshape(ch, rows);
注意,将通道数设置为 0 的值意味着通道数将与源保持相同。同样,将行数设置为 0 的值意味着图像的行数将保持不变。
注意,Mat::resize是另一个用于重塑矩阵的有用函数,但它只允许改变图像中的行数。在重塑矩阵或处理矩阵中的元素数量时,另一个有用的函数是Mat::total函数,它返回图像中的元素总数。
关于Mat类本身嵌入的功能就到这里。请确保阅读Mat类的文档,并熟悉你在这节中学到的方法的可能变体。
逐元素矩阵操作
元素级或逐元素矩阵操作是计算机视觉中的数学函数和算法,它们作用于矩阵的各个单独元素,换句话说,就是图像的像素。需要注意的是,逐元素操作可以并行化,这意味着矩阵元素的处理顺序并不重要。这个特性是本节和本章后续部分中函数和算法之间最重要的区别。
基本操作
OpenCV 提供了所有必要的函数和重载运算符,用于执行两个矩阵或一个矩阵与一个标量之间的所有四个基本操作:加法、减法、乘法和除法。
加法操作
add函数和+运算符可以用来添加两个矩阵的元素,或者一个矩阵和一个标量,如下面的例子所示:
Mat image = imread("Test.png");
Mat overlay = imread("Overlay.png");
Mat result;
add(image, overlay, result);
你可以将前面代码中的最后一行替换为以下代码:
result = image + overlay;
以下图片展示了两个图像加法操作的结果图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00021.jpeg
如果你想将单个标量值添加到Mat对象的全部元素中,你可以简单地使用以下类似的方法:
result = image + 80;
如果前面的代码在灰度图像上执行,结果会比源图像更亮。注意,如果图像有三个通道,你必须使用一个三项向量而不是单个值。例如,为了使 RGB 图像更亮,你可以使用以下代码:
result = image + Vec3b(80, 80, 80);
下面是一张图片,展示了当在它上执行前面的代码时,得到的更亮的结果图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00022.jpeg
在前面的示例代码中,只需简单地增加加数值,就可以得到更亮的图像。
加权加法
除了简单的两个图像相加外,你还可以使用加权加法函数来考虑被加的两个图像的权重。将其视为在add操作中为每个参与者设置不透明度级别。要执行加权加法,你可以使用addWeighted函数:
double alpha = 1.0; // First image weight
double beta = 0.30; // Second image weight
double gamma = 0.0; // Added to the sum
addWeighted(image, alpha, overlay, beta, gamma, result);
如果在上一节中的示例图片上执行加法操作,结果将类似于以下内容:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00023.jpeg
注意到与通常由照片编辑应用程序应用的水印类似的透明文本。注意代码中关于 alpha、beta 和 gamma 值的注释?显然,提供一个 beta 值为 1.0 将会使此示例与没有任何透明度的常规 add 函数完全相同。
减法运算
与将两个 Mat 对象相加类似,你也可以使用 subtract 函数或 - 运算符从一幅图像中减去另一幅图像的所有元素。以下是一个示例:
Mat image = imread("Test.png");
Mat overlay = imread("Overlay.png");
Mat result;
subtract(image, overlay, result);
前面代码中的最后一行也可以替换为以下内容:
result = image - overlay;
如果我们使用前面示例中的相同两幅图像进行减法运算,以下是结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00024.jpeg
注意从源图像中减去较高像素值(较亮像素)的结果会导致叠加文本的暗色。还要注意,减法运算依赖于其操作数的顺序,这与加法不同。尝试交换操作数,看看会发生什么。
就像加法一样,你也可以将一个常数与图像的所有像素相乘。你可以猜到,从所有像素中减去一个常数将导致图像变暗(取决于减去的值),这与加法运算相反。以下是一个使用简单的减法运算使图像变暗的示例:
result = image - 80;
如果源图像是一个三通道 RGB 图像,你需要使用一个向量作为第二个操作数:
result = image - Vec3b(80, 80, 80);
乘法和除法运算
与加法和减法类似,你也可以将一个 Mat 对象的所有元素与另一个 Mat 对象的所有元素相乘。同样,也可以进行除法运算。再次强调,这两种运算都可以使用矩阵和标量进行。乘法可以使用 OpenCV 的 multiply 函数(类似于 Mat::mul 函数)进行,而除法可以使用 divide 函数进行。
这里有一些示例:
double scale = 1.25;
multiply(imageA, imageB, result1, scale);
divide(imageA, imageB, result2, scale);
前面的代码中的 scale 是可以提供给 multiply 和 divide 函数的附加参数,用于缩放结果 Mat 对象中的所有元素。你还可以像以下示例中那样使用标量进行乘法或除法运算:
resultBrighter = image * 5;
resultDarker = image / 5;
显然,前面的代码将生成两个图像,一个比原始图像亮五倍,另一个比原始图像暗五倍。这里需要注意的是,与加法和减法不同,生成的图像不会均匀地变亮或变暗,你会注意到较亮区域变得非常亮,反之亦然。显然,这是乘法和除法运算的效果,在这些运算中,较亮像素的值在运算后增长或下降的速度比较小的值快得多。值得注意的是,这种相同的技术在大多数照片编辑应用程序中用于调整图像的亮暗区域。
位运算逻辑
就像基本操作一样,您也可以对两个矩阵的所有元素或一个矩阵和一个标量的所有元素执行位逻辑运算。因此,您可以使用以下函数:
-
bitwise_not -
bitwise_and -
bitwise_or -
bitwise_xor
从它们的名字就可以立即看出,这些函数可以执行 Not、And、Or 和 Exclusive OR 操作,但让我们通过一些实际示例来详细看看它们是如何使用的:
首先,bitwise_not 函数用于反转图像中所有像素的所有位。此函数与大多数照片编辑应用中可以找到的翻转操作具有相同的效果。以下是它的使用方法:
bitwise_not(image, result);
上述代码也可以替换为以下代码,它使用了 C++ 中的重载位运算 not 操作符 (~):
result = ~image;
如果图像是单色黑白图像,结果将包含一个所有白色像素被替换为黑色,反之亦然的图像。如果图像是 RGB 颜色图像,结果将是反转的(在二进制像素值的意义上),以下是一个示例图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00025.jpeg
bitwise_and 函数,或 & 操作符,用于对两个图像的像素或一个图像和一个标量的像素执行位运算 And。以下是一个示例:
bitwise_and(image, mask, result);
您可以直接使用 & 操作符,并写成以下内容:
result = image & mask;
bitwise_and 函数可以很容易地用来遮罩和提取图像中的某些区域。例如,以下图像展示了 bitwise_and 如何导致一个只通过白色像素并移除黑色像素的图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00026.jpeg
除了遮罩图像的某些区域外,位运算 And 还可以用来完全过滤掉一个通道。要执行此操作,您需要使用 & 操作符的第二种形式,它接受一个矩阵和一个标量,并对所有像素和该值执行 And 运算。以下是一个示例代码,可以用来遮罩(置零)RGB 颜色图像中的绿色颜色通道:
result = image & Vec3b(0xFF, 0x00, 0xFF);
现在,让我们继续到下一个位运算,即 Or 运算。bitwise_or 和 | 操作符都可以用来对两个图像或一个图像和一个标量执行位运算 Or。以下是一个示例:
bitwise_or(image, mask, result);
与位运算 And 类似,您可以在 Or 运算中使用 | 操作符,并简单地写成以下代码代替前面的代码:
result = image | mask;
如果使用 And 运算来通过非零像素(或非黑色像素),那么可以说 Or 运算用于通过任何输入图像中的像素值较高的像素(或较亮的像素)。以下是执行位运算 Or 的前例图像的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00027.jpeg
与位运算的 And 操作类似,你也可以使用位运算的 Or 操作来更新单个通道或图像的所有像素。以下是一个示例代码,展示了如何仅更新 RGB 图像中的绿色通道,使其所有像素的值达到最大可能值(即 255,或十六进制 FF),而其他通道保持不变:
result = image | Vec3b(0x00, 0xFF, 0x00);
最后,你可以使用 bitwise_xor 或 ^ 操作符在两个图像的像素之间,或图像与标量之间执行 Exclusive Or 操作。以下是一个示例:
bitwise_xor(image, mask, result);
或者简单地使用 ^ 操作符,并写成以下内容:
result = image ^ mask;
如果在上一节中的示例图像上执行 Exclusive Or 操作,以下是结果图像:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00028.jpeg
注意这个操作如何导致掩码区域的像素反转?通过在纸上写下像素值并尝试自己计算结果来思考这个原因。如果清楚地理解其行为,Exclusive Or 以及所有位运算都可以用于许多其他计算机视觉任务。
比较操作
比较两个图像(或给定值)非常有用,特别是用于生成可以在各种其他算法中使用的掩码,无论是用于跟踪图像中的某个感兴趣对象,还是在图像的孤立(掩码)区域执行操作。OpenCV 提供了一些函数来执行逐元素比较。例如,compare 函数可以用于比较两个图像。以下是方法:
compare(image1, image2, result, CMP_EQ);
前两个参数是参与比较的第一个和第二个图像。result 将保存到第三个 Mat 对象中,最后一个参数必须是从 CmpTypes 枚举中的一项,用于选择比较类型,可以是以下任何一种:
-
CMP_EQ:表示第一个图像等于第二个图像 -
CMP_GT:表示第一个图像大于第二个图像 -
CMP_GE:表示第一个图像大于或等于第二个图像 -
CMP_LT:表示第一个图像小于第二个图像 -
CMP_LE:表示第一个图像小于或等于第二个图像 -
CMP_NE:表示第一个图像不等于第二个图像
注意,我们仍在讨论逐元素操作,所以当我们说“第一个图像小于或等于第二个图像”时,我们实际上是指“第一个图像中的每个单独像素的值小于或等于第二个图像中其对应像素的值”,依此类推。
注意,你还可以使用重载的 C++ 操作符来实现与 compare 函数相同的目标。以下是每个单独比较类型的方法:
result = image1 == image2; // CMP_EQ
result = image1 > image2; // CMP_GT
result = image1 >= image2; // CMP_GE
result = image1 < image2; // CMP_LT
result = image1 <= image2; // CMP_LE
result = image1 != image2; // CMP_NE
inRange 函数是 OpenCV 中的另一个有用的比较函数,可以用来找到具有特定下限和上限值的像素。你可以使用任何现有的图像作为边界值矩阵,或者你可以自己创建它们。以下是一个示例代码,可以用来在灰度图像中找到介于 0 和 50 之间的像素值:
Mat lb = Mat::zeros(image.rows,
image.cols,
image.type());
Mat hb = Mat::ones(image.rows,
image.cols,
image.type()) * 50;
inRange(image, lb, hb, result);
注意,lb 和 hb 都是与源图像大小和类型相同的 Mat 对象,除了 lb 被填充为零,而 hb 被填充为 50 的值。这样,当调用 inRange 时,它会检查源图像中的每个像素及其对应的 lb 和 hb 中的像素,如果值在提供的边界之间,则将结果中的对应像素设置为白色。
以下图像展示了在示例图像上执行 inRange 函数的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00029.gif
min 和 max 函数,从它们的名字可以轻易猜出,是另外两个可以用来比较两张图像(逐元素)并找出最小或最大像素值的比较函数。以下是一个示例:
min(image1, image2, result);
或者你可以使用 max 来找到最大值:
max(image1, image2, result);
简单来说,这两个函数比较了相同大小和类型的两张图像的像素,并将结果矩阵中对应的像素设置为输入图像中的最小或最大像素值。
数学运算
除了我们之前学过的函数之外,OpenCV 还提供了一些处理逐元素数学运算的函数。在本节中,我们将简要介绍它们,但你可以,也应该亲自实验它们,以确保你熟悉它们,并且可以在你的项目中舒适地使用它们。
OpenCV 中的逐元素数学函数如下:
- 可以使用
absdiff函数来计算相同大小和类型的两张图像或图像和标量的像素之间的绝对差值。以下是一个示例:
absdiff(image1, image2, result);
在前面的代码中,image1、image2 和 result 都是 Mat 对象,结果中的每个元素代表 image1 和 image2 中对应像素的绝对差值。
- 可以使用
exp函数来计算矩阵中所有元素的指数:
exp(mat, result);
- 可以使用
log函数来计算矩阵中每个元素的自然对数:
log(mat, result);
pow函数可以用来将矩阵中的所有元素提升到给定的幂。此函数需要一个矩阵和一个double类型的值,该值将是幂值。以下是一个示例:
pow(mat, 3.0, result);
sqrt函数用于计算矩阵中所有元素的平方根,其用法如下:
sqrt(mat, result);
函数如 log 和 pow 不应与标准 C++ 库中同名的函数混淆。为了提高你代码的可读性,考虑在 C++ 代码中在函数名之前使用 cv 命名空间。例如,你可以这样调用 pow 函数:
cv::pow(image1, 3.0, result);
矩阵和数组操作
与本章中我们迄今为止看到的函数和算法不同,本节中的算法对图像(或矩阵)本身执行原子和完整的操作,并且不被视为迄今为止所描述的逐元素操作。如果你还记得,逐元素操作的规则是它们可以很容易地并行化,因为结果矩阵依赖于两个图像对应的像素,而本章我们将学习的函数和算法不易并行化,或者结果像素和值可能与其对应的源像素几乎没有关系,或者恰恰相反,结果像素可能同时依赖于一些或所有输入像素。
为外推制作边界
正如你将在本节和即将到来的章节中看到的那样,在处理许多计算机视觉算法时,最重要的处理问题之一是外推,或者简单地说,假设图像之外的像素不存在。你可能想知道,为什么我需要考虑不存在的像素,最简单的答案是,有许多计算机视觉算法不仅与单个像素工作,还与周围的像素工作。在这种情况下,当像素位于图像中间时,没有问题。但对于图像边缘的像素(例如,在最顶行),一些周围的像素将超出图像范围。这正是你需要考虑外推和非存在像素假设的地方。你会简单地假设这些像素为零值吗?也许假设它们与边界像素具有相同的值会更好?所有这些问题都在 OpenCV 中的一个名为 copyMakeBorder 的函数中得到了解决。
copyMakeBorder 允许我们在图像外部形成边界,并提供足够的定制选项来处理所有可能的场景。让我们通过几个简单的例子来看看 copyMakeBorder 的用法:
int top = 50;
int bottom = 50;
int left = 50;
int right = 50;
BorderTypes border = BORDER_REPLICATE;
copyMakeBorder(image,
result,
top,
bottom,
left,
right,
border);
如前例所示,copyMakeBorder 接受一个输入图像并生成一个 result 图像,就像我们迄今为止所学的 OpenCV 函数中的大多数一样。此外,此函数必须提供四个整数值,这些值代表添加到图像的 top(顶部)、bottom(底部)、left(左侧)和 right(右侧)边的像素数。然而,这里必须提供的最重要的参数是 border 类型参数,它必须是 BorderTypes 枚举的一个条目。以下是一些最常用的 BorderType 值:
-
BORDER_CONSTANT -
`BORDER_REPLICATE` -
BORDER_REFLECT -
BORDER_WRAP
注意,当使用 BORDER_CONSTANT 作为边界类型参数时,必须向 copyMakeBorder 函数提供一个额外的标量参数,该参数表示创建的边界的常量颜色值。如果省略此值,则假定为零(或黑色)。以下图像显示了在执行 copyMakeBorder 函数时在示例图像上的输出:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00030.jpeg
copyMakeBorder 以及许多其他 OpenCV 函数,在内部使用 borderInterpolate 函数来计算用于外推和创建非现有像素的捐赠像素的位置。您不需要直接调用此函数,所以我们将其留给您自己探索和发现。
翻转(镜像)和旋转图像
您可以使用 flip 函数翻转或镜像图像。此函数可以用于围绕 x 或 y 轴翻转图像,或者两者同时翻转,具体取决于提供的翻转 code。以下是此函数的使用方法:
int code = +1;
flip(image, result, code);
如果 code 为零,输入图像将垂直翻转/镜像(围绕 x 轴),如果 code 是正值,输入图像将水平翻转/镜像(围绕 y 轴),如果 code 是负值,输入图像将同时围绕 x 和 y 轴翻转/镜像。
另一方面,要旋转图像,您可以使用 rotate 函数。在调用 rotate 函数时,您需要注意提供正确的旋转标志,如下面的示例所示:
RotateFlags rt = ROTATE_90_CLOCKWISE;
rotate(image, result, rt);
RotateFlag 枚举可以是以下自解释的常量值之一:
-
ROTATE_90_CLOCKWISE -
ROTATE_180 -
ROTATE_90_COUNTERCLOCKWISE
以下图像展示了 flip 和 rotate 函数的所有可能结果。请注意,围绕两个轴翻转的结果与以下结果图像中的 180 度旋转相同:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00031.jpeg
如本章前面所述,Mat::t,即矩阵的转置,也可以与 flip 函数结合使用来旋转图像。
处理通道
OpenCV 提供了一些函数来处理通道,无论我们需要合并、拆分还是对它们执行各种操作。在本节中,我们将学习如何将图像拆分为其组成通道或使用多个单通道图像创建多通道图像。那么,让我们开始吧。
您可以使用 merge 函数合并多个单通道 Mat 对象并创建一个新的多通道 Mat 对象,如下面的示例代码所示:
Mat channels[3] = {ch1, ch2, ch3};
merge(channels, 3, result);
在前面的代码中,ch1、ch2 和 ch3 都是相同大小的单通道图像。结果将是一个三通道的 Mat 对象。
您还可以使用 insertChannel 函数向图像中插入新通道。以下是方法:
int idx = 2;
insertChannel(ch, image, idx);
在前面的代码中,ch是一个单通道Mat对象,image是我们想要在其中添加额外通道的矩阵,而idx指的是通道将被插入的位置的零基于索引号。
可以使用split函数来执行与merge函数相反的操作,即将多通道图像分割成多个单通道图像。以下是一个示例:
Mat channels[3];
split(image, channels);
前面的代码中的channels数组将包含三个大小相同的单通道图像,分别对应于图像中的每个单独的通道。
要从图像中提取单个通道,可以使用extractChannel函数:
int idx = 2;
Mat ch;
extractChannel(image, ch, idx);
在前面的代码中,很明显,位置idx的通道将从图像中提取并保存到ch中。
即使merge、split、insertChannel和extractChannel对于大多数用例已经足够,你仍然可能需要更复杂的通道打乱、提取或操作。因此,OpenCV 提供了一个名为mixChannels的函数,它允许以更高级的方式处理通道。让我们通过一个示例案例来看看mixChannels是如何使用的。假设我们想要将图像的所有通道向右移动。为了能够执行这样的任务,我们可以使用以下示例代码:
Mat image = imread("Test.png");
Mat result(image.rows, image.cols, image.type());
vector<int> fromTo = {0,1,
1,2,
2,0};
mixChannels(image, result, fromTo);
在前面的示例中,唯一重要的代码片段是fromTo向量,它必须包含对应于源图像和目标图像中通道号的值对。通道号,如往常一样,是基于 0 的索引,所以 0, 1 表示源图像中的第一个通道将被复制到结果中的第二个通道,依此类推。
值得注意的是,本节中所有之前的函数(合并、分割等)都是mixChannels函数的部分情况。
数学函数
在本节中我们将学习的函数和算法仅用于非元素级数学计算,这与我们在本章前面所看到的不同。这包括简单的函数,如mean或sum,或更复杂的操作,如离散傅里叶变换。让我们通过一些实例来浏览其中一些最重要的函数。
矩阵求逆
可以使用invert函数来计算矩阵的逆。这不应与bitwise_not函数混淆,后者会反转图像像素中的每个比特。invert函数不是元素级函数,并且需要将反转方法作为参数传递给此函数。以下是一个示例:
DecompTypes dt = DECOMP_LU;
invert(image, result, dt);
DecompTypes枚举包含可以在invert函数中用作分解类型的所有可能条目。以下是它们:
-
DECOMP_LU -
DECOMP_SVD -
DECOMP_EIG -
DECOMP_CHOLESKY -
DECOMP_QR
如果你对每个分解方法的详细描述感兴趣,请参阅 OpenCV 文档中的DecompTypes枚举。
元素的均值和总和
你可以使用mean函数来计算矩阵中元素的均值,或者说是平均值。以下是一个示例,展示了如何读取图像并计算并显示其所有单独通道的均值:
Mat image = imread("Test.png");
Mat result;
Scalar m = mean(image);
cout << m[0] << endl;
cout << m[1] << endl;
cout << m[2] << endl;
你可以使用sum函数以完全相同的方式,来计算矩阵中元素的求和:
Scalar s = sum(image);
OpenCV 还包括一个meanStdDev函数,可以用来同时计算矩阵中所有元素的均值和标准差。以下是一个示例:
Scalar m;
Scalar stdDev;
meanStdDev(image, m, stdDev);
与mean函数类似,meanStdDev函数也会分别计算每个单独通道的结果。
离散傅里叶变换
一维或二维数组的离散傅里叶变换,或者说图像的离散傅里叶变换,是计算机视觉中分析图像的许多方法之一。结果的理解完全取决于其应用的领域,而这本书中我们并不关心这一点,然而,在本节中我们将学习如何执行离散傅里叶变换。
简而言之,你可以使用dft函数来计算图像的傅里叶变换。然而,在安全调用dft函数之前,需要做一些准备工作。傅里叶变换的结果也是如此。让我们用一个示例代码来分解这个过程,并通过计算和显示之前章节中使用的示例图像的傅里叶变换来展示。
dft函数可以更高效地处理特定大小的矩阵(如 2 的幂,例如 2、4 和 8),这就是为什么在调用dft函数之前,最好将矩阵的大小增加到最接近的优化大小,并用零填充。这可以通过使用getOptimalDFTSize函数来完成。假设image是我们想要计算其离散傅里叶变换的输入图像,我们可以编写以下代码来计算并调整其大小以适应dft函数的优化大小:
int optRows = getOptimalDFTSize( image.rows );
int optCols = getOptimalDFTSize( image.cols );
Mat resizedImg;
copyMakeBorder(image,
resizedImg,
0,
optRows - image.rows,
0,
optCols - image.cols,
BORDER_CONSTANT,
Scalar::all(0));
如您所见,必须分别对行和列调用getOptimalDFTSize函数两次。您已经熟悉copyMakeBorder函数。调整图像大小并用零(或任何其他所需值)填充新像素是copyMakeBorder函数无数用例之一。
其余部分相当简单,我们需要形成一个双通道图像,并将其传递给dft函数,以在同一个矩阵中获得复数(实部和虚部)的结果。这将简化后续的显示过程。以下是具体操作方法:
vector<Mat> channels = {Mat_<float>(resizedImg),
Mat::zeros(resizedImg.size(), CV_32F)};
Mat complexImage;
merge(channels, complexImage);
dft(complexImage, complexImage);
我们已经学习了如何使用merge函数。在前面的代码中需要注意的唯一重要的事情是结果被保存到了与输入相同的图像中。complexImage现在包含两个通道,一个用于离散傅里叶变换的实部,另一个用于虚部。就是这样!我们现在有了结果,然而,为了能够显示它,我们必须计算结果的大小。下面是如何操作的:
split(complexImage, channels);
Mat mag;
magnitude(channels[0], channels[1], mag);
在前面的代码中,我们将复数结果分解为其组成通道,然后使用magnitude函数计算幅度。理论上,mag是一个可显示的结果,但在现实中,它包含的值远高于使用 OpenCV 可显示的值,因此我们需要在显示之前进行几个转换。首先,我们需要确保结果是按对数尺度,通过执行以下转换:
mag += Scalar::all(1);
log(mag, mag);
接下来,我们必须确保结果值被缩放并归一化,以便在0.0和1.0之间,以便由 OpenCV 的imshow函数显示。你需要使用normalize函数来完成这个任务:
normalize(mag, mag, 0.0, 1.0, CV_MINMAX);
你现在可以尝试使用imshow函数显示结果。以下是一个显示离散傅里叶变换结果的示例:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00032.gif
这个结果的问题在于,在结果的原点位于图像中心之前,需要交换四个象限。以下图像展示了在结果有一个位于中心的起点之前,必须如何交换四个结果象限:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00033.jpeg
下面的代码用于交换傅里叶变换结果的四个象限。注意我们首先找到结果中心,然后创建四个感兴趣区域(ROI)矩阵,然后交换它们:
int cx = mag.cols/2;
int cy = mag.rows/2;
Mat Q1(mag, Rect(0, 0, cx, cy));
Mat Q2(mag, Rect(cx, 0, cx, cy));
Mat Q3(mag, Rect(0, cy, cx, cy));
Mat Q4(mag, Rect(cx, cy, cx, cy));
Mat tmp;
Q1.copyTo(tmp);
Q4.copyTo(Q1);
tmp.copyTo(Q4);
Q2.copyTo(tmp);
Q3.copyTo(Q2);
tmp.copyTo(Q3);
dft函数接受一个额外的参数,可以用来进一步自定义其行为。此参数可以是来自DftFlags枚举的值的组合。例如,要执行逆傅里叶变换,你需要使用DFT_INVERSE参数调用dft函数:
dft(input, output, DFT_INVERSE);
这也可以通过使用idft函数来完成:
idft(input, output);
确保查看DftFlags枚举和dft函数文档,以获取有关 OpenCV 中离散傅里叶变换实现方式的更多信息。
生成随机数
随机数生成是计算机视觉中最广泛使用的算法之一,尤其是在测试给定范围内的随机值时。当使用 OpenCV 库时,你可以使用以下函数来生成包含随机值的值或矩阵:
randn函数可以用来填充一个矩阵或数组,使其包含具有给定平均值和标准差的随机数。以下是此函数的使用方法:
randn(rmat, mean, stddev);
randu函数与randn函数类似,用于用随机值填充数组,然而,这个函数使用的是下限和上限(两者都包含)来生成随机值。以下是一个示例:
randu(rmat, lowBand, highBand);
randShuffle函数,从其标题可以猜到,用于随机打乱数组或矩阵的内容。它就像以下示例中展示的那样简单使用:
randShuffle(array);
搜索和定位函数
当你在计算机视觉项目中工作时,你将面临无数的场景和案例,在这些场景和案例中,你需要寻找特定的像素,或者最大值(最亮点),等等。OpenCV 库包含许多可用于此目的的函数,这些函数是本节的主题。
定位非零元素
定位或计数非零元素可能会非常有用,尤其是在对图像进行阈值操作后寻找特定区域,或者寻找特定颜色覆盖的区域时。OpenCV 包含 findNonZero 和 countNonZero 函数,这些函数可以简单地让你找到或计数图像中具有非零(或明亮)值的像素。
下面是一个示例,展示了如何使用 findNonZero 函数在灰度图像中找到第一个非黑色像素并打印其位置:
Mat image = imread("Test.png", IMREAD_GRAYSCALE);
Mat result;
vector<Point> idx;
findNonZero(image, idx);
if(idx.size() > 0)
cout << idx[0].x << "," << idx[0].y << endl;
下面是另一个示例代码,展示了如何找到灰度图像中黑色像素的百分比:
Mat image = imread("Test.png", IMREAD_GRAYSCALE);
Mat result;
int nonZero = countNonZero(image);
float white = float(nonZero) / float(image.total());
float black = 1.0 - white;
cout << black << endl;
定位最小和最大元素
在图像或矩阵中定位最亮(最大)和最暗(最小)点是计算机视觉中图像搜索的最重要类型之一,尤其是在执行某些类型的阈值算法或模板匹配函数(我们将在接下来的章节中学习)之后。OpenCV 提供以下两个函数来定位矩阵中的全局最小和最大值及其位置:
-
minMaxIdx -
minMaxLoc
minMaxLoc 函数在整个图像(仅单通道图像)中搜索最亮和最暗的点,并返回最亮和最暗像素的值,以及它们的位置,而 minMaxIdx 函数返回找到的最小和最大位置的指针,而不是位置(带有 x 和 y 的 Point 对象)。以下是 minMaxLoc 函数的使用方法:
double minVal, maxVal;
Point minLoc, maxLoc;
minMaxLoc(image, &minVal, &maxVal, &minLoc, &maxLoc);
这是一个使用 minMaxIdx 函数的示例:
double minVal, maxVal;
int minIdx, maxIdx;
minMaxIdx(image, &minVal, &maxVal, &minIdx, &maxIdx);
查找表转换
在计算机视觉中,根据给定表中包含的所需替换值替换像素称为 查找表转换。一开始这可能听起来有些令人困惑,但它是一种非常强大且简单的方法,用于使用查找表修改图像。让我们通过一个实际示例来看看它是如何实现的。
假设我们有一个示例图像,我们需要将亮度像素(值大于175)替换为绝对白色,暗像素(值小于125)替换为绝对黑色,其余像素保持不变。为了执行此类任务,我们可以简单地使用查找表Mat对象以及 OpenCV 中的LUT函数:
Mat lut(1, 256, CV_8UC1);
for(int i=0; i<256; i++)
{
if(i < 125)
lut.at<uchar>(0, i) = 0;
else if(i > 175)
lut.at<uchar>(0, i) = 255;
else
lut.at<uchar>(0, i) = i;
}
Mat result;
LUT(image, lut, result);
以下图像展示了当在示例图像上执行此查找表转换时的结果。正如你所见,查找表转换可以在彩色(在右侧)和灰度(在左侧)图像上执行:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00034.jpeg
聪明地使用LUT函数可以为计算机视觉问题中的许多创意解决方案带来帮助,其中需要用给定值替换像素。
摘要
在本章中我们所看到的是使用 OpenCV 库在计算机视觉中进行矩阵和数组操作所能做到的一小部分。我们从对前几章中计算机视觉基本概念有坚实的背景知识开始,最终通过实际示例学习了大量算法和函数。我们学习了 ones、zeroes、单位矩阵、转置以及其他嵌入到Mat类核心中的函数。然后,我们继续学习许多逐元素算法。我们现在完全能够执行逐元素矩阵运算,如基本运算、比较和位运算。最后,我们学习了 OpenCV 核心模块中的非逐元素操作。创建边界、修改通道和离散傅里叶变换是我们在本章中学到的许多算法之一。
在下一章中,我们将学习用于过滤、绘制和其他功能的计算机视觉算法。即将到来的章节中涵盖的主题是在你步入高级计算机视觉开发世界以及用于高度复杂任务(如人脸或物体检测和跟踪)的算法之前所必需的。
问题
-
哪些逐元素数学运算和位运算会产生完全相同的结果?
-
OpenCV 中的
gemm函数的目的是什么?使用gemm函数,A*B的等价是什么? -
使用
borderInterpolate函数计算点(-10, 50)处不存在像素的值,边框类型为BORDER_REPLICATE。进行此类计算所需的函数调用是什么? -
创建与本章学习单位矩阵部分相同的单位矩阵,但使用
setIdentity函数而不是Mat::eye函数。 -
编写一个使用
LUT函数(查找表转换)的程序,当在灰度图像和彩色(RGB)图像上执行时,与bitwise_not(颜色反转)执行相同的任务。 -
除了对矩阵的值进行归一化外,
normalize函数还可以用来调整图像的亮度或暗度。请编写所需的函数调用,使用normalize函数来使灰度图像变暗和变亮。 -
使用
merge和split函数从图像(使用imread函数创建的 BGR 图像)中移除蓝色通道(第一个通道)。
第四章:绘图、过滤和变换
我们在前一章中从计算机视觉的基本和基本概念开始,最终学习了用于在矩阵和图像上执行广泛操作的许多算法和函数。首先,我们学习了嵌入到Mat类中以方便使用的各种函数,例如克隆(或获取完整且独立的副本)一个矩阵,计算两个矩阵的叉积和点积,获取矩阵的转置或逆,以及生成单位矩阵。然后我们转向学习 OpenCV 中的各种逐元素操作。正如我们所知,逐元素操作是可并行化的算法,它们对图像的所有单个像素(或元素)执行相同的过程。在这个过程中,我们还实验了这些操作对实际图像文件的影响。我们通过学习将图像视为整体的操作和函数来完成前一章,这些操作与逐元素操作不同,但在计算机视觉中它们仍然被视为矩阵操作。
现在,我们已经准备好深入挖掘,学习在计算机视觉应用中用于绘图、过滤和图像变换等任务的众多强大算法。正如前几章所述,这些算法类别整体上被认为是图像处理算法。在本章中,我们将从学习如何在空图像(类似于画布)或现有图像和视频帧上绘制形状和文本开始。本章第一部分中的示例还将包括一个关于如何向 OpenCV 窗口添加滑块条的教程,以便轻松调整所需参数。之后,我们将继续学习图像过滤技术,例如模糊图像、膨胀和腐蚀。本章我们将学习到的过滤算法和函数包括许多流行且广泛使用的算法,尤其是被专业照片编辑应用所采用。本章将包括一个关于图像变换算法的全面部分,包括如简单调整照片大小或复杂如像素重新映射的算法。我们将通过学习如何将色图应用于图像以变换其颜色来结束本章。
在本章中,我们将学习以下内容:
-
在图像上绘制形状和文本
-
将平滑滤波器应用于图像
-
将膨胀、腐蚀以及其他各种过滤器应用于图像
-
在图像上重新映射像素并执行几何变换算法
-
将色图应用于图像
技术要求
-
用于开发 C++或 Python 应用程序的 IDE
-
OpenCV 库
有关如何设置个人计算机并使其准备好使用 OpenCV 库开发计算机视觉应用程序的更多信息,请参阅第二章,开始使用 OpenCV。
你可以使用以下网址下载本章的源代码和示例:
github.com/PacktPublishing/Hands-On-Algorithms-for-Computer-Vision/tree/master/Chapter04
利用图像
毫无疑问,在开发计算机视觉应用时,最重要的任务之一就是在图像上绘制。想象一下,你想要在图片上打印时间戳,或者在图像的某些区域绘制矩形或椭圆,以及许多需要你在图像或形状(如矩形)上绘制文本和数字的类似例子。正如你所看到的,可以指出的例子非常明显且数量众多,因此,我们不妨直接从 OpenCV 中用于绘制的函数和算法开始。
在图像上打印文本
OpenCV 包含一个名为 putText 的非常易于使用的函数,用于在图像上绘制或打印文本。此函数需要一个图像作为输入/输出参数,这意味着源图像本身将被更新。因此,在调用此函数之前,务必在内存中制作原始图像的副本。你还需要为此函数提供一个起点,这仅仅是文本将被打印的点。文本的字体必须是 HersheyFonts 枚举中的一个条目,它可以取以下值之一(或它们的组合):
-
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
关于每个条目打印时的详细情况,你可以查看 OpenCV 或简单地在网上搜索有关 Hershey 字体的更多信息。
除了我们刚才提到的参数之外,你还需要一些额外的参数,例如文本的缩放、颜色、粗细和线型。让我们用一个简单的例子来逐一解释它们。
下面是一个演示 putText 函数用法的示例代码:
string text = "www.amin-ahmadi.com";
int offset = 25;
Point origin(offset, image.rows - offset);
HersheyFonts fontFace = FONT_HERSHEY_COMPLEX;
double fontScale = 1.5;
Scalar color(0, 242, 255);
int thickness = 2;
LineTypes lineType = LINE_AA;
bool bottomLeftOrigin = false;
putText(image,
text,
origin,
fontFace,
fontScale,
color,
thickness,
lineType,
bottomLeftOrigin);
当在上一章的示例图片上执行时,将创建以下结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00035.jpeg
显然,增加或减少 scale 将导致文本大小的增加或减少。thickness 参数对应于打印文本的粗细,等等。唯一值得讨论的参数是 lineType,在我们的示例中它是 LINE_AA,但它可以取 LineTypes 枚举中的任何值。以下是重要的线型及其差异,通过在白色背景上打印 W 字符来演示:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00036.gif
LINE_4 表示四连接线型,LINE_8 表示八连接线型。然而,LINE_AA,即抗锯齿线型,比其他两种线型绘制速度慢,但如图所示,它也提供了更好的质量。
LineTypes 枚举还包括一个 FILLED 条目,它用于用给定的颜色填充图像上绘制的形状。需要注意的是,OpenCV 中几乎所有绘图函数(不仅仅是 putText)都需要一个线型参数。
OpenCV 提供了两个与处理文本相关的函数,但不是用于绘制文本。第一个函数名为 getFontScaleFromHeight,它用于根据字体类型、高度(以像素为单位)和粗细获取所需的缩放值。以下是一个示例:
double fontScale = getFontScaleFromHeight(fontFace,
50, // pixels for height
thickness);
我们可以使用前面的代码代替在之前 putText 函数的示例使用中为 scale 提供一个常量值。显然,我们需要将 50 替换为我们文本所需的任何像素高度值。
除了 getFontScaleFromHeight,OpenCV 还包括一个名为 getTextSize 的函数,可以用来检索打印特定文本所需的宽度和高度。以下是一个示例代码,展示了我们如何使用 getTextSize 函数找出打印 "Example" 单词所需的像素宽度和高度,使用 FONT_HERSHEY_PLAIN 字体类型,缩放为 3.2,粗细为 2:
int baseLine;
Size size = getTextSize("Example",
FONT_HERSHEY_PLAIN,
3.2,
2,
&baseLine);
cout << "Size = " << size.width << " , " << size.height << endl;
cout << "Baseline = " << baseLine << endl;
结果应该看起来像以下这样:
Size = 216 , 30
Baseline = 17
这意味着文本需要 216 by 30 像素的空间来打印,基线将比文本底部远 17 像素。
绘制形状
您可以使用一组非常简单的 OpenCV 函数在图像上绘制各种类型的形状。这些函数都包含在 imgproc 模块中,就像 putText 函数一样,并且可以用来在给定点绘制标记、线条、箭头线条、矩形、椭圆、圆、多边形等。
让我们从 drawMarker 函数开始,该函数用于在图像上绘制给定类型的标记。以下是该函数如何用于在给定图像的中心打印标记的示例:
Point position(image.cols/2,
image.rows/2);
Scalar color = Scalar::all(0);
MarkerTypes markerType = MARKER_CROSS;
int markerSize = 25;
int thickness = 2;
int lineType = LINE_AA;
drawMarker(image,
position,
color,
markerType,
markerSize,
thickness,
lineType);
position 是标记的中心点,其余的参数几乎与我们之前在 putText 函数中看到的是一样的。这是 OpenCV 中大多数(如果不是所有)绘图函数的参数模式。唯一特定于 drawMarker 函数的参数是 markerSize,它只是标记的大小,以及 markerType,它可以取 MarkerTypes 枚举中的以下值之一:
-
MARKER_CROSS -
MARKER_TILTED_CROSS -
MARKER_STAR -
MARKER_DIAMOND -
MARKER_SQUARE -
MARKER_TRIANGLE_UP -
MARKER_TRIANGLE_DOWN
以下图表展示了之前列表中提到的所有可能的标记类型,当它们打印在白色背景上时,从左到右排列:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00037.gif
在 OpenCV 中使用 line 函数可以绘制线条。此函数需要两个点,并将绘制连接给定点的线条。以下是一个示例:
Point pt1(25, image.rows/2);
Point pt2(image.cols/2 - 25, image.rows/2);
Scalar color = Scalar(0,255,0);
int thickness = 5;
int lineType = LINE_AA;
int shift = 0;
line(image,
pt1,
pt2,
color,
thickness,
lineType,
shift);
shift参数对应于给定点的分数位数。您可以省略或简单地传递零以确保它对您的结果没有影响。
与line函数类似,arrowedLine也可以用来绘制带箭头的线。显然,给定点的顺序决定了箭头的方向。这个函数唯一需要的参数是tipLength参数,它对应于用于创建箭头尖端的线长百分比。以下是一个示例:
double tipLength = 0.2;
arrowedLine(image,
pt1,
pt2,
color,
thickness,
lineType,
shift,
tipLength);
要在图像上绘制一个圆,我们可以使用circle函数。以下是这个函数如何用来在图像中心绘制一个圆的示例:
Point center(image.cols/2,
image.rows/2);
int radius = 200;
circle(image,
center,
radius,
color,
thickness,
lineType,
shift);
除了center和radius,它们显然是圆的中心点和半径之外,其余参数与我们在本节中学到的函数和示例相同。
使用rectangle函数可以在图像上绘制矩形或正方形。这个函数与line函数非常相似,因为它只需要两个点。不同之处在于,提供给rectangle函数的点对应于矩形或正方形的左上角和右下角点。以下是一个示例:
rectangle(image,
pt1,
pt2,
color,
thickness,
lineType,
shift);
除了两个单独的Point对象外,这个函数还可以提供一个单一的Rect对象。以下是这样做的方法:
Rect rect(pt1,pt2);
rectangle(image,
color,
thickness,
lineType,
shift);
类似地,可以使用ellipse函数绘制椭圆。这个函数需要提供轴的大小以及椭圆的角度。此外,您可以使用起始和结束角度来绘制椭圆的全部或部分,或者,在其他情况下,绘制一个圆弧而不是椭圆。您可以猜测,将0和360作为起始和结束角度将导致绘制一个完整的椭圆。以下是一个示例:
Size axes(200, 100);
double angle = 20.0;
double startAngle = 0.0;
double endAngle = 360.0;
ellipse(image,
center,
axes,
angle,
startAngle,
endAngle,
color,
thickness,
lineType,
shift);
另一种调用ellipse函数的方法是使用RotatedRect对象。在这个函数的版本中,您必须首先创建一个具有给定宽度和高度(或者说大小)以及一个angle的RotatedRect,然后像下面这样调用ellipse函数:
Size size(150, 300);
double angle = 45.0;
RotatedRect rotRect(center,
axes,
angle);
ellipse(image,
rotRect,
color,
thickness,
lineType);
注意,使用这种方法,您不能绘制圆弧,这仅用于绘制完整的椭圆。
我们已经到了可以使用 OpenCV 绘图函数绘制的最后一种形状类型,那就是多边形形状。您可以通过使用polylines函数来绘制多边形形状。您必须确保创建一个点向量,它对应于绘制多边形所需的顶点。以下是一个示例:
vector<Point> pts;
pts.push_back(Point(100, 100));
pts.push_back(Point(50, 150));
pts.push_back(Point(50, 200));
pts.push_back(Point(150, 200));
pts.push_back(Point(150, 150));
bool isClosed = true;
polylines(image,
pts,
isClosed,
color,
thickness,
lineType,
shift);
isClosed参数用于确定多边形线是否必须闭合,即是否通过将最后一个顶点连接到第一个顶点。
以下图像展示了我们在前面的代码片段中使用的arrowedLine、circle、rectangle和polylines函数在绘制示例图像时的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00038.jpeg
在继续下一节并学习用于图像过滤的算法之前,我们将学习如何通过在 OpenCV 显示窗口中添加滑块来在运行时调整参数。当你实验不同的值以查看它们的效果时,这是一个非常有用的方法,以便在运行时尝试大量不同的参数,并且它允许通过简单地调整滑块(或滑块)的位置来改变变量的值。
首先,让我们看一个例子,然后进一步分解代码,学习如何使用 OpenCV 函数处理滑块。以下完整的例子展示了我们如何使用滑块在运行时调整图像上绘制的圆的半径:
string window = "Image"; // Title of the image output window
string trackbar = "Radius"; // Label of the trackbar
Mat image = imread("Test.png");
Point center(image.cols/2, image.rows/2); // A Point object that points to the center of the image
int radius = 25;
Scalar color = Scalar(0, 255, 0); // Green color in BGR (OpenCV default) color space
int thickness = 2; LineTypes lineType = LINE_AA; int shift = 0;
// Actual callback function where drawing and displaying happens
void drawCircle(int, void*)
{
Mat temp = image.clone();
circle(temp,
center,
radius,
color,
thickness,
lineType,
shift);
imshow(window, temp);
}
int main()
{
namedWindow(window); // create a window titled "Image" (see above)
createTrackbar(trackbar, // label of the trackbar
window, // label of the window of the trackbar
&radius, // the value that'll be changed by the trackbar
min(image.rows, image.cols) / 2, // maximum accepted value
drawCircle);
setTrackbarMin(trackbar, window, 25); // set min accespted value by trackbar
setTrackbarMax(trackbar, window, min(image.rows, image.cols) / 2); // set max again
drawCircle(0,0); // call the callback function and wait
waitKey();
return 0;
}
在前面的代码中,window 和 trackbar 是 string 对象,用于识别和访问特定窗口上的特定滑块。image 是包含源图像的 Mat 对象。center、radius、color、thickness、lineType 和 shift 是绘制圆所需的参数,正如我们在本章前面所学。drawCircle 是当使用滑块更新我们想要绘制的圆的 radius 值时将被调用的函数(确切地说,是回调函数)。这个函数必须具有示例中使用的签名,它有一个 int 和一个 void 指针作为其参数。这个函数相当简单;它只是克隆原始图像,在其上绘制一个圆,然后显示它。
main 函数是我们实际创建窗口和滑块的地方。首先,必须调用 namedWindow 函数来创建一个具有我们想要的窗口名称的窗口。然后,可以像示例中那样调用 createTrackbar 函数,在该窗口上创建一个滑块。请注意,滑块本身有一个名称,用于访问它。当应用程序运行时,这个名称也将打印在滑块旁边,以向用户显示其目的。调用 setTrackbarMin 和 setTrackbarMax 确保我们的滑块不允许 radius 值小于 25 或大于图像的宽度或高度(取较小者),然后除以 2(因为我们谈论的是半径,而不是直径)。
以下是一个截图,展示了我们的 window 以及其上的 trackbar,可以用来调整圆的 radius:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00039.jpeg
尝试调整以亲自看到圆的半径是如何根据滑块的位置变化的。确保在你想实验这本书中学到的函数或算法的参数时使用这种方法。请注意,你可以添加你需要的任意数量的滑块。然而,添加更多的滑块会在你的窗口上使用更多的空间,这可能会导致用户界面和体验不佳,从而使得程序难以使用,而不是简化,所以尽量明智地使用滑块。
图像过滤
无论你是在尝试构建一个执行高度复杂任务的计算机视觉应用程序,比如实时目标检测,还是简单地以某种方式修改输入图像,这真的并不重要。几乎不可避免的是,你将不得不应用某种类型的滤波器到你的输入或输出图像上。原因很简单——并不是所有的图片都准备好直接进行处理,大多数时候,应用滤波器使图像更平滑是确保它们可以被我们的算法处理的一种方式。
在计算机视觉中,你可以应用到图像上的滤波器种类繁多,但在这个章节中,我们将学习一些最重要的滤波器,特别是那些在 OpenCV 库中有实现,我们可以使用的滤波器。
模糊/平滑滤波器
模糊图像是图像滤波任务中最重要的一项,有许多算法可以执行这项任务,每个算法都有其自身的优缺点,我们将在本节中讨论这些内容。
让我们从用于平滑图像的最简单的滤波器开始,这个滤波器被称为中值滤波器,可以通过使用medianBlur函数来实现,如下面的示例所示:
int ksize = 5; // must be odd
medianBlur(image, result, ksize);
此滤波器简单地找到图像中每个像素的邻近像素的中值。ksize,或核大小参数,决定了用于模糊的核的大小,换句话说,决定了在模糊算法中考虑邻近像素的距离。以下图像展示了从 1 到 7 增加核大小的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00040.jpeg
注意,核大小必须是奇数,核大小为 1 将产生与输入图像完全相同的图像。因此,在之前的图像中,你可以看到从原始图像(最左侧)到核大小为 7 的模糊级别的增加。
注意,如果你需要,可以使用非常高的核大小,但这通常是不必要的,通常只有在需要去除极其嘈杂的图像的噪声时才需要。以下是一个核大小为 21 的示例图像,展示了其结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00041.jpeg
另一种模糊图像的方法是使用boxFilter函数。让我们通过一个示例代码来看看它是如何实现的,然后进一步分解以更好地理解其行为:
int ddepth = -1;
Size ksize(7,7);
Point anchor(-1, -1);
bool normalize = true;
BorderTypes borderType = BORDER_DEFAULT;
boxFilter(image,
result,
ddepth,
ksize,
anchor,
normalize,
borderType);
箱形滤波器被称为一种模糊方法,其中使用给定 ksize 和 anchor 点的 1 的矩阵进行图像模糊。与 medianBlur 函数相比,主要区别在于您实际上可以定义 anchor 点为任何非中心点的任何邻域。您还可以定义在此函数中使用的边界类型,而在 medianBlur 函数中,内部使用 BORDER_REPLICATE 并不能更改。有关边界类型的更多信息,您可能需要参考第三章,数组和矩阵操作。最后,normalize 参数允许我们将结果归一化到可显示的结果。
可以使用 ddepth 参数来更改结果的深度。然而,您可以使用 -1 来确保结果具有与源相同的深度。同样,可以提供 -1 值给 anchor 以确保使用默认的锚点。
下面的图像展示了前面示例代码的结果。右侧图像是左侧图像经过箱形滤波后的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00042.jpeg
我们可以通过使用 blur 函数执行完全相同的任务,换句话说,就是归一化箱形滤波器,如下面的示例代码所示:
Size ksize(7,7);
Point anchor(-1, -1);
BorderTypes borderType = BORDER_DEFAULT;
blur(image,
result,
ksize,
anchor,
borderType);
此示例代码的结果与之前看到的调用 boxFilter 的结果完全相同。这里明显的区别是,此函数不允许我们更改结果的深度,并且默认应用归一化。
除了标准的箱形滤波器外,您还可以使用 sqrBoxFilter 函数应用平方箱形滤波器。在此方法中,不是计算邻近像素的总和,而是计算它们平方的总和。以下是一个示例,与调用 boxFilter 函数非常相似:
int ddepth = -1;
Size ksize(7,7);
Point anchor(-1, -1);
bool normalize = true;
BorderTypes borderType = BORDER_DEFAULT;
sqrBoxFilter(image,
result,
ddepth,
ksize,
anchor,
normalize,
borderType);
boxFilter 和 sqrBoxFilter 函数的非归一化版本也可以用来获取图像中所有像素邻近区域的统计信息,它们的使用场景并不仅限于图像模糊。
计算机视觉中最受欢迎的模糊方法之一是 高斯模糊 算法,在 OpenCV 中可以通过使用 GaussianBlur 函数来实现。这个函数与之前我们了解过的模糊函数类似,需要指定核大小,以及 X 和 Y 方向上的标准差值,分别称为 sigmaX 和 sigmaY。以下是如何使用此函数的示例:
Size ksize(7,7);
double sigmaX = 1.25;
double sigmaY = 0.0;
BorderTypes borderType = BORDER_DEFAULT;
GaussianBlur(image,
result,
ksize,
sigmaX,
sigmaY,
borderType);
注意,sigmaY 的值为零意味着 sigmaX 的值也将用于 Y 方向。有关高斯模糊的更多信息,您可以阅读关于高斯函数的一般信息和 OpenCV 文档页面中的 GaussianBlur 函数。
在本节中我们将学习的最后一个平滑滤波器称为双边滤波器,可以通过使用bilateralFilter函数实现。双边滤波是一种强大的去噪和图像平滑方法,同时保留边缘。与之前看到的模糊算法相比,此函数也慢得多,CPU 密集度更高。让我们通过一个例子来看看如何使用bilateralFilter,然后分解所需的参数:
int d = 9;
double sigmaColor = 250.0;
double sigmaSpace = 200.0;
BorderTypes borderType = BORDER_DEFAULT;
bilateralFilter(image,
result,
d,
sigmaColor,
sigmaSpace,
borderType);
d,或过滤器大小,是参与过滤的像素邻域的直径。sigmaColor和sigmaSpace值都用于定义颜色效果并协调计算过滤值像素的颜色和坐标。以下是演示bilateralFilter函数在我们示例图像上执行效果的截图:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00043.jpeg
形态学滤波器
与平滑滤波器类似,形态学滤波器是改变每个像素值基于相邻像素值的算法,尽管明显的区别是它们没有模糊效果,并且主要用于在图像上产生某种形式的侵蚀或膨胀效果。这将在本节后面的几个动手示例中进一步阐明,但现在,让我们看看如何使用 OpenCV 函数执行形态学操作(也称为变换)。
您可以使用morphologyEx函数对图像执行形态学操作。此函数可以提供一个来自MorphTypes枚举的条目来指定形态学操作。以下是可用的值:
-
MORPH_ERODE:用于侵蚀操作 -
MORPH_DILATE:用于膨胀操作 -
MORPH_OPEN:用于开运算,或侵蚀图像的膨胀 -
MORPH_CLOSE:用于闭运算,或从膨胀图像中侵蚀 -
MORPH_GRADIENT:用于形态学梯度操作,或从膨胀图像中减去侵蚀图像 -
MORPH_TOPHAT:用于顶帽操作,或从源图像中减去开运算的结果 -
MORPH_BLACKHAT:用于黑帽操作,或从闭运算的结果中减去源图像
要理解前面列表中提到的所有可能的形态学操作,首先理解侵蚀和膨胀(列表中的前两项)的效果是很重要的,因为其余的只是这两种形态学操作的组合。让我们先通过一个例子来尝试侵蚀,同时学习如何使用morphologyEx函数:
MorphTypes op = MORPH_ERODE;
MorphShapes shape = MORPH_RECT;
Size ksize(3,3);
Point anchor(-1, -1);
Mat kernel = getStructuringElement(shape,
ksize,
anchor);
int iterations = 3;
BorderTypes borderType = BORDER_CONSTANT;
Scalar borderValue = morphologyDefaultBorderValue();
morphologyEx(image,
result,
op,
kernel,
anchor,
iterations,
borderType,
borderValue);
op,或操作,是前面提到的MorphTypes枚举中的一个条目。kernel,或结构元素,是用于形态学操作的核矩阵,它可以是手动创建的,也可以通过使用getStructuringElement函数创建。你必须为getStructuringElement提供shape形态、核大小(ksize)和anchor。shape可以是矩形、十字或椭圆,它只是MorphShapes枚举中的一个条目。iterations变量指的是形态学操作在图像上执行次数。borderType的解释方式与迄今为止我们看到的用于像素外推的所有函数完全相同。如果使用常量边界类型值,则morphologyEx函数还必须提供边界值,该值可以通过使用morphologyDefaultBorderValue函数检索,或手动指定。
以下是我们的先前代码(用于腐蚀)在示例图像上执行的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00044.jpeg
另一方面,膨胀操作是通过在前面示例中将op值替换为MORPH_DILATE来执行的。以下是膨胀操作的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00045.jpeg
对腐蚀和膨胀操作的高度简化描述是,它们会导致较暗像素的邻近像素变得更暗(在腐蚀的情况下)或较亮像素的邻近像素变得更亮(在膨胀的情况下),经过更多迭代后,这将导致更强烈且易于观察的效果。
如前所述,所有其他形态学操作都是腐蚀和膨胀的组合。以下是当我们对示例图像执行开闭操作时的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00046.jpeg
确保你自己尝试其余的形态学操作,以查看它们的效果。创建具有滑块的图形用户界面,并尝试更改iteration和其他参数的值,以查看它们如何影响形态学操作的结果。如果使用得当,形态学操作可以产生非常有趣的结果,并进一步简化你想要在图像上执行的操作。
在进入下一节之前,值得注意的是,你也可以使用erode和dilate函数执行完全相同的操作。尽管这些函数不需要操作参数(因为操作已经在它们的名称中),但其余的参数与morphologyEx函数完全相同。
基于导数的过滤器
在本节中,我们将学习基于计算和使用图像导数的滤波算法。为了理解图像中导数的概念,您可以回忆一下事实,即图像是矩阵,因此您可以在X或Y方向上计算(任何阶数)的导数,例如,在最简单的情况下,这相当于在某个方向上寻找像素的变化。
让我们从Sobel函数开始,该函数用于通过Sobel算子计算图像的导数。以下是这个函数在实际中的应用方法:
int ddepth = -1;
int dx = 1;
int dy = 1;
int ksize = 5;
double scale = 0.3;
double delta = 0.0;
BorderTypes borderType = BORDER_DEFAULT;
Sobel(image,
result,
ddepth,
dx,
dy,
ksize,
scale,
delta,
borderType);
ddepth,类似于在本章中之前示例中看到的,用于定义输出深度,使用-1确保结果具有与输入相同的深度。dx和dy用于设置X和Y方向上导数的阶数。ksize是Sobel算子的大小,可以是 1、3、5 或 7。scale用作结果的缩放因子,delta被加到结果上。
以下图像显示了使用前一个示例代码中的参数值调用Sobel函数的结果,但delta值为零(左侧)和 255(右侧):
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00047.jpeg
尝试设置不同的delta和scale值,并实验结果。还可以尝试不同的导数阶数,亲自看看效果。正如您从前面的输出图像中可以看到,计算图像的导数是计算图像边缘的一种方法。
您还可以使用spatialGradient函数,同时使用Sobel算子,在X和Y方向上计算图像的一阶导数。换句话说,调用一次spatialGradient相当于调用两次Sobel函数,用于两个方向的一阶导数。以下是一个示例:
Mat resultDX, resultDY;
int ksize = 3;
BorderTypes borderType = BORDER_DEFAULT;
spatialGradient(image,
resultDX,
resultDY,
ksize,
borderType);
注意,ksize参数必须是3,输入图像类型必须是灰度图,否则此函数将无法执行,尽管这可能在即将到来的 OpenCV 版本中有所改变。
与我们使用Sobel函数的方式类似,您可以使用Laplacian函数来计算图像的拉普拉斯。重要的是要注意,此函数本质上是对使用Sobel算子计算的X和Y方向的二阶导数求和。以下是一个演示Laplacian函数用法的示例:
int ddepth = -1;
int ksize = 3;
double scale = 1.0;
double delta = 0.0;
BorderTypes borderType = BORDER_DEFAULT;
Laplacian(image,
result,
ddepth,
ksize,
scale,
delta,
borderType);
在Laplacian函数中使用的所有参数已在之前的示例中描述,特别是Sobel函数。
任意滤波
OpenCV 通过使用filter2D函数支持在图像上应用任意滤波器。此函数能够创建我们之前学习到的许多算法的结果,但它需要一个kernel矩阵来提供。此函数只是将整个图像与给定的kernel矩阵进行卷积。以下是在图像上应用任意滤波器的示例:
int ddepth = -1;
Mat kernel{+1, -1, +1,
-1, +2, -1,
+1, -1, +1};
Point anchor(-1, -1);
double delta = 0.0;
BorderTypes borderType = BORDER_DEFAULT;
filter2D(image,
result,
ddepth,
kernel,
anchor,
delta,
borderType);
下面是这种任意滤波器的结果。你可以看到左侧的原始图像和右侧的滤波操作结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00048.jpeg
使用filter2D函数可以创建和使用的可能过滤器数量绝对没有限制。确保你尝试不同的核矩阵,并使用filter2D函数进行实验。你还可以在网上搜索流行的滤波核矩阵,并使用filter2D函数应用它们。
图像变换
在本节中,我们将学习用于以某种方式变换图像的计算机视觉算法。本节中我们将学习的算法包括改变图像内容或其内容解释方式的算法。
阈值算法
阈值算法用于将阈值值应用于图像的像素。这些算法可以用来有效地从包含可能区域或感兴趣像素且通过一定阈值值的图像中创建掩码。
你可以使用threshold函数将阈值值应用于图像的所有像素。threshold函数必须提供所需的阈值类型,并且它可以是ThresholdTypes枚举中的一个条目。以下是一个示例,说明如何使用threshold函数找到图像中最亮的部分:
double thresh = 175.0;
double maxval = 255.0;
ThresholdTypes type = THRESH_BINARY;
threshold(image,
result,
thresh,
maxval,
type);
thresh是最低阈值值,而maxval是允许的最大值。简单来说,介于thresh和maxval之间的所有像素值都被允许通过,从而创建出result。以下是一个使用示例图像演示的先前threshold操作的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00049.jpeg
增加阈值参数(或阈值值),你会注意到允许通过的像素更少。设置正确的阈值值需要经验和关于场景的知识。然而,在某些情况下,你可以开发出自动设置阈值或自适应阈值的程序。请注意,阈值类型完全影响threshold函数的结果。例如,THRESH_BINARY_INV将产生THRESH_BINARY的逆结果,依此类推。确保尝试不同的阈值类型,并亲自实验这个有趣且强大的函数。
另一种更复杂地将阈值应用于图像的方法是使用adaptiveThreshold函数,该函数与灰度图像一起工作。此函数将给定的maxValue参数分配给通过阈值标准的像素。除此之外,你必须提供一个阈值类型、自适应的阈值方法、定义像素邻域直径的块大小,以及从平均值中减去的常数值(取决于自适应阈值方法)。以下是一个示例:
double maxValue = 255.0;
AdaptiveThresholdTypes adaptiveMethod =
ADAPTIVE_THRESH_GAUSSIAN_C;
ThresholdTypes thresholdType = THRESH_BINARY;
int blockSize = 5;
double c = 0.0;
adaptiveThreshold(image,
result,
maxValue,
adaptiveMethod,
thresholdType,
blockSize,
c);
注意adaptiveMethod可以采用以下任何示例:
-
ADAPTIVE_THRESH_MEAN_C -
ADAPTIVE_THRESH_GAUSSIAN_C
blockSize参数值越高,自适应threshold方法中使用的像素就越多。以下是一个示例图像,展示了使用前面示例代码中的值调用adaptiveThreshold方法的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00050.jpeg
颜色空间和类型转换
将各种颜色空间和类型相互转换非常重要,尤其是在处理来自不同设备类型或打算在不同设备和格式上显示的图像时。让我们用一个非常简单的例子来看看这意味着什么。您将需要用于各种 OpenCV 函数和计算机视觉算法的灰度图像,而有些则需要 RGB 颜色图像。在这种情况下,您可以使用cvtColor函数在各个颜色空间和格式之间进行转换。
以下是将彩色图像转换为灰度的示例:
ColorConversionCodes code = COLOR_RGB2GRAY;
cvtColor(image,
result,
code);
code可以接受一个转换代码,该代码必须是ColorConversionCodes枚举中的一个条目。以下是一些最流行的颜色转换代码的示例,这些代码可以与cvtColor函数一起使用:
-
COLOR_BGR2RGB -
COLOR_RGB2GRAY -
COLOR_BGR2HSV
列表可以一直继续。请确保查看ColorConversionCodes枚举以获取所有可能的颜色转换代码。
几何变换
本节专门介绍几何变换算法和 OpenCV 函数。需要注意的是,几何变换这一名称是基于以下事实:属于这一类别的算法不会改变图像的内容,它们只是变形现有的像素,同时使用外推和内插方法来计算位于现有像素区域之外或重叠的像素。
让我们从最简单的几何变换算法开始,该算法用于调整图像大小。您可以使用resize函数调整图像大小。以下是此函数的使用方法:
Size dsize(0, 0);
double fx = 1.8;
double fy = 0.3;
InterpolationFlags interpolation = INTER_CUBIC;
resize(image,
result,
dsize,
fx,
fy,
interpolation);
如果dsize参数设置为非零大小,则fx和fy参数用于缩放输入图像。否则,如果fx和fy都为零,则输入图像的大小调整为给定的dsize。另一方面,interpolation参数用于设置用于缩放算法的interpolation方法,并且它必须是InterpolationFlags枚举中的条目之一。以下是interpolation参数的一些可能值:
-
INTER_NEAREST -
INTER_LINEAR -
INTER_CUBIC -
INTER_AREA -
INTER_LANCZOS4
请确保查看 OpenCV 文档中的InterpolationFlags页面,以了解每种可能方法的详细信息。
以下图像展示了用于调整图像大小的先前示例代码的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00051.jpeg
可能是最重要的几何变换算法,它能够执行大多数其他几何变换,这就是重映射算法,可以通过调用remap函数来实现。
remap函数必须提供两个映射矩阵,一个用于X方向,另一个用于Y方向。除此之外,还必须向remap函数提供一种插值和外推(边界类型)方法,以及在恒定边界类型的情况下,提供一个边界值。让我们首先看看这个函数是如何调用的,然后尝试几种不同的映射。以下是一个示例,展示了如何调用remap函数:
Mat mapX(image.size(), CV_32FC1);
Mat mapY(image.size(), CV_32FC1);
// Create maps here...
InterpolationFlags interpolation = INTER_CUBIC;
BorderTypes borderMode = BORDER_CONSTANT;
Scalar borderValue = Scalar(0, 0, 0);
remap(image,
result,
mapX,
mapY,
interpolation,
borderMode,
borderValue);
你可以创建无限数量的不同映射,并使用remap函数来调整图像大小、翻转、扭曲以及进行许多其他变换。例如,以下代码可以用来创建一个映射,这将导致输出图像进行垂直翻转:
for(int i=0; i<image.rows; i++)
for(int j=0; j<image.cols; j++)
{
mapX.at<float>(i,j) = j;
mapY.at<float>(i,j) = image.rows-i;
}
将前一个for循环中的代码替换为以下代码,remap函数调用的结果将是一个水平翻转的图像:
mapX.at<float>(i,j) = image.cols - j;
mapY.at<float>(i,j) = i;
除了简单的翻转之外,你还可以使用remap函数执行许多有趣的像素变形。以下是一个示例:
Point2f center(image.cols/2,image.rows/2);
for(int i=0; i<image.rows; i++)
for(int j=0; j<image.cols; j++)
{
// find i,j in the standard coordinates
double x = j - center.x;
double y = i - center.y;
// Perform any mapping for X and Y
x = x*x/750;
y = y;
// convert back to image coordinates
mapX.at<float>(i,j) = x + center.x;
mapY.at<float>(i,j) = y + center.y;
}
如前一个示例代码的行内注释所示,将 OpenCV 的i和j值(行和列号)转换为标准坐标系,并使用X和Y与已知的数学和几何函数一起使用,然后再将它们转换回 OpenCV 图像坐标,这是常见的做法。以下图像展示了前一个示例代码的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00052.jpeg
只要mapX和mapY的计算效率高,remap函数就非常强大且高效。请确保尝试这个函数,以了解更多的映射可能性。
计算机视觉和 OpenCV 库中有大量的几何变换算法,要涵盖所有这些算法需要一本自己的书。因此,我们将剩下的几何变换算法留给你去探索和尝试。请参考 OpenCV imgproc(图像处理)模块文档中的几何图像变换部分,以获取更多几何变换算法和函数。特别是,请确保了解包括getPerspectiveTransform和getAffineTransform在内的函数,这些函数用于找到两组点之间的透视和仿射变换。这些函数返回的变换矩阵可以用于通过使用warpPerspective和warpAffine函数来应用透视和仿射变换到图像上。
应用颜色图
我们将通过学习将色图应用于图像来结束本章。这是一个相当简单但功能强大的方法,可以用来修改图像的颜色或其总体色调。该算法简单地使用色图替换输入图像的颜色并创建一个结果。色图是一个包含 256 个颜色值的数组,其中每个元素代表源图像中相应像素值必须使用的颜色。我们将通过几个示例进一步解释,但在那之前,让我们看看色图是如何应用于图像的。
OpenCV 包含一个名为applyColorMap的函数,可以用来应用预定义的色图或用户创建的自定义色图。如果使用预定义的色图,applyColorMap必须提供一个色图类型,这必须是ColormapTypes枚举中的一个条目。以下是一个示例:
ColormapTypes colormap = COLORMAP_JET;
applyColorMap(image,
result,
colormap);
下面的图像展示了可以使用applyColorMap函数应用的各种预定义色图的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00053.jpeg
如前所述,你还可以创建自己的自定义色图。你只需确保遵循创建色图的说明。你的色图必须包含 256 个元素的大小(一个具有 256 行和 1 列的Mat对象)并且它必须包含颜色或灰度值,具体取决于你打算将色图应用于哪种类型的图像。以下是一个示例,展示了如何通过简单地反转绿色通道颜色来创建自定义色图:
Mat userColor(256, 1, CV_8UC3);
for(int i=0; i<=255; i++)
userColor.at<Vec3b>(i,0) = Vec3b(i, 255-i, i);
applyColorMap(image,
result,
userColor);
下面是前面示例代码的结果:
https://github.com/OpenDocCN/freelearn-ml-zh/raw/master/docs/hsn-algo-cv/img/00054.jpeg
概述
尽管我们试图涵盖计算机视觉中图像处理算法的最重要主题,但我们还有很长的路要走,还有更多的算法要学习。原因很简单,那就是这些算法可以用于各种应用。在本章中,我们学习了大量的广泛使用的计算机视觉算法,但还应注意的是,本章中我们所学的内容也是为了在你开始自己探索其他图像处理算法时能更容易一些。
我们从学习可以用于在图像上绘制形状和文本的绘图函数开始本章。然后我们转向学习最重要的计算机视觉主题之一:图像滤波。我们学习了如何使用平滑算法,实验了形态学滤波器,并了解了腐蚀、膨胀、开运算和闭运算。我们还尝试了一些简单的边缘检测算法,换句话说,基于导数的滤波器。我们学习了图像阈值处理和改变它们的颜色空间和类型。本章的最后部分介绍了几何变换和将颜色图应用于图像。我们甚至创建了自己的自定义颜色图。你可以在许多专业照片编辑或社交网络和照片共享应用中轻松找到我们在这章中学到的算法的痕迹。
在下一章中,我们将学习所有关于直方图的内容,包括它们的计算方式以及它们在计算机视觉中的应用。我们将涵盖在即将到来的章节中探索的某些对象检测和跟踪算法中至关重要的算法。
问题
-
编写一个程序,在整张图像上绘制一个红色、3 像素粗的十字标记。
-
创建一个带有滑块的窗口,用于更改
medianBlur函数的ksize值。kszise值的可能范围应在 3 到 99 之间。 -
对图像执行梯度形态学操作,考虑结构元素的核大小为 7,矩形形态学形状。
-
使用
cvtColor将彩色图像转换为灰度图,并确保使用threshold函数过滤掉最暗的 100 种灰色调。确保过滤后的像素在结果图像中设置为白色,其余像素设置为黑色。 -
使用
remap函数将图像调整为其原始宽度和高度的一半,从而保留原始图像的宽高比。使用默认的边界类型进行外推。 -
a) 使用颜色图将图像转换为灰度图。b) 将图像转换为灰度图并同时反转其像素。
-
你读过关于透视变换函数的内容吗?哪个 OpenCV 函数覆盖了所有类似的变换在一个单一函数中?
6134

被折叠的 条评论
为什么被折叠?



