定制开发电商网站建设多少钱,建设手机网站经验分享,172分销系统,东营网络科技有限公司原文#xff1a;OpenCV2 Computer Vision Application Programming Cookbook 协议#xff1a;CC BY-NC-SA 4.0 译者#xff1a;飞龙 本文来自【ApacheCN 计算机视觉 译文集】#xff0c;采用译后编辑#xff08;MTPE#xff09;流程来尽可能提升效率。 当别人说你没有底线… 原文OpenCV2 Computer Vision Application Programming Cookbook 协议CC BY-NC-SA 4.0 译者飞龙 本文来自【ApacheCN 计算机视觉 译文集】采用译后编辑MTPE流程来尽可能提升效率。 当别人说你没有底线的时候你最好真的没有当别人说你做过某些事的时候你也最好真的做过。 一、玩转图像
在本章中我们将介绍
安装 OpenCV 库使用 MS Visual C 创建 OpenCV 项目用 Qt 创建一个 OpenCV 项目加载显示和保存图像使用 Qt 创建 GUI 应用
简介
本章将教您 OpenCV 的基本元素并向您展示如何完成最基本的任务读取显示和保存图像。 在开始使用 OpenCV 之前需要安装该库。 这是一个简单的过程将在本章的第一部分中进行说明。
您还需要一个良好的开发环境IDE来运行您的 OpenCV 应用。 我们在这里提出两种选择。 首先是使用众所周知的 Microsoft Visual Studio 平台。 第二种选择是使用称为 Qt 的开源工具进行 C 项目开发。 两个秘诀将向您展示如何使用这两个工具来设置项目但是您也可以使用其他 C IDE。 实际上在本手册中将以与任何特定环境和操作系统无关的方式来呈现任务因此您可以自由使用所选择的一种。 但是请注意您需要使用适合您所使用的编译器和操作系统的 OpenCV 库的已编译版本。 如果您获得奇怪的行为或者您的应用崩溃而没有明显原因则可能是不兼容的症状。
安装 OpenCV 库
OpenCV 是用于开发计算机视觉应用的开源库。 根据 BSD 许可它可以在学术和商业应用中使用允许您自由使用分发和改编它。 此秘籍将向您展示如何在计算机上安装该库。
准备
当您访问 OpenCV 官方网站时您将找到该库的最新版本在线文档以及有关以下内容的许多其他有用资源 OpenCV。
操作步骤
在 OpenCV 网站上转到与您选择的平台Linux/Unix/Mac 或 Windows相对应的“下载”页面。 从那里您将能够下载 OpenCV 包。 然后通常在名称与库版本相对应的目录下例如OpenCV2.2将其解压缩。 完成此操作后您将找到目录的集合尤其是包含 OpenCV 文档的doc目录包含所有包含文件的include目录包含所有源文件的modules目录是的它是开源的以及samples目录包含许多小示例可帮助您入门。
如果您在 Windows 下使用 Visual Studio则还可以选择下载与您的 IDE 和 Windows 平台相对应的可执行安装包。 执行此安装程序不仅会安装源库还将安装构建应用所需的所有预编译二进制文件。 在这种情况下您准备开始使用 OpenCV。 如果没有您需要采取一些其他步骤。
为了在您选择的环境下使用 OpenCV您需要使用适当的 C 编译器生成库二进制文件。 要构建 OpenCV您需要使用这个页面上提供的 CMake 工具。 CMake 是另一个开源软件工具旨在使用与平台无关的配置文件来控制软件系统的编译过程。 因此您需要下载并安装 CMake。 然后您可以使用命令行来运行它但是使用带有其图形用户界面GUI的 CMake 更容易。 在后一种情况下您需要指定包含 OpenCV 库的文件夹以及将包含二进制文件的文件夹。 然后单击配置以选择所需的编译器此处选择了 Visual Studio 2010然后再次单击配置如以下屏幕快照所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1OcHKtO9-1681873909538)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_01.jpg)]
现在您可以通过单击生成按钮来生成makefiles和工作区文件。 这些文件将允许您编译库。 这是安装过程的最后一步。
编译该库将使其可用于您的开发环境。 如果您选择了像 Visual Studio 这样的 IDE那么您要做的就是打开 CMake 为您创建的顶级解决方案文件。 然后您发出构建解决方案命令。 在 Unix 环境中您将通过运行make utility命令使用生成的makefiles。
如果一切顺利现在应该在指定目录中拥有已编译且随时可用的 OpenCV 库。 除了我们已经提到的目录之外该目录还将包含一个bin目录其中包含已编译的库。 您可以将所有内容移动到首选位置例如c:\OpenCV2.2然后将bin目录添加到系统路径在 Windows 下这是通过打开控制面板完成的。 HTG5]系统工具在高级选项卡下您会找到环境变量按钮。
工作原理
从 2.2 版开始OpenCV 库分为几个模块。 这些模块内置在lib目录中的库文件中。 他们是
opencv_core模块包含库的核心功能尤其是基本数据结构和算术功能。包含主要图像处理功能的opencv_imgproc模块。opencv_highgui模块包含图像和视频读写功能以及其他用户界面功能。opencv_features2d模块包含特征点检测器和描述符以及特征点匹配框架。opencv_calib3d模块包含相机校准两视图几何估计和立体功能。opencv_video模块包含运动估计特征跟踪以及前景提取功能和类。opencv_objdetect模块包含诸如面部和人物检测器之类的对象检测功能。
该库还包括其他工具模块其中包含机器学习功能opencv_ml 计算几何算法opencv_flann 贡献代码opencv_contrib 过时代码opencv_legacy 和 gpu 加速代码opencv_gpu 。
所有这些模块都有与之关联的头文件位于include目录中。 因此典型的 OpenCV C 代码将从包含所需的模块开始。 例如这是建议的声明样式
#include opencv2/core/core.hpp
#include opencv2/imgproc/imgproc.hpp
#include opencv2/highgui/highgui.hpp如果您看到以以下内容开头的 OpenCV 代码 #include cv.h这是因为在将库重构为模块之前它使用的是旧样式。
更多
您还可以访问位于以下位置的 OpenCV SVN 服务器正在开发的最新代码。
您会发现有很多示例可以帮助您学习如何使用该库并提供许多开发技巧。
使用 MS Visual C 创建 OpenCV 项目
使用 MS Visual C您可以轻松地为 Windows 创建 OpenCV 应用。 您可以构建简单的控制台应用也可以使用漂亮的图形用户界面GUI创建更复杂的应用。 由于这是最简单的选项因此我们将在此处创建一个简单的控制台应用。 我们将使用 Visual Studio 2010但是相同的原理也适用于 Microsoft IDE 的任何其他版本因为不同版本中的菜单和选项非常相似。
首次运行 Visual Studio 时可以采用某种方式进行设置以使 C 成为您的默认开发环境。 这样当您启动 IDE 时它将处于 Visual C 模式。
我们假定您已按照先前秘籍中的说明在C:\OpenCV2.2目录下安装了 OpenCV。
准备
使用 Visual Studio 时了解解决方案和项目之间的区别很重要。 基本上解决方案由多个项目组成每个项目是一个不同的软件模块例如程序和库。 这样您的解决方案的项目可以共享文件和库。 通常您为解决方案创建一个主目录其中包含所有项目目录。 但是您也可以将解决方案和项目分组到一个目录中。 这是一个项目解决方案中最常做的事情。 随着您对 VC 的熟悉和构建更复杂的应用您应该利用多项目解决方案结构。
另外在编译和执行 Visual C 项目时可以在两种不同的配置下进行Debug 和 Release。 调试模式可以帮助您创建和调试应用。 这是一个受保护程度更高的环境例如它将告诉您应用是否包含内存泄漏或者它将在运行时检查是否正确使用了某些功能。 但是它生成速度较慢的可执行文件。 这就是为什么一旦您的应用经过测试并准备好使用后便可以在“发布”模式下构建它。 这将产生可执行文件您将分发给应用的用户。 请注意您可能会在调试模式下完美运行代码但在发布模式下却遇到问题。 然后您需要进行更多测试以找出潜在的错误来源。 调试和发布模式并非 Visual C 独有大多数 IDE 也支持这两种编译模式。
操作步骤
现在我们准备创建我们的第一个项目。 这是通过使用文件 | 项目… | 新建项目菜单选项来完成的。 您可以在此处创建不同的项目类型。 让我们从最简单的选项开始即选择一个 Win32 控制台应用如以下屏幕快照所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NBvMX6vQ-1681873909539)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_02.jpg)]
您需要指定要在何处创建项目以及要为项目指定的名称。 还有一个选项可以为解决方案创建或不创建目录右下角的复选框。 如果选中此选项将创建一个其他目录使用您指定的名称该目录将包含您的解决方案目录。 如果您只是简单地取消选中此选项则仍将创建一个解决方案文件扩展名.sln但此文件将包含在同一单个项目目录中。 单击下一步然后单击转到 Win32 应用向导的应用设置窗口。 如以下屏幕截图所示那里提供了许多选项。 我们将简单地创建一个空项目。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yVtY8CJS-1681873909539)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_03.jpg)]
请注意我们还没有选中预编译头选项该选项是 MS Visual Studio 特定的功能可以使编译过程更快。 由于我们希望保留在 ANSI C 标准之内因此我们将不使用此选项。 如果单击完成则将创建您的项目。 它暂时是空的但是我们很快会添加一个主文件。
但是首先为了能够编译和运行您将来的 OpenCV 应用您需要告诉 Visual C 在哪里可以找到 OpenCV 库并包含文件。 由于将来可能会创建多个 OpenCV 项目因此最好的选择是创建一个属性表您可以在项目之间重复使用。 这是通过属性管理器完成的。 如果在当前的 IDE 中尚不可见则可以从视图菜单中访问它。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYWgWRcN-1681873909540)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_04.jpg)]
在 Visual C 2010 中属性页是描述您的项目设置的 XML 文件。 现在我们将通过右键单击调试 | 创建一个新的 Win32 项目的节点并选择添加新项目属性表选项如以下屏幕截图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WG98jAHD-1681873909540)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_05.jpg)]
一旦我们单击添加就会添加新的属性表。 现在我们需要对其进行编辑。 只需双击属性表的名称然后选择 VC 目录如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CO3DW0rc-1681873909540)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_06.jpg)]
编辑包含目录文本字段并将路径添加到 OpenCV 库的包含文件中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-coFk9e9l-1681873909541)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_07.jpg)]
对库目录执行相同的操作。 这次您将路径添加到 OpenCV 库文件中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zezzB1gi-1681873909541)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_08.jpg)]
重要的是要注意我们在属性表中使用了 OpenCV 库的显式路径。 通常使用环境变量来指定库位置是一种更好的做法。 这样如果切换到库的另一个版本则只需更改此变量的定义使其指向库的新位置。 同样对于团队项目不同的用户可能已将库安装在不同的位置。 使用环境变量将避免需要为每个用户编辑属性表。 因此如果将环境变量OPENCV2_DIR定义为c:\OpenCV2.2则两个 OpenCV 目录将在属性表中指定为$(OPENCV_DIR)\include和$(OPENCV_DIR)\lib。
下一步是指定 OpenCV 库文件这些文件需要与您的代码链接才能生成可执行应用。 根据应用您可能需要不同的 OpenCV 模块。 由于我们要在所有项目中重用此属性表因此我们将仅添加运行本书的应用所需的库模块。 转到链接器节点的输入项目如以下屏幕截图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B3PFhMph-1681873909541)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_09.jpg)]
编辑其他依赖项文本字段并添加以下库模块列表
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H3IMTRxV-1681873909541)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_10.jpg)]
请注意我们指定的库名称以字母d结尾。 这些是“调试”模式的二进制文件。 您将需要为发布模式创建另一个几乎相同的属性表。 您遵循相同的步骤但是将其添加到发布 | Win32 节点。 这次指定了库名但没有在末尾添加d。
现在我们准备创建编译和运行我们的第一个应用。 我们使用解决方案资源管理器添加新的源文件然后右键单击源文件节点。 您选择添加新项目…这使您有机会指定main.cpp作为此 C 文件的名称
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bsV9XAgK-1681873909542)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_11.jpg)]
您也可以使用**文件 | 新建 | 文件…**菜单选项来执行此操作。 现在让我们构建一个简单的应用它将在默认目录下显示名为img.jpg的图像。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GPRfVD7E-1681873909542)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_12.jpg)]
一旦复制了上图中的代码将在后面进行说明就可以编译它并使用屏幕顶部工具栏中的启动绿色箭头来运行它。 您将看到图像显示五秒钟。 这里有一个例子
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KyvBjWLa-1681873909542)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_13.jpg)]
如果是这样那么您已经完成了第一个成功的 OpenCV 应用 如果程序在执行时失败则可能是因为找不到图像文件。 请参阅以下部分以了解如何将其放置在正确的目录中。
工作原理
当您单击启动调试按钮或按F5时将编译您的项目然后执行。 您还可以通过选择构建菜单下的构建解决方案F7来编译项目。 第一次编译项目时将创建一个Debug目录。 这将包含可执行文件扩展名.exe。 同样您也可以通过使用绿色箭头按钮右侧的下拉菜单或使用构建菜单下的选项配置管理器简单地选择发布管理选项来创建发行版本。 然后将创建一个Release目录。
当使用 Visual Studio 的启动按钮执行项目时默认目录将始终是包含解决方案文件的目录。 但是如果您通过双击.exe文件通常为Release目录选择在 IDE 外部即从 Windows 资源管理器执行应用则默认目录将变为一个包含可执行文件的目录。 因此在执行此应用之前请确保图像文件位于相应目录中的 。
另见
本章后面的“加载显示和保存图像”秘籍解释了我们在此任务中使用的 OpenCV 源代码。
使用 Qt 创建 OpenCV 项目
Qt 是用于 C 应用的完整集成开发环境IDE最初由挪威软件公司 Trolltech 开发该公司于 2008 年被诺基亚收购。 它是根据 LPGL 开源许可以及商业和付费许可开发专有项目而提供的。 它由两个独立的元素组成一个称为 Qt Creator 的跨平台 IDE以及一组 Qt 类库和开发工具。 使用 Qt 软件开发工具包SDK开发 C 应用有很多好处
这是 Qt 社区开发的一个开放源代码计划使您可以访问不同 Qt 组件的源代码。它是跨平台的这意味着您可以开发可在不同操作系统例如 WindowsLinuxMac OS X 等上运行的应用。它包括一个完整的跨平台 GUI 库该库遵循有效的面向对象和事件驱动的模型。Qt 还包括多个跨平台库用于开发多媒体图形数据库多线程Web 应用以及许多其他对设计高级应用有用的有趣构建基块。
准备
可以从这个页面下载 Qt。 如果选择 LPGL 许可证则它是免费的。 您应该下载完整的 SDK。 但是请确保选择适合您平台的 Qt 库包。 显然由于我们正在处理开源软件因此始终可以在您选择的平台下重新编译该库。
在这里我们使用 Qt Creator 1.2.1 版和 Qt 4.6.3 版。 请注意在 Qt Creator 的项目选项卡下可以管理可能已安装的不同 Qt 版本。 这样可以确保您始终可以使用适当的 Qt 版本来编译项目。
操作步骤
启动 Qt 时它将询问您是否要创建一个新项目或是否要打开一个新项目。 您也可以通过在文件菜单下并选择**新建…**选项来创建新项目。 要复制我们在上一个秘籍中所做的操作我们将选择Qt4 Console Application 如以下屏幕截图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XMbss9G4-1681873909542)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_14.jpg)]
然后您可以指定一个名称和一个项目位置如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6qdhcxAr-1681873909543)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_15.jpg)]
以下屏幕将要求您选择要包含在项目中的模块。 只需保持默认状态为选中状态然后单击下一步然后完成即可。 然后创建一个空的控制台应用如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-c5TeLoZC-1681873909543)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_16.jpg)]
Qt 生成的代码创建QCoreApplication对象并调用其exec()方法。 仅当您的应用需要事件处理器来处理与 GUI 的用户交互时才需要这样做。 在我们简单的打开和显示图像示例中这不是必需的。 我们可以简单地将生成的代码替换为上一个任务中使用的代码。 然后简单的打开并显示图像程序将如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cDlVQybu-1681873909543)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_17.jpg)]
为了能够编译该程序需要指定 OpenCV 库文件和头文件的位置。 使用 Qt此信息在项目文件扩展名为.pro中给出该文件是描述项目参数的简单文本文件。 您可以通过选择相应的项目文件在 Qt Creator 中编辑此项目文件如以下屏幕截图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-miu3OOaG-1681873909544)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_18.jpg)]
通过在项目文件的末尾添加以下行来提供构建 OpenCV 应用所需的信息
INCLUDEPATH C:\OpenCV2.2\include\LIBS -LC:\OpenCV2.2\lib \
-lopencv_core220 \
-lopencv_highgui220 \
-lopencv_imgproc220 \
-lopencv_features2d220 \
-lopencv_calib3d220提示
下载示例代码
您可以从这个页面下载从帐户购买的所有 Packt 图书的示例代码文件。 如果您在其他地方购买了此书则可以访问这个页面并注册以将文件直接通过电子邮件发送给您。
现在可以准备编译和执行程序了。 通过单击左下绿色箭头或通过按Ctrl R来完成此操作。 您还可以使用项目选项卡的构建选项来设置调试和发布模式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nKyp4A49-1681873909544)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_19.jpg)]
工作原理
Qt 项目由项目文件描述。 这是一个文本文件其中声明了一个变量列表其中包含构建项目所需的相关信息。 实际上此文件由名为qmake的软件工具处理Qt 在请求编译时会调用该工具。 项目文件中定义的每个变量都与值列表关联。 Qt 中 Qmake 可以识别的主要变量如下
TEMPLATE 定义项目的类型应用库等。CONFIG 指定编译器在构建项目时应使用的不同选项。HEADERS 列出项目的头文件。SOURCES 列出项目的源文件.cpp。QT 声明所需的 Qt 扩展模块和库。 默认情况下包括核心和 GUI 模块。 如果要排除其中之一请使用-表示法。INCLUDEPATH 指定应搜索的头文件目录。LIBS 包含应与项目链接的库文件列表。 您将标志–L用于目录路径并将标志–l用于库名称。
还定义了其他几个变量但是最常用的是此处列出的变量。
更多
Qmake 项目文件中可以使用许多其他功能。 例如可以定义范围以添加适用于特定平台的声明
win32 {# declarations for Windows 32 platforms only
}
unix {# declarations for Unix 32 platforms only
}您也可以使用pkg-config工具包。 它是一个开源工具可帮助使用正确的编译器选项和库文件。 当使用 CMake 安装 OpenCV 时unix-install包含一个opencv.pc文件该文件由pkg-config读取以确定编译参数。 一个多平台的 Qmake 项目文件可以如下所示
unix { CONFIG link_pkgconfigPKGCONFIG opencv
}Win32 {INCLUDEPATH C:\OpenCV2.2\include\LIBS -LC:\OpenCV2.2\lib \
-lopencv_core220 \-lopencv_highgui220 \-lopencv_imgproc220 \-lopencv_features2d220 \-lopencv_calib3d220
}另见
下一个秘籍“加载显示和保存图像”解释了我们在此任务中使用的 OpenCV 源代码。
有关 QtQt Creator 和所有 Qt 扩展模块的完整文档请访问网站。
加载显示和保存图像
前面的两个秘籍教您如何创建一个简单的 OpenCV 项目但是我们没有解释所使用的 OpenCV 代码。 该任务将向您展示如何执行 OpenCV 应用开发中所需的最基本的操作。 这些步骤包括从文件加载输入图像在窗口上显示图像以及将输出图像存储在磁盘上。
准备
使用 MS Visual Studio 或 Qt创建一个具有准备就绪的main函数的新控制台应用。 有关如何进行的操作请参见前两个秘籍。
操作步骤
首先要做的是声明一个将保留图像的变量。 在 OpenCV 2 下您定义了cv::Mat类的对象。
cv::Mat image;此定义创建大小为 0 乘 0 的图像。这可以通过调用cv::Mat方法size()进行确认该方法允许您读取此图像的当前大小。 它返回一个包含图像高度和宽度的结构
std::cout size: image.size().height , image.size().width std::endl;接下来对读取函数的简单调用将从文件读取图像对其进行解码然后分配内存
image cv::imread(img.jpg);现在您可以使用该图像了。 但是您应该首先检查是否已正确读取图像如果找不到文件文件损坏或不是可识别的格式则会发生错误。 图像的有效性通过以下方式测试
if (!image.data) { // no image has been created...
}成员变量data实际上是指向将包含图像数据的已分配存储块的指针。 当未读取图像时将其简单地设置为 0。 您可能要对此图像进行的第一件事是显示它。 您可以使用 OpenCV 提供的highgui模块来执行此操作。 首先声明要在其上显示图像的窗口然后指定要在此特殊窗口上显示的图像
cv::namedWindow(Original Image); // define the window
cv::imshow(Original Image, image); // show the image现在您通常将对图像进行一些处理。 OpenCV 提供了多种处理功能本书中将探讨其中的一些功能。 让我们从一个非常简单的例子开始它将简单地水平翻转图像。 OpenCV 中的几种图像转换可以在原地执行这意味着该转换直接应用于输入图像不创建新图像。 翻转方法就是这种情况。 但是我们总是可以创建另一个矩阵来保存输出结果这就是我们要做的
cv::Mat result;
cv::flip(image,result,1); // positive for horizontal// 0 for vertical, // negative for both结果显示在另一个窗口上
cv::namedWindow(Output Image);
cv::imshow(Output Image, result);由于它是一个控制台窗口将在main函数的结尾处终止因此我们添加了一个额外的highgui方法以等待用户键然后再结束程序
cv::waitKey(0);然后您可以在两个不同的窗口中看到输入和输出图像。 最后您可能希望将处理后的图像保存在磁盘上。 使用以下highgui函数可以完成此操作
cv::imwrite(output.bmp, result);文件扩展名确定将使用哪个编解码器保存图像。
工作原理
OpenCV 的 C API 中定义的所有类和函数都在名称空间cv中定义。 您有两种选择来访问它们。 首先在主函数定义之前添加以下声明
using namespace cv;或者像在本秘籍中一样在所有的 OpenCV 类名和函数名前加上cv::命名空间规范。
类cv::Mat是用于保存图像显然还有其他矩阵数据的数据结构。 默认情况下它们的大小为零但您也可以指定初始大小
cv::Mat ima(240,320,CV_8U,cv::Scalar(100));在这种情况下您还需要指定每个矩阵元素的类型此处CV_8U对应于 1 字节像素图像。 字母U表示未签名。 您也可以使用字母S声明带符号的数字。 对于彩色图像您将指定三个通道CV_8UC3。 您还可以声明大小为 16 和 32 的整数有符号或无符号例如CV_16SC3。 您还可以访问 32 位和 64 位浮点数例如CV_32F。
当cv::Mat对象超出范围时分配的内存将自动释放。 这非常方便因为可以避免出现内存泄漏问题。 此外cv::Mat类实现引用计数和浅表复制以便在将图像分配给另一图像时不复制图像数据即像素并且两个图像都指向同一存储块。 这也适用于按值传递或按值返回的图像。 保留引用计数以便仅在销毁对图像的所有引用时才释放内存。 如果您希望创建一个包含原始图像新副本的图像则可以使用copyTo()方法。 您可以通过在此项目的示例中声明一些额外的图像来测试此行为如下所示
cv::Mat image2, image3;
image2 result; // the two images refer to the same data
result.copyTo(image3); // a new copy is created现在如果再次翻转输出图像并显示另外两个图像您将看到image2也受到转换的影响因为它指向的图像数据与结果图像相同而图像的副本image3保持不变。 cv::Mat对象的此分配模型还意味着您可以安全地编写返回图像的函数或类方法
cv::Mat function() {// create imagecv::Mat ima(240,320,CV_8U,cv::Scalar(100));// return itreturn ima;
}如果我们从main函数中调用此函数 // get a gray-level imagecv::Mat gray function();gray变量现在将保留该函数创建的图像而无需分配额外的内存。 确实只有图像的浅表副本将从返回的cv::Mat实例传输到灰度图像。 当ima局部变量超出范围时将取消分配此变量但是由于关联的参考计数器指示其内部图像数据正在被另一个实例即gray变量引用因此其内存块没有被释放。
但是对于类应该小心不要返回图像类属性。 这是一个容易出错的实现示例
class Test {// image attributecv::Mat ima;public:// constructor creating a gray-level imageTest() : ima(240,320,CV_8U,cv::Scalar(100)) {}// method return a class attribute, not a good idea...cv::Mat method() { return ima; }
};在这里如果函数调用此类的方法则它将获得图像属性的浅表副本。 如果以后再修改此副本则class属性也将被修改这可能会影响该类的后续行为反之亦然。 为避免此类错误您应该返回属性的副本。
更多
在 OpenCV 的版本 2 中引入了新的 C 接口。 以前使用并且仍然可以使用类似 C 的函数和结构。 特别地使用IplImage结构来操纵图像。 该结构继承自 IPL 库即英特尔图像处理库该库现已与 IPP 库英特尔集成性能原始库集成在一起。 如果使用通过旧 C 接口创建的代码和库则可能需要操纵这些IplImage结构。 幸运的是有一种方便的方法可以将IplImage转换为cv::Mat对象。
IplImage* iplImage cvLoadImage(c:\\img.jpg);
cv::Mat image4(iplImage,false);函数cvLoadImage是用于加载图像的 C 接口函数。 cv::Mat对象的构造器中的第二个参数指示该数据将不会被复制如果想要新的副本请将其设置为true而false是默认值因此可以将其省略 IplImage和image4将共享相同的图像数据。 您需要在此处小心不要创建悬空的指针。 因此将IplImage指针封装到 OpenCV 2 提供的引用计数指针类中更为安全
cv::PtrIplImage iplImage cvLoadImage(c:\\img.jpg);否则如果您需要释放由IplImage结构指向的内存则需要明确地执行以下操作 cvReleaseImage(iplImage);请记住您应该避免使用此过时的数据结构。 相反请始终使用cv::Mat。
使用 Qt 创建 GUI 应用
Qt 提供了丰富的库来构建具有专业外观的复杂 GUI。 使用 Qt CreatorGUI 创建过程变得很容易。 此秘籍将向您展示如何使用 Qt 构建 OpenCV 应用用户可以使用 GUI 进行控制。
准备
启动 Qt Creator我们将使用它来创建 GUI 应用。 也可以不使用此工具来创建 GUI但是使用可视化 IDE可在其中轻松拖放小部件是构建美观 GUI 的最简单方法。
操作步骤
选择创建新项目…然后选择 Qt GUI 应用如以下屏幕快照所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DS2V0ohe-1681873909544)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_20.jpg)]
给您的项目命名和位置。 如果然后单击下一步您将看到已选中 QtGUI 模块。 由于我们不需要其他模块因此您现在可以单击完成。 这将创建您的新项目。 除了常用的项目文件.pro和main.cpp文件之外您还会看到两个mainwindow文件它们定义了包含 GUI 窗口的类。 您还将找到一个扩展名为.ui的文件该文件描述了 UI 布局。 实际上如果双击它将会看到当前的用户界面如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IJqK5TyH-1681873909544)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_21.jpg)]
您可以在上面拖放不同的小部件。 像前面的示例一样放下两个按钮。 您可以调整它们的大小并调整窗口的大小以使其美观。 您还应该重命名按钮标签。 只需单击文本然后插入您选择的名称。
现在我们添加一个信号方法来处理单击按钮事件。 右键单击第一个按钮然后在上下文菜单中选择转到插槽…。 然后显示可能的信号列表如以下屏幕快照所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mfB5DG4T-1681873909544)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_22.jpg)]
只需选择clicked()信号即可。 这是处理按钮按下事件的事件。 这样您将被带到mainwindow.cpp文件。 您将看到已添加了新方法。 这是在收到click()信号时调用的时隙方法
#include mainwindow.h
#include ui_mainwindow.h
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent), ui(new Ui::MainWindow)
{ui-setupUi(this);
}
MainWindow::~MainWindow()
{delete ui;
}
void MainWindow::on_pushButton_clicked()
{
}
为了能够显示然后处理图像我们需要定义一个cv::Mat类成员变量。 这是在MainWindow类类的头文件中完成的。 现在此标头的内容如下
#ifndef MAINWINDOW_H
#define MAINWINDOW_H#include QtGui/QMainWindow
#include QFileDialog
#include opencv2/core/core.hpp
#include opencv2/highgui/highgui.hppnamespace Ui
{class MainWindow;
}
class MainWindow : public QMainWindow
{Q_OBJECT
public:MainWindow(QWidget *parent 0);~MainWindow();
private:Ui::MainWindow *ui;cv::Mat image; // the image variable
private slots:void on_pushButton_clicked();
};#endif // MAINWINDOW_H请注意我们还包括了core.hpp和highgui.hpp头文件。 正如我们在前面的秘籍中所了解的那样我们一定不要忘记编辑项目文件以附加 OpenCV 库信息。
然后可以添加 OpenCV 代码。 第一个按钮打开源图像。 这是通过将以下代码添加到相应的插槽方法来完成的
void MainWindow::on_pushButton_clicked()
{QString fileName QFileDialog::getOpenFileName(this,tr(Open Image), ., tr(Image Files (*.png *.jpg *.jpeg *.bmp)));image cv::imread(fileName.toAscii().data());cv::namedWindow(Original Image);cv::imshow(Original Image, image);
}然后通过右键单击第二个按钮来创建新的插槽。 第二个插槽将对所选输入图像执行一些处理。 以下代码将简单地翻转图像
void MainWindow::on_pushButton_2_clicked()
{cv::flip(image,image,1);cv::namedWindow(Output Image);cv::imshow(Output Image, image);
}现在您可以编译并运行该程序您的 2 键 GUI 将允许您选择图像并进行处理。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AGBAQrbx-1681873909550)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_23.jpg)]
输入和输出图像显示在我们定义的两个highgui窗口上。
工作原理
在 Qt 的 GUI 编程框架下对象使用信号和插槽进行通信。 每当窗口小部件更改状态或发生事件时都会发出信号。 该信号具有预定义的签名如果另一个对象想要接收该信号则它必须定义一个具有相同签名的插槽。 因此插槽是一种特殊的类方法当它所连接的信号发出时会自动调用。
信号和插槽被定义为类方法但必须在指定插槽和信号的 Qt 访问下声明。 当您在按钮上添加插槽时这就是 Qt Creator 所做的即
private slots:void on_pushButton_clicked();信号和插槽是松散耦合的也就是说信号不知道与连接插槽的对象有关的任何信息而插槽也不知道是否连接了信号。 同样许多插槽可以连接到一个信号并且一个插槽可以接收来自许多物体的信号。 唯一的要求是信号的签名和时隙方法必须匹配。
从QObject类继承的所有类都可以包含信号和插槽。 这些通常是小部件类的子类QWidget的子类但是任何其他类都可以定义插槽和信号。 实际上信号和时隙概念是一种非常强大的类通信机制。 但是它特定于 Qt 框架。
在 Qt 中主窗口是类MainWindow的实例。 您可以通过在MainWindow类定义中声明的成员变量ui来访问它。 另外GUI 的每个小部件也是一个对象。 创建 GUI 时指向您已添加到主窗口的每个小部件实例的指针与ui变量相关联。 因此您可以访问程序中每个窗口小部件的属性和方法。 例如如果要在选择输入图像之前禁用处理按钮则您需要做的就是在 GUI 初始化时在MainWindow构造器中调用以下方法。
ui-pushButton_2-setEnabled(false);指针变量pushbutton_2在此对应于处理按钮。 然后当成功加载图像时您可以启用按钮在打开图像按钮中
if (image.data) {ui-pushButton_2-setEnabled(true);
}还值得注意的是在 Qt 下GUI 的布局在 XML 文件中已完全描述。 这是带有.ui扩展名的文件。 如果进入项目目录并使用文本编辑器打开.ui文件则将能够读取该文件的 XML 内容。 定义了几个 XML 标签。 在此秘籍中介绍的示例应用的情况下您将找到两个定义为QPushButton的窗口小部件类标记。 名称与这些窗口小部件类的标记关联该名称与附加到ui对象的指针变量的名称相对应。 其中的每一个都定义了描述其位置和大小的几何属性。 还定义了许多其他属性标签。 Qt Creator 有一个属性选项卡显示每个小部件的属性值。 因此即使 Qt Creator 是创建 GUI 的最佳工具您也可以编辑.ui XML 文件来创建和修改 GUI。
更多
使用 Qt在 GUI 上直接显示图像相对容易。 您需要做的就是在窗口中添加一个标签对象。 然后将图像分配给该标签以显示该图像。 请记住您可以通过ui指针在我们的示例中为ui-label的相应指针属性访问标签实例。 但是此图像必须是QImage类型即处理图像的 Qt 数据结构。 转换相对简单只是需要反转三个颜色通道的顺序从cv::Mat中的 BGR 到QImage中的 RGB。 我们可以使用cv::cvtColor函数。 然后我们简单的 GUI 应用的处理按钮可以更改为
void MainWindow::on_pushButton_2_clicked()
{cv::flip(image,image,1); // process the image// change color channel orderingcv::cvtColor(image,image,CV_BGR2RGB); // Qt imageQImage img QImage((const unsigned char*)(image.data), image.cols,image.rows,QImage::Format_RGB888);// display on labelui-label-setPixmap(QPixmap::fromImage(img)); // resize the label to fit the imageui-label-resize(ui-label-pixmap()-size());
}结果现在将输出图像直接显示在 GUI 上如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Kp4EmE6p-1681873909550)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_01_24.jpg)]
另见
有关 Qt GUI 模块以及信号和插槽机制的更多信息请查阅位于这个页面的在线 Qt 文档。
二、操纵像素
在本章中我们将介绍
访问像素值用指针扫描图像使用迭代器扫描图像编写有效的图像扫描循环使用邻居访问扫描图像执行简单的图像算术定义兴趣区域
简介
为了构建计算机视觉应用您必须能够访问图像内容并最终修改或创建图像。 本章将教您如何操作图像元素又称像素。 您将学习如何扫描图像并处理其每个像素。 您还将学习如何有效地执行此操作因为即使尺寸适中的图像也可能包含数万个像素。
从根本上讲图像是数值矩阵。 这就是为什么 OpenCV 2 使用cv::Mat数据结构来操作它们的原因。 矩阵的每个元素代表一个像素。 对于灰度图像“黑白”图像像素为无符号的 8 位值其中 0 对应于黑色而 255 对应于白色。 对于彩色图像每个像素需要三个这样的值才能代表通常的三个原色通道红绿蓝。 因此在这种情况下矩阵元素由值的三元组组成。
如上一章所述OpenCV 还允许您创建具有不同类型例如整数CV_8U和浮点数[CV_32F的像素值的矩阵或图像。 这些对于在某些图像处理任务中存储例如中间值非常有用。 大多数操作可以应用于任何类型的矩阵其他操作则需要特定类型的矩阵或者仅适用于给定数量的通道。 因此对函数或方法的先决条件有充分的了解对于避免常见的编程错误至关重要。
在本章中我们使用以下彩色图像作为输入请参见本书的网站以彩色方式查看该图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0QG7GNoT-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_01.jpg)]
访问像素值
为了访问矩阵的每个单独元素您只需要指定其行号和列号即可。 将返回对应的元素在多通道图像的情况下该元素可以是单个数值或值的向量。
准备
为了说明对像素值的直接访问我们将创建一个简单的函数在图像中添加椒盐噪声。 顾名思义椒盐噪声是一种特殊类型的噪声其中某些像素被白色或黑色像素代替。 当某些像素的值在传输过程中丢失时这种类型的噪声可能会出现在错误的通信中。 在我们的例子中我们将简单地随机选择一些像素并将其分配为白色。
操作步骤
我们创建一个接收输入图像的函数。 这是将由我们的函数修改的图像。 为此我们使用了传递引用机制。 第二个参数是我们要覆盖白色值的像素数
void salt(cv::Mat image, int n) {for (int k0; kn; k) {// rand() is the MFC random number generator// try qrand() with Qtint i rand()%image.cols;int j rand()%image.rows;if (image.channels() 1) { // gray-level imageimage.atuchar(j,i) 255; } else if (image.channels() 3) { // color imageimage.atcv::Vec3b(j,i)[0] 255; image.atcv::Vec3b(j,i)[1] 255; image.atcv::Vec3b(j,i)[2] 255; }}
}该函数由单个循环组成该循环将n乘以255值乘以随机选择的像素。 在此使用随机数生成器选择像素列i和行j。 请注意我们通过检查与每个像素关联的通道数来区分灰度图像和彩色图像的两种情况。 在灰度图像的情况下将的数字255分配给单个 8 位值。 对于彩色图像需要为三个原色通道分配255以获得白色像素。
您可以通过向其传递先前打开的图像来调用此函数 // open the imagecv::Mat image cv::imread(boldt.jpg);// call function to add noisesalt(image,3000);// display imagecv::namedWindow(Image);cv::imshow(Image,image);生成的图像如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6SrciH3a-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_02.jpg)]
工作原理
类cv::Mat包括几种访问图像不同属性的方法。 公共成员变量cols和rows为您提供图像中的列数和行数。 对于元素访问cv::Mat具有方法at(int y, int x)。 但是必须在编译时知道方法返回的类型并且由于cv::Mat可以保存任何类型的元素因此程序员需要指定期望的返回类型。 这就是at方法已被实现为模板方法的原因。 因此在调用它时必须指定图像元素类型如下所示 image.atuchar(j,i) 255; 重要的是要注意确保指定的类型与矩阵中包含的类型匹配是程序员的责任。 at方法不执行任何类型转换。
在彩色图像中每个像素与三个分量相关联红色绿色和蓝色通道。 因此包含彩色图像的cv::Mat将返回三个 8 位值的向量。 OpenCV 具有针对此类短向量的定义类型称为cv::Vec3b。 它是 3 个unsigned char的向量。 这就解释了为什么元素访问彩色像素的像素写为 image.atcv::Vec3b(j,i)[channel] value; 索引channel指定三个颜色通道之一。
2 元素和 4 元素向量cv::Vec2b和cv::Vec4b以及其他元素类型也存在类似的向量类型。 在此后一种情况下最后一个字母由short的sint的ifloat的f和double的d替换。 所有这些类型都是使用模板类cv::VecT,N定义的其中T是类型N是向量元素的数量。
更多
使用cv::Mat类的at方法有时会很麻烦因为必须为每个调用将返回的类型指定为模板参数。 在已知矩阵类型的情况下可以使用cv::Mat_类它是cv::Mat的模板子类。 此类定义了一些其他方法但没有新的数据属性因此可以将指向一个类的指针或引用直接转换为另一个类。 在其他方法中operator()允许直接访问矩阵元素。 因此如果image是对uchar矩阵的引用则可以编写 cv::Mat_uchar im2 image; // im2 refers to imageim2(50,100) 0; // access to row 50 and column 100由于cv::Mat_元素的类型是在创建变量时声明的因此operator()方法在编译时就知道要返回哪种类型。 除了编写时间短之外使用operator()方法可提供与at方法完全相同的结果。
另见
“编写高效的图像扫描循环”秘籍可讨论此方法的效率。
用指针扫描图像
在大多数图像处理任务中需要扫描图像的所有像素才能执行计算。 考虑到将需要访问的大量像素以有效的方式执行此任务至关重要。 本秘籍以及下一篇秘籍将向您展示实现图像扫描循环的不同方法。 该秘籍使用指针算法。
准备
我们将通过完成一个简单的任务来说明图像扫描过程减少图像中的颜色数量。
彩色图像由 3 通道像素组成。 这些通道中的每一个对应于三种原色红色绿色蓝色之一的强度值。 由于这些值均为 8 位unsigned char因此颜色总数为256x256x256超过 1600 万种颜色。 因此为减少分析的复杂性有时减少图像中的颜色数量很有用。 一种简单的方法可以将 RGB 空间细分为相等大小的多维数据集。 例如如果将每个尺寸的颜色数量减少 8那么您将获得总共32x32x32的颜色。 然后原始图像中的每种颜色在色彩缩减图像中被分配一个新的颜色值该值对应于其所属的多维数据集中心的值。
因此基本的色彩缩减算法很简单。 如果N是缩小因子则对于图像中的每个像素以及该像素的每个通道将值除以N整数除法因此会丢失提示。 然后将结果乘以N这将为您提供N在输入像素值以下的倍数。 只需加N / 2即可获得N的两个相邻倍数之间的间隔的中心位置。如果对每个 8 位通道值重复此过程则总共将获得256 / N x 256 / N x 256 / N可能的颜色值。
操作步骤
我们的色彩缩减函数的签名如下
void colorReduce(cv::Mat image, int div64);用户提供图像和每个通道的缩小系数。 此处在原位中完成**处理即通过该函数修改了输入图像的像素值。 请参见“本秘籍的更多内容”部分提供了具有输入和输出参数的更通用的函数签名。
通过创建遍历所有像素值的双循环即可简单地完成处理
void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of lines// total number of elements per lineint nc image.cols * image.channels(); for (int j0; jnl; j) {// get the address of row juchar* data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ---------------------data[i] data[i]/div*div div/2;// end of pixel processing ----------------} // end of line }
}可以使用以下代码片段测试此函数 // read the imageimage cv::imread(boldt.jpg);// process the imagecolorReduce(image);// display the imagecv::namedWindow(Image);cv::imshow(Image,image);例如这将为您提供以下图像请参见本书的网站以彩色查看此图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8BdOaPy-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_03.jpg)]
工作原理
在彩色图像中图像数据缓冲区的前 3 个字节给出左上像素的 3 个颜色通道值接下来的 3 个字节是第一行第二个像素的值依此类推请注意OpenCV 使用 默认情况下BGR 通道顺序因此蓝色通常是第一个通道。 宽度为W且高度为H的图像将需要WxHx3个uchar的存储块。 但是出于效率原因可以用很少的额外像素来填充行的长度。 这是因为某些多媒体处理器芯片例如 Intel MMX 架构在行数为 4 或 8 的倍数时可以更有效地处理图像。 值将被忽略。 OpenCV 将填充行的长度指定为关键字。 显然如果未用多余像素填充图像则有效宽度将等于实际图像宽度。 数据属性cols为您提供图像宽度即列数属性rows为您提供图像高度而step数据属性为您提供有效宽度。 字节数。 即使您的图像不是uchar的类型step仍会为您提供连续的字节数。 像素元素的大小由方法elemSize给出例如对于 3 通道短整数矩阵CV_16SC3elemSize将返回 6。 图像中的通道数由nchannels方法给出对于灰度图像为 1对于彩色图像为 3。 最后方法total返回矩阵中像素的总数即矩阵项。
然后每行的像素值数量由下式给出 int nc image.cols * image.channels(); 为了简化指针算术的计算cv::Mat类提供了一种直接为您提供图像行地址的方法。 这是ptr方法。 这是一个模板方法返回行号j的地址 uchar* data image.ptruchar(j);注意在处理语句中我们可以等效地使用指针算法在列之间移动。 所以我们可以这样写 *data *data/div*div div2;更多
本秘籍中介绍的色彩缩减函数仅提供完成此任务的一种方法。 人们还可以使用其他色彩缩减公式。 该函数的更通用版本也将允许指定不同的输入和输出图像。 通过考虑图像数据的连续性还可以使图像扫描更有效。 最后也可以使用常规的低级指针算法来扫描图像缓冲区。 以下各小节将讨论所有这些元素。
其他颜色缩减秘籍
在我们的示例中通过利用整数除法来实现色彩缩减该整数除法将除法结果取整为最接近的较低整数 data[i] data[i]/div*div div/2;还可以使用模运算符计算出减少的颜色该运算符将我们带到div的最接近倍数1D 减少因子 data[i] data[i] – data[i]%div div/2;但是此计算要慢一些因为它需要两次读取每个像素值。
另一种选择是使用按位运算符。 确实如果我们将缩减因子限制为 2 的幂即divpow(2,n)则屏蔽像素值的前n位将为我们提供div的最接近的较低倍数。 该掩码可以通过简单的移位来计算 // mask used to round the pixel valueuchar mask 0xFFn; // e.g. for div16, mask 0xF0颜色减少将通过以下方式给出 data[i] (data[i]mask) div/2;通常按位运算会导致非常高效的代码因此当需要效率时它们可以构成强大的替代方案。
具有输入和输出参数
在我们的色彩缩减示例中该变换直接应用于输入图像这称为原地变换。 这样不需要额外的图像来保存输出结果这在需要时可以节省内存使用。 但是在某些应用中用户希望保持原始图像不变。 然后在调用该函数之前将迫使用户创建图像的副本。 请注意创建图像的相同深层副本的最简单方法是调用clone方法例如 // read the imageimage cv::imread(boldt.jpg);// clone the imagecv::Mat imageClone image.clone();// process the clone// orginal image remains untouchedcolorReduce(imageClone);// display the image resultcv::namedWindow(Image Result);cv::imshow(Image Result,imageClone);通过定义一个向用户提供使用或不使用原地处理选项的函数可以避免这种额外的过载。 该方法的签名将是
void colorReduce(const cv::Mat image, // input image cv::Mat result, // output imageint div64);请注意现在将输入图像作为const引用传递这意味着该图像不会被该函数修改。 如果首选原地处理则将同一图像指定为输入和输出
colorReduce(image,image);如果没有则可以提供另一个cv::Mat实例例如
cv::Mat result;
colorReduce(image,result);此处的关键是首先验证输出图像是否具有分配的数据缓冲区该缓冲区的大小和像素类型与输入图像的大小和像素类型匹配。 非常方便的是此检查封装在[HTG1]的create方法中。 这是必须使用新的大小和类型重新分配矩阵时使用的方法。 如果偶然地矩阵已经具有指定的大小和类型则不执行任何操作并且该方法仅返回而无需接触实例。 因此我们的函数应该仅从对create的调用开始该调用将构建与输入图像大小和类型相同的矩阵如有必要 result.create(image.rows,image.cols,image.type());请注意create始终创建连续图像即没有填充的图像。 分配的内存块的大小为total()*elemSize() 。然后使用两个指针完成循环 for (int j0; jnl; j) {// get the addresses of input and output row jconst uchar* data_in image.ptruchar(j);uchar* data_out result.ptruchar(j);for (int i0; inc; i) {// process each pixel ---------------------data_out[i] data_in[i]/div*div div/2;// end of pixel processing ----------------} // end of line 在提供相同图像作为输入和输出的情况下此函数变得完全等同于本秘籍中介绍的第一个版本。 如果提供另一个图像作为输出则该函数将正常运行而不管函数调用之前是否分配了该图像。
高效扫描连续图像
前面我们曾解释过出于效率的考虑可以在每行的末尾用额外的像素填充图像。 但是有趣的是当未填充图像时可以将图像视为WxH像素的长一维数组。 方便的cv::Mat方法可以告诉我们是否已填充图像。 如果图像不包含填充像素则isContinuous方法返回true。
在某些特定的处理算法中可以通过在一个较长循环中处理图像来利用图像的连续性。 然后我们的处理函数将编写如下
void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of linesint nc image.cols * image.channels(); if (image.isContinuous()) {// then no padded pixelsnc nc*nl; nl 1; // it is now a 1D array}// this loop is executed only once// in case of continuous imagesfor (int j0; jnl; j) { uchar* data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ---------------------data[i] data[i]/div*div div/2;// end of pixel processing ----------------} // end of line }
}现在当连续性测试告诉我们图像不包含填充像素时我们通过将宽度设置为 1 并将高度设置为WxH来消除外部循环。 注意这里还可以使用reshape方法。 在这种情况下您将编写以下内容 if (image.isContinuous()) {// no padded pixelsimage.reshape(1, // new number of channelsimage.cols*image.rows) ; // new number of rows}int nl image.rows; // number of linesint nc image.cols * image.channels(); 方法reshape无需任何内存复制或重新分配即可更改矩阵尺寸。 第一个参数是新的通道数第二个参数是新的行数。 列数会相应调整。
在这些实现中内部循环按顺序处理所有图像像素。 当将几个小图像同时扫描到同一循环中时此方法特别有利。
低级指针算法
在cv::Mat类中图像数据包含在unsigned char的存储块中。 该存储块第一个元素的地址由data属性给定该属性返回一个无符号的char指针。 因此要在图像的开头开始循环您可以编写
uchar *data image.data;通过使用有效宽度移动行指针可以完成从一行到另一行的移动
data image.step; // next linestep方法为您提供一行中的字节总数包括填充的像素。 通常您可以按以下方式获取行j和列i的像素地址
// address of pixel at (j,i) that is image.at(j,i)
data image.dataj*image.stepi*image.elemSize(); 但是即使这在我们的示例中可行也不建议以这种方式进行。 除了容易出错外这种方法也不适用于兴趣区域。 本章末尾讨论了兴趣区域。
另见
“编写高效的图像扫描循环”秘籍用于讨论此处介绍的扫描方法的效率。
使用迭代器扫描图像
在面向对象的编程中通常使用迭代器完成对数据集合的循环。 迭代器是专门构建的类用于遍历集合的每个元素隐藏了如何针对给定的集合专门对每个元素进行迭代。 信息隐藏原理的这种应用使扫描集合变得更加容易。 此外无论使用哪种类型的集合它的形式都相似。 标准模板库STL具有与其每个集合类关联的迭代器类。 然后OpenCV 提供一个cv::Mat迭代器类该类与 C STL 中的标准迭代器兼容。
准备
在此秘籍中我们再次使用先前秘籍中描述的色彩缩减示例。
操作步骤
可以通过首先创建cv::MatIterator_对象来获得cv::Mat实例的迭代器对象。 与cv::Mat_子类的情况一样下划线表示这是模板方法。 实际上由于使用了图像迭代器来访问图像元素因此必须在编译时就知道返回类型。 然后将迭代器声明如下 cv::MatIterator_cv::Vec3b it;另外您还可以使用Mat_模板类中定义的iterator类型 cv::Mat_cv::Vec3b::iterator it;然后您可以使用常规的begin和end迭代器方法遍历像素但这些方法又是模板方法。 因此我们的色彩缩减函数现在编写如下
void colorReduce(cv::Mat image, int div64) {// obtain iterator at initial positioncv::Mat_cv::Vec3b::iterator it image.begincv::Vec3b();// obtain end positioncv::Mat_cv::Vec3b::iterator itend image.endcv::Vec3b();// loop over all pixelsfor ( ; it! itend; it) {// process each pixel ---------------------(*it)[0] (*it)[0]/div*div div/2;(*it)[1] (*it)[1]/div*div div/2;(*it)[2] (*it)[2]/div*div div/2;// end of pixel processing ----------------}
}请记住这里的迭代器返回cv::Vec3b因为我们正在处理彩色图像。 使用解引用operator[]访问每个颜色通道元素。
工作原理
使用迭代器无论扫描哪种集合都始终遵循相同的模式。
首先使用适当的专用类在我们的示例中为cv::Mat_cv::Vec3b::iterator或cv::MatIterator_cv::Vec3b创建迭代器对象。
然后您将获得一个在起始位置在我们的示例中为图像的左上角初始化的迭代器。 这是使用begin方法完成的。 对于cv::Mat实例您将其获取为image.begincv::Vec3b()。 您还可以在迭代器上使用算术。 例如如果您希望从图像的第二行开始则可以在image.begincv::Vec3b()image.rows处初始化cv::Mat迭代器。 可以使用end方法类似地获得收藏的结束位置。 但是如此获得的迭代器就在您的集合之外。 这就是为什么您的迭代过程到达最终位置时必须停止的原因。 您还可以在此迭代器上使用算术例如如果希望在最后一行之前停止则最终迭代将在迭代器达到image.endcv::Vec3b()-image.rows时停止。
初始化迭代器后您将创建一个遍历所有元素的循环直到到达末尾为止。 典型的while循环如下所示 while (it! itend) {// process each pixel ---------------------...// end of pixel processing ----------------it;}operator是用于移至下一个元素的那个。 您还可以指定更大的步长。 例如it10将每 10 个像素处理一次。
最后在处理循环内部使用解引用operator*来访问当前元素您可以使用该元素读取例如element *it;或写入例如*it element;。 请注意如果收到对const cv::Mat的引用或者希望表示当前循环不修改cv::Mat实例则也可以创建使用的常量迭代器。 这些声明如下 cv::MatConstIterator_cv::Vec3b it;或者 cv::Mat_cv::Vec3b::const_iterator it;更多
在此秘籍中使用模板方法begin和end获得迭代器的开始和结束位置。 就像我们在本章第一章中所做的那样我们也可以使用对cv::Mat_实例的引用来获得它们。 这样可以避免在begin和end方法中指定迭代器类型的需要因为在创建cv::Mat_引用时就指定了该迭代器类型。 cv::Mat_cv::Vec3b cimage image;cv::Mat_cv::Vec3b::iterator it cimage.begin();cv::Mat_cv::Vec3b::iterator itend cimage.end();另见
“编写高效的图像扫描循环”秘籍讨论了扫描图像时迭代器的效率。
另外如果您不熟悉面向对象编程中迭代器的概念以及如何在 ANSI C 中实现迭代器则应阅读有关 STL 迭代器的教程。 您只需用关键字“STL 迭代器”在网络上搜索就可以找到许多关于该主题的参考。
编写有效的图像扫描循环
在本章的先前秘籍中我们介绍了扫描图像以处理其像素的不同方法。 在本秘籍中我们将比较这些不同方法的效率。
当您编写图像处理函数时效率通常是一个问题。 在设计函数时经常需要检查代码的计算效率以发现可能会减慢程序速度的任何瓶颈。
但是必须注意的是除非有必要否则不应以降低程序清晰度为代价进行优化。 简单的代码的确总是更容易调试和维护。 只有对程序效率至关重要的代码部分才应进行严重优化。
操作步骤
为了测量一个函数或部分代码的执行时间存在一个非常方便的称为cv::getTickCount()的 OpenCV 函数。 此函数为您提供自上次启动计算机以来发生的时钟周期数。 由于我们希望以毫秒为单位给出代码部分的执行时间因此我们使用了另一种方法cv::getTickFrequency() 。 这给了我们每秒的循环数。 为了获得给定函数或部分代码的计算时间而使用的常用模式将是
double duration;
duration static_castdouble(cv::getTickCount());colorReduce(image); // the function to be testedduration static_castdouble(cv::getTickCount())-duration;
duration / cv::getTickFrequency(); // the elapsed time in ms持续时间结果应在函数的多次调用中取平均值。
在colorReduce函数的测试中我们还实现了使用at方法进行像素访问的函数版本。 然后此实现的主循环将读为 for (int j0; jnl; j) {for (int i0; inc; i) {// process each pixel ---------------------image.atcv::Vec3b(j,i)[0]image.atcv::Vec3b(j,i)[0]/div*div div/2;image.atcv::Vec3b(j,i)[1] image.atcv::Vec3b(j,i)[1]/div*div div/2;image.atcv::Vec3b(j,i)[2] image.atcv::Vec3b(j,i)[2]/div*div div/2;// end of pixel processing ----------------} // end of line }工作原理
在此报告本章中colorReduce函数的不同实现的执行时间。 一台机器的绝对运行时数会有所不同这里我们使用的是奔腾双核 2.2GHz。 看看它们的相对差异是很有趣的。 我们的测试报告减少分辨率为4288x2848像素的图像的颜色所需的平均时间。 下表中汇总了结果并在下面进行了讨论
方法平均时间data[i] data[i]/div*div div/2 ;37ms*data *data/div*div div/2;37ms*data v - v%div div/2;52ms*data *datamask div/2;35mscolorReduce(input, output);44msiimage.cols*image.channels();65msMatIterator67ms.at(j,i)80ms3-channel loop29ms
首先我们比较通过指针扫描图像的“更多内容”部分中介绍的三种计算色彩缩减的方法第 1-4 行。不出所料使用按位运算符的版本最快执行时间为35ms。 使用整数除法的版本取37ms而取模的版本取52ms。 最快与最慢之间相差近 50% 因此重要的是要花一些时间来确定在图像循环中计算结果的最有效方法因为净影响可能非常显着。 注意当指定需要重新分配的输出图像而不是原地处理第 5 行时执行时间变为44ms。 额外的持续时间代表内存分配的开销。
在循环中应避免重复计算可能会预先计算的值。 这显然会浪费时间。 例如如果您替换颜色减少函数的以下内部循环 int nc image.cols * image.channels(); ...for (int i0; inc; i) {与此 for (int i0; iimage.cols * image.channels(); i) {那是一个循环您需要一次又一次地计算一行中的元素总数。 您将获得65ms的运行时比35ms的原始版本第 6 行慢 80%。
使用迭代器第 7 行的色彩缩减函数版本如秘籍“使用迭代器扫描图像”所示在67ms处的结果较慢。 迭代器的主要目的是简化图像扫描过程并减少出错的可能性。 不一定要优化此过程。
使用上一节末尾介绍的at方法的实现要慢得多第 8 行。 获得80ms的运行时。 然后应将这种方法用于图像像素的随机访问但在扫描图像时绝对不要使用。
即使处理的元素总数相同使用较少语句的较短循环通常比使用单个语句的较长循环更有效地执行。 同样如果您要对一个像素应用N个不同的计算请全部执行一个循环而不要编写N个连续的循环每次计算一次。 然后您应该偏爱循环在较长的循环中进行更多的工作而较长的循环会减少计算量。 举例来说我们可以处理内部循环中的所有三个通道并在列数上进行迭代而不是使用原始版本其中循环遍历元素总数即像素数的 3 倍 。 然后将颜色减少函数编写如下这是最快的版本
void colorReduce(cv::Mat image, int div64) {int nl image.rows; // number of linesint nc image.cols ; // number of columns// is it a continous image?if (image.isContinuous()) {// then no padded pixelsnc nc*nl; nl 1; // it is now a 1D array}int n static_castint(log(static_castdouble(div))/log(2.0));// mask used to round the pixel valueuchar mask 0xFFn; // e.g. for div16, mask 0xF0// for all pixels for (int j0; jnl; j) {// pointer to first column of line juchar* data image.ptruchar(j);for (int i0; inc; i) {// process each pixel ---------------------*data *datamask div/2;*data *datamask div/2;*data *datamask div/2;// end of pixel processing ----------------} // end of line }
}通过此修改执行时间现在为29ms第 9 行。 我们还添加了连续性测试该连续性测试在连续图像的情况下会产生一个循环而不是对行和列进行常规的双循环。 对于非常大的图像如我们在测试中使用的图像这种优化并不重要但总的来说使用此策略始终是一种很好的做法因为它可以大大提高速度。
更多
多线程是提高算法效率的另一种方法尤其是自多核处理器问世以来。 OpenMP 和英特尔线程构建模块TBB是在并发编程中用于创建和管理线程的两种流行的 API。
另见
看看“执行简单图像算术”秘籍了解使用 OpenCV 2 算术图像运算符的色彩缩减方法的实现。
使用邻居访问扫描图像
在图像处理中通常具有基于相邻像素的值来计算每个像素位置处的值的处理函数。 当该邻域包含上一行和下一行的像素时则需要同时扫描图像的几行。 此秘籍向您展示如何做。
准备
为了说明这一秘籍我们将应用处理函数以使图像清晰。 它基于拉普拉斯算子将在第 6 章中进行讨论。 在图像处理中确实是众所周知的结果如果从图像中减去其拉普拉斯算子则会放大图像边缘从而获得更清晰的图像。 该锐化运算符的计算如下
sharpened_pixel 5*current-left-right-up-down;其中left是当前像素左侧的像素up是前一行对应的像素依此类推。
操作步骤
这次处理无法原地完成。 用户需要提供输出图像。 图像扫描是通过使用三个指针完成的一个指针用于当前行一个指针用于上一行另一个指针用于下一行。 另外由于每个像素计算都需要访问相邻像素因此无法为图像的第一行和最后一行的像素以及第一列和最后一列的像素计算值。 然后可以将循环编写如下
void sharpen(const cv::Mat image, cv::Mat result) {// allocate if necessaryresult.create(image.size(), image.type()); for (int j 1; jimage.rows-1; j) { // for all rows // (except first and last)const uchar* previous image.ptrconst uchar(j-1); // previous rowconst uchar* current image.ptrconst uchar(j); // current rowconst uchar* next image.ptrconst uchar(j1); // next rowuchar* output result.ptruchar(j); // output rowfor (int i1; iimage.cols-1; i) {*output cv::saturate_castuchar(5*current[i]-current[i-1]-current[i1]-previous[i]-next[i]); }}// Set the unprocess pixels to 0result.row(0).setTo(cv::Scalar(0));result.row(result.rows-1).setTo(cv::Scalar(0));result.col(0).setTo(cv::Scalar(0));result.col(result.cols-1).setTo(cv::Scalar(0));
}如果我们将此函数应用于测试图像的灰度版本则会获得以下示例
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BKmM6Y8I-1681873909551)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_04.jpg)]
工作原理
为了访问上一行和下一行的相邻像素必须简单定义共同增加的其他指针。 然后您可以在扫描循环中访问这些行的像素。
在输出像素值的计算中对运算结果调用模板函数cv::saturate_cast。 这是因为经常发生这样的情况对像素应用数学表达式会导致结果超出允许的像素值范围小于 0 或大于 255。 然后的解决方案是恢复该 8 位范围内的值。 这是通过将负值更改为 0 并将值更改为 255 至 255 来完成的。这正是cv::saturate_castuchar函数所做的。 此外如果输入参数是浮点数则结果将四舍五入到最接近的整数。 您显然可以将此函数与其他类型一起使用以确保结果将保持在此类型定义的范围内。
由于邻域未完全定义而无法处理的边界像素需要单独处理。 在这里我们将它们简单地设置为 0。在其他情况下可以对这些像素执行一些特殊的计算但是在大多数情况下花费时间来处理这些很少的像素是没有意义的。 在我们的函数中使用两种特殊方法将这些边界像素设置为 0。 第一个是row及其对偶的col。 它们返回一个特殊的cv::Mat实例该实例由参数中指定的单行或单列组成。 这里没有进行复制因为如果修改此一维矩阵的元素它们也将在原始图像中被修改。 这就是调用方法setTo时所做的事情。 此方法为矩阵的所有元素分配一个值。 因此声明 result.row(0).setTo(cv::Scalar(0));将值 0 分配给结果图像第一行的所有像素。 对于 3 通道彩色图像可以使用cv::Scalar(a,b,c)指定三个值以分配给像素的每个通道。
更多
当在像素邻域上完成计算时通常用核矩阵表示它。 该核描述了如何将计算中涉及的像素进行组合以获得所需的结果。 对于此秘籍中使用的锐化过滤器核为
0-10-15-10-10
除非另有说明否则当前像素对应于核的中心。 核每个单元中的值代表一个乘以相应像素的因子。 然后将所有这些乘法的总和给出核应用于像素的结果。 核的大小对应于邻域的大小此处为3x3。 使用这种表示法可以看出按照锐化过滤器的要求当前像素的四个水平和垂直邻居都乘以 -1而当前像素的水平和垂直邻居都乘以 5。 除了方便的表示之外它是信号处理中卷积概念的基础。 核定义了应用于图像的过滤器。
由于过滤是图像处理中的常见操作因此 OpenCV 定义了执行此任务的特殊函数 cv::filter2D函数。 要使用它只需定义一个核以矩阵的形式。 然后使用图像和核调用该函数并返回过滤后的图像。 因此使用此函数可以很容易地重新定义锐化函数如下所示
void sharpen2D(const cv::Mat image, cv::Mat result) {// Construct kernel (all entries initialized to 0)cv::Mat kernel(3,3,CV_32F,cv::Scalar(0));// assigns kernel valueskernel.atfloat(1,1) 5.0;kernel.atfloat(0,1) -1.0;kernel.atfloat(2,1) -1.0;kernel.atfloat(1,0) -1.0;kernel.atfloat(1,2) -1.0;//filter the imagecv::filter2D(image,result,image.depth(),kernel);
}此实现产生与上一个完全相同的结果并且具有相同的效率。 但是对于较大的核使用filter2D方法是有利的因为在这种情况下它使用更有效的算法。
另见
第 6 章“过滤图像”对图像过滤的概念进行了更多说明。
执行简单的图像运算
图像可以以不同的方式组合。 由于它们是规则矩阵因此可以相加相减相乘或相除。 OpenCV 提供了各种图像算术运算符本秘籍中将讨论它们的用法。
准备
让我们处理第二个图像使用算术运算符将其合并到输入图像中。 以下是第二张图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J6sygKVt-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_06.jpg)]
操作步骤
在这里我们添加两个图像。 当需要创建一些特殊效果或将信息覆盖在图像上时此函数很有用。 我们通过调用cv::add函数或更精确地说是cv::addWeighted函数来实现此目的因为我们需要加权和即 cv::addWeighted(image1,0.7,image2,0.9,0.,result);该操作将产生一个新图像如以下屏幕截图所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aXmFQC1h-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_07.jpg)]
工作原理
所有二进制算术函数的工作方式均相同。 提供了两个输入第三个参数指定了输出。 在某些情况下可以指定在操作中用作标量乘数的权重。 这些函数中的每一个都有几种风格。 cv::add是多种形式的可用函数的典范 // c[i] a[i]b[i];cv::add(imageA,imageB,resultC); // c[i] a[i]k;cv::add(imageA,cv::Scalar(k),resultC); // c[i] k1*a[1]k2*b[i]k3; cv::addWeighted(imageA,k1,imageB,k2,k3,resultC);// c[i] k*a[1]b[i]; cv::scaleAdd(imageA,k,imageB,resultC);对于某些函数您还可以指定一个掩码 // if (mask[i]) c[i] a[i]b[i]; cv::add(imageA,imageB,resultC,mask); 如果应用遮罩则仅对遮罩值不为null的像素遮罩必须为 1 通道执行该操作。 看看cv::subtractcv::absdiff cv::multiply和cv::divide函数的不同形式。 还可以使用按位运算符cv::bitwise_andcv::bitwise_orcv::bitwise_xor和cv::bitwise_not。 查找每个元素的最大或最小像素值的运算符cv::min和cv::max也非常有用。
在所有情况下始终使用函数cv::saturate_cast请参见前面的秘籍以确保结果保持在定义的像素值域内即避免上溢或下溢。
图像必须具有相同的尺寸和类型如果输出图像与输入尺寸匹配则将重新分配输出图像。 而且由于操作是按元素执行的因此输入图像之一可以用作输出。
也可以使用将单个图像作为输入的几种运算符cv::sqrtcv::powcv::abscv::cuberootcv::exp和cv::log。 实际上几乎所有需要对图像执行的操作都具有 OpenCV 函数。
更多
也可以在cv::Mat实例或cv::Mat实例的各个通道上使用常规的 C 算术运算符。 以下两个小节说明了如何执行此操作。
重载的图像运算符
非常方便的是大多数算术函数在 OpenCV 2 中都有相应的运算符重载。因此对cv::addWeighted的调用可以写为
result 0.7*image10.9*image2;这是一种更紧凑的形式也更易于阅读。 这两种写加权总和的方法是等效的。 特别是在两种情况下函数cv::saturate_cast仍将被调用。
大多数 C 运算符已被重载。 其中按位运算符 |, ^~minmax和abs函数比较运算符和! 这些后来返回一个 8 位二进制图像。 您还会发现矩阵乘法m1*m2其中m1和m2都是cv::Mat实例矩阵求逆m1.inv()转置m1.t()行列式m1.determinant()向量范数v1.norm() 叉积v1.cross(v2)点积v1.dot(v2)等。 在这种情况下您还可以定义op运算符例如。
在“编写高效的图像扫描循环”秘籍中我们提出了一种色彩缩减函数该函数是通过使用循环扫描图像像素以对其执行一些算术运算而编写的。 根据我们在这里学到的知识可以使用输入图像上的算术运算符简单地重写此函数即 image(imagecv::Scalar(mask,mask,mask))cv::Scalar(div/2,div/2,div/2);cv::Scalar的使用是由于我们正在处理彩色图像。 执行与在“编写高效的图像扫描循环”秘籍中所做的相同测试我们获得89ms的执行时间。 这主要是因为如所写该表达式需要调用两个函数按位与和标量和而不是在一个图像循环内执行完整的操作。 即使生成的代码并非始终是最佳的使用图像运算符也使代码如此简单并且程序员如此高效以至于在大多数情况下都应考虑使用它们。
分割图像通道
有时您可能需要独立处理图像的不同通道。 例如您可能只想在图像的一个通道上执行操作。 当然您可以在图像扫描循环中实现此目的。 但是您也可以使用cv::split函数它将彩色图像的三个通道复制到三个不同的cv::Mat实例中。 假设我们只想将雨图像添加到蓝色通道。 以下是我们将如何进行 // create vector of 3 imagesstd::vectorcv::Mat planes;// split 1 3-channel image into 3 1-channel imagescv::split(image1,planes);// add to blue channelplanes[0] image2;// merge the 3 1-channel images into 1 3-channel imagecv::merge(planes,result);cv::merge函数执行双重操作即从三个 1 通道图像创建彩色图像。
定义兴趣区域
有时仅需要在图像的一部分上应用处理函数。 该秘籍将教您如何在图像内定义兴趣区域。
准备
假设我们要组合两个大小不同的图像。 例如假设我们要在测试图像中添加以下小徽标
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w5u0FbJU-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_08.jpg)]
但是函数cv::add需要两张相同大小的图像。 在这种情况下可以定义兴趣区域ROI可以在其上应用cv::add。 只要 ROI 与我们徽标图像的大小相同这将起作用。 ROI 的位置将确定徽标将在图像中插入的位置。
操作步骤
第一步包括定义 ROI。 定义后可以将 ROI 作为常规cv::Mat实例进行操作。 关键是 ROI 指向与其父映像相同的数据缓冲区。 然后将徽标插入如下 // define image ROIcv::Mat imageROI;imageROI image(cv::Rect(385,270,logo.cols,logo.rows));// add logo to image cv::addWeighted(imageROI,1.0,logo,0.3,0.,imageROI);然后获得以下图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-how2Bpky-1681873909552)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_09.jpg)]
由于徽标的颜色已添加到图像的颜色中还可能应用了饱和度因此视觉效果将不总是令人满意的。 因此最好将图像的像素值简单地设置为该图像出现的徽标值。 为此您可以使用遮罩将徽标复制到 ROI // define ROIimageROI image(cv::Rect(385,270,logo.cols,logo.rows));// load the mask (must be gray-level)cv::Mat mask cv::imread(logo.bmp,0);// copy to ROI with masklogo.copyTo(imageROI,mask);然后结果图像为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-inL8eUmC-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_02_10.jpg)]
工作原理
定义 ROI 的一种方法是使用cv::Rect实例。 顾名思义它通过指定左上角的位置构造器的前两个参数和矩形的大小后两个参数给出的宽度和高度来描述矩形区域。
还可以使用行和列范围来描述 ROI。 范围是从开始索引到结束索引的连续序列不包括在内。 cv::Range结构用于表示此概念。 因此可以从两个范围定义 ROI例如在我们的示例中ROI 可以等效地定义如下
cv::Mat imageROI image(cv::Range(270,270logo.rows), cv::Range(385,385logo.cols))cv::Mat的operator()返回另一个cv::Mat实例该实例随后可用于子序列调用中。 ROI 的任何变换都会影响相应区域中的原始图像因为图像和 ROI 共享相同的图像数据。 由于 ROI 的定义不会复制数据因此无论 ROI 的大小如何它都将在固定时间内执行。
如果要定义由图像的某些行组成的 ROI可以使用以下调用
cv::Mat imageROI image.rowRange(start,end) ;同样对于由某些图像列组成的 ROI
cv::Mat imageROI image.colRange(start,end) ;秘籍“使用访问邻居扫描图像”中使用的方法row和col是这些后来方法的特殊情况其中开始索引和结束索引相等以便定义一个在线或单列 ROI。
三、使用类处理图像
在本章中我们将介绍
在算法设计中使用策略模式使用控制器与处理模块通信使用单例设计模式使用模型-视图-控制器架构设计应用转换色彩空间
简介
好的计算机视觉程序始于好的编程习惯。 构建无错误的应用仅仅是开始。 您真正想要的是一个应用您和与您一起工作的程序员将能够轻松适应新需求的发展。本章将向您展示如何充分利用一些面向对象的编程原理以便建立高质量的软件程序。 特别是我们将介绍一些重要的设计模式这些模式将帮助您构建由易于测试维护和重用的组件组成的应用。
设计模式是软件工程中众所周知的概念。 基本上设计模式是对软件设计中经常出现的一般性问题的一种可重复使用的合理解决方案。 已经引入了许多软件模式并有据可查。 好的程序员应该对这些现有模式有一定的了解。
本章还有第二个目标。 它将教您如何使用图像颜色。 本章中使用的示例将向您展示如何检测给定颜色的像素最后的秘籍将说明如何使用不同的颜色空间。
在策略设计中使用策略模式
策略设计模式的目标是将算法封装到一个类中。 这样将给定算法替换为另一个算法或将多个算法链接在一起以构建更复杂的过程变得更加容易。 另外该模式通过将尽可能多的复杂性隐藏在直观的编程接口后面从而促进了算法的部署。
准备
假设我们要构建一种简单的算法该算法将识别图像中具有给定颜色的所有像素。 然后算法必须接受图像和颜色作为输入并返回显示具有指定颜色的像素的二进制图像。 我们希望接受颜色的容差将是运行算法之前要指定的另一个参数。
操作步骤
该算法的核心过程很容易构建。 这是一个遍历每个像素的简单扫描循环将其颜色与目标颜色进行比较。 使用我们在上一章中学到的知识该循环可以写为 // get the iteratorscv::Mat_cv::Vec3b::const_iterator itimage.begincv::Vec3b();cv::Mat_cv::Vec3b::const_iterator itendimage.endcv::Vec3b();cv::Mat_uchar::iterator itout result.beginuchar();// for each pixelfor ( ; it! itend; it, itout) {// process each pixel ---------------------// compute distance from target colorif (getDistance(*it)minDist) {*itout 255;} else {*itout 0;}// end of pixel processing ----------------}cv::Mat变量image表示输入图像而result表示二进制输出图像。 因此第一步包括设置所需的迭代器。 这样即可轻松实现扫描for循环。 每次迭代都会检查当前像素颜色和目标颜色之间的距离是否在minDist定义的公差范围内。 如果是这种情况则将值255白色分配给输出图像如果不是则分配0黑色。 要计算两个颜色值之间的距离请使用getDistance方法。 有多种计算此距离的方法。 例如可以计算包含 RGB 颜色值的 3 个向量之间的欧式距离。 在我们的案例中为使计算简单有效我们简单地将 RGB 值的绝对差求和也称为城市街区距离。 getDistance方法的定义如下 // Computes the distance from target color.int getDistance(const cv::Vec3b color) const {return abs(color[0]-target[0])abs(color[1]-target[1])abs(color[2]-target[2]);}请注意我们如何使用cv::Vec3d来保存代表颜色的 RGB 值的三个unsigned chars。 变量target显然是指指定的目标颜色正如我们将要看到的它在我们定义的类算法中被定义为类变量。 现在让我们完成处理方法的定义。 用户将提供输入图像图像扫描完成后将返回结果
cv::Mat ColorDetector::process(const cv::Mat image) {// re-allocate binary map if necessary// same size as input image, but 1-channelresult.create(image.rows,image.cols,CV_8U);*processing loop above goes here*...return result;
}每次调用此方法时检查是否需要重新分配包含结果二进制映射的输出图像以适合输入图像的大小这一点很重要。 这就是为什么我们使用cv::Mat的create方法。 请记住只有在指定的大小和深度与当前图像结构不符时该图像才会继续进行重新分配。
现在我们已经定义了核心处理方法让我们看看应该添加哪些其他方法来部署此算法。 先前我们确定了算法需要哪些输入和输出数据。 因此我们首先定义将保存此数据的类属性
class ColorDetector {private:// minimum acceptable distanceint minDist; // target colorcv::Vec3b target; // image containing resulting binary mapcv::Mat result;为了创建封装我们的算法的类的实例并命名为ColorDetector我们需要定义一个构造器。 请记住策略设计模式的目标之一是使算法部署尽可能容易。 可以定义的最简单的构造器是一个空的构造器。 它将在有效状态下创建类算法的实例。 然后我们希望构造器将所有输入参数初始化为其默认值或通常能带来良好结果的已知值。 在我们的案例中我们认为距离 100 通常是可以接受的公差。 我们还设置了默认的目标颜色。 我们没有特殊原因选择黑色。 目的是确保我们始终从可预测和有效的输入值开始 // empty constructorColorDetector() : minDist(100) { // default parameter initialization heretarget[0] target[1] target[2] 0;}此时创建我们的类算法实例的用户可以立即使用有效图像调用process方法并获得有效输出。 这是“策略”模式的另一个目标即确保算法始终以有效参数运行。 显然此类的用户将想要使用自己的设置。 这是通过为用户提供适当的获取器和获取器来完成的。 让我们从颜色容差参数开始 // Sets the color distance threshold.// Threshold must be positive, // otherwise distance threshold is set to 0.void setColorDistanceThreshold(int distance) {if (distance0)distance0;minDist distance;}// Gets the color distance thresholdint getColorDistanceThreshold() const {return minDist;}注意我们如何首先检查输入的有效性。 同样这是为了确保我们的算法永远不会在无效状态下运行。 可以类似地设置目标颜色 // Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {// BGR ordertarget[2] red;target[1] green;target[0] blue;}// Sets the color to be detectedvoid setTargetColor(cv::Vec3b color) {target color;}// Gets the color to be detectedcv::Vec3b getTargetColor() const {return target;}这次有趣的是我们为用户提供了setTagertColor方法的两个定义。 在第一个版本中将三个颜色分量指定为三个参数而在第二个版本中cv::Vec3b用于保存颜色值。 同样目标是促进使用我们的类算法。 用户只需选择最适合需求的安装员即可。
工作原理
一旦使用策略设计模式将算法封装到一个类中就可以通过创建此类的实例来进行部署。 通常实例将在程序初始化时创建。 可以读取和显示算法参数的默认值。 对于具有 GUI 的应用可以使用不同的小部件文本字段滑块等读取和设置参数值以便用户可以轻松地使用它们。 但是在介绍 GUI 之前这将在本章后面完成让我们首先编写一个简单的main函数该函数将运行我们的颜色检测算法
int main()
{// 1\. Create image processor objectColorDetector cdetect;// 2\. Read input imagecv::Mat image cv::imread(boldt.jpg);if (!image.data)return 0; // 3\. Set input parameterscdetect.setTargetColor(130,190,230); // here blue skycv::namedWindow(result);// 4\. Process the image and display the resultcv::imshow(result,cdetect.process(image));cv::waitKey();return 0;
}在上一章介绍的彩色图像上运行该程序会产生以下输出
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2KfPYYB1-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_01.jpg)]
显然我们在此类中封装的算法相对简单只有一个扫描循环和一个公差参数。 当要实现的算法更加复杂具有多个步骤并包含多个参数时策略设计模式将变得非常强大。
更多
要计算两个颜色向量之间的距离我们使用以下简单公式
return abs(color[0]-target[0])abs(color[1]-target[1])abs(color[2]-target[2]);但是OpenCV 包含用于计算向量的欧几里得范数的函数。 因此我们可以计算出如下距离
return static_castint(cv::normint,3(cv::Vec3i(color[0]-target[0],color[1]-target[1],color[2]-target[2])));然后使用getDistance方法的此定义将获得非常相似的结果。 在此我们使用cv::Vec3i整数的 3 个向量因为相减的结果是整数值。
从第 2 章回忆起OpenCV 矩阵和向量数据结构包括基本算术运算符的定义这也很有趣。 例如如果要添加两个cv::Vec3i向量a和b并将结果分配给c则可以简单地编写
c ab;或者可以为距离计算提出以下定义
return static_castint(cv::normuchar,3(color-target);乍一看这个定义可能是正确的但是这是错误的。 这是因为所有这些运算符总是包含对saturate_cast的调用请参阅上一章中的秘籍“使用访问邻居扫描图像”以确保结果保持在输入类型的域内此处为uchar。 因此在目标值大于相应颜色值的情况下将分配值 0 而不是预期的负值。
另见
由 A. Alexandrescu 引入的基于策略的类设计是策略设计模式的一个有趣变体其中在编译时选择算法。
Erich Gamma 等人Addison-Wesley于 1994 年出版的《设计模式可重用的面向对象软件的元素》是关于该主题的经典书籍之一。
另请参阅“使用模型-视图-控制器模式”秘籍构建基于 GUI 的应用以了解如何在具有 GUI 的应用中使用策略模式。
使用控制器与处理模块通信
在构建更复杂的应用时您将需要创建可以组合在一起的多种算法以完成一些高级任务。 因此正确设置应用并让所有类一起通信将变得越来越复杂。 这样将应用的控制集中在一个类中就变得很有利。 这是控制器背后的想法。 它是应用中的一个特定对象起着重要的作用我们将在本秘籍中对其进行探讨。
准备
使用两个按钮创建一个基于对话框的简单应用一个按钮用于选择图像另一个按钮用于开始处理如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqP46HGa-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_02.jpg)]
在这里我们使用先前秘籍的ColorDetector类。
操作步骤
控制器的角色是首先创建执行应用所需的类。 在这里它只是一堂课。 另外我们需要两个成员变量以保留对输入和输出结果的引用
class ColorDetectController {private:// the algorithm classColorDetector *cdetect;cv::Mat image; // The image to be processedcv::Mat result; // The image resultpublic:ColorDetectController() { //setting up the applicationcdetect new ColorDetector();}然后您需要定义用户控制应用所需的所有设置器和获取器 // Sets the color distance thresholdvoid setColorDistanceThreshold(int distance) {cdetect-setColorDistanceThreshold(distance);}// Gets the color distance thresholdint getColorDistanceThreshold() const {return cdetect-getColorDistanceThreshold();}// Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {cdetect-setTargetColor(red,green,blue);}// Gets the color to be detectedvoid getTargetColor(unsigned char red, unsigned char green, unsigned char blue) const {cv::Vec3b color cdetect-getTargetColor();red color[2];green color[1];blue color[0];}// Sets the input image. Reads it from file.bool setInputImage(std::string filename) {image cv::imread(filename);if (!image.data)return false;elsereturn true;}// Returns the current input image.const cv::Mat getInputImage() const {return image;}您还需要一种将被调用以启动该过程的方法 // Performs image processing.void process() {result cdetect-process(image);}以及获得处理结果的方法 // Returns the image result from the latest processing.const cv::Mat getLastResult() const {return result;}最后在应用终止并释放控制器时清理所有内容非常重要 // Deletes processor objects created by the controller.~ColorDetectController() {delete cdetect;}工作原理
使用上面的控制器类程序员可以轻松地为将执行算法的应用构建接口。 程序员无需了解所有类如何连接在一起也不必找出必须调用哪个类的方法才能使所有程序正常运行。 这全部由控制器类完成。 唯一的要求是创建该控制器类的实例。
控制器中定义的设置器和获取器是您认为部署算法所需的那些。 这些方法只是在适当的类中调用相应的方法。 同样这里的简单示例仅包含一种类算法但是在大多数情况下将涉及多个类实例。 因此控制器的作用是将请求重定向到适当的类并简化与这些类的接口。 作为这种简化的示例请考虑方法setTargetColor和getTargetColor 。 他们都使用uchar设置并获取感兴趣的颜色。 这消除了应用程序员了解cv::Vec3b类的任何知识。
在某些情况下控制器还准备应用程序员提供的数据。 这是我们在setInputImage方法的情况下所做的其中将与给定文件名相对应的图像加载到内存中。 该方法返回true或false取决于加载操作是否成功也可能引发异常来处理这种情况。
最后方法process是运行该算法的方法。 该方法不返回结果必须调用另一个方法才能获得最新处理结果。
现在要使用此控制器创建一个非常基本的基于对话框的应用只需将ColorDetectController成员变量添加到对话框类此处称为colordetect。 如果是 MFC 对话框则“打开”按钮将如下所示
// Callback method of Open button.
void OnOpen()
{// MFC widget to select a file of type bmp or jpgCFileDialog dlg(TRUE, _T(*.bmp), NULL,OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_HIDEREADONLY,_T(image files (*.bmp; *.jpg) |*.bmp;*.jpg|All Files (*.*)|*.*||),NULL);dlg.m_ofn.lpstrTitle _T(Open Image);// if a filename has been selectedif (dlg.DoModal() IDOK) {// get the path of the selected filenamestd::string filename dlg.GetPathName(); // set and display the input imagecolordetect.setInputImage(filename);cv::imshow(Input Image,colordetect.getInputImage());}
}第二个按钮执行该过程并显示结果
// Callback method of Process button.
void OnProcess()
{// target color is hard-coded herecolordetect.setTargetColor(130,190,230);// process the input image and display resultcolordetect.process();cv::imshow(Output Result,colordetect.getLastResult());
}显然一个更完整的应用将包括其他小部件以允许用户设置算法参数。
另见
另请参见“使用模型视图控制器模式构建基于 GUI 的应用”的秘籍该模式提供了由 GUI 控制的应用的更多扩展示例。
使用单例设计模式
单例是另一种流行的设计模式用于促进对类实例的访问并确保在程序执行期间仅存在该类的一个实例。 在此秘籍中我们使用单例访问控制器对象。
准备
我们使用先前秘籍的ColorDetectController类。 为了获得单例类将对其进行修改。
操作步骤
首先要做的是添加一个私有静态成员变量该变量将保留对单个类实例的引用。 另外为了禁止构造其他类实例将构造器设为私有 class ColorDetectController {private:// pointer to the singletonstatic ColorDetectController *singleton; ColorDetector *cdetect;// private constructorColorDetectController() { //setting up the applicationcdetect new ColorDetector();}此外您还可以将副本构造器和operator设为私有以确保没有人可以创建单例唯一实例的副本。 当类的用户要求此类的实例时将按需创建单例对象。 这可以使用静态方法完成该方法会创建实例如果尚不存在然后返回指向该实例的指针 // Gets access to Singleton instancestatic ColorDetectController *getInstance() {// Creates the instance at first callif (singleton 0)singleton new ColorDetectController;return singleton;}请注意但是单例的此实现不是线程安全的。 因此当并发线程需要访问单例实例时不应使用它。
最后由于单例实例是动态创建的因此用户在不再需要它时必须将其删除。 同样这是通过静态方法完成的 // Releases the singleton instance of this controller.static void destroy() {if (singleton ! 0) {delete singleton;singleton 0;}}由于singleton是静态成员变量因此必须在.cpp文件中定义。 这样做如下
#include colorDetectController.hColorDetectController *ColorDetectController::singleton0; 工作原理
由于可以通过公共静态方法获取单例因此所有包含单例类声明的类都可以访问单例对象。 这对于某些复杂 GUI 的几个小部件类可以访问的控制器对象特别有用。 无需前面的秘籍中的任何一个 GUI 类中的成员变量。 对话框类的两个回调方法将如下编写
// Callback method of Open button.
void OnOpen()
{...// if a filename has beed selectedif (dlg.DoModal() IDOK) {// get the path of the selected filenamestd::string filename dlg.GetPathName(); // set and display the input imageColorDetectController::getInstance()-setInputImage(filename);cv::imshow(Input Image,ColorDetectController::getInstance()-getInputImage());}
}// Callback method of Process button.
OnProcess()
{// target color is hard-coded hereColorDetectController::getInstance()-setTargetColor(130,190,230);// process the input image and display resultColorDetectController::getInstance()-process();cv::imshow(Output Result,ColorDetectController::getInstance()-getLastResult());
}当应用关闭时必须释放单例实例
// Callback method of Close button.
void OnClose()
{// Releases the Singleton.ColorDetectController::getInstance()-destroy();OnOK();
}如此处所示将控制器封装在单例内时从任何类获取对此实例的访问变得更加容易。 但是此应用的更严格实现将需要更精细的 GUI。 这在下一个秘籍中完成该秘籍通过介绍模型-视图-控制器架构总结了在应用设计中使用模式的讨论。
使用模型-视图-控制器架构设计应用
前面的秘籍使您可以发现三种重要的设计模式策略控制器和单例模式。 本秘籍介绍了一种架构模式其中将这三种模式与其他类结合使用。 正是模型视图控制器或 MVC 的目的是产生一个将应用逻辑与用户界面清楚地分开的应用。 在本秘籍中我们将使用 MVC 模式使用 Qt 构建基于 GUI 的应用。 但是在实际操作之前我们先简要介绍一下该模式。
准备
顾名思义MVC 模式包含三个主要组件。 现在我们将看看它们各自的作用。
模型包含有关应用的信息。 它保存了应用处理的所有数据。 产生新数据时它将通知控制器控制器随后将要求视图显示新结果。 通常模型会将几种算法组合在一起可能按照策略模式实现。 所有这些算法都是模型的一部分。
视图对应于用户界面。 它由不同的小部件组成这些小部件将数据呈现给用户并允许用户与应用进行交互。 它的作用之一是将用户发出的命令发送到控制器。 当有新数据可用时它会刷新以显示新信息。
控制器是将视图和模型桥接在一起的模块。 它从视图接收请求并将请求中继到模型中的适当方法。 当模型更改其状态时也会通知它因此要求刷新视图以显示此新信息。
操作步骤
与前面的秘籍一样我们将使用ColorDetector类。 这将是我们的模型其中包含应用逻辑和基础数据。 我们还实现了一个控制器它是ColorDetectController类。 然后通过选择最合适的窗口小部件可以轻松构建更复杂的 GUI。 例如使用 Qt可以构建以下接口
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-toAkzqQN-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_03.jpg)]
打开图像按钮用于选择和打开图像。 可以通过按选择颜色按钮选择要检测的颜色。 这将打开一个颜色选择器小部件下面以黑白打印可轻松选择所需的颜色
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XW7LZjEv-1681873909553)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_04.jpg)]
然后使用滑块选择要使用的正确阈值。 然后通过按处理按钮处理图像并显示结果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4q7xcgCQ-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_03_05.jpg)]
工作原理
在 MVC 架构下用户界面仅调用控制器方法。 它不包含任何应用数据也不实现任何应用逻辑。 因此很容易用另一个接口替换一个接口。 在这里添加了颜色选择器小部件QColorDialog一旦选择了颜色就会从选择颜色插槽中调用适当的控制器方法
QColor color QColorDialog::getColor(Qt::green, this);
if (color.isValid()) {
ColorDetectController::getInstance()-setTargetColor(color.red(),color.green(),color.blue());
}通过QSlider小部件设置阈值。 当单击处理按钮时将读取此值这还将触发处理并显示结果
ColorDetectController::getInstance()-setColorDistanceThreshold(ui-verticalSlider_Threshold-value());
ColorDetectController::getInstance()-process();
cv::Mat resulting ColorDetectController::getInstance()-getLastResult();
if (!resulting.empty())displayMat(resulting);实际上Qt 的 GUI 库大量使用了 MVC 模式。 它使用信号概念的概念以使 GUI 的所有小部件与数据模型保持同步。
另见
Qt 在线文档可以帮助您了解有关 MVC 模式的 Qt 实现的更多信息。
第 1 章的“使用 Qt 创建 GUI 应用”秘籍以简要介绍 Qt GUI 框架及其信号和插槽模型。
转换色彩空间
本章教您如何将算法封装到类中。 这样通过简化的接口该算法变得更易于使用。 封装还允许您修改算法的实现而不会影响使用该算法的类。 在此秘籍中说明了此原理在此秘籍中我们将修改ColorDetector类算法以使用其他颜色空间。 因此此秘籍将是引入 OpenCV 颜色转换的机会。
准备
RGB 颜色空间或 BGR取决于存储颜色的顺序基于红色绿色和蓝色加法原色的使用。 之所以选择这些是因为将它们组合在一起可以产生各种颜色的色域。 实际上人类视觉系统还基于三色感知的颜色视锥细胞敏感度位于红色绿色和蓝色光谱附近。 它通常是数字图像中的默认色彩空间因为这是获取色彩的方式。 捕获的光通过红色绿色和蓝色过滤器。 另外在数字图像中调节红色绿色和蓝色通道使得当以等量组合时获得灰度级强度即从黑色(0,0,0)到白色(255,255,255)。
不幸的是使用 RGB 颜色空间计算颜色之间的距离并不是衡量两种给定颜色相似度的最佳方法。 确实RGB 不是在感知上均匀的色彩空间。 这意味着给定距离处的两种颜色可能看起来非常相似而相隔相同距离的其他两种颜色看起来会非常不同。
为了解决该问题已经引入了具有感知上均匀的特性的其他色彩空间。 特别地CIE Lab 是一种这样的色彩空间。 通过将我们的图像转换到该空间图像像素和目标颜色之间的欧几里得距离将有意义地成为两种颜色之间视觉相似性的度量。 我们将在此秘籍中展示如何修改先前的应用以与 CIE Lab 一起使用。
操作步骤
通过使用 OpenCV 函数cv::cvtColor可以轻松完成不同颜色空间之间的转换。 让我们在处理方法开始时将输入图像转换为 CIE Lab 颜色空间
cv::Mat ColorDetector::process(const cv::Mat image) {// re-allocate binary map if necessary// same size as input image, but 1-channelresult.create(image.rows,image.cols,CV_8U);// re-allocate intermediate image if necessaryconverted.create(image.rows,image.cols,image.type());// Converting to Lab color space cv::cvtColor(image, converted, CV_BGR2Lab);// get the iterators of the converted image cv::Mat_cv::Vec3b::iterator it converted.begincv::Vec3b();cv::Mat_cv::Vec3b::iterator itend converted.endcv::Vec3b();// get the iterator of the output image cv::Mat_uchar::iterator itout result.beginuchar();// for each pixelfor ( ; it! itend; it, itout) {...变量converted包含颜色转换后的图像。 在ColorDetector类中将其定义为类属性
class ColorDetector {private:// image containing color converted imagecv::Mat converted;我们还需要转换输入的目标颜色。 为此我们创建了一个仅包含 1 个像素的临时图像。 请注意您需要保持与先前秘籍相同的签名即用户继续以 RGB 提供目标颜色 // Sets the color to be detectedvoid setTargetColor(unsigned char red, unsigned char green, unsigned char blue) {// Temporary 1-pixel imagecv::Mat tmp(1,1,CV_8UC3);tmp.atcv::Vec3b(0,0)[0] blue;tmp.atcv::Vec3b(0,0)[1] green;tmp.atcv::Vec3b(0,0)[2] red;// Converting the target to Lab color space cv::cvtColor(tmp, tmp, CV_BGR2Lab);target tmp.atcv::Vec3b(0,0);}如果使用此修改后的类编译了先前秘籍的应用则现在它将使用 CIE Lab 颜色空间检测目标颜色的像素。
工作原理
当图像从一种颜色空间转换为另一种颜色空间时线性或非线性变换将应用于每个输入像素以产生输出像素。 输出图像的像素类型将与输入图像之一匹配。 即使大多数时候使用 8 位像素也可以对浮点图像使用色彩转换在这种情况下通常假定像素值在0和1.0之间变化或整数图像 像素通常在0和65535之间变化。 但是像素值的确切范围取决于特定的色彩空间。 例如对于 CIE Lab 颜色空间L通道在0和100之间变化而a和b色度分量在-127和127之间变化 。
可以使用最常用的色彩空间。 这只是为 OpenCV 函数提供正确的掩码的问题。 其中包括 YCrCb它是 JPEG 压缩中使用的色彩空间。 为了从 BGR转换为 YCrCb掩码应为CV_BGR2YCrCb。 请注意具有三种常规原色红色绿色和蓝色的表示形式按 RGB 顺序或 BRG 顺序可用。
HSV 和 HLS 颜色空间也很有趣因为它们将颜色分解为其色相和饱和度分量以及值或亮度分量这是人类描述颜色的一种更自然的方式。
您也可以将彩色图像转换为灰度图像。 输出将是一个 1 通道图像 cv::cvtColor(color, gray, CV_BGR2Gray);也可以在另一个方向上进行转换但是最终得到的彩色图像的 3 个通道将用灰度图像中的相应值完全填充。
另见
第 4 章中“使用平均移位算法找到对象”的秘籍使用 HSV 颜色空间在图像中找到对象。
关于色彩空间理论有许多很好的参考资料。 其中以下是完整且最新的参考文献E. DuboisMorgan 和 Claypool 于 2009 年 10 月发表的《色彩空间的结构和特性以及彩色图像的表示》。
四、使用直方图计算像素
在本章中我们将介绍
计算图像直方图应用查询表修改图像外观均衡图像直方图反投影直方图来检测特定图像内容使用均值平移算法查找对象使用直方图比较检索相似图像
简介
图像由具有不同值颜色的像素组成。 像素值在整个图像上的分布构成了此图像的重要特征。 本章介绍图像直方图的概念。 您将学习如何计算直方图以及如何使用它来修改图像的外观。 直方图还可以用于表征图像的内容并检测图像中的特定对象或纹理。 其中一些技巧将在本章中介绍。
计算图像直方图
图像由像素组成每个像素具有不同的值。 例如在 1 通道灰度图像中每个像素的值介于 0黑色和 255白色之间。 根据图片内容您会发现图像内部布置的每种灰色阴影的数量不同。
直方图是一个简单的表格它给出了图像或有时是一组图像中具有给定值的像素数。 因此灰度图像的直方图将具有 256 个条目或箱子。 箱子 0 给出值为 0 的像素数箱子 1 给出值为 1 的像素数依此类推。 显然如果将直方图的所有条目相加则应该获得像素总数。 直方图也可以归一化以使箱子的总和等于 1。在这种情况下每个箱子给出图像中具有该特定值的像素的百分比。
入门
定义一个简单的控制台项目并准备使用如下图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rUh4bnYD-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_01.jpg)]
操作步骤
使用cv::calcHist函数可以很容易地用 OpenCV 计算直方图。 这是一个通用函数可以计算任何像素值类型的多通道图像的直方图。 通过专门针对 1 通道灰度图像的类让它更易于使用
class Histogram1D {private:int histSize[1]; // number of binsfloat hranges[2]; // min and max pixel valueconst float* ranges[1];int channels[1]; // only 1 channel used herepublic:Histogram1D() {// Prepare arguments for 1D histogramhistSize[0] 256;hranges[0] 0.0;hranges[1] 255.0;ranges[0] hranges; channels[0] 0; // by default, we look at channel 0}使用定义的成员变量可以使用以下方法来完成灰度直方图的计算 // Computes the 1D histogram.cv::MatND getHistogram(const cv::Mat image) {cv::MatND hist;// Compute histogramcv::calcHist(image, 1, // histogram from 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram1, // it is a 1D histogramhistSize, // number of binsranges // pixel value range);return hist;}现在您的程序只需要打开一个图像创建一个Histogram1D实例然后调用getHistogram方法 // Read input imagecv::Mat image cv::imread(../group.jpg,0); // open in bw// The histogram objectHistogram1D h;// Compute the histogramcv::MatND histo h.getHistogram(image);
这里的histo对象是具有 256 个条目的简单一维数组。 因此您可以通过简单地遍历此数组来读取每个箱子 // Loop over each binfor (int i0; i256; i) cout Value i histo.atfloat(i) endl;
在本章开头显示的图像中某些显示的值将显示为
...
Value 7 159
Value 8 208
Value 9 271
Value 10 288
Value 11 340
Value 12 418
Value 13 432
Value 14 472
Value 15 525
...从这一系列值中提取任何直观的含义显然很困难。 因此通常可以方便地将直方图显示为函数例如使用条形图。 下面的方法创建这样的图 // Computes the 1D histogram and returns an image of it.cv::Mat getHistogramImage(const cv::Mat image){// Compute histogram firstcv::MatND hist getHistogram(image);// Get min and max bin valuesdouble maxVal0;double minVal0;cv::minMaxLoc(hist, minVal, maxVal, 0, 0);// Image on which to display histogramcv::Mat histImg(histSize[0], histSize[0], CV_8U,cv::Scalar(255));// set highest point at 90% of nbinsint hpt static_castint(0.9*histSize[0]);// Draw a vertical line for each bin for( int h 0; h histSize[0]; h ) {float binVal hist.atfloat(h);int intensity static_castint(binVal*hpt/maxVal);// This function draws a line between 2 points cv::line(histImg,cv::Point(h,histSize[0]),cv::Point(h,histSize[0]-intensity),cv::Scalar::all(0));}return histImg;}使用此方法您可以获得以线条绘制的条形图形式的直方图特征图像 // Display a histogram as an imagecv::namedWindow(Histogram);cv::imshow(Histogram,h.getHistogramImage(image));
结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYo6I02U-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_02.jpg)]
从该直方图可以看出图像显示出中等灰度级值的大峰值和大量的较暗像素。 这两组主要分别对应于图像的背景和前景。 这可以通过在这两个组之间的过渡处对图像进行阈值化来验证。 为此可以使用方便的 OpenCV 函数即cv::threshold函数 。 这是必须在图像上应用阈值以创建二进制图像时使用的函数。 在这里我们将图像的阈值限制在直方图的高峰值灰度值 60增加之前的最小值 cv::Mat thresholded;cv::threshold(image,thresholded,60,255,cv::THRESH_BINARY);生成的二进制图像清楚地显示了背景/前景分割
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Cof7RItq-1681873909554)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_03.jpg)]
工作原理
函数cv::calcHist具有许多参数可以在多种情况下使用。 大多数情况下直方图将是单个 1 通道或 3 通道图像之一。 但是该函数允许您指定分布在多个图像上的多通道图像。 这就是为什么要将图像数组输入此函数的原因。 第 6 个参数指定直方图的维数例如对于 1D 直方图为 1。 在具有指定维数的数组中列出要在直方图计算中考虑的通道。 在我们的类实现中该单个通道默认为通道 0第三个参数。 直方图本身由每个维度中的仓数第七个参数整数数组和每个维度中的最小值和最大值第八个参数2 元素数组组成的数组描述。 也可以定义不均匀的直方图在这种情况下您需要指定每个箱子的限制。
对于许多 OpenCV 函数可以指定一个掩码指示要在计数中包括哪些像素然后忽略掩码值为 0 的所有像素。 可以指定两个附加的可选参数它们都是布尔值。 第一个指示直方图是否均匀默认为均匀。 第二个选项使您可以累积多个直方图计算的结果。 如果最后一个参数为true则图像的像素数将添加到在输入直方图中找到的当前值。 当一个人想要计算一组图像的直方图时这很有用。
生成的直方图存储在cv::MatND实例中。 这是用于处理 N 维矩阵的通用类。 方便地此类为尺寸为 1、2 和 3 的矩阵定义了at方法。这就是为什么我们能够这样写 float binVal hist.atfloat(h);在getHistogramImage方法中访问 1D 直方图的每个箱子时。 注意直方图中的值存储为float。
更多
本秘籍中介绍的类别Histogram1D通过将cv::calcHist函数限制为一维直方图来简化了函数。 这对于灰度图像很有用。 类似地我们可以定义一个可用于计算彩色 BGR 图像直方图的类
class ColorHistogram {private:int histSize[3];float hranges[2];const float* ranges[3];int channels[3];public:ColorHistogram() {// Prepare arguments for a color histogramhistSize[0] histSize[1] histSize[2] 256;hranges[0] 0.0; // BRG rangehranges[1] 255.0;ranges[0] hranges; // all channels have the same range ranges[1] hranges; ranges[2] hranges; channels[0] 0; // the three channels channels[1] 1; channels[2] 2; }在这种情况下直方图将是三维的。 因此我们需要为三个维度中的每个维度指定一个范围。 对于 BGR 图像这三个通道具有相同的[0,255]范围。 在准备好参数之后通过以下方法计算出颜色直方图 cv::MatND getHistogram(const cv::Mat image) {cv::MatND hist;// Compute histogramcv::calcHist(image, 1, // histogram of 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram3, // it is a 3D histogramhistSize, // number of binsranges // pixel value range);return hist;}返回一个三维cv::Mat实例。 该矩阵具有256 * 3个元素表示超过 1600 万个条目。 在许多应用中最好在计算出如此大的直方图之前减少颜色的数量请参阅第 2 章。 或者您也可以使用cv::SparseMat数据结构该数据结构用于表示大型稀疏矩阵即非零元素很少的矩阵而不会占用太多内存。 cv::calcHist函数具有返回一个这样的矩阵的版本。 因此很容易修改先前的方法以使用cv::SparseMatrix cv::SparseMat getSparseHistogram(const cv::Mat image) {cv::SparseMat hist(3,histSize,CV_32F);// Compute histogramcv::calcHist(image, 1, // histogram of 1 image onlychannels, // the channel usedcv::Mat(), // no mask is usedhist, // the resulting histogram3, // it is a 3D histogramhistSize, // number of binsranges // pixel value range);return hist;}另见
本章稍后的秘籍“反投影直方图以检测特定的图像内容”该方法将使用颜色直方图来检测特定的图像内容。
应用查询表修改图像外观
图像直方图使用可用的像素强度值捕获渲染场景的方式。 通过分析图像上像素值的分布可以使用此信息来修改并可能改善图像。 本秘籍说明了如何使用由查找表表示的简单映射函数来修改图像的像素值。
操作步骤
查找表是简单的一对一或多对一函数用于定义如何将像素值转换为新值。 对于常规灰度图像它是一维数组具有 256 个条目。 该表的条目i给出了相应灰度的新强度值即 newIntensity lookup[oldIntensity];OpenCV 中的函数cv::LUT将查找表应用于图像以生成新图像。 我们可以将此函数添加到我们的Histogram1D类中 cv::Mat applyLookUp(const cv::Mat image, // input imageconst cv::Mat lookup) { // 1x256 uchar matrix// the output imagecv::Mat result;// apply lookup tablecv::LUT(image,lookup,result);return result;}工作原理
当将查找表应用于图像时会生成新图像其中像素强度值已按照查找表的规定进行了修改。 这样的简单转换如下 // Create an image inversion tableint dim(256);cv::Mat lut(1, // 1 dimensiondim, // 256 entriesCV_8U); // ucharfor (int i0; i256; i) {lut.atuchar(i) 255-i;}此转换仅使像素强度反转即强度 0 变为 255强度 1 变为 254依此类推。 在图像上应用这样的查找表将产生原始图像的底片。 在上一个秘籍的图像上在这里可以看到结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VnIgITrf-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_04.jpg)]
更多
您还可以定义一个查找表以尝试改善图像的对比度。 例如如果您观察到第一个秘籍中显示的上一张图像的原始直方图则很容易注意到未使用整个范围的可能的强度值特别是对于此图像在图片中未使用较亮的强度值。 因此可以拉伸直方图以产生具有扩大对比度的图像。 该程序旨在检测图像直方图中计数为非零的最低imin和最高imax强度值。 然后可以重新映射强度值以使imin值重新定位为强度 0并且为imax赋值 255。强度i之间的线性映射简单如下
255.0*(i-imin)/(imax-imin)0.5);因此完整图像拉伸方法将如下所示 cv::Mat stretch(const cv::Mat image, int minValue0) {// Compute histogram firstcv::MatND hist getHistogram(image);// find left extremity of the histogramint imin 0;for( ; imin histSize[0]; imin ) {std::couthist.atfloat(imin)std::endl;if (hist.atfloat(imin) minValue)break;}// find right extremity of the histogramint imax histSize[0]-1;for( ; imax 0; imax-- ) {if (hist.atfloat(imax) minValue)break;}// Create lookup tableint dim(256);cv::Mat lookup(1, // 1 dimensiondim, // 256 entriesCV_8U); // uchar// Build lookup tablefor (int i0; i256; i) {// stretch between imin and imaxif (i imin) lookup.atuchar(i) 0;else if (i imax) lookup.atuchar(i) 255;// linear mappingelse lookup.atuchar(i) static_castuchar(255.0*(i-imin)/(imax-imin)0.5);}// Apply lookup tablecv::Mat result;result applyLookUp(image,lookup);return result;}一旦计算出该方法请注意对我们的applyLookUp方法的调用。 同样在实践中不仅忽略具有 0 值的箱子而且忽略计数。 例如小于给定值在此定义为minValue的条目也可能是有利的。 该方法称为
// ignore starting and ending bins with less than 100 pixels
cv::Mat streteched h.stretch(image,100);然后在这里看到生成的图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IS9ObYYf-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_05.jpg)]
如以下屏幕快照所示具有以下扩展的直方图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DWJBv8Ku-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_06.jpg)]
另见
“均衡图像直方图”秘籍为您提供了另一种改善图像对比度的方法。
均衡图像直方图
在先前的秘籍中我们展示了如何通过拉伸直方图来改善图像的对比度使其直达所有可用强度值范围。 这种策略确实构成了可以有效改善图像的简单解决方案。 但是在许多情况下图像的视觉缺陷并不是其使用的强度范围太窄。 而是某些强度值比其他强度值使用得更频繁。 本章第一章中显示的直方图就是这种现象的一个很好的例子。 确实可以很好地表现出中灰色强度而较暗和较亮的像素值却很少。 实际上人们可以认为高质量的图像应该平等地利用所有可用的像素强度。 这是直方图均衡概念的思想即使图像直方图尽可能平坦。
操作步骤
OpenCV 提供了易于使用的函数可以执行直方图均衡化。 可以这样调用 cv::Mat equalize(const cv::Mat image) {cv::Mat result;cv::equalizeHist(image,result);return result;}将以下屏幕截图应用于我们的图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TL6gHf5A-1681873909555)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_07.jpg)]
该图像具有以下直方图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xH83mm10-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_08.jpg)]
当然直方图不能完全平坦因为查询表是全局的多对一转换。 但是可以看出直方图的总体分布现在比原始分布更均匀。
工作原理
在完全一致的直方图中所有面元都有相同数量的像素。 这意味着 50% 的像素强度低于 12825% 的像素强度低于 64依此类推。 可以使用以下规则来表达该观察结果在均匀的直方图中像素的p%的强度值必须小于或等于255 * p%。 这是用于均衡直方图的规则强度i的映射应处于与强度值低于i的像素百分比相对应的强度。 因此可以根据以下公式构建所需的查询表
lookup.atuchar(i) static_castuchar(255.0*p[i]);其中p[i]是强度低于或等于i的像素数。 函数p[i]通常称为累积直方图即它是一个直方图其中包含小于或等于给定强度的像素数而不包含具有特定强度值的像素。
通常直方图均衡化可以大大改善图像的外观。 但是视视觉内容而定结果的质量可能因图像而异。
反投影直方图来检测特定图像内容
直方图是图像内容的重要特征。 如果查看显示特定纹理或特定对象的图像区域则该区域的直方图可以看作是一个函数给出给定像素属于该特定纹理或对象的概率。 在本秘籍中您将学习如何将图像直方图有利地用于检测特定图像内容。
操作步骤
假设您有一张图片并且希望检测其中的特定内容例如在下面的屏幕快照中是天空中的云彩。 首先要做的是选择一个兴趣区域其中包含您要寻找的样本。 此区域是在以下测试屏幕截图上绘制的矩形内的区域
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmzGyyDM-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_09.jpg)]
在我们的程序中兴趣区域的获取如下 cv::Mat imageROI;imageROI image(cv::Rect(360,55,40,50)); // Cloud region然后您提取此 ROI 的直方图。 使用本章第一部分中定义的Histogram1D类可以轻松完成此操作 Histogram1D h;cv::MatND hist h.getHistogram(imageROI);通过对该直方图进行归一化我们获得一个函数该函数给出给定强度值的像素属于定义区域的概率 cv::normalize(histogram,histogram,1.0);对直方图进行反投影包括将输入图像中的每个像素值替换为在归一化的直方图中读取的相应像素值。
cv::calcBackProject(image,1, // one imagechannels, // the channels usedhistogram, // the histogram we are backprojectingresult, // the resulting back projection imageranges, // the range of values, for each dimension255.0 // a scaling factor
);结果是以下概率图具有从亮低概率到暗高概率的参考区域概率
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lWN2bIVc-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_10.jpg)]
如果在此图像上应用阈值我们将获得最可能的“云”像素
cv::threshold(result, result, 255*threshold, 255, cv::THRESH_BINARY);[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6o2dLaVv-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_11.jpg)]
工作原理
前面的结果可能令人失望因为除了云之外还错误地检测了其他区域。 重要的是要了解概率函数是从简单的灰度直方图中提取的。 图像中的许多其他像素与云像素共享相同的强度并且在反投影直方图时相同强度的像素将被相同的概率值替换。 改善检测结果的一种解决方案是使用颜色信息。 但是为此我们需要修改对cv::calBackProject的调用。
函数cv::calBackProject与cv::calcHist函数相似。 第一个参数指定输入图像。 然后您需要列出要使用的通道号。 这次传递给函数的直方图是一个输入参数。 应该对其进行规范化并且其尺寸应与通道列表数组之一以及ranges参数之一匹配。 如cv::calcHist中所述这是一个float数组每个float数组指定每个通道的范围最小和最大值。 结果输出是图像即计算的概率图。 由于每个像素都被在对应的箱子位置处的直方图中找到的值替换因此所得图像的值介于 0.0 和 1.0 之间假定已提供标准化的直方图作为输入。 最后一个参数允许您选择将这些值乘以给定因子来重新缩放这些值。
更多
现在让我们看看如何在直方图反投影算法中使用颜色信息。 我们首先定义一个封装反向投影过程的类。 首先我们定义所需的属性并初始化数据
class ContentFinder {private:float hranges[2];const float* ranges[3];int channels[3];float threshold;cv::MatND histogram;public:ContentFinder() : threshold(-1.0f) {ranges[0] hranges; // all channels have same range ranges[1] hranges; ranges[2] hranges; }接下来我们定义一个阈值参数该参数将用于创建显示检测结果的二进制图。 如果此参数设置为负值则将返回原始概率图 // Sets the threshold on histogram values [0,1]void setThreshold(float t) {threshold t;}// Gets the thresholdfloat getThreshold() {return threshold;}输入直方图必须归一化 // Sets the reference histogramvoid setHistogram(const cv::MatND h) {histogram h;cv::normalize(histogram,histogram,1.0);}要对直方图进行背投您只需指定图像范围此处假设所有通道都具有相同的范围以及使用的通道列表 cv::Mat find(const cv::Mat image, float minValue, float maxValue, int *channels, int dim) {cv::Mat result;hranges[0] minValue;hranges[1] maxValue;for (int i0; idim; i)this-channels[i] channels[i];cv::calcBackProject(image, 1, // input imagechannels, // list of channels usedhistogram, // the histogram we are usingresult, // the resulting backprojectionranges, // the range of values255.0 // the scaling factor);}// Threshold back projection to obtain a binary imageif (threshold0.0)cv::threshold(result, result, 255*threshold, 255, cv::THRESH_BINARY);return result;}现在让我们在上面使用的图像的彩色版本上使用 BGR 直方图。 这次我们将尝试检测蓝天区域。 我们将首先加载彩色图像使用第 2 章的色彩缩减函数减少颜色数量然后定义关注区域 ColorHistogram hc;// load color imagecv::Mat color cv::imread(../waves.jpg);// reduce colorscolor hc.colorReduce(color,32);// blue sky areacv::Mat imageROI color(cv::Rect(0,0,165,75)); 接下来您计算直方图并使用find方法检测图像的天空部分 cv::MatND hist hc.getHistogram(imageROI);ContentFinder finder;finder.setHistogram(hist);finder.setThreshold(0.05f);// Get back-projection of color histogramCv::Mat result finder.find(color);上一部分的图像彩色版本的检测结果在此处显示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ky1z53Fn-1681873909556)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_12.jpg)]
另见
下一个秘籍将使用 HSV 颜色空间来检测图像中的对象。 这是可用于检测某些图像内容的许多替代解决方案中的另一个。
使用均值移动算法查找对象
直方图反投影的结果是一个概率图该概率图表示在特定图像位置找到给定图像内容的概率。 假设我们现在知道图像中某个对象的大概位置则可以使用概率图找到该对象的确切位置。 最有可能的是在给定窗口内最大化此概率的那个。 因此如果我们从一个初始位置开始并反复移动那么应该可以找到确切的对象位置。 这是通过均值平移算法完成的。
操作步骤
假设我们已经确定了一个感兴趣的对象这里是狒狒的脸如下面的彩色屏幕截图所示请参见本书的网站以查看此彩色图片
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CHFaPpwS-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_13.jpg)]
这次我们将通过使用 HSV 颜色空间的色相通道来描述此对象。 这意味着我们需要将图像转换为 HSV 图像然后提取色调通道并计算已定义 ROI 的 1D 色调直方图 // Read reference imagecv::Mat image cv::imread(../baboon1.jpg);// Baboons face ROIcv::Mat imageROI image(cv::Rect(110,260,35,40));// Get the Hue histogramint minSat65;ColorHistogram hc;cv::MatND colorhist hc.getHueHistogram(imageROI,minSat);可以看出色调直方图是使用我们添加到ColorHistogram类中的便捷方法获得的 // Computes the 1D Hue histogram with a mask.// BGR source image is converted to HSV// Pixels with low saturation are ignoredcv::MatND getHueHistogram(const cv::Mat image, int minSaturation0) {cv::MatND hist;// Convert to HSV color spacecv::Mat hsv;cv::cvtColor(image, hsv, CV_BGR2HSV);// Mask to be used (or not)cv::Mat mask;if (minSaturation0) {// Spliting the 3 channels into 3 imagesstd::vectorcv::Mat v;cv::split(hsv,v);// Mask out the low saturated pixelscv::threshold(v[1],mask,minSaturation,255,cv::THRESH_BINARY);}// Prepare arguments for a 1D hue histogramhranges[0] 0.0;hranges[1] 180.0;channels[0] 0; // the hue channel // Compute histogramcv::calcHist(hsv, 1, // histogram of 1 image onlychannels, // the channel usedmask, // binary maskhist, // the resulting histogram1, // it is a 1D histogramhistSize, // number of binsranges // pixel value range);return hist;}然后将生成的直方图输入到我们的ContentFinder类实例中 ContentFinder finder;finder.setHistogram(colorhist);现在让我们打开第二个图像我们要在其中定位新狒狒的脸部位置。 该图像需要转换为 HSV 空间 image cv::imread(../baboon3.jpg);// Display imagecv::namedWindow(Image 2);cv::imshow(Image 2,image);// Convert to HSV spacecv::cvtColor(image, hsv, CV_BGR2HSV);// Split the imagecv::split(hsv,v);// Identify pixels with low saturationcv::threshold(v[1],v[1],minSat,255,cv::THRESH_BINARY);接下来让我们使用先前获得的直方图获得该图像的色相通道的反投影 // Get back-projection of hue histogramresult finder.find(hsv,0.0f,180.0f,ch,1);// Eliminate low stauration pixelscv::bitwise_and(result,v[1],result);现在从初始矩形区域即原始图像中狒狒脸的位置开始OpenCV 的cv::meanShift算法将在新的狒狒脸部位置更新rect对象 cv::Rect rect(110,260,35,40);cv::rectangle(image, rect, cv::Scalar(0,0,255));cv::TermCriteria criteria(cv::TermCriteria::MAX_ITER,10,0.01);cv::meanShift(result,rect,criteria);初始和新面部位置显示在以下屏幕截图中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XVjVvFvF-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_14.jpg)]
工作原理
在此示例中我们使用了 HSV 颜色空间的色相成分来表征我们要寻找的对象。 因此必须先转换图像。 当使用CV_BGR2HSV标志时色相分量是所得图像的第一通道。 这是一个 8 位分量其中色相从 0 到 180 变化使用cv::cvtColor时转换后的图像与源图像的类型相同。 为了提取色调图像使用cv::split函数将 3 通道 HSV 图像分为三个 1 通道图像。 将这三个图像放入std::vector实例并且色调图像是向量的第一项即索引 0。
使用颜色的色相分量时考虑其饱和度这是向量的第二项总是很重要的。 实际上当颜色的饱和度低时色相信息变得不稳定且不可靠。 这是由于以下事实对于低饱和色BG 和 R 分量几乎相等。 这使得很难确定所代表的确切颜色。 因此我们决定忽略具有低饱和度的颜色的色相成分。 也就是说它们不计入直方图中使用方法getHueHistogram使用参数minSat掩盖饱和度低于此阈值的像素并且将它们从反投影结果中消除使用cv::bitwise_and运算符可在调用cv::meanShift之前消除所有具有低饱和度颜色的正检测像素。
均值平移算法是定位概率函数的局部最大值的迭代过程。 它通过找到预定义窗口内数据点的质心或加权均值来实现。 然后算法将窗口中心移动到质心位置并重复此过程直到窗口中心收敛到稳定点为止。 OpenCV 实现定义了两个停止条件最大迭代次数和窗口中心位移值在该值以下位置被认为已收敛到稳定点。 这两个条件存储在cv::TermCriteria实例中。 cv::meanShift函数返回执行的迭代次数。 显然结果的质量取决于所提供的概率图的质量以及给定的初始位置。
另见
均值漂移算法已广泛用于视觉跟踪。 第 10 章将更详细地探讨对象跟踪问题。
OpenCV 还提供了 CamShift 算法的实现该算法是均值偏移的改进版本其中窗口的大小和方向可以更改。
使用直方图比较检索相似的图像
基于内容的图像检索是计算机视觉中的重要问题。 它包括查找一组呈现类似于给定查询图像的内容的图像。 由于我们已经知道直方图是表征图像内容的有效方法因此有理由认为直方图可用于解决基于内容的检索问题。
这里的关键是能够通过简单地比较两个图像的直方图来测量两个图像之间的相似度。 需要定义一个测量函数该函数将估计两个直方图之间的差异或相似程度。 过去已经提出了各种这样的措施并且 OpenCV 在cv::compareHist函数的实现中提出了很少的措施。
操作步骤
为了将参考图像与图像集合进行比较并找到与该查询图像最相似的图像我们创建了ImageComparator类。 这包含对查询图像和输入图像的引用以及它们的直方图cv::MatND实例。 另外由于我们将使用颜色直方图进行比较因此使用了ColorHistogram类
class ImageComparator {private:cv::Mat reference;cv::Mat input;cv::MatND refH;cv::MatND inputH;ColorHistogram hist;int div;public:ImageComparator() : div(32) {}为了获得可靠的相似性度量必须减少颜色数量。 因此该类包括一个颜色减少因子该因子将应用于查询和输入图像 // Color reduction factor// The comparison will be made on images with// color space reduced by this factor in each dimensionvoid setColorReduction( int factor) {div factor;}int getColorReduction() {return div;}使用适当的设置器指定查询图像该设置器还对图像进行颜色还原 void setReferenceImage(const cv::Mat image) {reference hist.colorReduce(image,div);refH hist.getHistogram(reference);}最后compare方法将参考图像与给定的输入图像进行比较。 该方法返回一个分数指示两个图像的相似程度。 double compare(const cv::Mat image) {input hist.colorReduce(image,div);inputH hist.getHistogram(input);return cv::compareHist(refH,inputH,CV_COMP_INTERSECT);}
};此类可用于检索类似于给定查询图像的图像。 后者最初提供给类实例 ImageComparator c;c.setReferenceImage(image);在这里我们使用的查询图像是本章前面的秘籍“将直方图反投影来检测特定图像内容”中显示的海滩图像的彩色版本。 将该图像与以下所示的一系列图像进行了比较。 图像从最相似到最小显示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lH5B1eH7-1681873909557)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_04_15.jpg)]
工作原理
大多数直方图比较措施都基于逐个箱比较即在比较直方图的箱时不使用相邻箱。 因此重要的是在测量两个颜色直方图的相似度之前减小颜色空间。 也可以使用其他色彩空间。
对cv::compareHist的调用非常简单。 您只需输入两个直方图函数就会返回测得的距离。 使用标志指定要使用的特定测量方法。 在ImageComparator类中使用相交方法带有标志CV_COMP_INTERSECT。 此方法仅针对每个箱子比较每个直方图中的两个值并保持最小值。 那么相似性度量只是这些最小值的总和。 因此具有没有共同颜色的直方图的两个图像的相交值将为 0而两个相同直方图的值将等于像素总数。
其他可用的方法是对方块之间的归一化平方差求和的卡方标志CV_COMP_CHISQR基于信号中使用的归一化互相关运算符的相关方法标志CV_COMP_CORREL 处理以测量两个信号之间的相似性以及统计中使用的 Bhattacharyya 度量标志CV_COMP_BHATTACHARYYA来估计两个概率分布之间的相似性。
另见
OpenCV 文档描述了不同直方图比较度量中使用的确切公式。
地球移动距离这也是另一种流行的直方图比较方法。 此方法的主要优点是它考虑了在相邻箱中找到的值来评估两个直方图的相似性。 在 Y.i RubnerC. TomasiL. 发表的文章《地球移动者的距离作为图像检索的度量》中进行了描述。
五、通过形态学运算转换图像
在本章中我们将介绍
使用形态学过滤器腐蚀和膨胀图像使用形态过滤器开放和闭合图像使用形态过滤器检测边缘和角点使用分水岭分割图像用 GrabCut 算法提取前景对象
简介
形态滤波是 1960 年代开发的一种用于分析和处理离散图像的理论。 它定义了一系列运算符这些运算符通过使用预定义的形状元素探测图像来变换图像。 该形状元素与像素邻域相交的方式决定了运算结果。 本章介绍最重要的形态运算符。 它还探讨了使用处理图像形态的算法进行图像分割的问题。
使用形态过滤器腐蚀和膨胀图像
侵蚀和膨胀是最基本的形态学操纵子。 因此我们将在第一个秘籍中介绍它们。
数学形态学的基本工具是结构元素。 简单地将结构元素定义为在其上定义了原点的像素形状的配置也称为定位点。 应用形态学过滤器包括使用此结构元素探测图像的每个像素。 当结构元素的原点与给定像素对齐时其与图像的交点定义了一组像素在这些像素上应用了特定的形态学运算。 原则上结构元素可以是任何形状但是最常见的是使用简单的形状例如以原点为中心的正方形圆形或菱形主要是出于效率方面的考虑。
准备
由于形态过滤器通常适用于二进制图像因此我们将使用在上一章的第一个秘籍中通过阈值处理生成的二进制图像。 但是由于在形态学上惯例是使前景对象由高白色像素值表示而背景由低黑色像素值表示因此我们对图像进行了否定。 用形态学术语来说以下图像是上一章中产生的图像的补充
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSpiMTSN-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_01.jpg)]
操作步骤
侵蚀和膨胀在 OpenCV 中作为cv::erode和cv::dilate的简单函数实现。 它们的用法很简单 // Read input imagecv::Mat image cv::imread(binary.bmp);// Erode the imagecv::Mat eroded; // the destination imagecv::erode(image,eroded,cv::Mat());// Display the eroded imagecv::namedWindow(Eroded Image););cv::imshow(Eroded Image,eroded);// Dilate the imagecv::Mat dilated; // the destination imagecv::dilate(image,dilated,cv::Mat());// Display the dilated imagecv::namedWindow(Dilated Image);cv::imshow(Dilated Image,dilated);在下面的屏幕快照中可以看到这些函数调用产生的两个图像。 首先显示侵蚀
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sphCIlUx-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_02.jpg)]
其次是膨胀结果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qsSM1r3f-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_03.jpg)]
工作原理
与所有其他形态过滤器一样此秘籍的两个过滤器在每个像素周围的一组像素或邻域上运行这由结构元素定义。 回想一下当应用于给定像素时结构化元素的锚点与此像素位置对齐并且与结构化元素相交的所有像素都包含在当前集中。 侵蚀用定义的像素集中找到的最小像素值替换当前像素。 膨胀是互补运算符它用定义的像素集中找到的最大像素值替换当前像素。 由于输入的二进制图像仅包含黑色0和白色255像素因此每个像素都由白色或黑色像素替换。
描绘这两个运算符效果的一个好方法是根据背景黑色和前景白色对象进行思考。 对于腐蚀如果结构化元素放置在给定像素位置时接触背景即相交集中的像素之一是黑色则该像素将被发送到背景。 在散布的情况下如果背景像素上的结构元素触摸前景对象则将为该像素分配白色值。 这解释了为什么在侵蚀的图像中物体的尺寸减小了。 观察一些非常小的物体可以视为“嘈杂的”背景像素是如何被完全消除的。 类似地膨胀的对象现在更大并且其中的一些“孔”已被填充。
默认情况下OpenCV 使用3x3正方形结构元素。 当在函数调用中将空矩阵 cv::Mat()指定为第三个参数时将获得该默认结构元素就像在上一个示例中所做的那样。 您还可以通过提供一个矩阵其中非零元素定义结构元素来指定所需大小和形状的结构元素。 在以下示例中将应用7x7结构元素 cv::Mat element(7,7,CV_8U,cv::Scalar(1));cv::erode(image,eroded,element);在这种情况下效果显然更具破坏性如下所示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ixTsgEtm-1681873909558)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_04.jpg)]
获得相同结果的另一种方法是在图像上重复应用相同的结构元素。 这两个函数有一个可选参数来指定重复次数 // Erode the image 3 times.cv::erode(image,eroded,cv::Mat(),cv::Point(-1,-1),3);原点参数cv::Point(-1,-1)表示原点位于矩阵的中心默认值可以在结构元素上的任何位置进行定义。 获得的图像将与我们使用7x7结构元素获得的图像相同。 确实对图像进行两次腐蚀就好比对具有自身膨胀结构元素的图像进行腐蚀。 这也适用于扩张。
最后由于背景/前景的概念是任意的因此我们可以进行以下观察这是侵蚀/膨胀运算符的基本属性。 用结构元素腐蚀前景对象可以看作是图像背景部分的扩张。 或更正式地
图像的侵蚀等同于补充图像的膨胀的补充。图像的膨胀等效于补充图像的侵蚀的补充。
更多
重要的是要注意即使我们在这里对二进制图像应用了形态过滤器也可以将它们应用于具有相同定义的灰度图像。
另请注意OpenCV 形态函数支持原地处理。 这意味着您可以将输入图像用作目标图像。 所以你可以这样写 cv::erode(image,image,cv::Mat());OpenCV 为您创建所需的临时映像以使其正常工作。
另见
下一个秘籍将级联应用腐蚀和膨胀过滤器以产生新的运算符。
使用形态学过滤器检测边缘和角落以将形态学过滤器应用到灰度图像上。
使用形态过滤器开放和闭合图像
先前的秘籍介绍了两个基本的形态运算符膨胀和侵蚀。 由此可以定义其他运算符。 接下来的两个秘籍将介绍其中的一些。 此秘籍中介绍了开放和闭合运算符。
操作步骤
为了应用高级形态过滤器需要将cv::morphologyEx函数与相应的函数代码一起使用。 例如以下调用将应用结束运算符 cv::Mat element5(5,5,CV_8U,cv::Scalar(1));cv::Mat closed;cv::morphologyEx(image,closed,cv::MORPH_CLOSE,element5);请注意这里我们使用5x5的结构元素使过滤器的效果更加明显。 如果输入前面秘籍的二进制图像则可获得
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-U9fMDXT8-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_05.jpg)]
同样应用形态学打开运算符将得到以下图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iC6lCqoY-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_06.jpg)]
这是从以下代码获得的 cv::Mat opened;cv::morphologyEx(image,opened,cv::MORPH_OPEN,element5);工作原理
开放和闭合过滤器仅根据基本腐蚀和膨胀操作进行定义
闭合被定义为图像膨胀的腐蚀。开放被定义为图像腐蚀的膨胀。
因此可以使用以下调用来计算图像的关闭 // dilate original imagecv::dilate(image,result,cv::Mat()); // in-place erosion of the dilated imagecv::erode(result,result,cv::Mat()); 通过反转这两个函数调用可以获得打开。
在检查关闭过滤器的结果时可以看到白色前景对象的小孔已被填充。 过滤器还将几个相邻的对象连接在一起。 基本上任何太小而不能完全容纳结构元素的孔或间隙都将被过滤器消除。
相反打开过滤器消除了场景中的一些小物体。 所有太小而无法包含结构元素的元素均已删除。
这些过滤器通常用于对象检测。 关闭过滤器将错误地分成较小碎片的对象连接在一起而打开过滤器则消除了由图像噪声引入的小斑点。 因此顺序使用它们是有利的。 如果我们的测试二进制图像是连续关闭和打开的则将获得仅显示场景中主要对象的图像如下所示。 如果希望优先进行噪声过滤也可以在关闭之前应用打开过滤器但这会以消除一些碎片对象为代价。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yiMIag9k-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_07.jpg)]
应该注意的是对图像多次应用相同的打开和类似的关闭操作符没有任何效果。 实际上在孔被第一开口填充的情况下对该相同过滤器的附加应用将不会对图像产生任何其他变化。 用数学术语来说这些运算符被认为是幂等的。
使用形态过滤器检测边缘和角点
形态过滤器也可以用于检测图像中的特定特征。 在本秘籍中我们将学习如何检测灰度图像中的线和角。
入门
在此秘籍中将使用以下图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FbarcGck-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_08.jpg)]
操作步骤
让我们定义一个名为MorphoFeatures的类它将使我们能够检测图像特征
class MorphoFeatures {private:// threshold to produce binary imageint threshold;// structuring elements used in corner detectioncv::Mat cross;cv::Mat diamond;cv::Mat square;cv::Mat x;使用cv::morphologyEx函数的适当过滤器检测线路非常容易
cv::Mat getEdges(const cv::Mat image) {// Get the gradient imagecv::Mat result;cv::morphologyEx(image,result,cv::MORPH_GRADIENT,cv::Mat());// Apply threshold to obtain a binary imageapplyThreshold(result);return result;
}二进制边缘图像是通过该类的简单私有方法获得的
void applyThreshold(cv::Mat result) {// Apply threshold on resultif (threshold0)cv::threshold(result, result, threshold, 255, cv::THRESH_BINARY);
}然后在主要函数中使用此类然后按以下方式获取边缘图像
// Create the morphological features instance
MorphoFeatures morpho;
morpho.setThreshold(40);// Get the edges
cv::Mat edges;
edges morpho.getEdges(image); 结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rw7lEfz6-1681873909559)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_09.jpg)]
使用形态学角点检测角点有点复杂因为它不是直接在 OpenCV 中实现的。 这是使用非正方形结构元素的一个很好的例子。 实际上它需要定义四个不同的结构元素形状分别为正方形菱形十字形和 X 形。 这是在构造器中完成的为简单起见所有这些结构化元素都具有固定的5x5尺寸
MorphoFeatures() : threshold(-1), cross(5,5,CV_8U,cv::Scalar(0)),diamond(5,5,CV_8U,cv::Scalar(1)), square(5,5,CV_8U,cv::Scalar(1)),x(5,5,CV_8U,cv::Scalar(0)){// Creating the cross-shaped structuring elementfor (int i0; i5; i) {cross.atuchar(2,i) 1;cross.atuchar(i,2) 1; }// Creating the diamond-shaped structuring elementdiamond.atuchar(0,0) 0;diamond.atuchar(0,1) 0;diamond.atuchar(1,0) 0;diamond.atuchar(4,4) 0;diamond.atuchar(3,4) 0;diamond.atuchar(4,3) 0;diamond.atuchar(4,0) 0;diamond.atuchar(4,1) 0;diamond.atuchar(3,0) 0;diamond.atuchar(0,4) 0;diamond.atuchar(0,3) 0;diamond.atuchar(1,4) 0;// Creating the x-shaped structuring elementfor (int i0; i5; i) {x.atuchar(i,i) 1;x.atuchar(4-i,i) 1; }
}在检测角点特征时所有这些结构元素都会级联应用以获得最终的角点贴图
cv::Mat getCorners(const cv::Mat image) {cv::Mat result;// Dilate with a cross cv::dilate(image,result,cross);// Erode with a diamondcv::erode(result,result,diamond);cv::Mat result2;// Dilate with a X cv::dilate(image,result2,x);// Erode with a squarecv::erode(result2,result2,square);// Corners are obtained by differencing// the two closed imagescv::absdiff(result2,result,result);// Apply threshold to obtain a binary imageapplyThreshold(result);return result;
}为了更好地可视化检测结果以下方法在二进制图上每个检测到的点上在图像上绘制一个圆
void drawOnImage(const cv::Mat binary, cv::Mat image) {cv::Mat_uchar::const_iterator it binary.beginuchar();cv::Mat_uchar::const_iterator itend binary.enduchar();// for each pixel for (int i0; it! itend; it,i) {if (!*it) cv::circle(image,cv::Point(i%image.step,i/image.step),5,cv::Scalar(255,0,0));}
}然后使用以下代码在图像上检测角点
// Get the corners
cv::Mat corners;
corners morpho.getCorners(image);// Display the corner on the image
morpho.drawOnImage(corners,image);
cv::namedWindow(Corners on Image);
cv::imshow(Corners on Image,image);然后检测到的角的图像如下。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ikDDH2US-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_10.jpg)]
工作原理
帮助理解形态运算符对灰度图像的影响的一种好方法是将图像视为拓扑浮雕其中灰度对应于海拔或海拔。 在这种情况下明亮的区域对应于山脉而较暗的区域则构成地形的山谷。 同样由于边缘对应于较暗像素和较亮像素之间的快速过渡因此可以将其描绘为陡峭的悬崖。 如果在这样的地形上应用腐蚀运算符最终结果将是用某个邻域中的最小值替换每个像素从而减小其高度。 结果随着山谷的扩大悬崖将被“侵蚀”。 扩张具有完全相反的效果即悬崖将在山谷上空获得地形。 但是在两种情况下平稳度即恒定强度的区域将保持相对不变。
上述观察结果导致了一种检测图像边缘或悬崖的简单方法。 这可以通过计算膨胀图像和侵蚀图像之间的差异来完成。 由于这两个变换后的图像大部分在边缘位置不同因此差异会突出图像的边缘。 输入cv::MORPH_GRADIENT自变量时这正是cv::morphologyEx函数所做的事情。 显然结构元素越大检测到的边缘将越厚。 该边缘检测运算符也称为 Beucher 梯度下一章将更详细地讨论图像梯度的概念。 注意也可以通过简单地从扩张后的图像中减去原始图像或从原始图像中减去侵蚀图像来获得类似的结果。 产生的边缘将更薄。
角点检测要复杂一些因为它使用了四个不同的结构元素。 该运算符未在 OpenCV 中实现但我们在这里展示它是为了演示如何定义和组合各种形状的结构化元素。 这个想法是通过使用两个不同的结构元素对图像进行扩张和腐蚀来封闭图像。 选择这些元素以使它们的直边保持不变但是由于它们各自的作用将影响角点的边缘。 让我们使用由单个白色正方形组成的以下简单图像更好地了解此非对称关闭操作的效果
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PMGsfU66-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_11.jpg)]
第一个正方形是原始图像。 当用十字形结构元素进行扩张时方形边缘会扩大除了在十字形不会碰到方形的角点处。 这是中间的方块说明的结果。 然后这个扩张的图像被结构元素侵蚀这次该元素具有菱形形状。 这种侵蚀使大多数边缘恢复到其原始位置但由于它们没有膨胀因此将角进一步推向了另一端。 然后获得左方格可以看到它已经失去了尖角。 使用 X 形和方形结构元素重复相同的过程。 这两个元素是先前元素的旋转版本因此将以 45 度方向捕获角。 最后对两个结果求差将提取角点特征。
另见
The article, Morphological gradients by J.-F. Rivest, P. Soille, S. Beucher, ISETs symposium on electronic imaging science and technology, SPIE, Feb. 1992, for more on morphological gradient.The article A modified regulated morphological corner detector by F.Y. Shih, C.-F. Chuang, V. Gaddipati, Pattern Recognition Letters , volume 26, issue 7, May 2005, for more information on morphological corner detection.使用分水岭分割图像
分水岭变换是一种流行的图像处理算法用于将图像快速分割为同质区域。 它依赖于这样的想法当图像被视为拓扑浮雕时均匀区域对应于由陡峭边缘界定的相对平坦的盆地。 由于其简单性该算法的原始版本往往会过分分割图像从而产生多个小区域。 这就是 OpenCV 提出该算法的变体的原因该变体使用了一组预定义的标记来指导图像段的定义。
操作步骤
分水岭分割是通过使用cv::watershed函数获得的。 此函数的输入是一个 32 位带符号整数标记图像其中每个非零像素代表一个标签。 想法是标记图像的某些像素这些像素当然属于给定区域。 根据该初始标记分水岭算法将确定其他像素所属的区域。 在本秘籍中我们将首先将标记图像创建为灰度图像然后将其转换为整数图像。 我们方便地将此步骤封装到WatershedSegmenter类中
class WatershedSegmenter {private:cv::Mat markers;public:void setMarkers(const cv::Mat markerImage) {// Convert to image of intsmarkerImage.convertTo(markers,CV_32S);}cv::Mat process(const cv::Mat image) {// Apply watershedcv::watershed(image,markers);return markers;}获得这些标记的方式取决于应用。 例如某些预处理步骤可能导致识别出属于感兴趣对象的某些像素。 然后分水岭将用于从该初始检测中划定整个对象。 在本秘籍中我们将仅使用本章中使用的二进制图像来识别相应原始图像的动物这是在第 4 章开头显示的图像。
因此从二进制图像中我们需要确定肯定属于前景的像素动物和肯定属于背景的像素主要是草。 在这里我们将用标签 255 标记前景像素并用标签 128 标记背景像素此选择完全是任意的除 255 以外的任何标签编号都可以使用。 其他像素即标记未知的像素的赋值为 0。就目前而言二进制图像包含太多属于图像各个部分的白色像素。 然后我们将严重腐蚀该图像以便仅保留属于重要对象的像素 // Eliminate noise and smaller objectscv::Mat fg;cv::erode(binary,fg,cv::Mat(),cv::Point(-1,-1),6);结果如下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7XNwEsBX-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_12.jpg)]
请注意仍然存在属于背景林的一些像素。 让我们简单地保留它们。 因此它们将被认为对应于感兴趣的对象。 类似地我们还通过对原始二进制图像进行大的扩张来选择背景的一些像素 // Identify image pixels without objectscv::Mat bg;cv::dilate(binary,bg,cv::Mat(),cv::Point(-1,-1),6);cv::threshold(bg,bg,1,128,cv::THRESH_BINARY_INV);产生的黑色像素对应于背景像素。 这就是为什么在膨胀后立即将阈值运算分配给这些像素的值 128 的原因。然后获得以下图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-otxv8zFP-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_13.jpg)]
这些图像被组合以形成标记图像 // Create markers imagecv::Mat markers(binary.size(),CV_8U,cv::Scalar(0));markers fgbg;请注意我们在此处如何使用重载的operator来组合图像。 这是将用作分水岭算法输入的图像
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQNh0p2k-1681873909560)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_14.jpg)]
然后按以下方式获得分段 // Create watershed segmentation objectWatershedSegmenter segmenter;// Set markers and processsegmenter.setMarkers(markers);segmenter.process(image);然后更新标记图像以便为每个零像素分配一个输入标签之一而属于找到的边界的像素的值为 -1。 标签的结果图像如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZvDNMTOr-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_15.jpg)]
边界图像为
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0PuhOhH4-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_16.jpg)]
工作原理
正如我们在前面的秘籍中所做的那样我们将在分水岭算法的描述中使用拓扑图类比。 为了创建分水岭分割其想法是从级别 0 开始逐渐淹没图像。随着“水”级别的逐渐增加达到级别 1、2、3 等形成了集水盆地。 这些流域的大小也逐渐增加因此两个不同流域的水最终将合并。 发生这种情况时将创建分水岭以使两个盆地保持分离。 一旦水位达到最大水位这些创建的盆地和集水区就构成了集水区分割。
如人们所料洪水过程最初会形成许多小的单个盆地。 当所有这些合并时会创建许多分水岭线从而导致图像过度分割。 为了克服该问题已经提出了对该算法的修改其中泛洪处理从预定的标记像素组开始。 由这些标记创建的盆地根据分配给初始标记的值进行标记。 当两个具有相同标签的盆地合并时不会创建分水岭从而防止了过度分割。
这就是调用cv::watershed函数时发生的情况。 输入的标记图像将更新以产生最终的分水岭分割。 用户可以输入带有任意数量标签的标记图像其中未知标签的像素保留为 0。标记图像被选择为 32 位带符号整数的图像以便能够定义 255 个以上的标签。 它还允许将特殊值 -1 分配给与分水岭相关的像素。 这是cv::watershed函数返回的内容。 为方便显示结果我们引入了两种特殊方法。 第一个返回标签的图像分水岭的值为 0。 这可以通过阈值轻松完成 // Return result in the form of an imagecv::Mat getSegmentation() {cv::Mat tmp;// all segment with label higher than 255// will be assigned value 255markers.convertTo(tmp,CV_8U);return tmp;}类似地第二种方法返回一个图像其中分水岭线的值设置为 0其余图像为 255。这一次cv::convertTo方法用于实现以下结果 // Return watershed in the form of an imagecv::Mat getWatersheds() {cv::Mat tmp;// Each pixel p is transformed into// 255p255 before conversionmarkers.convertTo(tmp,CV_8U,255,255);return tmp;}转换之前应用的线性变换允许将 -1 像素转换为 0因为-1 * 255 255 0。
值大于 255 的像素被分配值为 255。这是由于将有符号整数转换为无符号字符时应用了饱和操作。
另见
The article The viscous watershed transform by C. Vachier, F. Meyer, Journal of Mathematical Imaging and Vision, volume 22, issue 2-3, May 2005, for more information on the watershed transform.下一个秘籍介绍了另一个图像分割算法该算法也可以将图像分割为背景和前景对象。
使用 GrabCut 算法提取前景对象
OpenCV 提出了另一种流行的图像分割算法GrabCut 算法的实现。 该算法不是基于数学形态学的但是我们在这里介绍它因为它显示了与前面秘籍中提出的分水岭分割算法的一些相似之处。 GrabCut 在计算上比分水岭贵但通常可以产生更准确的结果。 当一个人想要在静止图像中提取前景对象例如将一个对象从一张图片剪切并粘贴到另一张图片时这是最好的算法。
操作步骤
cv::grabCut函数易于使用。 您只需要输入图像并将其某些像素标记为属于背景或前景。 基于此部分标记该算法将确定完整图像的前景/背景分割。
指定输入图像的部分前景/背景标签的一种方法是定义一个矩形在其中包含前景对象 // Open imageimage cv::imread(../group.jpg);// define bounding rectangle// the pixels outside this rectangle// will be labeled as background cv::Rect rectangle(10,100,380,180);然后此矩形之外的所有像素将被标记为背景。 除了输入图像及其分割图像之外调用cv::grabCut函数还需要定义两个矩阵其中包含该算法构建的模型 cv::Mat result; // segmentation (4 possible values)cv::Mat bgModel,fgModel; // the models (internally used)// GrabCut segmentationcv::grabCut(image, // input imageresult, // segmentation resultrectangle, // rectangle containing foreground bgModel,fgModel, // models5, // number of iterationscv::GC_INIT_WITH_RECT); // use rectangle注意我们如何使用cv::GC_INIT_WITH_RECT标志作为函数的最后一个参数来指定使用边界矩形模式下一节将讨论其他可用模式。 输入/输出分割图像可以具有四个值之一
cv::GC_BGD用于肯定属于背景的像素例如在我们的示例中矩形外部的像素cv::GC_FGD用于肯定属于前景的像素在我们的示例中没有cv::GC_PR_BGD用于可能属于背景的像素cv::GC_PR_FGD用于可能属于前景的像素在我们的示例中为矩形内像素的初始值。
通过提取值等于cv::GC_PR_FGD的像素我们得到了分割的二进制图像 // Get the pixels marked as likely foregroundcv::compare(result,cv::GC_PR_FGD,result,cv::CMP_EQ);// Generate output imagecv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255));image.copyTo(foreground,// bg pixels are not copiedresult); 要提取所有前景像素即其值等于cv::GC_PR_FGD或cv::GC_FGD可以简单地检查第一位的值 // checking first bit with bitwise-andresult result1; // will be 1 if FG 这是可能的因为这些常数定义为值 1 和 3而其他两个常数定义为 0 和 2。在我们的示例中由于分割图像不包含cv::GC_FGD像素仅cv::GC_BGD像素已输入。
最后通过以下带有遮罩的复制操作我们获得了前景对象的图像在白色背景上 // Generate output imagecv::Mat foreground(image.size(),CV_8UC3,cv::Scalar(255,255,255)); // all white imageimage.copyTo(foreground,result); // bg pixels not copied结果图像如下
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-e1pFidVo-1681873909561)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/opencv2-cv-app-prog-cb/img/3241OS_05_17.jpg)]
工作原理
在前面的示例中GrabCut 算法能够通过简单地指定在其中包含这些对象四个动物的矩形来提取前景对象。 备选地还可以将值cv::GC_BGD和cv::GC_FGD分配给作为cv::grabCut函数的第二自变量提供的分割图像的某些特定像素。 然后您可以将GC_INIT_WITH_MASK指定为输入模式标志。 这些输入标签可以例如通过要求用户交互式地标记图像的一些元素来获得。 也可以组合这两种输入模式。
使用此输入信息GrabCut 通过以下步骤创建背景/前景分割。 最初将前景标签cv::GC_PR_FGD临时分配给所有未标记的像素。 基于当前分类该算法将像素分为相似颜色的群集即背景为K群集前景为K群集。 下一步是通过在前景像素和背景像素之间引入边界来确定背景/前景分割。 这是通过优化过程来完成的该过程尝试将像素与相似的标签连接起来并对在强度相对均匀的区域中放置边界施加了惩罚。 通过使用图切割算法可以有效地解决此优化问题该方法可以通过将它表示为连通图在上面应用切割来组成最佳配置从而找到问题的最佳解决方案。 所获得的分割为像素产生新的标签。 然后可以重复聚类过程并再次找到新的最佳分割依此类推。 因此GrabCut 是一个迭代过程可逐步改善分割结果。 根据场景的复杂性可以在或多或少的迭代中找到一个好的解决方案在简单情况下一个迭代就足够了。
这解释了该函数的上一个最后一个参数用户可以在其中指定要应用的迭代次数。 算法维护的两个内部模型作为函数的参数传递并返回这样如果希望通过执行其他迭代来改善细分结果则可以再次使用上次运行的模型调用该函数。
另见
The article by C. Rother, V. Kolmogorov and A. Blake, GrabCut: Interactive Foreground Extraction using Iterated Graph Cuts in ACM Transactions on Graphics (SIGGRAPH) volume 23, issue 3, August 2004, that describes in detail the GrabCut algorithm.