OpenCV3-和-Qt5-计算机视觉-全-
OpenCV3 和 Qt5 计算机视觉(全)
零、前言
成为软件开发人员的时间比以往任何时候都没有。 只要环顾四周,就很可能会看到至少两个不同的设备,例如计算机,智能手机,智能手表或平板电脑,上面运行着一些应用,可以帮助您完成各种日常任务或娱乐音乐,看电影 ,视频游戏等。 每年,市场上都会引入数百种新设备,并且需要新版本的操作系统来跟上它们,以便为应用开发人员提供更好的界面,以创建可更好地利用诸如高分辨率等基础资源的软件。 显示器,各种传感器等。 结果,软件开发框架必须适应并支持不断增长的平台。 考虑到这一点,Qt 可能是同时提供功能,速度,灵活性和易用性的最成功的跨平台软件开发框架之一,在创建需要以下功能的软件时,它是首选。 在各种平台上都具有吸引力和一致性。
近年来,特别是随着功能更强大的处理器以较低的价格出现,台式计算机及其手持式对等设备的角色已转向执行更苛刻和更复杂的任务,例如计算机视觉。 无论是用于智能电影或照片编辑,保护敏感建筑物,对生产线中的物体计数,还是通过自动驾驶汽车检测交通标志,车道或行人,计算机视觉正越来越多地用于解决此类实时问题。 曾经只能由人类解决的问题。 这是 OpenCV 框架进入现场的地方。 在过去的几年中,OpenCV 已成长为功能完善的跨平台计算机视觉框架,其重点是速度和性能。 在世界各地,开发人员和研究人员都在使用 OpenCV 来实现其计算机视觉应用的思想和算法。
本书旨在帮助您掌握 Qt 和 OpenCV 框架的基本概念,使您轻松地自己继续开发和交付跨多种平台的计算机视觉应用。 能够轻松遵循本书所涵盖主题的唯一假设是,您熟悉并熟悉 C++ 编程概念,例如类,模板,继承等。 即使整本书中涵盖的教程,屏幕截图和示例都是基于 Windows 操作系统的,但仍会在必要时提及 MacOS 和 Linux 操作系统的区别。
这本书是几个月努力工作的结果,没有劳伦斯·韦加斯(Lawrence Veigas)的宝贵帮助,他的完美编辑是不可能的。 卡尔·菲利普·布尔(Karl Phillip Buhr)诚实而有见地的评论; 帕塔·科塔里(Parth Kothari),没有他,这本书就不可能实现; 还有 Zainab Bootwala,Prajakta Naik,Aaron Lazar,Supriya Thabe,Tiksha Sarang,Rekha Nair,Jason Monteiro,Nilesh Mohite,以及 Packt Publishing 的所有人都为您以及世界各地的读者制作并发行了这本书 。
这本书是给谁的
本书面向有兴趣构建计算机视觉应用的读者。 期望具备 C++ 编程的中级知识。 即使没有 Qt5 和 OpenCV 3 知识,但如果您熟悉这些框架,您也会受益。
本书涵盖的内容
第 1 章,“OpenCV 和 Qt 简介”进行了所有必需的初始化。 从何处以及如何获得 Qt 和 OpenCV 框架开始,本章将介绍如何安装,配置并确保在开发环境中正确设置了所有内容。
第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”,带您完成 Qt Creator IDE,我们将使用它来开发所有应用。 在本章中,您将学习如何创建和运行应用项目。
第 3 章,“创建综合的 Qt + OpenCV 项目”经历了综合应用所需的最常见功能,包括样式,国际化以及对各种语言,插件等的支持。 上。 通过此过程,我们将自己创建一个全面的计算机视觉应用。
第 4 章,“Mat和QImage”奠定了基础,并教您编写计算机视觉应用所需的基本概念。 在本章中,您将学习有关 OpenCV Mat类和 Qt QImage类的所有知识,以及如何在两个框架之间进行转换和传递等等。
第 5 章,“图形视图框架”教您如何使用 Qt 图形视图框架及其基础类,以便轻松,有效地显示和操纵应用中的图形。
第 6 章,“OpenCV 中的图像处理”指导您完成 OpenCV 框架提供的图像处理功能。 您将了解有关转换,过滤器,色彩空间,模板匹配等的信息。
第 7 章,“特征和描述符”都是关于从图像中检测关键点,从关键点提取描述符并将它们相互匹配的。 在本章中,您将学习各种关键点和描述符提取算法,并最终使用它们来检测和定位图像中的已知对象。
第 8 章,“多线程”教给您有关 Qt 框架提供的多线程功能的全部信息。 您将学习互斥锁,读写锁,信号灯和各种线程同步工具。 本章还将向您介绍 Qt 中的低级(QThread)和高级(QtConcurrent)多线程技术。
第 9 章,“视频分析”涵盖了如何使用 Qt 和 OpenCV 框架正确处理视频。 您将学习使用 MeanShift 和 CAMShift 算法以及其他视频处理功能进行对象跟踪。 本章还全面概述了视频处理的所有基本概念,例如直方图和反投影图像。
第 10 章,“调试和测试”向您介绍 Qt Creator IDE 的调试功能以及如何配置和设置。 在本章中,您还将通过编写示例单元测试来了解 Qt 框架提供的单元测试功能,该示例单元测试可以在每次构建项目时手动或自动运行。
第 11 章,“链接和部署”教您动态或静态地构建 OpenCV 和 Qt 框架。 在本章中,您还将学习有关在各种平台上部署 Qt 和 OpenCV 应用的信息。 在本章的最后,我们将使用 Qt Installer 框架创建一个安装程序。
第 12 章,“Qt Quick 应用”向您介绍 Qt Quick 应用和 QML 语言。 在本章中,您将学习 QML 语言语法,以及如何将其与 Qt Quick Designer 一起使用来为台式机和移动平台创建漂亮的 Qt Quick 应用。 您还将在本章中学习如何集成 QML 和 C++。
充分利用这本书
虽然本书的初始章节涵盖了所有必需的工具和软件,正确的版本以及安装和配置的方式,但以下是可以用作快速参考的列表:
- 安装了 Windows,MacOS 或 Linux(例如 Ubuntu)操作系统最新版本的常规计算机。
- Microsoft Visual Studio(在 Windows 上)
- Xcode(在 MacOS 上)
- CMake
- Qt 框架
- OpenCV 框架
要了解当前的常规计算机是什么,可以在线搜索或询问当地商店。 但是,您已经拥有的一个很可能足以让您入门。
下载示例代码文件
您可以从 www.packtpub.com 的帐户中下载本书的示例代码文件。 如果您在其他地方购买了此书,则可以访问 www.packtpub.com/support 并注册以将文件直接通过电子邮件发送给您。
您可以按照以下步骤下载代码文件:
- 登录或登录 www.packtpub.com 。
- 选择支持选项卡。
- 单击代码下载和勘误。
- 在搜索框中输入书籍的名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压缩或解压缩文件夹:
- Windows 的 WinRAR/7-Zip
- Mac 的 Zipeg/iZip/UnRarX
- Linux 的 7-Zip/PeaZip
本书的代码包也托管在 GitHub 上。 我们还从这里提供了丰富的书籍和视频目录中的其他代码包。 去看一下!
下载彩色图像
我们还提供了 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。 您可以在此处下载。
使用约定
本书中使用了许多文本约定。
CodeInText:指示文本,数据库表名称,文件夹名称,文件名,文件扩展名,路径名,虚拟 URL,用户输入和 Twitter 句柄中的代码字。 这是一个示例:“QApplication类是负责控制应用的控制流,设置等的主要类。”
代码块设置如下:
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
当我们希望引起您对代码块特定部分的注意时,相关行或项目以粗体显示:
#include "mainwindow.h"
#include
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
任何命令行输入或输出的编写方式如下:
binarycreator -p packages -c config.xml myinstaller
粗体:表示新术语,重要单词或您在屏幕上看到的单词。 例如,菜单或对话框中的单词会出现在这样的文本中。 这是一个示例:“单击下一步按钮将您移至下一个屏幕。”
警告或重要提示如下所示。
提示和技巧如下所示。
一、OpenCV 和 Qt 简介
在最基本的形式和形状中,“计算机视觉”是一个术语,用于标识用于使数字设备具有视觉感觉的所有方法和算法。 这意味着什么? 好吧,这就是听起来的确切含义。 理想情况下,计算机应该能够通过标准相机(或与此相关的任何其他类型的相机)的镜头看到世界,并且通过应用各种计算机视觉算法,它们应该能够检测甚至识别并计数人脸。 图像中的对象,检测视频馈送中的运动,然后执行更多操作,这些操作乍一看只能是人类的期望。 因此,要了解计算机视觉的真正含义,最好知道计算机视觉旨在开发方法以实现所提到的理想,使数字设备具有查看和理解周围环境的能力。 值得注意的是,大多数时间计算机视觉和图像处理可以互换使用(尽管对这个主题的历史研究可能证明应该相反)。 但是,尽管如此,在整本书中,我们仍将使用“计算机视觉”一词,因为它是当今计算机科学界中更为流行和广泛使用的术语,并且因为正如我们将在本章稍后看到的那样,“图像处理”是 OpenCV 库的模块,我们还将在本章的后续页面中介绍,并且还将在其完整的一章中介绍它。
计算机视觉是计算机科学中当今最受欢迎的学科之一,它被用于各种应用,从检测癌变组织的医疗工具到可以帮助制作所有闪亮音乐视频和电影的视频编辑软件,以及军事级目标检测器可帮助在地图上找到交通标志检测器的特定位置,以帮助无人驾驶汽车找到路。 好吧,很显然,我们无法完成为计算机视觉命名的所有可能性,但是我们可以确定,这是一个有趣的话题,并且将会存在很长时间。 还值得一提的是,计算机视觉领域的工作和职业市场正在迅速扩大,并且它正在日趋增长。
在计算机视觉开发人员和专家使用的最受欢迎的工具中,有两个最著名的开源社区框架,OpenCV 和 Qt 也在您的书名中。 每天,世界各地成千上万的开发人员,从成熟的公司到创新的初创公司,都使用这两个框架为各种行业(例如,我们提到的行业)构建应用,这正是您将学到的东西。 这本书。
在本章中,我们将介绍以下主题:
- 引入 Qt,这是一个开放源代码和跨平台的应用开发框架
- 引入 OpenCV,一个开源的跨平台计算机视觉框架
- 如何在 Windows,MacOS 和 Linux 操作系统上安装 Qt
- 如何从 Windows,MacOS 和 Linux 操作系统上的源代码构建 OpenCV
- 配置您的开发环境以结合使用 Qt 和 OpenCV 框架来构建应用
- 使用 Qt 和 OpenCV 构建您的第一个应用
需要什么?
这是在本章引言中所说的之后最明显的问题,但是对于它的答案也是我们学习计算机视觉的第一步。 本书适用于熟悉 C++ 编程语言并希望开发功能强大且外观精美的计算机视觉应用的开发人员,而这些应用可以在不同的操作系统上很好地完成工作,而无需付出太多努力。 本书旨在带您踏上激动人心的旅程,遍历计算机视觉的不同主题,着重于动手练习并一次一步地发展所学内容。
具有足够 C++ 经验的任何人都知道,使用原始 C++ 代码并取决于特定于 OS 的 API 来编写视觉上丰富的应用并非易事。 因此,几乎每个 C++ 开发人员(或至少是认真从事 C++ 工作的认真的开发人员)都使用一个或另一个框架来简化此过程。 Qt 是最广为人知的 C++ 框架。 实际上,它是首选,或者不是首选。 另一方面,如果您的目标是开发一个处理图像或可视化数据集的应用,则 OpenCV 框架可能是第一个(也是最受欢迎的)地址。 因此,这就是为什么本书着重介绍 Qt 和 OpenCV 的结合的原因。 如果不使用 Qt 和 OpenCV 等强大的框架,就不可能为不同的台式机和移动平台开发性能最佳的计算机视觉应用。
总结所讲内容,请确保您至少具有 C++ 编程语言的中级知识。 如果您不熟悉类,抽象类,继承,模板或指针之类的术语,请考虑首先阅读有关 C++ 的书。 对于所有其他主题,尤其是涵盖的所有动手实践主题,本书保证为您包括的所有示例和教程提供清晰的解释(或参考特定的文档页面)。 当然,要获得对 Qt 和 OpenCV 中如何实现模块和类的非常详细和深入的了解,您需要熟悉更多的资源,研究,有时甚至是核心数学计算,或者对如何使用 Qt 和 OpenCV 进行低级理解。 计算机或操作系统在现实世界中的性能,这完全超出了本书的范围。 但是,对于本书中涵盖的所有算法和方法,您将简要了解它们的用途,使用方式,时间和位置以及足够的指导原则,以使您可以继续深入研究。
Qt 简介
您已经听说过它,甚至可能在不知道的情况下使用它。 它是许多世界著名的商业和开源应用的基础,例如 VLC Player,Calibre 等。 Qt 框架被大多数所谓的财富 500 强公司使用,我们甚至无法开始定义它在世界上许多应用开发团队和公司中的使用和流行程度。 因此,我们将从介绍开始,然后从那里开始。
首先,让我们简单介绍一下 Qt 框架,以使我们步入正轨。 清晰地了解整个事情,没有什么比让您更喜欢框架了。 因此,Qt 框架是一个开放源代码应用开发框架,目前由 Qt 公司构建和管理,它广泛用于创建外观丰富且跨平台的应用,这些应用可以在很少的不同操作系统或设备上运行,而无需任何负担。 为了进一步分解,开源是其中最明显的部分。 这意味着您可以访问 Qt 的所有源代码。 通过丰富的外观,我们意味着 Qt 框架中存在足够的资源和功能来编写非常漂亮的应用。 对于最后一部分,跨平台,这基本上意味着,例如,如果使用针对 Microsoft Windows 操作系统的 Qt 框架模块和类开发应用,则可以按原样针对 MacOS 或 Linux 对其进行编译和构建。 ,而无需更改任何代码(几乎),只要您在应用中不使用任何非 Qt 或平台特定的库即可。
在撰写本书时,Qt 框架(或以下简称 Qt)的版本为 5.9.X,其中包含许多模块,几乎可用于开发应用的任何目的。 Qt 将这些模块分为以下四个主要类别:
- Qt Essentials
- Qt 附加组件
- 附加模块
- 技术预览模块
让我们看看它们是什么以及它们包括什么,因为在整本书中我们将与之打交道。
Qt Essentials
这些是 Qt 承诺可在所有受支持平台上使用的模块。 它们基本上是 Qt 的基础,并且包含几乎所有 Qt 应用使用的大多数类。 Qt Essential 模块包括所有通用模块和类。 确实要注意通用一词,因为这正是这些模块的用途。 以下是用于快速研究现有模块并供以后参考的简要列表:
| 模块 | 说明 |
|---|---|
| Qt Core | 这些是其他模块使用的核心非图形类。 |
| Qt GUI | 这些是图形用户界面(GUI)组件的基类。 这些包括 OpenGL。 |
| Qt Multimedia | 这些是音频,视频,广播和照相机功能的类。 |
| Qt Multimedia Widgets | 这些是基于小部件的类,用于实现多媒体功能。 |
| Qt Network | 这些是使网络编程更轻松,更可移植的类。 |
| Qt QML | 这些是 QML 和 JavaScript 语言的类。 |
| Qt Quick | 这是一个声明性框架,用于使用自定义用户界面构建高度动态的应用。 |
| Qt Quick Controls | 这些是可重用的基于 Qt Quick 的 UI 控件,用于创建经典的桌面样式用户界面。 |
| Qt Quick Dialogs | 这些类型可以在 Qt Quick 应用中创建系统对话框并与之交互。 |
| Qt Quick Layouts | 这些布局是用于在用户界面中安排基于 Qt Quick 2 的项目的项目。 |
| Qt SQL | 这些是使用 SQL 进行数据库集成的类。 |
| Qt Test | 这些是用于单元测试 Qt 应用和库的类。 |
| Qt Widgets | 这些是使用 C++ 小部件扩展 Qt GUI 的类。 |
有关更多信息,请参考这里。
请注意,不可能涵盖本书中的所有模块和所有类,并且可能不是一个好主意,并且在大多数情况下,我们将坚持我们需要的任何模块和类; 但是,到本书结尾时,您将很容易自己独自探索 Qt 中所有众多强大的模块和类。 在接下来的章节中,您将学习如何在您的项目中包括模块和类,因此,现在,让我们不要花太多时间来烦恼,而只专注于了解 Qt 的真正含义以及它在我们的脑海中所包含的内容。
Qt 附加组件
这些模块可能在所有平台上都可用或不可用。 这意味着它们用于开发特定功能,而不是 Qt Essentials 的通用性质。 这些类型的模块的一些示例是 Qt 3D,Qt 打印支持,Qt WebEngine,Qt 蓝牙等等。 您可以始终参考 Qt 文档以获取这些模块的完整列表,实际上,它们太多了,因此无法在此处列出。 在大多数情况下,您只需看一下就可以简要了解模块的用途。
有关此的更多信息,您可以参考这里。
附加模块
这些模块提供了附加功能,并获得了 Qt 的商业许可。 是的,您猜对了,这些模块仅在 Qt 的付费版本中可用,而在 Qt 的开源版本和免费版本中不提供,但它们的主要目的是帮助我们完成非常具体的任务,本书完全不需要。 您可以使用 Qt 文档页面获取列表。
有关此的更多信息,您可以参考这里。
技术预览模块
顾名思义,这些模块通常是在不能保证在所有情况下都能正常工作的状态下提供的。 它们可能包含也可能不包含 bug 或其他问题,并且它们仍在开发中,并且作为测试和反馈目的的预览。 一旦开发了模块并使其变得足够成熟,它就可以在前面提到的其他类别中使用,并且已从技术预览类别中删除。 在撰写本书时,这些类型的模块的一个示例是 Qt Speech,该模块旨在增加对 Qt 应用中文本到语音的支持。 如果您希望成为一名成熟的 Qt 开发人员,那么始终关注这些模块总是一个好主意。
有关此的更多信息,您可以参考这里。
Qt 支持的平台
当我们谈论开发应用时,该平台可能具有许多不同的含义,包括 OS 类型,OS 版本,编译器类型,编译器版本和处理器的架构(32 位,64 位,ARM 等)。 Qt 支持许多(如果不是全部)知名平台,并且通常足够快以在新平台发布时赶上它们。 以下是在撰写本书时(Qt 5.9)Qt 支持的平台列表。 请注意,您可能不会使用这里提到的所有平台,但可以使您了解 Qt 到底有多强大和跨平台:
| 平台 | 编译器 | 注解 |
|---|---|---|
| Windows | ||
| Windows 10(64 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | |
| Windows 10(32 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | |
| Windows 8.1(64 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | |
| Windows 8.1(32 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | |
| Windows 7(64 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | |
| Windows 7(32 位) | MSVC 2017,MSVC 2015,MSVC 2013,MinGW 5.3 | MinGW 构建的 gcc 5.3.0(32 位) |
| Linux/X11 | ||
| openSUSE 42.1(64 位) | GCC 4.8.5 | |
| RedHat 企业版 Linux 6.6(64 位) | GCC 4.9.1 | devtoolset-3 |
| RedHat 企业版 Linux 7.2(64 位) | GCC 5.3.1 | devtoolset-4 |
| Ubuntu 16.04(64 位) | Canonical 提供的 GCC | |
| (Linux 32/64 位) | GCC 4.8,GCC 4.9,GCC 5.3 | |
| MacOS | ||
| macOS 10.10、10.11、10.12 | 苹果提供的 Clang | |
| 嵌入式平台:嵌入式 Linux,QNX,INTEGRITY | ||
| 嵌入式 Linux | GCC | ARM Cortex-A,具有基于 GCC 的工具链的英特尔板 |
| QNX 6.6.0、7.0(armv7le 和 x86) | QNX 提供的 GCC | 主机:RHEL 6.6(64 位),RHEL 7.2(64 位),Windows 10(64 位),Windows 7(32 位) |
| INTEGRITY 11.4.x | 由 Green Hills 提供的 INTEGRITY | 主机:64 位 Linux |
| 移动平台:Android,iOS,通用 Windows 平台(UWP) | ||
| 通用 Windows 平台(UWP)(x86,x86_64,armv7) | MSVC 2017,MSVC 2015 | 主机:Windows 10 |
| iOS 8、9、10(armv7,arm64) | 苹果提供的 Clang | macOS 10.10 主机 |
| Android(API 等级:16) | Google 提供的 GCC,MinGW 5.3 | 主机:RHEL 7.2(64 位),macOS 10.12,Windows 7(64 位) |
参考这里
正如您将在下一节中看到的那样,我们将在 Windows 上使用 Microsoft Visual C++ 2015(或从此处开始,简称为 MSVC 2015)编译器,因为 Qt 和 OpenCV(您将在后面学习)都高度支持它。 我们还将在 Linux 上使用 GCC,在 MacOS 操作系统上使用 Clang。 所有这些都是免费和开源的工具,或者由操作系统提供者提供。 尽管我们的主要开发系统将是 Windows,但只要 Windows 与其他版本之间存在差异,我们就会介绍 Linux 和 MacOS 操作系统。 因此,整本书中的默认屏幕截图将是 Windows 的默认屏幕截图,并在它们之间存在任何严重差异的地方提供 Linux 和 MacOS 屏幕截图,而不仅仅是路径,按钮颜色等等之间的细微差异。
Qt Creator
Qt Creator 是用于开发 Qt 应用的 IDE(集成开发环境)的名称。 在本书中,我们还将使用 IDE 来创建和构建项目。 值得注意的是,可以使用任何其他 IDE(例如 Visual Studio 或 Xcode)创建 Qt 应用,并且 Qt Creator 并不是构建 Qt 应用的要求,而是 Qt 框架安装程序随附的轻量级功能强大的 IDE。 默认。 因此,它具有的最大优势是易于与 Qt 框架集成。
以下是 Qt Creator 的屏幕截图,显示了处于代码编辑模式的 IDE。 在下一章中将介绍有关如何使用 Qt Creator 的详细信息,尽管我们也将在本章稍后的部分中尝试一些测试,而无需过多介绍:

OpenCV 简介
现在,是时候介绍 OpenCV,开源计算机视觉库或框架(如果需要的话)了,因为 OpenCV 本身可以互换使用它们,并且在本书中也可能会发生。 但是,在大多数情况下,我们只会坚持使用 OpenCV。 好吧,让我们先听听它的真正含义,然后在需要的地方对其进行分解。
OpenCV 是一个开放源代码和跨平台的库,用于开发计算机视觉应用。 着眼于速度和性能,它在各种模块中包含数百种算法。 这些模块也分为两种类型:Main和Extra模块。 OpenCV 主要模块只是 OpenCV 社区内构建和维护的所有模块,它们是 OpenCV 提供的默认包的一部分。
这与 OpenCV 的额外模块形成了鲜明的对比,后者的模块或多或少是第三方库和包装程序的包装器,这些第三方库和接口将它们集成到 OpenCV 构建中。 以下是一些不同模块类型的示例,并分别对其进行了简要说明。 值得注意的是,OpenCV 中模块的数量(有时甚至是顺序)可以随着时间而改变,因此,最好记住的一点是,只要有可能,只要访问 OpenCV 文档页面即可。 错位,或者如果某些东西不在以前。
主要模块
这是一些 OpenCV 主模块的示例。 请注意,它们只是 OpenCV 中的少数几个模块(可能是使用最广泛的模块),而涵盖所有这些模块不在本书的讨论范围内,但是对 OpenCV 包含的内容有所了解是很有意义的,就像我们在本章前面的 Qt 中看到的东西。 他们来了:
- 核心功能或简称为
core模块包含所有其他 OpenCV 模块使用的所有基本结构,常量和函数。 例如,在此模块中定义了臭名昭著的 OpenCVMat类,在本书的其余部分中,我们几乎将在每个 OpenCV 示例中使用该类。 第 4 章,“Mat和QImage”将涵盖此和密切相关的 OpenCV 模块以及 Qt 框架的相应部分。 - 图像处理或
imgproc模块包含许多用于图像过滤,图像转换的算法,顾名思义,它用于一般图像处理。 我们将在第 6 章,“OpenCV 中的图像处理”中介绍此模块及其功能。 - 2D 特征框架模块或
features2d包含用于特征提取和匹配的类和方法。 它们将在第 7 章,“特征和描述符”中进行详细介绍。 - 视频模块包含用于主题的算法,例如运动估计,背景减法和跟踪。 该模块以及 OpenCV 的其他类似模块,将在第 9 章,“视频分析”中介绍。
额外模块
如前所述,额外模块主要是第三方库的包装器,这意味着它们仅包含集成这些模块所需的接口或方法。 示例附加模块将是text模块。 此模块包含用于在图像或 OCR(光学字符识别)中使用文本检测的接口,并且您还将需要这些第三方模块来进行此项工作,因此不涉及这些第三方模块。 作为本书的一部分,但您始终可以查看 OpenCV 文档以获取额外模块的更新列表及其使用方式。
有关此的更多信息,您可以参考这里。
OpenCV 支持的平台:如前所述,在应用开发的情况下,平台不仅仅是操作系统。 因此,我们需要知道 OpenCV 支持哪些操作系统,处理器架构和编译器。 OpenCV 是高度跨平台的,几乎像 Qt 一样,您可以为所有主要操作系统(包括 Windows,Linux,macOS,Android 和 iOS)开发 OpenCV 应用。 稍后我们将看到,我们将在 Windows 上使用 MSVC 2015(32 位)编译器,在 Linux 上使用 GCC,在 MacOS 上使用 Clang。 还需要注意的是,我们需要自己使用 OpenCV 的源代码来构建 OpenCV,因为目前,尚未为上述编译器提供预构建的二进制文件。 但是,正如您稍后将看到的,如果您具有正确的工具和说明,则可以轻松地为任何操作系统构建 OpenCV。
安装 Qt
在本节中,我们将执行必需的步骤,以在您的计算机上设置完整的 Qt SDK(软件开发工具包)。 我们将从在 Windows 操作系统上设置 Qt 开始,并在需要时记下 Linux(在我们的情况下为 Ubuntu,但对于所有 Linux 发行版几乎相同)和 MacOS 操作系统。 所以,让我们开始吧。
准备安装 Qt
为了能够安装和使用 Qt,我们需要首先创建一个 Qt 帐户。 尽管这不是强制性的,但仍强烈建议您这样做,因为您可以访问与此单一,统一和免费帐户有关的所有 Qt。 对于要安装的 Qt 的任何最新版本,您将需要 Qt 帐户凭据,只有在创建 Qt 帐户后才能使用。 为此,首先,您需要使用自己喜欢的浏览器访问 Qt 网站。 链接在这里。
这是它的屏幕截图:

在这里,您必须使用“登录”按钮下方的“创建 Qt 帐户”页面来使用您的电子邮件地址。 该过程几乎与网上任何类似的帐户创建过程相同。 可能会要求您输入验证码图像以证明您不是机器人,或者单击电子邮件中的激活链接。 完成 Qt 要求的过程后,您将拥有自己的 Qt 帐户用户,即您的电子邮件和密码。 请记录下来,因为稍后将需要它。 从现在开始,我们将其称为您的 Qt 帐户凭据。
在哪里获取?
至此,我们开始下载 Qt 开发所需的工具。 但是,从哪里开始呢? Qt 通过 Qt 下载网页维护所有正式发布的版本。 这里是一个链接。
如果打开浏览器并导航到上一个网页,将会看到一个非常简单的网页(类似于文件浏览器程序),然后您需要从中自行选择合适的文件:

Qt 在此处发布了其所有官方工具,并且您将看到,Last Modify列将一直在变化。 有些条目不是很常见,有些则更多。 目前,我们不会详细介绍每个文件夹包含的内容以及它们的用途,但是正如您将在本书后面看到的那样,我们所需的几乎所有工具都在一个安装文件中,和qt文件夹下。 因此,通过单击每个条目,导航到以下文件夹:qt/5.9/5.9.1/
您应该注意,访问此页面时可能会有一个较新的版本,或者该版本可能不再可用,因此您需要从前面提到的 Qt 下载页面开始,然后逐步进入最新的[ Qt version文件夹。 或者,您可以使用 Qt 下载主页中的存档链接始终访问以前的 Qt 版本。
以下是您需要从前面的文件夹下载的文件:
对于 Windows:qt-opensource-windows-x86-5.9.1.exe
对于 macOS:qt-opensource-mac-x64-5.9.1.dmg
对于 Linux:qt-opensource-linux-x64-5.9.1.run
这些是预先构建的 Qt 库,并包含每个提到的操作系统的完整 Qt SDK。 这意味着您无需自己构建 Qt 库即可使用它们。 这些安装文件通常包括以下内容以及我们将使用的工具:
- Qt Creator(版本 4.3.1)
- 每个操作系统支持的所有编译器和架构的预构建库:
- Windows 桌面,Windows Mobile
- Linux 桌面
- MacOS 桌面和 iOS
- Android(在所有平台上)
Windows 用户:Qt 安装包还包括其中包含的 MinGW 编译器,但是由于我们将使用另一个编译器,即 MSVC 2015,因此您实际上与它没有任何关系。 尽管安装它不会造成任何伤害。
如何安装?
您需要通过执行下载的安装文件来开始安装。 如果您使用的是 Windows 或 MacOS 操作系统,则只需运行下载的文件。 但是,如果您使用的是 Linux,则可能需要先使下载的.run文件成为可执行文件,然后才能实际运行它。 可以在 Linux 上执行以下命令以使安装程序文件可执行:
chmod +x qt-opensource-linux-x64-5.9.1.run
或者,您可以简单地右键单击.run文件,并使用属性对话框使其可执行:

请注意,即使没有下载任何内容,您仍然需要可以正常使用的互联网连接,这只是为了确认您的 Qt 帐户凭据。 运行安装程序将为您显示以下一系列需要完成的对话框。 只要对话框上的说明足够,请确保已阅读并提供所需的内容,然后按“下一步”,“同意”或类似按钮继续操作。 如以下屏幕快照所示,您需要提供 Qt 帐户凭据才能继续安装。 这些对话框在所有操作系统上都是相同的:

其余对话框未在此处显示,但是它们几乎是不言自明的,并且,如果您曾经在任何计算机上安装任何应用,那么您肯定会看到类似的对话框,并且通常不需要对其进行介绍。
Windows 用户
安装 Windows 版 Qt 时,请确保在“选择组件”对话框上,选中“msvc2015 32 位”选项旁边的复选框。 其余的是可选的,但是值得注意的是,安装所有平台(或在 Qt 中称为 Kits)通常需要太多空间,并且在某些情况下会影响 Qt Creator 的性能。 因此,只需确保选择您将真正使用的任何东西。 就本书而言,它只是您绝对需要的 msvc2015 32 位选项。
Windows 用户要注意的重要事项:您还需要安装至少启用了 C++ 桌面开发功能的 Visual Studio 2015。 Microsoft 为 Visual Studio 提供了不同类型的许可证。 您可以下载社区版本用于教育目的,这对于本书的示例来说绝对足够,并且是免费提供的,但是使用 Enterprise,Professional 或其他类型的 Visual Studio 也可以,只要它们具有 MSVC 2015 32 位编译器。
MacOS 用户
如果您没有在 Mac 上安装 XCode,则在为 Mac OS 安装 Qt 时,将面临以下对话框(或一个非常类似的对话框,具体取决于您使用的 MacOS 的版本)。

不幸的是,仅单击“安装”按钮是不够的,尽管看起来很明显,但是安装按钮比安装 Xcode 花费的时间少得多。 您仍然需要通过单击“获取 Xcode”按钮直接从 App Store 获取 Xcode 来确保在 Mac 上安装了 Xcode,或者在安装 Qt 时会遇到以下问题:

使用 App Store 安装最新版本的 Xcode(在撰写本书时,Xcode 8.3.3 已可用),然后继续进行 Qt 安装。
在“选择组件”对话框上,确保至少选择 MacOS 版本。 您不需要其余的组件,但是安装它们不会造成伤害,除了它可能占用您计算机的大量空间之外。
Linux 用户
在为 Linux 安装 Qt 时,在“选择组件”对话框上,确保选择(至少)桌面 GCC(32 位或 64 位,具体取决于您的操作系统)。 您会注意到,默认情况下将安装 Qt Creator,并且不需要检查任何选项。
安装完成后,您将在计算机上安装以下应用:
- Qt Creator:这是我们在整本书中将用来构建应用的主要 IDE。
- Qt Assistant:此应用用于查看 Qt 帮助文件。 它提供了查看 Qt 文档的有用功能。 尽管如此,Qt Creator 还提供了上下文相关的帮助,并且还具有自己的内置且非常方便的帮助查看器。
- Qt Designer:这用于使用 Qt 小部件设计 GUI。 同样,Qt Creator 也内置了此设计器,但是如果您更喜欢使用其他 IDE 而不是 Qt Creator,则仍可以使用 Designer 来帮助 GUI 设计过程。
- Qt Linguist:如果您要构建多语言应用,这将是非常有用的帮助。 Qt Linguist 有助于简化翻译并将翻译后的文件集成到您的版本中。
对于 Windows 和 MacOS 用户,这是 Qt 安装故事的结尾,但是 Linux 用户仍然需要多做一些事情,即安装应用开发,构建工具以及一些 Linux 所需的运行时库。 Qt 始终使用操作系统提供的编译器和构建工具。 默认情况下,Linux 发行版通常不包括那些工具,因为它们仅由开发人员使用,而未被普通用户使用。 因此,要安装它们(如果尚未安装),可以从终端运行以下命令:
sudo apt-get install build-essential libgl1-mesa-dev
您可以始终参考 Qt 文档页面以获取所有 Linux 发行版所需的命令,但是,在本书中,我们假定发行版为 Ubuntu/Debian。 但是,请注意,通常,所有 Linux 发行版的命令在模式上都非常相似。
有关此的更多信息,您可以参考这里。
测试 Qt 的安装
您现在可以安全地运行 Qt Creator 并使用它创建出色的应用。 现在,让我们确保我们的 Qt 安装正确运行。 现在就不用理会细节了,因为我们将在本书的过程中介绍所有细节,尤其是如果您认为自己不了解幕后的真实情况,请不要担心。 只需运行 Qt Creator 并按下显示的大New Project按钮,如下所示:

在接下来出现的窗口中,选择“应用”,“Qt Widgets 应用”,然后单击“选择”,如以下屏幕截图所示:

在下一个窗口中,您需要提供一个名称和文件夹(将在其中创建测试项目),然后单击“下一步”继续。 如果要为 Qt 项目使用专用文件夹,请确保选中“用作默认项目位置”复选框。 您只需执行一次,然后所有项目将在该文件夹中创建。 现在,我们只需要输入名称和路径,因为我们仅要测试 Qt 安装,然后单击“下一步”。 您将看到与以下屏幕快照类似的内容:

在下一个窗口中,您需要选择一个所谓的 Kit 来构建您的应用。 选择一个以Desktop Qt 5.9.1开头的名称,然后单击“下一步”。 根据在 Qt 安装过程中选择的组件,您在这里可能有多个选择,并且取决于系统上安装的操作系统和编译器,您可能有多个工具包,其名称以Desktop开头,因此确保选择我们将在本书中使用的编译器,如下所示:
- Windows 上的 msvc2015 32 位
- MacOS 上的 Clang
- Linux 上的 GCC
根据前面提到的工具选择正确的工具包后,可以单击“下一步”继续进行操作:

您真的不需要麻烦接下来出现的两个窗口,只需单击“下一步”就足以测试我们的 Qt 安装。 第一个窗口使创建新类更加容易,第二个窗口使您可以选择版本控制工具并跟踪代码中的更改:

在最后一个窗口上单击“完成”按钮后,您将进入 Qt Creator 中的“编辑”模式。 我们将在下一章介绍 Qt Creator 的不同方面,因此,现在,只需单击“运行”按钮(或按Ctrl + R)即可开始编译(并清空)您的测试应用,如下所示:

根据计算机的速度,完成构建过程将需要一些时间。 片刻之后,您应该看到测试(以及第一个)Qt 应用正在运行。 它只是一个空的应用,与下面的屏幕快照类似,其目的是确保我们的 Qt 安装能够按预期运行。 显然,在不同的操作系统上,空的 Qt 应用看上去可能与此略有不同,并且不同的视觉选项可能会影响整个颜色或窗口的显示方式。 不过,您新建的应用应该看起来与此处看到的窗口完全相同(或非常相似):

如果您的应用没有出现,请确保再次阅读说明。 另外,请确保没有任何冲突的 Qt 安装或其他可能干扰 Qt 安装的设置。 有关 Qt Creator 或其他 Qt 工具意外行为的答案,请始终参考文档页面和 Qt 社区。 长期以来,Qt 一直是一个开源项目,它已经成长为庞大而忠实的用户群体,他们渴望在互联网上分享他们的知识并回答 Qt 同行用户面临的问题。 因此,密切关注 Qt 社区是个好主意,因为您已经拥有可用于访问 Qt 论坛的统一 Qt 帐户。 这是您为继续进行 Qt 安装过程而创建的用户名和密码。
安装 OpenCV
在本章的这一部分,您将学习如何使用其源代码构建 OpenCV。 如您将在后面看到的,并且与本节的标题相反,我们并不是真正地以类似于 Qt 安装的方式来安装 OpenCV。 这是因为 OpenCV 通常不为所有编译器和平台提供预构建的二进制文件,而实际上它根本不为 MacOS 和 Linux 提供预构建的二进制文件。 在 OpenCV 的最新 Win 包中,仅包含适用于 64 位 MSVC 2015 的预构建二进制文件,这些二进制文件与我们将使用的 32 位版本不兼容,因此,自己构建 OpenCV 来了解如何进行安装是一个非常好的主意。 它还具有构建适合您需要的 OpenCV 框架库的优势。 您可能要排除一些选项以简化 OpenCV 安装,或者可能要为其他编译器(例如 MSVC 2013)进行构建。因此,有很多理由需要您自己从源代码构建 OpenCV。
准备构建 OpenCV
互联网上的大多数开放源代码框架和库,或者至少是希望保持 IDE 中立的库和库(这意味着可以使用任何 IDE 进行配置和构建的项目,以及不依赖于特定 IDE 的项目而工作),使用 CMake 或类似的make系统。 我猜这也会回答诸如“到底为什么我需要 CMake?”和“为什么他们不能仅仅提供库并使用它完成?”之类的问题,或类似的其他问题。 因此,我们需要 CMake 能够使用源配置和构建 OpenCV。 CMake 是一个开放源代码和跨平台应用,它允许配置和构建开放源代码项目(或应用,库等),并且您可以在上一节中提到的所有操作系统上下载和使用它。 在撰写本书时,可以从 CMake 网站下载页面下载 CMake 3.9.1 版。
在继续前进之前,请确保下载并安装在计算机上。 CMake 安装没有什么特别的需要注意的,除了您必须确保安装 GUI 版本这一事实外,这是我们将在下一部分中使用的内容,并且它是提供的链接中的默认选项。 较早。
从哪里获得 OpenCV?
OpenCV 在其网站的“发布”页面下维护其官方和稳定版本:

在这里,您始终可以找到适用于 Windows,Android 和 iOS 的最新版本的 OpenCV 源代码,文档和预构建的二进制文件。 随着新版本的发布,它们会添加到页面顶部。 在撰写本书时,版本 3.3.0 是 OpenCV 的最新版本,这就是我们将使用的版本。 因此,事不宜迟,您应该继续进行操作,并通过单击 3.3.0 版的“源”链接来下载源。 将source zip文件下载到您选择的文件夹中,将其提取出来,并记下提取的路径,因为稍后我们将使用它。
如何构建?
现在,我们拥有构建 OpenCV 所需的所有工具和文件,我们可以通过运行 CMake GUI 应用来启动该过程。 如果正确安装了 CMake,则应该能够从桌面,开始菜单或扩展坞运行它,具体取决于您的操作系统。
Linux 用户应在终端中运行以下命令,然后再继续进行 OpenCV 构建。 这些基本上是 OpenCV 本身的依赖关系,需要在配置和构建它之前就位:
sudp apt-get install libgtk2.0-dev and pkg-config
运行 CMake GUI 应用后,需要设置以下两个文件夹:
- “源代码在哪里”文件夹应设置为您下载和提取 OpenCV 源代码的位置
- 可以将“生成二进制文件的位置”文件夹设置为任何文件夹,但是通常在源代码文件夹下创建一个名为
build的子文件夹并将其选择为二进制文件文件夹
设置这两个文件夹后,您可以通过单击“配置”按钮继续前进,如以下屏幕截图所示:

单击配置按钮将启动配置过程。 如果构建文件夹尚不存在,可能会要求您创建该文件夹,您需要通过单击“是”按钮来对其进行回答。 如果您仍然觉得自己只是在重复书中的内容,请不要担心。 当您继续阅读本书和说明时,所有这些都会陷入。 现在,让我们仅关注在计算机上构建和安装 OpenCV。 考虑到此安装过程并不像单击几个“下一步”按钮那样简单,并且一旦开始使用 OpenCV,一切都会变得有意义。 因此,在接下来出现的窗口中,选择正确的生成器,然后单击“完成”。 有关每个操作系统上正确的生成器类型,请参阅以下说明:
Windows 用户:您需要选择Visual Studio 142015。请确保您未选择 ARM 或 Win64 版本或其他 Visual Studio 版本。
MacOS 和 Linux 用户:您需要选择Unix Makefile。
您将在 CMake 中看到一个简短的过程,完成后,您将能够设置各种参数来配置您的 OpenCV 构建。 有许多参数需要配置,因此我们将直接影响那些直接影响我们的参数。
确保选中BUILD_opencv_world选项旁边的复选框。 这将允许将所有 OpenCV 模块构建到单个库中。 因此,如果您使用的是 Windows,则只有一个包含所有 OpenCV 功能的 DLL 文件。 正如您将在后面看到的那样,当您要部署计算机视觉应用时,这样做的好处是仅使用一个 DLL 文件即可。 当然,这样做的明显缺点是您的应用安装程序的大小会稍大一些。 但是同样,易于部署将在以后证明更加有用。
更改构建参数后,您需要再次单击“配置”按钮。 等待重新配置完成,最后单击“生成”按钮。 这将使您的 OpenCV 内部版本可以编译。 在下一部分中,如果使用 Windows,MacOS 或 Linux 操作系统,则需要执行一些不同的命令。 因此,它们是:
Windows 用户:转到您先前在 CMake 中设置的 OpenCV 构建文件夹(在我们的示例中为c:\dev\opencv\build)。 应该有一个 Visual Studio 2015 解决方案(即 MSVC 项目的类型),您可以轻松地执行和构建 OpenCV。 您也可以立即单击 CMake 上“生成”按钮旁边的“打开项目”按钮。 您也可以只运行 Visual Studio 2015 并打开您刚为 OpenCV 创建的解决方案文件。
打开 Visual Studio 之后,需要从 Visual Studio 主菜单中选择“批量生成”。 就在Build下:

确保在Build列中为ALL_BUILD和INSTALL启用了复选框,如以下屏幕截图所示:

对于 MacOS 和 Linux 用户:在切换到在 CMake 中选择的Binaries文件夹后,运行终端实例并执行以下命令。 要切换到特定文件夹,您需要使用cd命令。 进入 OpenCV 构建文件夹(应该是打开 CMake 时选择的家庭文件夹)之后,需要执行以下命令。 系统将要求您提供管理密码,只需提供密码,然后按Enter即可继续构建 OpenCV:
sudo make
这将触发构建过程,并且可能需要花费一些时间,具体取决于您的计算机速度。 等到所有库的构建完成后,进度条将达到 100%。
漫长的等待之后,仅剩下一个命令可以为 MacOS 和 Linux 用户执行。 如果您使用的是 Windows,则可以关闭 Visual Studio IDE 并继续进行下一步。
MacOS 和 Linux 用户:构建完成后,在关闭终端实例之前,请在仍位于 OpenCV build文件夹中的情况下执行以下命令:
sudo make install
对于非 Windows 用户,这最后一个命令将确保您的计算机上已安装 OpenCV,并且可以完全使用。 如果您没有错过本节中的任何命令,则可以继续进行。 您已经准备好使用 OpenCV 框架来构建计算机视觉应用。
配置 OpenCV 的安装
还记得我们提到过 OpenCV 是一个框架,您将学习如何在 Qt 中使用它吗? 好吧,Qt 提供了一种非常易于使用的方法,可以在您的 Qt 项目中包括任何第三方库,例如 OpenCV。 为了能够在 Qt 中使用 OpenCV,您需要使用一种特殊的文件,称为 PRO 文件。 PRO 文件是用于添加第三方模块并将其包含在 Qt 项目中的文件。 请注意,您只需要执行一次此操作,在本书的其余部分中,您将在所有项目中使用此文件,因此,它是 Qt 配置中非常关键(但非常简单)的一部分。
首先在您选择的文件夹中创建一个文本文件。 我建议使用与 OpenCV 构建相同的文件夹,因为这可以帮助确保将所有与 OpenCV 相关的文件都放在一个文件夹中。 但是,从技术上来讲,该文件可以位于计算机上的任何位置。 将文件重命名为opencv.pri并使用任何文本编辑器将其打开,然后在 PRO 文件中写入以下内容:
Windows 用户:到目前为止,您的 OpenCV 库文件应该位于您先前在 CMake 上设置的 OpenCV 构建文件夹中。 build文件夹中应该有一个名为install的子文件夹,其中包含所有必需的 OpenCV 文件。 实际上,现在您可以删除所有其他内容,如果需要在计算机上保留一些空间,则只保留这些文件,但是将 OpenCV 源代码保留在计算机上始终是一个好主意,我们将在最后几章中特别需要它,并且将涵盖更高级的 OpenCV 主题。 因此,这是 PRO 文件中需要的内容(请注意路径分隔符,无论使用什么操作系统,都始终需要在 PRO 文件中使用/):
INCLUDEPATH += c:/dev/opencv/build/install/include
Debug: {
LIBS += -lc:/dev/opencv/build/install/x86/vc14/lib/opencv_world330d
}
Release: {
LIBS += -lc:/dev/opencv/build/install/x86/vc14/lib/opencv_world330
}
无需说明,在前面的代码中,如果在 CMake 配置期间使用了其他文件夹,则需要替换路径。
MacOS 和 Linux 用户:只需将以下内容放入opencv.pri文件中:
INCLUDEPATH += /usr/local/include
LIBS += -L/usr/local/lib \
-lopencv_world
Windows 用户还有一件事,那就是将 OpenCV DLLs文件夹添加到PATH环境变量中。 只需打开“系统属性”窗口,然后在PATH中添加一个新条目。 它们通常用;隔开,因此之后只需添加一个新的即可。 请注意,此路径仅与 Windows 操作系统相关,并且可以在其中找到 OpenCV 的DLL文件,从而简化了构建过程。 Linux 和 MacOS 的用户不需要为此做任何事情。
测试 OpenCV 的安装
最糟糕的时刻已经过去,我们现在准备深入研究计算机视觉世界,并开始使用 Qt 和 OpenCV 构建令人兴奋的应用。 尽管最后一步称为“测试 OpenCV”,但实际上它是您将要编写的第一个 Qt + OpenCV 应用,就像乍看起来一样简单。 在本节中,我们的目的不是打扰任何事情的工作方式以及幕后发生的事情,而只是确保我们正确地配置了所有内容,并避免在本书后面的内容中浪费与配置相关的问题。 如果您已按照说明进行了所有操作并以正确的顺序执行了所有指令,那么到现在为止,您不必担心任何事情,但是最好验证,这就是我们现在要做的。
因此,我们将使用一个非常简单的应用来验证我们的 OpenCV 安装,该应用将从硬盘读取图像文件并仅显示它。 同样,不要打扰任何与代码相关的细节,因为我们将在接下来的章节中介绍所有这些细节,而只是专注于手头的任务,即测试我们的 OpenCV 安装。 首先运行 Qt Creator,然后创建一个新的控制台应用。 在测试 Qt 安装之前,您已经完成了非常相似的任务。 您需要遵循完全相同的说明,除了必须使用 Qt Widget 之外,还必须确保选择Qt Console Application。 像以前一样重复所有类似的步骤,直到最终进入 Qt Creator 编辑模式。 如果询问您有关构建系统的信息,请选择qmake,默认情况下应选择qmake,因此您只需要继续前进即可。 确保为您的项目命名,例如QtCvTest。 这次,不用单击“运行”按钮,而是双击项目的 PRO 文件,您可以在 Qt Creator 屏幕左侧的资源管理器中找到该文件,然后在项目的 PRO 文件末尾添加以下行 :
include(c:/dev/opencv/opencv.pri)
请注意,实际上,这是应始终避免的硬编码类型,正如我们将在后面的章节中看到的那样,我们将编写适用于所有操作系统的更复杂的 PRO 文件。 无需更改任何一行; 但是,由于我们只是在测试我们的 OpenCV 安装,因此现在可以进行一些硬编码来简化一些事情,而不会因更多配置细节而使您不知所措。
因此,回到我们正在做的事情,当您通过按Ctrl + S保存 PRO 文件时,您会注意到快速的过程并在项目浏览器和opencv.pri文件将出现在资源管理器中。 您可以随时从此处更改opencv.pri的内容,但是您可能永远不需要这样做。 忽略类似注释的行,并确保您的 PRO 文件与我在此处的文件相似:
QT += core
QT -= gui
CONFIG += c++11
TARGET = QtCvTest
CONFIG += console
CONFIG -= app_bundle
TEMPLATE = app
SOURCES += main.cpp
DEFINES += QT_DEPRECATED_WARNINGS
include(c:/dev/opencv/opencv.pri)
项目的 PRO 文件中的这一行简单的代码基本上是本章所有工作的结果。 现在,我们只需在我们要使用 Qt 和 OpenCV 构建的每个计算机视觉项目中包含此简单代码段,就可以将 OpenCV 添加到我们的 Qt 项目中。
在接下来的章节中,我们将学习 Qt 中的 PRO 文件以及之前代码的所有内容。 但是,现在让我们继续前进,知道该文件负责我们项目的配置。 因此,最后一行几乎是不言自明的,仅表示我们要向我们的 Qt 项目添加 OpenCV 包含头文件和库。
现在,您实际上可以编写一些 OpenCV 代码。 打开您的main.cpp文件并更改其内容,使其与此类似:
#include <QCoreApplication>
#include "opencv2/opencv.hpp"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
using namespace cv;
Mat image = imread("c:/dev/test.jpg");
imshow("Output", image);
return a.exec();
}
默认情况下,您的main.cpp文件应该已经具有前面代码中的大部分内容,但是您会注意到顶部的include行和负责从我的计算机读取和显示测试图像的三行。 您可以替换任何其他图像的路径(只需确保暂时保留 JPG 或 PNG 文件),确保该图像文件存在并且可访问非常重要,否则,即使安装是正确的,我们的测试仍然可能失败。 整个代码几乎是不言自明的,但是再一次,由于我们只是测试我们的 OpenCV 版本,因此您现在不应该理会这些代码,因此只需按“运行”按钮以显示您的图像文件即可。 您应该在计算机上看到类似于以下屏幕截图的内容:

总结
在本章中,向您介绍了计算机视觉的一般概念以及 Qt 和 OpenCV 框架,并了解了它们的整体模块化结构,还简要了解了它们在所有平台上跨平台的重要性。 两者都支持。 您还学习了如何在计算机上安装 Qt 以及如何使用其源代码构建 OpenCV。 到目前为止,除了本章中提到的标准构建之外,您应该有足够的信心甚至可以尝试一些其他配置来构建 OpenCV。 通过简单地查看它们包含的文件夹和文件,探索这些巨大框架的一些未知而又深入的部分总是一个好主意。 最后,您学习了如何配置开发计算机以使用 Qt 和 OpenCV 构建应用,甚至还构建了第一个应用。 在下一章中,您将首先构建控制台应用,然后继续构建 Qt 小部件应用,以了解有关 Qt Creator 的更多信息。 您还将了解 Qt 项目的结构以及如何在 Qt 和 OpenCV 框架之间创建跨平台集成。 下一章将是本书中实际计算机视觉开发和编程示例的开始,并将为整本书中的动手示例奠定基础。
二、创建我们的第一个 Qt 和 OpenCV 项目
自从 Qt 和 OpenCV 框架引入开源社区和世界以来已经有很长时间了,但是直到最近人们才开始意识到将两者结合使用的好处,并且这种结合在计算机视觉专家中很受欢迎。 。 我们很幸运,因为我们处于这两个框架的发展历史的一个阶段,在这两个框架中,它们都已经足够成长,可以很容易地组合在一起,而几乎不需要付出任何努力。 这些框架的稳定性也不再是问题,它们有时被用来构建在非常敏感的硬件上运行的应用。 即使在互联网上进行简短搜索也可以证明这一点。 正如我们将在本章中了解到的那样,Qt Creator 已成为几乎完全成熟的 IDE,它提供了非常简单的机制来使用 OpenCV 集成和构建计算机视觉应用。 现在,我们已经了解了第 1 章,“OpenCV 和 Qt 简介”中遇到的所有安装和配置,我们将仅专注于使用 Qt 和 OpenCV 构建应用。
在本章中,我们将通过学习有关 Qt Creator IDE 以及如何使用它来创建项目的方式开始动手工作,因为在本书的其余部分和所构建的任何内容中,我们实际上都使用 Qt Creator。 您将了解它提供的所有好处,并了解为什么它在其简单性,外观和感觉上都是非常强大的 IDE。 您将了解 Qt Creator 的设置和详细信息,以及如何更改它们以满足您的需求。 您还将了解 Qt 项目文件,源代码,用户界面等。 在下一章第 3 章,“创建综合的 Qt + OpenCV 项目”时,我们将在下一章中使用 Qt Creator 构建应用时的所有幕后细节。 ,但在本章中,我们还将介绍一些有用的细节,以使您对真实项目的结构有清晰的了解。 在创建应用的上下文中将涵盖所有这些主题,以便您通过重复本章在此处学习的相同任务来更好地理解。
您应该注意,本章将学到的内容将帮助您节省大量时间,但是只有当您在计算机上真正地重复了所有这些并且实际上总是尝试使用它时,才会如此。 您可以使用 Qt Creator 进行 C++ 编程,甚至适用于非 Qt 应用。
最后,我们将通过创建一个实际的计算机视觉应用并将一些基本的图像处理算法应用于图像来结束本章。 本章的目的是使您为本书的其余部分做好准备,并使您熟悉本书中将要遇到的一些关键字,例如信号,插槽,小部件等。
在本章中,我们将介绍以下主题:
- 配置和使用 Qt Creator IDE
- 创建 Qt 项目
- Qt Creator 中的小部件
- 创建跨平台的 Qt + OpenCV 项目文件
- 使用 Qt Creator 设计用户界面
- 使用 Qt Creator 编写用户界面代码
什么是 Qt Creator?
Qt Creator 与 Qt 框架不是一回事。 是的,这是对的; 它只是由 Qt 框架创建的 IDE。 这使许多对这些术语有些陌生的人感到困惑。 那么,这到底意味着什么? 在一个非常基本的定义中,这意味着您可以使用 Qt Creator 或任何其他 IDE 来创建 Qt 应用。 在某个时刻,当 Qt 框架充斥着类和函数时,负责 Qt 的人们决定使用出色的 Qt 框架本身来创建 IDE,瞧! 一个没有操作系统和 C++ 编译器类型的 IDE 诞生了。 Qt Creator 是一个 IDE,它支持与 Qt 框架更好地集成,它是开源的(基本上意味着您可以免费使用它),它是跨平台的,并且几乎包含了 IDE 所需的所有工具。 这是 Qt Creator 中的欢迎模式的屏幕截图:

请注意,我们一定会使用所有 Qt Creator 功能,但是在深入研究之前先知道它的功能是一个好主意。 以下是 Qt Creator 的一些最重要的功能:
- 使用会话管理多个 IDE 状态
- 管理多个 Qt 项目
- 设计用户界面
- 编辑代码
- 在所有 Qt 支持的平台上构建和运行应用
- 调试应用
- 上下文相关帮助
根据您认为重要的内容,您可能可以在列表中添加更多项,但是前面的列表中提到的本质上是 IDE(集成开发环境)的定义,它应该是提供应用开发所需的和绝对必要的所有功能的工具。 另外,您始终可以查阅 Qt 文档以了解 Qt Creator 的其他功能。
IDE 概览
在本节中,我们将漫步 Qt Creator 的不同部分。 搬到新地方时,最好先了解周围的环境和周围环境。 您一开始可能不会注意到这些差异,但是实际上,这是非常相似的情况,您将逐渐意识到。 您将在整本书中使用 Qt Creator 环境,并且在阅读本书的整个过程中,基本上都将使用它,并且以后希望在您的职业生涯中长期从事个人项目, 或研究。 因此,让我们开始散步,开始接触事物,看看真正发生了什么。
让我们回到本章的第一张图片。 您在此处看到的是 Qt Creator 的初始屏幕,或者稍后将看到,它是 Qt Creator 的欢迎模式。 您可能会注意到,即使您安装了相同版本的 Qt,此处的图标和颜色还是与计算机上的略有不同。 不用担心,正如您稍后将看到的那样,它只是一个主题,您将学习如何根据自己的样式和喜好对其进行更改。 实际上,您会在整本书中看到来自 Qt 不同主题的屏幕截图,但请记住,这只是外观和感觉,与功能无关。 Qt Creator 的设计使其可以在其中的不同模式之间极其快速和轻松地进行切换。 切换到每种模式几乎完全改变了 Qt GUI 主要区域中的内容,并且完全达到了自己独特的目的。 让我们看看 Qt 支持哪些模式以及它们的用途。
Qt Creator 模式
Qt Creator 具有六种不同的模式,可以帮助您打开项目,编辑代码,设计用户界面等。 让我们浏览以下列表,然后尝试查看它们的确切含义:
- 欢迎
- 编辑
- 设计
- 调试
- 项目
- 帮助
我相信您已经注意到,在我们进一步详细介绍它们之前,您可以使用 Qt Creator 屏幕左侧的按钮在不同模式之间进行切换,如以下屏幕截图所示:

对于 Qt Creator 所做的几乎所有事情,都有专用的键盘快捷键,并且在不同模式之间进行切换也是这种情况。 您可以通过简单地将鼠标悬停在屏幕上而不用花一会儿时间来了解屏幕上所有内容的快捷键,然后会弹出一个提示框,告诉您有关此内容的更多信息,因此我们不会涵盖快捷键序列的完整列表,因为您可以使用上述方法轻松找到最新的热键。 如下面的屏幕截图所示,我将鼠标光标停留在“设计模式”按钮上,它确实告诉了我按钮的用途(即切换到设计模式)和键盘快捷键,即Ctrl + 3:

现在,我们将了解有关 Qt Creator 中不同模式及其用途的更多信息。 您应该注意,仅列出并遍历 Qt Creator 中每个功能的细节不在本书的讨论范围之内,但是我们肯定会涵盖本书中使用的 Qt Creator 的所有方面。 Qt Creator 和有关 Qt 的几乎所有内容都在迅速发展,最好始终关注文档页面并亲自尝试新功能或更改的功能。
欢迎模式
这是打开 Qt Creator 时的初始模式,可以始终使用左侧的Welcome按钮将其切换为:

关于此模式,最重要的注意事项是它实际上具有三种不同的子模式,分别提到如下:
- 项目
- 示例
- 指南
项目
此屏幕(或“欢迎”模式的子模式)可用于使用“新建项目”按钮创建新的 Qt 项目。 在第 1 章,“OpenCV 和 Qt 简介”中,您已经非常简短地体验了它的完成方式。 如果单击“打开项目”按钮,也可以打开计算机上保存的所有项目。 还有一个包含“最近的项目”的列表,该列表非常有用,既可以提醒您正在处理的内容,又可以作为访问它们的快捷方式。 在此模式下也有可见的会话,这是 Qt Creator 的一些最有趣的功能。 会话用于存储 IDE 的状态,并在以后需要时恢复该状态。 在本书中,我们不会理会会话,但是如果正确使用它们,它们将非常方便,并且可以在开发过程中节省大量时间。
借助示例,在 Qt Creator 中了解会话将非常简单。 假设您正在某个项目上,并且在 Qt Creator 中打开了一些项目,或者在代码中设置了一些断点,依此类推。 诸如此类的所有信息都存储在所谓的会话中,并且可以通过在会话之间进行切换来轻松地恢复。
如果单击“新建项目”按钮,将显示“新建项目”窗口,该窗口允许您根据要开发的内容选择项目类型(或模板)。 稍后您将看到,我们将仅使用Applications/Qt Widgets Application和Library/C++ Library选项,因为遍历所有可能的 Qt 项目模板不在本书的范围之内。 但是,如下面的屏幕快照所示,“新建项目”窗口包含三个部分,您只需选择它们,就可以对每种项目类型获得非常有用的描述。 使用第一个列表和第二个列表单击任何项目类型后(在下面的屏幕截图中),它们的描述将出现在第三个窗格中。 这是选择Qt Widgets Application项目类型时出现的描述(请参见下图,尤其是 3 号窗格):
- 为桌面创建 Qt 应用,包括基于 Qt Designer 的主窗口
- 预选用于构建应用的桌面 Qt(如果有)
- 支持平台:台式机
如您所见,它包含了非常有用的洞察力,有助于您了解此类型的模板适用于哪种项目。 尝试遍历所有各种选项以熟悉项目类型。 知道可能的项目类型是一个好主意,即使您不会立即使用它们。 以下是“新建项目”窗口的屏幕截图:

示例
这是我在 Qt Creator 中最喜欢的部分之一,毫无疑问,这是了解 Qt 并了解如何使用它的最重要的地方之一。 这里有很多带有解释的示例,只需单击一下即可进行构建。 示例中还有一个搜索栏,可用于使用搜索关键字搜索不同的示例。
指南
当前,这部分内容与示例非常相似,因为它旨在训练 Qt 开发人员,但主要区别在于它包含视频演示和示例。 确保您不时浏览它们,以获取有关新功能以及如何使用它们的最新信息。
在进入下一个 Qt Creator 模式(即编辑模式)之前,我们需要创建一个新项目。 在本章的其余部分中,我们将使用此示例,因为其余模式需要一个项目供我们使用。 现在您已经熟悉了欢迎模式,您可以继续创建一个新的 Qt Widgets 应用。 当我们测试 Qt 和 OpenCV 安装时,您已经在第 1 章,“OpenCV 和 Qt 简介”中创建了一个项目。 您需要重复完全相同的步骤。 这次,只需确保将项目命名为Hello_Qt_OpenCV即可。 这是您需要采取的步骤:
- 在“欢迎”模式中单击“新建项目”按钮,或按
Ctrl + N。 - 选择“应用”,然后在“新建项目”窗口中选择“Qt Widgets 应用”。
- 将项目名称设置为
Hello_Qt_OpenCV,然后选择要在其中创建文件夹。如果您之前已做过此操作,并且选中了“用作默认项目位置”复选框,则无需在此处更改任何有关“在其中创建”的内容。 然后,单击“下一步”。 - 选择唯一的桌面工具包选项,具体取决于您的操作系统。 然后,单击“下一步”。
- 保持类信息不变。 默认情况下,它应该是
MainWindow,这是确定的,然后单击“下一步”。 - 在项目管理页面上时,只需单击完成。 现在,您的项目已经准备就绪,您可以按照本章的其余示例和主题进行操作。
编辑模式
使用 Qt Creator 时,编辑模式可能是您将花费大部分时间的模式。 它主要用于代码编辑以及有关 Qt 项目基于文本的源文件的所有内容。 您始终可以使用屏幕右侧的“编辑”按钮切换到“编辑”模式,如下所示:

首先,让我们看一下下面的屏幕快照,该屏幕快照涉及在编辑模式下可见的不同窗格。 如您所见,共有三个部分。 以1突出显示的部分是主编码区域,2是左侧边栏,3是右侧边栏。 默认情况下,只有左侧边栏是可见的,但是您可以使用屏幕底部每一侧箭头所指向的小按钮来打开或关闭每个边栏。 关于每个窗格(侧边栏和中心的主要编码区域)要注意的最重要事实是,可以使用每个窗格顶部的箭头指出的按钮来拆分,复制或更改它们的模式。 侧:

主代码编辑区是一个轻量级的代码编辑器,它允许代码完成,代码突出显示和上下文相关帮助,这些基本上就是您将要使用的最重要的内容。 稍后将看到,您可以使用首选的颜色,字体等进行配置。 您也可以使用顶部的“拆分”按钮拆分代码编辑器区域,并同时处理多个文件。 尝试键入一些您知道的代码,类或任何 C++ 代码,并随代码完成一起玩,还可以尝试通过在代码编辑器中的鼠标光标位于 Qt 类上时按F1来使用上下文相关帮助。 这些工具将在很长一段时间内成为您最好的朋友,尤其是当您将来开始处理自己的项目时。
您可以为左右两侧的侧栏中的每个窗格选择以下不同的模式:
- 项目:这包含打开的项目及其包含文件的列表。
- 打开:这些文档仅显示您已经打开的文件。 您可以通过单击每个按钮旁边的
X按钮将其关闭。 - 书签:显示您在代码中创建的所有书签。 使用此窗格和功能可以在编程期间以及以后在测试和调试代码时节省大量时间。
- 文件系统:这基本上是文件浏览器窗格。 请注意,此窗格显示项目文件夹中的所有文件(如果您选中窗格中的相关复选框,甚至会显示隐藏文件),还可以用于浏览计算机上的其他文件夹,而不仅仅是当前项目。
- 类视图:可用于查看当前项目中类的层次结构。
- 大纲:与“类视图”不同,它显示了当前开源文件中所有方法和符号的层次结构,而不是整个项目。 在前面的屏幕截图中,该窗格是右侧栏上激活的窗格。
- 测试:这将显示项目中所有可用的测试。
- 类型和包含层次结构:从其标题可以猜到,它可以用于查看类的层次结构和包含的标头的层次结构。
重要的是要注意,根据您的编程习惯,您可能会经常使用某些窗格,而很少使用某些其他窗格,因此请确保将其设置为适合您自己的样式和需求,并在编程时节省大量时间。
设计模式
这是您进行所有用户界面设计的方式。 您可以使用 Qt Creator 屏幕左侧的“设计”按钮切换到“设计”模式。 请注意,如果此按钮显示为灰色(表示该按钮处于非活动状态),则需要首先选择一个用户界面文件(*.ui),因为使用设计器只能打开ui文件。 为此,您可以双击左窗格(“项目”窗格)中的mainwindow.ui文件:

设计模式包含功能强大的 GUI 设计器所需的所有工具。 它具有 WYSIWYG(所见即所得)类型的 GUI 编辑器,允许添加,删除,编辑或编写可从中添加或删除的 Qt 窗口小部件的代码。 用户界面。
Qt 小部件是 Qt 用户界面上最基本的组件类型。 基本上,用户界面上的所有内容(包括整个窗口本身),例如按钮,标签,文本框,都是 Qt 窗口小部件。 Qt 窗口小部件都是QWidget类的所有子类,这使它们可以接收用户输入事件(例如,鼠标和键盘事件),并在用户界面上自行绘制(或绘制)。 因此,必须从QWidget类中子类化任何具有可视外观并打算放在用户界面上的 Qt 类。 在整本书中,您将学习许多 Qt 小部件类,但是一些示例将是QPushButton,QProgressBar,QLineEdit等。 他们的目的几乎可以从他们的名字中立即辨认出来。
请注意,所有 Qt 类(没有任何明显的例外)的名称都以Q(大写)开头。
在设计模式下有一个 Qt Creator 的屏幕截图(如下所示)。 如此处所示,它与我们在“编辑”模式下看到的内容非常相似,屏幕分为三个主要部分。 您可以在中间的主要区域以任何方式拖放,调整大小,删除或直观地编辑用户界面。 在屏幕的左侧,有可以添加到用户界面的小部件列表。 您应该尝试拖放其中的一些(基本上是其中的任何一个),只是为了使设计师大致上满意并更好地了解其工作原理。 在本书的后面,我们将设计很多不同的用户界面,并逐步为您介绍许多功能,但是最好自己尝试一些设计并至少熟悉一些功能,这是一个很好的主意。 这一切的感觉。 在屏幕右侧,您可以在用户界面上查看小部件的分层视图,并修改每个小部件的属性。 因此,如果继续进行操作,并在用户界面中添加了一些小部件,您会注意到,无论何时选择其他小部件,属性及其值都会根据该特定小部件而变化。 在这里,您可以编辑设计者可用的小部件的所有属性:
与大多数其他 IDE 一样,您通常可以通过许多不同的途径来实现相同的目标。 例如,您可以使用编辑器从代码中设置窗口小部件的大小,甚至可以采用不推荐的方式在文本编辑器中修改其 UI 文件。 您应该能够根据自己的特定需求做出决定,因为没有一种方法是最好的,而且它们都只是在不同情况下适用。 通常,最好在用户界面编辑器中设置初始属性,并根据需要在整个代码中更新它们的值。 您将在本章的后面部分中对此进行了解。

在用户界面设计器中央部分的底部,您可以看到“动作编辑器”和“信号与插槽编辑器”。 要了解它们的工作原理,以及实际上 Qt 的工作原理,我们需要首先了解 Qt 中的信号和插槽。 因此,从定义信号和插槽的概念开始我们的第一次相遇开始,然后再通过一个真实的示例进行体验,这是一个非常好的主意。
Qt 框架对标准 C++ 编程的最重要添加是信号和插槽机制,这也是使 Qt 如此易于学习且功能强大的原因。 这绝对也是 Qt 与其他框架之间最重要的区别。 可以将它视为 Qt 对象和类之间的消息传递方法(或顾名思义,只是发出信号)。 每个 Qt 对象都可以发出可以连接到另一个(或相同)对象中的插槽的信号。 让我们通过一个简单的例子进一步分解它。 QPushButton是一个 Qt 小部件类,您可以将其添加到 Qt 用户界面中以创建按钮。 它包含许多信号,包括明显的按下信号。 另一方面,在我们创建Hello_Qt_OpenCV项目时自动创建的MainWindow(以及所有 Qt 窗口)包含一个名为close的插槽,可用于简单地关闭项目的主窗口 。 我相信您可以想象如果将按钮的按下信号连接到窗口的关闭插槽会发生什么。 有很多方法可以将信号连接到插槽,因此,从现在开始,在本书的其余部分中,只要需要在示例中使用它们,我们就会学习它们的每一种。
设计用户界面
从这里开始学习如何将 Qt 小部件添加到用户界面,并使它们对用户输入和其他事件做出反应。 Qt Creator 提供了非常简单的工具来设计用户界面并为其编写代码。 您已经看到了设计模式下可用的不同窗格和工具,因此我们可以从示例开始。 通过选择mainwindow.ui文件(这是我们从编辑模式进入主窗口的用户界面文件),确保首先切换到设计模式(如果尚未进入设计模式)。
在设计模式下,您可以在用户界面上查看可使用的 Qt 小部件列表。 从这些图标和名称可以立即识别出大多数这些小部件的用途,但是仍然有一些特定于 Qt 的小部件。 这是默认情况下 Qt Creator 中代表所有可用布局和小部件的屏幕截图:

以下是对 Qt Creator 设计模式(或从现在开始简称为 Designer)中可用小部件的简要说明,如前面的屏幕快照所示。 在设计器模式下,小部件基于其行为的相似性进行分组。 在继续进行列表操作时,请自己亲自尝试设计器中的每个功能,以感觉到将它们放置在用户界面上时的外观。 为此,您可以使用设计器模式将每个窗口小部件拖放到窗口上:
- 布局:这些布局用于管理窗口小部件的显示方式。 在外观上,它们是不可见的(因为它们不是
QWidget子类),并且它们仅影响添加到它们的小部件。 请注意,布局根本不是小部件,它们是用来管理小部件的显示方式的逻辑类。 尝试在用户界面上放置任何布局小部件,然后在其中添加一些按钮或显示小部件,以查看其布局如何根据布局类型进行更改。 查看每个示例图片以了解它们的行为。- 垂直布局:它们用于具有垂直布局,即一列小部件。 (此布局的等效 Qt 类称为
QVBoxLayout)。 以下是它的屏幕截图:
- 垂直布局:它们用于具有垂直布局,即一列小部件。 (此布局的等效 Qt 类称为




- 分隔符:类似于布局,它们在视觉上不可见,但会影响将其他窗口小部件添加到布局时的显示方式。 请参阅示例图像,并确保自己尝试在小部件之间尝试使用两个垫片。 间隔符的类型为
QSpacerItem,但是通常,它们绝不能直接在代码中使用。- 水平分隔符:这些可用于在一行中的两个小部件之间插入一个空格:


- 按钮:这些只是按钮。 它们用于提示操作。 您可能会注意到,单选按钮和复选框也在该组中,这是因为它们都继承自
QAbstractButton类,该类是一个抽象类,提供了类按钮小部件所需的所有接口。 - 按钮:这些按钮可用于在用户界面上添加带有文本和/或图标的简单按钮(此小部件的等效 Qt 类称为
QPushButton)。 - 工具按钮:这些按钮与按钮非常相似,但通常添加到工具栏中
Qt 窗口共有 3 种不同类型的条(实际上,一般来说是 Windows),它们在小部件工具箱中不可用,但是可以通过右键单击 Windows 中的窗口来创建,添加或删除它们。 设计器模式,然后从右键菜单中选择相关项目。 它们是:
1.菜单栏(QMenuBar)
2.工具栏(QToolBar)
3.状态栏(QStatusBar)
菜单栏是显示在窗口顶部的典型水平主菜单栏。 菜单中可以有任意数量的项目和子项目,每个项目和子项目都可以触发一个动作(QAction)。 您将在接下来的章节中了解有关操作的更多信息。 以下是菜单栏示例:

工具栏是一个可移动面板,其中可以包含与特定任务相对应的工具按钮。 这是一个示例工具栏。 请注意,它们可以在 Qt 窗口内移动甚至移出:

状态栏是底部的一个简单的水平信息栏,对于大多数基于窗口的应用是通用的。 每当在 Qt 中创建一个新的主窗口时,这三种类型的条形都将添加到该窗口中。 请注意,一个窗口上只能有一个菜单栏和一个状态栏,但是可以有任意数量的状态栏。 如果不需要它们,则需要将它们从“设计器”窗口右侧的对象层次结构中删除。 现在您已经熟悉了 Qt 中的三个不同的条形,可以从“Qt 欢迎”模式中的示例中搜索Application Example,以进一步了解它们,以及是否可以进一步自定义它们。
-
命令链接按钮:这是 Windows Vista 风格的命令链接按钮。 它们基本上是用于在向导中代替单选按钮的按钮,因此,当按下命令链接按钮时,这类似于使用单选框选择一个选项,然后在向导对话框中单击“下一步”。 (此小部件的等效 Qt 类称为
QCommandLinkButton)。 -
对话框按钮框:如果您希望按钮适应对话框中的操作系统样式,这将非常有用。 它有助于以一种更适合系统当前样式的方式在对话框上显示按钮(此小部件的等效 Qt 类称为
QDialogButtonBox)。 -
项目视图(基于模型):这基于模型视图控制器(MVC)设计模式; 它们可用于表示不同类型容器中的模型数据。
如果您完全不熟悉 MVC 设计模式,那么我建议您在这里停顿一下,首先通读一本综合性的文章,以确保至少对它是什么以及如何使用 MVC(尤其是 Qt)有一个基本的了解。 Qt 文档中名为“模型/视图编程”的文章,您可以从 Qt Creator 中的“帮助”模式访问该文章。 出于本书的目的,我们不需要非常详细的信息和对 MVC 模式的理解。 但是,由于它是非常重要的架构,您肯定会在以后的项目中遇到它,因此我建议您花一些时间来学习它。 不过,在第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们将介绍 Qt 和 OpenCV 中使用的不同设计模式,但我们将主要关注本书的目的,因为它是一个非常全面的主题,并且遍历本书中所有可能的设计模式将完全没有用。
- 项目小部件(基于项目):这类似于基于模型的项目视图,不同之处在于它们不是基于 MVC 设计模式,并且它们提供了简单的 API 来添加,删除或修改他们的项目
- 列表小部件:类似于列表视图,但是具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
QListWidget) - 树形小部件:这类似于树形视图,但具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
QTreeWidget) - 表格小部件:这类似于表视图,但是具有基于项目的 API,用于添加,删除和修改其项目(此窗口小部件的等效 Qt 类称为
QTableWidget)
- 列表小部件:类似于列表视图,但是具有基于项目的 API,可以添加,删除和修改其项目(此小部件的等效 Qt 类称为
- 容器:这些容器用于在用户界面上对小部件进行分组。 容器可以包含小部件,因为可以从其标题中猜测
- 分组框:这是一个带有标题和边框的简单分组框(此小部件的等效 Qt 类称为
QGroupBox)。 - 滚动区域:这提供了一个可滚动区域,非常适合显示由于屏幕尺寸小或可见数据量大而无法完全看到的内容(此小部件的等效 Qt 类称为
QScrollArea) 。 - 工具箱:可用于将小部件分组在不同选项卡的列中。 选择每个选项卡将显示(扩展)其包含的小部件,并隐藏(折叠)其他选项卡的内容。 (此小部件的等效 Qt 类称为
QToolBox)。 - 选项卡小部件:可用于在选项卡式页面中显示不同组的小部件。 通过单击每个页面(或一组窗口小部件)的相关选项卡(此窗口小部件的等效 Qt 类称为
QTabWidget),可以切换到该页面。 - 堆叠式窗口小部件:与“标签”窗口小部件类似,但是始终只有一页(或窗口小部件组)可见。 当您希望将不同的用户界面设计到一个文件中并根据用户操作在它们之间进行切换(使用代码)时,此功能特别有用(此小部件的等效 Qt 类称为
QStackedWidget)。 - 框架:可用作我们要为其构建框架的小部件的占位符。 此窗口小部件也是具有框架的所有窗口小部件的基类(此窗口小部件的等效 Qt 类称为
QFrame)。 - 小部件:与
QWidget类相同,它是所有 Qt 小部件的基本类型。 这个小部件几乎不包含任何内容,当我们要创建自己的小部件类型(除了现有的 Qt 小部件)时,它很有用。 - MDI 区域:可用于在窗口或 Qt 小部件(此小部件的等效 Qt 类称为
QMdiArea)内创建所谓的“多文档接口”。
- 分组框:这是一个带有标题和边框的简单分组框(此小部件的等效 Qt 类称为

要使用 Designer 在 MDI 区域内创建新窗口,只需在空白处单击鼠标右键,然后从菜单中选择“添加子窗口”。 同样,“下一个子窗口”,“上一个子窗口”,“级联”,“平铺”和“子窗口/删除”都是仅在右键单击“MDI 区域”小部件时才有效的选项。
QAxWidget仅适用于 Windows OS 上的用户。 但是,即使在 Windows 上,仅将QAxWidget添加到窗口中也无法使它正常工作,因为它依赖于称为axcontainer的 Qt 模块。 目前,您可以跳过将此小部件添加到窗口的操作,但是在本章稍后介绍了如何向您的 Qt 项目中添加不同的 Qt 模块之后,您可以稍后再次尝试。
- 输入小部件:听起来完全一样。 您可以使用以下小部件获取用户输入数据。
- 组合框:有时称为下拉列表; 它可以用来选择列表中的选项,而屏幕上的空间却很少。 任何时候,只有选定的选项可见。 用户甚至可以输入自己的输入值,具体取决于其配置。 (此小部件的等效 Qt 类称为
QComboBox): - 字体组合框:类似于组合框,但可用于选择字体系列。 字体列表是使用计算机上的可用字体创建的。
- 行编辑:可用于输入和显示单行文本(此小部件的等效 Qt 类称为
QLineEdit)。 - 文本编辑:可用于输入和显示多行富文本格式。 重要的是要注意,这个小部件实际上是成熟的 WYIWYG 富文本编辑器(此小部件的等效 Qt 类称为
QTextEdit)。 - 纯文本编辑:可用于查看和编辑多行文本。 可以将其视为类似于记事本的简单小部件(此小部件的等效 Qt 类称为
QPlainTextEdit)。 - 旋转框:用于输入整数或离散的值集,例如月份名称(此小部件的等效 Qt 类称为
QSpinBox)。 - 双重旋转框:类似于旋转框,但是它接受双精度值(此小部件的等效 Qt 类称为
QDoubleSpinBox)。 - 时间编辑:可用于输入时间值。(此小部件的等效 Qt 类称为
QTimeEdit)。 - 日期编辑:可用于输入日期值(此小部件的等效 Qt 类称为
QDateEdit)。 - 日期/时间编辑:可用于输入日期和时间值(此小部件的等效 Qt 类称为
QDateTimeEdit)。 - 拨盘:类似于滑块,但具有圆形和类似拨盘的形状。 它可用于输入指定范围内的整数值(此小部件的等效 Qt 类称为
QDial)。 - 水平/垂直条:可用于添加水平和垂直滚动功能(此小部件的等效 Qt 类称为
QScrollBar)。 - 水平/垂直滑块:可用于输入指定范围内的整数值(此小部件的等效 Qt 类称为
QSlider)。 - 按键序列编辑:可用于输入键盘快捷键(此小部件的等效 Qt 类称为
QKeySequenceEdit)。
- 组合框:有时称为下拉列表; 它可以用来选择列表中的选项,而屏幕上的空间却很少。 任何时候,只有选定的选项可见。 用户甚至可以输入自己的输入值,具体取决于其配置。 (此小部件的等效 Qt 类称为
不应将此与QKeySequence类混淆,该类根本不是小部件。 QKeySequenceEdit用于从用户那里获取QKeySequence。 在拥有QKeySequence之后,我们可以将其与QShortcut或QAction类结合使用以触发不同的函数/插槽。 本章稍后将介绍信号/插槽的介绍。
- 显示小部件:可用于显示输出数据,例如数字,文本,图像,日期等:
- 标签:可用于显示数字,文本,图像或电影(此小部件的等效 Qt 类称为
QLabel)。 - 文本浏览器:它与
Text Edit小部件几乎相同,但是具有在链接之间导航的附加功能(此小部件的等效 Qt 类称为QTextBrowser)。 - 图形视图:可用于显示图形场景的内容(此小部件的等效 Qt 类称为
QGraphicsView)。
- 标签:可用于显示数字,文本,图像或电影(此小部件的等效 Qt 类称为
我们整本书中将使用的最重要的小部件可能是图形场景(或QGraphicsScene),它将在第 5 章,“图形视图框架”中进行介绍。
请注意,OpenGL 是计算机图形学中一个完全独立且高级的主题,它完全不在本书的讨论范围内。 但是,如前所述,最好了解 Qt 中存在的工具和小部件,以便您进行进一步的研究。
QML 的简介将在第 12 章, “Qt Quick 应用”中介绍。 现在,请确保我们没有在用户界面中添加任何QQuickWidget小部件,因为我们需要向项目中添加其他模块才能使其正常工作。 本章将介绍如何向 Qt 项目添加模块。
Hello_Qt_OpenCV
现在,我们可以开始为Hello_Qt_OpenCV项目设计用户界面。 明确项目规格清单,然后根据需求设计用户友好的 UI,然后在纸上画出用户界面(或者如果不是大项目,请牢记在心)始终是一个好主意。 ,最后开始使用 Designer 创建它。 当然,此过程需要使用现有 Qt 小部件的经验以及创建自己的小部件的足够经验,但这最终会发生,您只需要继续练习即可。
因此,首先,让我们回顾一下我们需要开发的应用的规范。 比方说:
- 此应用必须能够将图像作为输入(接受的图像类型必须至少包括
*.jpg,*.png和*.bmp文件)。 - 此应用必须能够应用模糊过滤器。 用户必须能够选择中值模糊或高斯模糊类型来过滤输入图像(使用默认的一组参数)。
- 此应用必须能够保存输出图像和输出图像的文件类型(或扩展名,换句话说),并且它必须可由用户选择(
*.jpg,*.png或*.bmp)。 - 用户应该能够选择在保存时查看输出图像。
- 重新启动应用时,应保留并重新加载用户界面上设置的所有选项,包括模糊过滤器类型以及最后打开和保存的图像文件。
- 当用户要关闭应用时,应提示他们。
对于我们的情况,这应该足够了。 通常,您不应超额交付或交付不足。 这是设计用户界面时的重要规则。 这意味着您应确保所有要求均已成功满足,同时,您还没有在要求列表中添加不需要(或不需要)的任何内容。
对于这样的需求(或规格)列表,可能有无数的用户界面设计; 但是,这是我们将创建的一个。 请注意,这是我们的程序在执行时的外观。 显然,标题栏和样式可能会有所不同,具体取决于操作系统,但这基本上就是:

它看起来很简单,它包含了该任务所需的所有组件,并且界面几乎是不言自明的。 因此,将要使用此应用的人实际上并不需要了解很多功能,他们只需猜测所有输入框,单选按钮,复选框等的用途。
在 Designer 上查看时,这是相同的 UI:

现在该为我们的项目创建用户界面了:
-
要创建此用户界面,首先需要从主窗口中删除菜单栏,状态栏和工具栏,因为我们不需要它们。 右键单击顶部的菜单栏,然后选择“删除菜单栏”。 接下来,右键单击窗口上的任意位置,然后选择“删除状态栏”。 最后,右键单击顶部的工具栏,然后单击“删除工具栏”。
-
现在,在您的窗口中添加一个水平布局; 这是上一图像顶部可见的布局。 然后,在其中添加标签,行编辑和按钮,如上图所示。
-
通过双击标签并输入
Input Image :来更改标签的文本。 (这与选择标签并使用屏幕右侧的属性编辑器将文本属性值设置为Input Image :相同。)
几乎所有具有text属性的 Qt 小部件都允许使用其文本进行这种类型的编辑。 因此,从现在开始,当我们说Change the text of the widget X to Y时,这意味着双击并设置文本或使用设计器中的属性编辑器。 我们可以很容易地将此规则扩展到属性编辑器中可见的窗口小部件的所有属性,并说Change the W of X to Y。 在这里,显然,W是设计者的属性编辑器中的属性名称,X是小部件名称,Y是需要设置的值。 这将在设计 UI 时为我们节省大量时间。
- 添加一个组框,然后添加两个单选按钮,类似于上图所示。
- 接下来,添加另一个水平布局,然后在其中添加
Label,Line Edit和Push Button。 这将是在复选框正上方的底部看到的布局。 - 最后,在窗口中添加一个复选框。 这是底部的复选框。
- 现在,根据前面的图像,更改窗口上所有小部件的文本。 您的 UI 即将准备就绪。 现在,您可以通过单击屏幕左下方的“运行”按钮来尝试运行它。 确保您没有按下带有错误的“运行”按钮。 这是一个:

这将产生与您之前看到的相同的用户界面。 现在,如果您尝试调整窗口的大小,您会注意到在调整窗口大小或最大化窗口时,所有内容都保持原样,并且它不会响应应用大小的更改。 要使您的应用窗口响应大小更改,您需要为centralWidget设置布局。 还需要对屏幕上的分组框执行此操作。
Qt 小部件均具有centralWidget属性。 这是 Qt 设计器中特别用于 Windows 和容器小部件的东西。 使用它,您可以设置容器或窗口的布局,而无需在中央窗口小部件上拖放布局窗口小部件,只需使用设计器顶部的工具栏即可:

您可能已经注意到工具栏中的四个小按钮(如前面的屏幕快照所示),它们看起来与左侧小部件工具箱中的布局完全一样(如下所示):

因此,让我们就整本书中的简单快速解释达成另一条规则。 每当我们说Set the Layout of X to Y时,我们的意思是首先选择小部件(实际上是容器小部件或窗口),然后使用顶部工具栏上的布局按钮选择正确的布局类型。
-
根据前面信息框中的描述,选择窗口(这意味着,单击窗口上的空白而不是任何小部件上的空白)并将其布局设置为
Vertical。 -
对组框执行相同操作; 但是,这一次,将布局设置为水平。 现在,您可以尝试再次运行程序。 如您现在所见,它会调整其所有小部件的大小,并在需要时移动它们,以防更改窗口大小。 窗口内的组框也发生了同样的情况。
-
接下来需要更改的是小部件的
objectName属性。 这些名称非常重要,因为在 C++ 代码中使用它们来访问窗口上的小部件并与其进行交互。 对于每个小部件,请使用以下屏幕截图中显示的名称。 请注意,该图像显示了对象层次结构。 您还可以通过双击对象层次结构窗格中的小部件来更改objectName属性:

从理论上讲,您可以为objectName属性使用任何 C++ 有效的变量名,但实际上,最好始终使用有意义的名称。 考虑对本书中使用的变量或小部件名称遵循相同或相似的命名约定。 它基本上是 Qt 开发人员遵循的命名约定,它还有助于提高代码的可读性。
编写 Qt 项目的代码
现在,我们的用户界面已经完全设计好了,我们可以从为应用编写代码开始。 现在,我们的应用基本上只不过是一个用户界面,它实际上什么也不做。 我们首先需要将 OpenCV 添加到我们的项目中。 在第 1 章,“OpenCV 和 Qt 简介”中,您已经看到了将 OpenCV 添加到 Qt 项目的简短介绍。 现在,我们将采取进一步的措施,并确保可以按照第 1 章中的说明正确安装和配置 OpenCV,并且可以在所有三个主要操作系统上编译和构建我们的项目,而无需进行任何更改。“OpenCV 和 Qt 简介”。
因此,首先在代码编辑器中打开项目的 PRO 文件,这将是 Qt Creator 中的“编辑”模式。 您可能已经注意到,它称为Hello_Qt_OpenCV.pro。 您需要在该文件的末尾添加以下代码:
win32: {
include("c:/dev/opencv/opencv.pri")
}
unix: !macx {
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
unix: macx {
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
-lopencv_world
}
注意右括号前的代码; win32表示 Windows 操作系统(仅适用于桌面应用,不适用于 Windows 8、8.1 或 10 特定应用),unix: !macx表示 Linux 操作系统,unix: macx表示 MacOS 操作系统。
您的PRO文件中的这段代码允许 OpenCV 包含在内并在您的 Qt 项目中可用。 还记得我们在第 1 章,“OpenCV 和 Qt 简介”中创建了一个PRI文件吗? Linux 和 MacOS 用户可以将其删除,因为在那些操作系统中不再需要该文件。 只有 Windows 用户可以保留它。
请注意,在 Windows OS 中,您可以将前面的include行替换为 PRO 文件的内容,但这在实践中并不常见。 另外,值得提醒的是,您需要在PATH中包含 OpenCV DLLs 文件夹,否则当您尝试运行它时,应用将崩溃。 但是,它仍然可以正确编译和构建。 要更加熟悉 Qt PRO 文件的内容,可以在 Qt 文档中搜索qmake并阅读有关内容。 不过,我们还将在第 3 章,“创建综合的 Qt + OpenCV 项目”中进行简要介绍。
我们不会讨论这些代码行在每个操作系统上的确切含义,因为这不在本书的讨论范围之内,但是值得注意并足以知道何时构建应用(换句话说,编译和编译)。 链接),这些行将转换为所有 OpenCV 头文件,库和二进制文件,并包含在您的项目中,以便您可以轻松地在代码中使用 OpenCV 函数。
现在我们已经完成了配置工作,让我们开始为用户界面上的每个需求及其相关的小部件编写代码。 让我们从inputPushButton开始。
从现在开始,我们将使用其唯一的objectName属性值引用用户界面上的任何窗口小部件。 将它们视为可以在代码中使用以访问这些小部件的变量名。
这是我们项目的编码部分所需的步骤:
- 再次切换到设计器,然后右键单击
inputPushButton。 然后,从出现的菜单中选择“转到插槽...”。 将显示的窗口包括此小部件发出的所有信号。 选择pressed(),然后单击确定:

- 您会注意到,您是从设计器自动转到代码编辑器的。 另外,现在
mainwindow.h文件中添加了新函数。 - 在
mainwindow.h中,添加了以下内容:
private slots:
void on_inputPushButton_pressed();
这是自动添加到mainwindow.cpp的代码:
void MainWindow::on_inputPushButton_pressed()
{
}
因此,显然需要在刚刚创建的on_inputPushButton_pressed()函数中编写负责inputPushButton的代码。 如本章前面所述,这是将信号从小部件连接到另一个小部件上的插槽的多种方法之一。 让我们退后一步,看看发生了什么。 同时,请注意刚刚创建的函数的名称。 inputPushButton小部件具有一个称为被按下的信号(因为它是一个按钮),该信号仅在被按下时才发出。 在我们的单个窗口小部件(MainWindow)中创建了一个新插槽,称为on_inputPushButton_pressed。 很方便,想到的第一个问题是,如果我自己在mainwindow.h和mainwindow.cpp中编写了这些代码行,而不是右键单击小部件并选择“转到插槽”,将会发生什么情况? ,答案是,这是完全一样的。 因此,总而言之,每当inputPushButton小部件发出按下信号时,Qt 都会自动理解它需要在on_inputPushButton_pressed()中执行代码。 在 Qt 开发中,这被称为按名称连接插槽,它仅遵循以下约定自动将信号连接至插槽on_objectName_signal(parameters)。
在此,objectName应该替换为发送信号的小部件的objectName属性的值,signal替换为信号名称,parameters替换为确切的信号编号和参数类型。
现在我们知道如何将窗口上的窗口小部件的信号连接到窗口本身的插槽,或者换句话说,既然我们知道必须添加一个函数并为窗口小部件的信号编写代码,我们可以节省一些时间,并避免使用诸如The code for the signal X of the widget Y之类的句子进行重复说明,这意味着要使用我们刚刚学习的方法添加一个负责信号的时隙。 因此,在本例中,作为第一个示例,让我们为inputPushButton小部件的pressed信号编写代码。
根据应用的要求,我们需要确保用户可以打开图像文件。 成功打开图像文件后,我们会将路径写入inputLineEdit小部件的text属性,以便用户可以看到他们选择的完整文件名和路径。 首先让我们看一下代码的外观,然后逐步介绍它:
void MainWindow::on_inputPushButton_pressed()
{
QString fileName = QFileDialog::getOpenFileName(this,
"Open Input Image",
QDir::currentPath(),
"Images (*.jpg *.png *.bmp)");
if(QFile::exists(fileName))
{
ui->->setText(fileName);
}
}
要访问用户界面上的小部件或其他元素,只需使用ui对象。 例如,可以通过ui类并通过编写以下行来简单地访问用户界面中的inputLineEdit小部件:
ui-> inputLineEdit
第一行实际上是大代码的简化版本。 正如您将在本书中学习的那样,Qt 提供了许多方便的函数和类来满足日常编程需求,例如将它们打包成非常短的函数。 首先让我们看看我们刚刚使用了哪些 Qt 类:
QString:这可能是 Qt 最重要和广泛使用的类别之一。 它代表 Unicode 字符串。 您可以使用它来存储,转换,修改字符串以及对字符串进行无数其他操作。 在此示例中,我们仅使用它来存储QFileDialog类读取的文件名。QFileDialog:可以用来选择计算机上的文件或文件夹。 它使用底层操作系统 API,因此对话框的外观可能有所不同,具体取决于操作系统。QDir:此类可用于访问计算机上的文件夹并获取有关它们的各种信息。QFile:可用于访问文件以及从文件中读取或写入文件。
前面提到的将是对每个类的非常简短的描述,并且如您从前面的代码中所见,它们每个都提供了更多的功能。 例如,我们仅在QFile中使用了静态函数来检查文件是否存在。 我们还使用了QDir类来获取当前路径(通常是应用从中运行的路径)。 代码中唯一需要更多说明的是getOpenFileName函数。 第一个参数应该是parent小部件。 这在 Qt 中非常重要,它用于自动清除内存,如果出现对话框和窗口,则要确定父窗口。 这意味着每个对象在销毁子对象时也应负责清理其子对象,如果是窗户,则由其父窗口打开它们。 因此,通过将this设置为第一个参数,我们告诉编译器(当然还有 Qt)此类负责QFileDialog类实例。 getOpenFileName函数的第二个参数显然是文件选择对话框窗口的标题,下一个参数是当前路径。 我们提供的最后一个参数可确保仅显示应用需求中的三种文件类型:*.jpg,*.png和*.bmp文件。
仅当首先将其模块添加到您的项目中,然后将其头文件包含在您的源文件中时,才可以使用任何 Qt 类。 要将 Qt 模块添加到 Qt 项目,您需要在项目的PRO文件中添加类似于以下内容的行:
QT += module_name1 module_name2 module_name3 ...
module_name1等可以替换为可以在 Qt 文档中找到的每个类的实际 Qt 模块名称。
您可能已经注意到项目的 PRO 文件中已经存在以下代码行:
QT += core gui
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
这仅表示core和gui模块应包含在您的项目中。 它们是两个最基本的 Qt 模块,包括许多 Qt 基础类。 第二行表示,如果您使用的 Qt 框架的主要版本号高于四个,则还应包含widgets模块。 这是因为以下事实:在 Qt 5 之前,widgets模块是gui模块的一部分,因此无需将其包含在PRO文件中。
至于包含文件,它始终与类名本身相同。 因此,在我们的情况下,我们需要在源代码中添加以下类,以使前面的代码起作用。 最好的位置通常是头文件的顶部,因此在我们的例子中就是mainwindow.h文件。 确保在顶部具有以下类别:
#include <QMainWindow>
#include <QFileDialog>
#include <QDir>
#include <QFile>
尝试一下,然后运行程序以查看结果。 然后,将其关闭并再次返回到设计器。 现在,我们需要将代码添加到outputPushButton小部件。 只需重复与inputPushButton相同的过程,但是这次,在outputPushButton上进行此操作,并为其编写以下代码:
void MainWindow::on_outputPushButton_pressed()
{
QString fileName = QFileDialog::getSaveFileName(this,
"Select Output Image",
QDir::currentPath(),
"*.jpg;;*.png;;*.bmp");
if(!fileName.isEmpty())
{
ui->outputLineEdit->setText(fileName);
using namespace cv;
Mat inpImg, outImg;
inpImg = imread(ui->inputLineEdit->text().toStdString());
if(ui->medianBlurRadioButton->isChecked())
cv::medianBlur(inpImg, outImg, 5);
else if(ui->gaussianBlurRadioButton->isChecked())
cv::GaussianBlur(inpImg, outImg, Size(5, 5), 1.25);
imwrite(fileName.toStdString(), outImg);
if(ui->displayImageCheckBox->isChecked())
imshow("Output Image", outImg);
}
}
您还需要向项目添加OpenCV标头。 将它们添加到mainwindow.h文件顶部的添加 Qt 类头的位置,如下所示:
#include "opencv2/opencv.hpp"
现在,让我们回顾一下我们刚刚编写的代码,看看它的真正作用。 如您所见,这一次,我们在QFileDialog类和标题中使用了getSaveFileName函数,并且过滤器也有所不同。 这是必需的,以便用户在要保存输出图像时分别选择每种图像类型,而不是在打开它们时看到所有图像。 这次,我们也没有检查文件的存在,因为这将由QFileDialog自动完成,因此仅检查用户是否确实选择了某项就足够了。 在以下几行中,我们编写了一些特定于 OpenCV 的代码,在接下来的章节中,我们将越来越多地了解这些功能。 在第 1 章,“OpenCV 和 Qt 简介”中,您还以很小的剂量使用了它,因此,它们到现在为止还不完全陌生。 但是,我们将再次简短地讨论它们,并继续介绍 IDE 和Hello_Qt_OpenCV应用。
所有OpenCV函数都包含在cv名称空间中,因此我们确保我们是 OpenCV namespace cv的using。 然后,为了读取输入图像,我们使用了imread函数。 这里要注意的重要一点是 OpenCV 使用 C++ std::string类,而 Qt 的QString应该转换为该格式,否则,当您尝试运行该程序时会遇到错误。 只需使用QString的toStdString函数即可完成。 注意,在这种情况下,QString是inputLineEdit小部件的text()函数返回的值。
接下来,根据选择的过滤器类型,我们使用medianBlur或gaussianBlur函数进行简单的 OpenCV 过滤。
请注意,在这种情况下,我们为这些 OpenCV 函数使用了一些默认参数,但是如果我们使用旋转小部件从用户那里获得它们,那就更好了。 还是滑块部件? 也许是一个不错的拨盘小部件? 完成本章后,请尝试一下。 这个想法很简单,它旨在帮助您学习如何在这些框架中自己发现新的可能性。 尽管如此,您将在第 3 章,“创建全面的 Qt + OpenCV 项目”中学习如何使用许多小部件,甚至创建自己的小部件。
最后,已过滤的输出图像outImg被写入所选文件。 根据displayImageCheckBox小部件设置的条件也会显示它。
到这个时候,我们还有两个要求。 首先是,在关闭程序时将所有小部件的状态保存在窗口中,并在重新打开程序时将其重新加载。 另一个要求(最后一个要求)是在用户想要关闭程序时提示他们。 让我们从最后一个开始,因为这意味着我们需要知道如何编写在关闭窗口时需要执行的代码。 这非常简单,因为 Qt 的QMainWindow类(我们的窗口所基于的类)是QWidget,并且它已经具有一个虚拟 C++ 函数,我们可以覆盖和使用它。 只需将以下代码行添加到您的MainWindow类中:
protected:
void closeEvent(QCloseEvent *event);
这应该进入mainwindow.h文件中的类定义。 专用插槽前的线路似乎是个好地方。 现在,切换到mainwindow.cpp并将以下代码段添加到文件末尾:
void MainWindow::closeEvent(QCloseEvent *event)
{
int result = QMessageBox::warning(this,
"Exit",
"Are you sure you want to close this program?",
QMessageBox::Yes,
QMessageBox::No);
if(result == QMessageBox::Yes)
{
event->accept();
}
else
{
event->ignore();
}
}
我想您已经注意到我们现在又引入了两个 Qt 类,这意味着我们也需要将它们的包含标头添加到mainwindow.h。 考虑以下:
QMessageBox:根据消息的目的,它可以用于显示带有简单图标,文本和按钮的消息QCloseEvent:这是许多 Qt 事件(QEvent)类之一,其目的是传递有关窗口关闭事件的参数
该代码几乎是不言自明的,因为您已经知道警告函数的第一个参数是什么。 这是用来告诉 Qt 我们的MainWindow类负责此消息框。 记录用户选择的结果,然后,基于此结果,关闭事件被接受或忽略,非常简单。 除此之外,我们仍然需要保存设置(小部件上的文本以及复选框和单选框的状态)并加载它们。 如您所知,保存设置的最佳位置是closeEvent函数。 在代码的event->accept();行之前怎么样? 让我们向MainWindow类添加两个私有函数,一个私有函数加载名为loadSettings的设置,另一个私有函数保存名为saveSettings的设置。 在本章中,我们将学习最后一个 Qt 类,它称为QSettings。 因此,首先将其包含行添加到mainwindow.h中,然后将以下两个函数定义添加到MainWindow类中,再次在Ui::MainWindow *ui;行正下方的mainwindow.h中,在私有成员中:
void loadSettings();
void saveSettings();
这是loadSettings函数所需的代码:
void MainWindow::loadSettings()
{
QSettings settings("Packt",
"Hello_OpenCV_Qt",
this);
ui->inputLineEdit->setText(settings.value("inputLineEdit",
"").toString());
ui->outputLineEdit->setText(settings.value("outputLineEdit",
"").toString());
ui->medianBlurRadioButton
->setChecked(settings.value("medianBlurRadioButton",
true).toBool());
ui->gaussianBlurRadioButton
->setChecked(settings.value("gaussianBlurRadioButton",
false).toBool());
ui->displayImageCheckBox
->setChecked(settings.value("displayImageCheckBox",
false).toBool());
}
这是给saveSettings的:
void MainWindow::saveSettings()
{
QSettings settings("Packt",
"Hello_OpenCV_Qt",
this);
settings.setValue("inputLineEdit",
ui->inputLineEdit->text());
settings.setValue("outputLineEdit",
ui->outputLineEdit->text());
settings.setValue("medianBlurRadioButton",
ui->medianBlurRadioButton->isChecked());
settings.setValue("gaussianBlurRadioButton",
ui->gaussianBlurRadioButton->isChecked());
settings.setValue("displayImageCheckBox",
ui->displayImageCheckBox->isChecked());
}
在构造它时,需要为QSettings类提供组织名称(仅作为示例,我们使用"Packt")和应用名称(在我们的情况下为"Hello_Qt_OpenCV")。 然后,它只记录您传递给setValue函数的所有内容,并使用value函数将其返回。 我们所做的只是将要保存的所有内容传递给setValue函数,例如单行编辑小部件中的文本等,然后在需要时重新加载它。 请注意,QSettings像这样使用时,会自己照顾存储位置,并使用每个操作系统的默认位置来保留特定于应用的配置。
现在,只需将loadSettings函数添加到MainWindow类的构造器中。 您应该具有一个如下所示的构造器:
ui->setupUi(this);
loadSettings();
在event->accept()之前,将saveSettings函数添加到closeEvent即可。 现在,我们可以尝试第一个应用。 让我们尝试运行和过滤图像。 选择两个过滤器中的每一个,看看它们的区别是什么。 尝试使用该应用并查找其问题。 尝试通过向其添加更多参数来对其进行改进,依此类推。 以下是该应用运行时的屏幕截图:

尝试关闭它,并使用我们的退出确认代码查看一切是否正常。

我们编写的程序显然并不完美,但是它列出了您从 Qt Creator IDE 入门到本书各章所需要了解的几乎所有内容。 Qt Creator 中还有另外三个Modes尚未见过,我们将把调试模式和项目模式留给第 12 章,“Qt Quick 应用”,其中我们将深入研究构建,测试和调试计算机视觉应用的概念。 因此,让我们简要地通过 Qt Creator 的非常重要的“帮助”模式以及Options之后,结束我们的 IDE 之旅。
帮助模式
使用左侧的帮助按钮切换到 Qt Creator 的帮助模式:

关于 Qt Creator 帮助模式的最重要的事情是,您可以使用它来找出正确的事实,除了可以从字面上搜索与 Qt 相关的所有内容并查看每个类和模块的无数示例之外, 每个类都需要的模块。 为此,只需切换到索引模式并搜索要在应用中使用的 Qt 类。 这是一个例子:

如您所见,可以使用索引并搜索QMessageBox类的文档页面。 注意描述之后的前两行:
#include <QMessageBox>
QT += widgets
这基本上意味着,为了在项目中使用QMessageBox,必须在源文件中包含QMessageBox标头,并将小部件模块添加到PRO文件中。 尝试搜索本章中使用的所有类,然后在文档中查看其示例。 Qt Creator 还提供了非常强大的上下文相关帮助。 您只需在任何 Qt 类上用鼠标单击F1,它的文档页面都将在编辑模式下的代码编辑器中获取:

Qt Creator 选项窗口
您可以通过单击工具,然后单击选项,从其主菜单访问 Qt Creator 选项。 Qt Creator 允许非常高级别的自定义,因此您会发现 Qt Creator 的“选项”页面和选项卡具有很多要配置的参数。 对于大多数人(包括我自己)来说,Qt Creator 的默认选项几乎可以满足他们需要做的所有事情,但是有些任务在不知道如何配置 IDE 的情况下将无法完成。 考虑以下屏幕截图:

您可以使用左侧的按钮在页面之间切换。 每个页面包含许多选项卡,但它们都与同一组相关。 以下是每组选项的主要用途:
-
环境:该设置通常包含与 Qt Creator 外观有关的设置。 您可以在此处更改主题(在本章开头提到),字体和文本大小,语言以及所有设置。
-
文本编辑器:这组设置包括与代码编辑器相关的所有内容。 在这里,您可以更改设置,例如代码突出显示,代码完成等。
-
FakeVim:适用于熟悉 Vim 编辑器的人。 在这里,他们可以在 Qt Creator 中启用 Vim 样式的代码编辑并进行配置。
-
帮助:可以猜到,它包含与 Qt Creator 的帮助模式和上下文相关帮助功能有关的所有选项。
-
C++:在这里,您可以找到与 C++ 编码和代码编辑相关的设置。
-
Qt Quick:可在此处找到影响 Qt Quick 设计器和 QML 代码编辑的选项。 我们将在第 12 章,“Qt Quick 应用”中学习有关 QML 的更多信息。
-
生成并运行:这可能是 Qt Creator 中最重要的选项页面。 此处的设置直接影响您的应用构建和运行体验。 我们将在,“链接和部署”中配置一些设置,在这里您将了解有关 Qt 中的静态链接的信息。
-
调试器:包含与 Qt Creator 中的调试模式相关的设置。 您将在第 10 章,“调试和测试”中了解更多信息。
-
设计器:可用于配置 Qt Creator 模板项目和其他与“设计”模式相关的设置。
-
分析器:包括与 Clang 代码分析器,QML 分析器等相关的设置。 涵盖它们超出了本书的范围。
-
版本控制:Qt 与许多版本控制系统(例如 Git 和 SVN)提供了非常可靠的集成。 在这里,您可以配置 Qt Creator 中与版本控制相关的所有设置。
-
设备:如您在第 12 章,“Qt Quick 应用”中所看到的,将在其中使用它来配置 Qt Creator 进行 Android 开发,包括与设备相关的所有设置。
-
代码粘贴:这可以用于配置 Qt Creator 可以用于诸如代码共享之类的任务的某些第三方服务。
-
Qbs:涵盖 Qbs 完全超出了本书的范围,我们将不需要它。
-
测试设置:包含与 Qt 测试相关的设置,等等。 我们将在第 10 章(调试和测试)中介绍 Qt 测试,您将在其中学习如何为 Qt 应用编写单元测试。
除此之外,您始终可以使用 Qt Creator 的过滤器工具立即在“选项”窗口中找到所需的设置:

总结
本章不仅仅介绍 Qt Creator,而且正是我们所需要的,以便轻松地继续下一章,专注于构建内容,而不是重复的说明和配置技巧。 我们学习了如何使用 Qt Creator 来设计用户界面并为用户界面编写代码。 我们介绍了一些使用最广泛的 Qt 类,以及它们如何包装在不同的模块中。 通过了解不同的 Qt Creator 模式并同时构建应用,我们现在可以自己练习更多,甚至可以改善编写的应用。 下一章将是我们在其中构建可扩展的基于插件的计算机视觉应用框架的一章,该书将在本书的其余部分中继续进行,直到最后一章为止。 在下一章中,我们将学习 Qt 和 OpenCV 中的不同设计模式,以及如何使用类似的模式来构建易于维护和扩展的应用。
三、创建一个全面的 Qt + OpenCV 项目
由于某些随机情况,专业应用永远不会成为专业。 它们从一开始就是这样设计的。 当然,说起来容易做起来难,但是如果您已经知道如何创建可以轻松扩展,维护,扩展和定制的应用的黄金法则,这仍然很容易。 黄金法则只是一个简单的概念,幸运的是 Qt 框架已经具有实现的手段,并且正在以模块化方式构建应用。 请注意,从这个意义上讲,模块化不仅意味着库或不同的源代码模块,而且在意义上说模块化是指应用的每个职责和功能都是独立于其他职责创建和构建的。 实际上,这正是 Qt 和 OpenCV 本身创建的方式。 即使来自不同背景的不同开发人员也可以很容易地扩展模块化的应用。 模块化的应用可以扩展为支持许多不同的语言,主题(样式或外观),或者更好的是,许多不同的功能。
在本章中,我们将承担一项非常重要且至关重要的任务,即为使用 Qt 和 OpenCV 框架的综合计算机视觉应用构建基础结构(或架构)。 您将学习如何创建 Qt 应用,这些应用即使在部署(交付给用户)后也可以扩展。 这实际上意味着很多事情,包括如何向应用添加新语言,如何向应用添加新样式,以及最重要的是如何构建基于插件的 Qt 应用,可以通过向其添加新插件来对其进行扩展。
我们将通过遍历 Qt 项目的结构和包含的文件,来了解构建 Qt 应用时通常会发生什么情况。 然后,我们将了解 Qt 和 OpenCV 中一些使用最广泛的设计模式,以及这两个框架如何享受使用这些设计模式的优势。 然后,我们将学习如何创建可以使用插件扩展的应用。 我们还将学习有关在应用中添加新样式和新语言的信息。 在本章结束时,我们将能够创建一个全面的计算机视觉应用的基础,该应用是跨平台,多语言,基于插件的,并且具有可自定义的外观。 此基础应用将在接下来的两章中扩展:第 4 章,“Mat和QImage”和第 5 章,“图形视图框架”,然后在本书的其余部分中使用插件,尤其是在第 6 章,“OpenCV 中的图像处理”之后,我们将开始深入研究计算机视觉主题和 OpenCV 库。
在本章中,我们将介绍以下主题:
- Qt 项目的结构和 Qt 构建过程
- Qt 和 OpenCV 中的设计模式
- Qt 应用中的样式
- Qt 应用中的语言
- 如何使用 Qt Linguist 工具
- 如何在 Qt 中创建和使用插件
背景
在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中,您学习了如何创建一个简单的 Qt + OpenCV 应用Hello_Qt_OpenCV。 该项目几乎包括 Qt 提供的所有基本功能,尽管我们没有过多地讨论如何将项目构建到具有用户界面和(几乎可以接受)行为的应用中。 在本节中,您将了解单击“运行”按钮时幕后发生的情况。 这将帮助我们更好地了解 Qt 项目的结构以及项目文件夹中每个文件的用途。 首先,打开项目文件夹,并逐个浏览几个文件。 因此,我们在Hello_Qt_OpenCV文件夹中包含以下内容:
Hello_Qt_OpenCV.pro
Hello_Qt_OpenCV.pro.user
main.cpp
mainwindow.cpp
mainwindow.h
mainwindow.ui
Hello_Qt_OpenCV.pro列表中的第一个文件基本上是构建项目时 Qt 处理的第一个文件。 这称为 Qt 工程文件,内部的 Qt 程序qmake负责处理该文件。 让我们看看它是什么。
qmake工具
qmake工具是一个程序,可使用*.pro文件中的信息来帮助创建 makefile。 这仅意味着,qmake使用非常简单的语法(相对于其他make系统中更复杂的语法),生成了正确编译和构建应用所需的所有命令,并将所有生成的文件放入Build文件夹中 。
生成 Qt 项目时,它将首先创建一个新的生成文件夹,默认情况下,该文件夹与项目文件夹位于同一级别。 在我们的情况下,此文件夹应具有类似于build-Hello_Qt_OpenCV-Desktop_Qt_5_9_1_*-Debug的名称,其中*可以不同,具体取决于平台,您可以在项目文件夹所在的同一文件夹中找到它。 Qt(使用qmake和其他一些您将在本章中学习的工具)和 C++ 编译器生成的所有文件都位于此文件夹及其子文件夹中。 这称为项目的Build文件夹。 这也是创建和执行应用的地方。 例如,如果您使用的是 Windows,则可以在Build文件夹的debug或release子文件夹中找到Hello_Qt_OpenCV.exe文件(在许多其他文件中)。 因此,从现在开始,我们将该文件夹(及其子文件夹)称为Build文件夹。
例如,我们已经知道在 Qt PRO 文件中包含以下行会导致在我们的应用中添加 Qt 的core和gui模块:
QT += core gui
让我们在Hello_Qt_OpenCV.pro文件中进一步浏览; 以下几行立即引起注意:
TARGET = Hello_Qt_OpenCV
TEMPLATE = app
这些行仅表示TARGET名称为Hello_Qt_OpenCV,这是我们项目的名称,TEMPLATE类型app表示我们的项目是一个应用。 我们还有以下内容:
SOURCES += \
main.cpp \
mainwindow.cpp
HEADERS += \
mainwindow.h
FORMS += \
mainwindow.ui
很明显,这就是在项目中包含头文件,源文件和用户界面文件(窗体)的方式。 我们甚至将自己的代码添加到 PRO 文件中,如下所示:
win32: {
include("c:/dev/opencv/opencv.pri")
}
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
unix: macx{
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
-lopencv_world
}
您已经了解到,这就是 Qt 如何查看 OpenCV 并将其用于 Qt 项目中。 在 Qt 帮助索引中搜索qmake Manual,以获取有关qmake中所有可能的命令和功能的更多信息,以及有关其工作方式的更多详细信息。
qmake处理完我们的 Qt 项目文件后,它开始寻找项目中提到的源文件。 自然地,每个 C++ 程序在其源文件之一(不在头文件中)中都具有main函数(单个且唯一的main函数),我们的应用也不例外。 我们的应用的main函数是由 Qt Creator 自动生成的,它位于main.cpp文件中。 让我们打开main.cpp文件,看看其中包含什么:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
前两行用于包含我们当前的mainwindow.h标头和QApplication标头文件。 QApplication类是负责控制应用的控制流,设置等的主要类。 在main函数内,您在此处看到的是 Qt 如何创建Event Loop以及其底层信号/时隙机制和事件处理系统如何工作的基础:
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
为了最简单地描述它,创建了QApplication类的实例,并将应用参数(通常通过命令行或终端传递)传递给名为a的新实例。 然后,创建我们的MainWindow类的实例,然后将其显示出来。 最后,调用QApplication类的exec()函数,以便应用进入主循环,并保持打开状态直到关闭窗口。
要了解事件循环的工作原理,请尝试删除最后一行,看看会发生什么。 当您运行应用时,您可能会注意到该窗口实际上只显示了很短的时间,然后立即关闭。 这是因为我们的应用不再具有事件循环,它立即到达应用的末尾,并且所有内容都从内存中清除,因此该窗口被关闭。 现在,将那行写回去,正如您所期望的,窗口保持打开状态,因为仅当在代码中的某个地方(任何地方)调用了exit()函数时,exec()函数才返回,并且它返回由exit()设置的值 ]。
现在,让我们继续下三个文件,它们具有相同的名称,但扩展名不同。 它们是mainwindow标头,源和用户界面文件。 现在,您将在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中了解负责代码和应用程序用户界面的实际文件。 这给我们带来了另外两个 Qt 内部工具,称为元对象编译器和用户界面编译器。
元对象编译器(MOC)
我们已经知道,标准 C++ 代码中没有信号和插槽之类的东西。 那么,如何通过使用 Qt 在 C++ 代码中拥有这些附加功能呢? 这还不是全部。 稍后您将学习到,您甚至可以向 Qt 对象添加新属性(称为动态属性),并执行类似的许多其他操作,这不是标准 C++ 编程的功能。 好了,这些可以通过使用名为moc的 Qt 内部编译器来使用。 在将 Qt 代码实际传递给真正的 C++ 编译器之前,moc工具会处理您的类头文件(在我们的示例中为mainwindow.h文件),以生成启用上述 Qt 特定功能所需的代码。 您可以在Build文件夹中找到这些生成的源文件。 他们的名字以moc_开头。
您可以在 Qt 文档中阅读有关moc工具的全部信息,但是这里值得一提的是moc搜索具有 Qt 类定义且包含Q_OBJECT宏的所有头文件。 该宏必须始终包含在想要支持信号,插槽和其他 Qt 支持功能的 Qt 类中。
这是我们在mainwindow.h文件中所包含的内容:
...
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
explicit MainWindow(QWidget *parent = 0);
~MainWindow();
...
如您所见,我们的自动生成的类头文件在其私有部分中已经具有Q_OBJECT宏。 因此,这基本上是创建作为QObject(或任何其他 Qt 对象)的子类的类(不仅是窗口类,而且一般是任何 Qt 类)的标准方法,该类将支持 Qt 支持的功能,例如信号和插槽。
现在,让我们继续看看如何通过 C++ 代码访问 Qt 用户界面文件中的小部件。 如果您尝试以Edit模式或任何其他文本编辑器查看mainwindow.ui文件,则实际上它们是仅包含属性和一些其他信息的 XML 文件,这些信息仅与小部件的显示方式有关。 答案在于最终的 Qt 内部编译器,您将在本章中对其进行了解。
用户界面编译器(UIC)
每当构建带有用户界面的 Qt 应用时,就会执行称为uic的 Qt 内部工具来处理*.ui文件并将其转换为可在 C++ 代码中使用的类和源代码。 在我们的情况下,mainwindow.h被转换为ui_mainwindow.h文件,您可以再次在Build文件夹中找到该文件。 您可能已经注意到了这一点,但是让我们提到您的mainwindow.cpp文件已经包含了此头文件。 检查文件的最顶部,您会发现以下两个包含行:
#include "mainwindow.h"
#include "ui_mainwindow.h"
您已经知道mainwindow.h文件的位置和位置(在Project文件夹中),并且刚刚了解到ui_mainwindow.h实际上是位于Build文件夹内的生成的源文件。
如果查看ui_mainwindow.h文件的内容,您会注意到一个名为Ui_MainWindow的类,它具有两个函数:setupUi和retranslateUi。 setupUi函数已自动添加到mainwindow.h函数中的MainWindow类构造器中。 该函数仅负责根据mainwindow.ui文件中的设置在用户界面上进行所有设置。 您将在本章稍后的内容中了解retranslateUi函数以及在制作多语言 Qt 应用时如何使用它。
将所有 Qt 生成的文件都保存在Build文件夹中之后,将它们传递给 C++ 编译器,就像其他任何 C++ 程序一样,进行编译,然后链接到Build文件夹中以创建我们的应用。 Windows 用户应注意,使用 Qt Creator 运行应用时,所有 DLL 文件路径均由 Qt Creator 解析,但是如果尝试从Build文件夹中运行程序,则会遇到多个错误消息,并且您的应用会崩溃或根本就不会启动。 您将在第 10 章,“调试和测试”中学习如何解决此问题,在这里您将学习如何正确地将应用交付给用户。
设计模式
即使我们假设本书的读者不是“设计模式丹尼尔”,但提醒自己为什么存在设计模式以及为什么成功的框架(例如 Qt)广泛使用不同的设计模式仍然是一个很好的主意。 好吧,首先,设计模式只是软件开发任务的许多解决方案之一,它不是唯一的解决方案; 实际上,在大多数情况下,它甚至不是最快的解决方案。 但是,设计模式绝对是解决软件开发问题的最结构化的方式,它有助于确保对添加到程序中的所有内容都使用一些预定义的类似于模板的结构。
设计模式具有用于各种问题的名称,例如创建对象,它们如何运行,如何处理数据等等。 埃里克·伽玛(Eric Gamma),理查德·赫尔姆(Richard Helm),拉尔夫·E·约翰逊(Ralph E. Johnson)和约翰·弗利斯赛德(John Vlissides)(称为四人帮)在其名为《设计模式:可重用的面向对象软件基础》的书中描述了许多最广泛使用的设计模式。 被认为是计算机科学中设计模式的实际参考书。 如果您不熟悉设计模式,那么在继续本章之前,您应该花一些时间来学习该主题。 了解软件开发中的反模式也是一个好主意。 如果您是本主题的新手,可能会因发现某些反模式的普遍性而感到惊讶,并且确保始终避免使用反模式至关重要。
以下是 Qt 和 OpenCV 框架中使用的一些最重要的设计模式(按字母顺序排列),以及实现这些设计模式的类或函数的简要说明和一些示例。 请密切注意下表中的“示例案例”列,以概述与每个设计模式相关的某些类或函数。 尽管如此,在本书的学习过程中以及各种示例中,您将学习动手实践中使用的类:
由于 OpenCV 框架的性质以及它不是用于构建日常生活应用,复杂的用户界面等的通用框架,因此它无法实现 Qt 使用的所有设计模式, 而且,相比之下,OpenCV 仅实现了这些模式的一小部分。 尤其是由于 OpenCV 追求速度和效率的目标,在大多数情况下,首选全局函数和类似底层的实现。 然而,只要速度和效率不是目标,就有一些 OpenCV 类可以实现设计模式,例如抽象工厂。 有关示例,请参见“示例案例”列。
| 设计模式 | 说明 | 示例案例 |
|---|---|---|
| 抽象工厂 | 这可用于创建所谓的工厂类,该工厂类能够以各种可能的方式创建对象并控制新对象的创建,例如防止对象具有超过定义数量的实例。 | 在本章中,我们将学习如何使用这种设计模式来编写基于插件的 Qt 应用。DescriptorMatcher抽象类中的create()函数是 OpenCV 中此设计模式的示例。 |
| 命令 | 使用此设计模式,可以用对象表示动作。 这允许诸如组织动作顺序,记录动作,还原动作等功能。 | QAction:此类允许创建特定的动作并将其分配给小部件。例如,可以使用QAction类创建带有图标和文本的Open File操作,然后可以将其分配给主菜单项和键盘快捷键(例如Ctrl + O),依此类推。 |
| 组合 | 这用于创建由子对象组成的对象。 当制作可以由许多简单对象组成的复杂对象时,这特别有用。 | QObject:这是所有 Qt 类的基础。QWidget:这是所有 Qt 小部件的基础。任何具有树状设计架构的 Qt 类都是组合模式的示例。 |
| 外观(或外观) | 通过提供更简单的接口,可以将其用于封装操作系统(或与此相关的任何系统)的低级功能。 包装器和适配器设计模式的定义非常相似。 | QFile:这些可用于读取/写入文件。基本上,所有 Qt 类都是围绕操作系统的低级 API 的包装,它们都是外观设计模式的示例。 |
| 蝇量(或桥接或私有实现) | 此设计模式的目标是避免数据复制并在相关对象之间使用共享数据(除非另有需要)。 | QString:可用于存储和操作 Unicode 字符串。实际上,许多 Qt 类都喜欢这些设计模式,这些设计模式有助于在需要对象副本时将指针传递到共享数据空间,从而导致更快的对象复制和更少的内存空间使用。 当然,具有更复杂的代码。 |
| 备忘 | 这可用于保存和(以后)加载对象的状态。 | 这种设计模式等同于编写一个能够存储 Qt 对象的所有属性并还原它们以创建新属性的类。 |
| 元对象(或反射) | 在此设计模式中,所谓的元对象用于描述对象的详细信息,以便更健壮地访问该对象。 | QMetaObject:这仅包含有关 Qt 类的元信息。涵盖 Qt 的元对象系统的细节不在本书的讨论范围之内,但是简单地说,每个 Qt 程序都首先使用 Qt 元对象编译器(MOC)进行编译以生成所需的元对象,然后再由实际的对象进行编译。 C++ 编译器。 |
| 单态 | 这允许同一类的多个实例以相同的方式行为。 (通常,通过访问相同的数据或执行相同的功能。) | QSettings:用于提供应用设置的保存/加载。我们已经在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中使用了QSettings类,以加载和保存相同类的两个不同实例。 |
| MVC(模型视图控制器) | 这是一种广泛使用的设计模式,用于将应用或数据存储机制(模型)的实现与用户界面或数据表示形式(视图)和数据操作(控制器)分开。 | QTreeView:这是模型视图的树状实现。QFileSystemModel:用于基于本地文件系统的内容获取数据模型。QFileSystemModel(或任何其他QAbstractItemModel)与QTreeView(或任何其他QAbstractItemView)的组合可以导致 MVC 设计模式的实现。 |
| 观察者(或发布/订阅) | 此设计模式用于使对象可以监听(或观察)其他对象的变化并做出相应的响应。 | QEvent:这是所有 Qt 事件类的基础。将QEvent(及其所有众多子类)视为观察者设计模式的低级实现。 另一方面,Qt 支持signal和slot机制,这是使用观察者设计模式的更方便,更高级的方法。我们已经在第 2 章,“创建第一个 Qt 和 OpenCV 项目”中使用了QCloseEvent(QEvent的子类)。 |
| 序列化器 | 在创建可用于读取或写入其他对象的类(或对象)时使用此模式。 | QTextStream:可用于在文件或其他 IO 设备中读取和写入文本。QDataStream:可用于从 IO 设备和文件读取或写入二进制数据。 |
| 单例 | 这可用于将类限制为仅单个实例。 | QApplication:可用于以各种方式处理 Qt 小部件应用。确切地说,QApplication中的instance()函数(或全局qApp指针)是单例设计模式的示例。OpenCV 中的cv::theRNG()函数(用于获取默认的随机数生成器(RNG))是单例实现的示例。 请注意,RNG 类本身不是单例。 |
参考文献:
Design Patterns: Elements of Reusable Object-Oriented Software, by Eric Gamma, Richard Helm, Ralph E. Johnson and John Vlissides (referred to as the Gang of Four)
An Introduction to Design Patterns in C++ with Qt, second Edition, by Alan Ezust and Paul Ezust
通常,前面的列表不应该被视为设计模式的完整列表,因为它仅关注 Qt 和 OpenCV 设计模式,而仅针对本书而言就足够了。 如果您对该主题感兴趣,请考虑阅读提到的参考书,但是正如前面所提到的,就本书而言,您只需要上述清单即可。
检查上一个列表中提到的每个类的文档页面是一个很好的主意。 您可以为此使用 Qt Creator 帮助模式,并在索引中搜索每个类,查看每个类的代码示例,甚至尝试自己使用它们。 这不仅是学习 Qt 的最佳方法,而且是学习不同设计模式的实际实现和行为的最佳方法之一。
Qt 资源系统
在下一部分中,您将学习如何向我们的应用添加样式和多语言支持,但是在此之前,我们必须熟悉 Qt 资源系统。 简而言之,这是 Qt 中将字体,图标,图像,翻译文件,样式表文件等资源文件添加到我们的应用(和库)中的方法。
Qt 支持使用*.qrc文件(资源收集文件)进行资源管理,这些文件只是 XML 文件,其中包含有关需要包含在我们的应用中的资源文件的信息。 让我们看一个简单的示例,并在我们的Hello_Qt_openCV应用中包含一个图标,以更好地了解 Qt 资源系统的工作方式:
- 确保已在 Qt Creator 中打开了
Hello_Qt_OpenCV项目。 选择文件,然后选择新建文件或项目。 在新文件窗口中,确保从左侧的第二个列表中选择 Qt,然后选择 Qt 资源文件。 考虑以下屏幕截图:

- 单击“选择...”按钮,然后在下一个屏幕中,将名称设置为
resources。 默认情况下,该路径应设置为您的项目文件夹,因此请保持原样。 单击下一步,然后单击完成。 最后,您将在项目中添加一个名为resources.qrc的新文件。 如果您在 Qt Creator 中打开此文件(通过右键单击并选择“在编辑器中打开”),将在 Qt Creator 中显示资源编辑器。 - 在这里,您可以使用“添加”按钮打开以下两个选项:
新增档案
添加前缀
在这里,文件就是您要添加到项目中的任何文件。 但是,前缀基本上是一个伪文件夹(如果需要,则是一个容器),其中包含许多文件。 请注意,这不一定表示项目文件夹中的文件夹或子文件夹,而仅仅是表示形式和对资源文件进行分组的一种方式。
- 首先单击“添加前缀”,然后在“前缀”字段中输入
images。 - 然后,单击“添加文件”,然后选择所需的图像文件(就我们的示例而言,您计算机上的任何
*.jpg文件都可以)。

在此示例中,我们使用了与第 1 章,“Qt 和 OpenCV 简介”和第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”。 请注意,资源文件应位于项目文件夹中或其中的子文件夹中。 否则,您将得到确认,如以下屏幕截图所示; 如果是这种情况,请单击复制,然后将资源文件保存在项目文件夹中:

而已。 现在,当您构建并运行Hello_Qt_OpenCV应用时,图像文件将包含在应用中,并且可以像操作系统上存在的文件一样进行访问。 但是,该路径与常规文件路径有些不同。 在我们的示例中,test.jpg文件的路径如下:
img/test.jpg
您可以在 Qt Creator 中展开*.qrc文件,然后右键单击每个资源文件,然后选择“复制路径”或“复制 URL”选项以复制每个文件的路径或 URL。 只要需要常规路径,就可以使用该路径;只要需要资源文件的 URL(Qt 中的QUrl类),URL 就会有用。 需要特别注意的是,由于 Qt 资源系统是 Qt 的内部功能,因此 OpenCV 可能无法使用这些路径并访问资源中的文件。 但是,这些文件仅供应用本身使用(通常用于与用户界面相关的任务),因此您可能永远都不需要将它们与 OpenCV 一起使用。
现在,您可以通过将新图像文件设置为按钮图标来尝试新图像文件。 尝试选择用户界面上的任何按钮,然后在属性编辑器中找到icon属性,然后通过按下旁边的小下拉按钮选择“选择资源”。 现在,您只需选择添加为按钮图标的图像即可:

这基本上是关于如何将图标设置为支持图标的 Qt 小部件的教程。 当您想在应用中包含任何其他类型的资源并在运行时使用它时,逻辑完全相同。 您只需要假设 Qt 资源系统是某种辅助文件系统,然后使用其中的文件,就像在文件系统上使用常规文件一样。
设置应用的样式
Qt 支持使用QStyle类和 Qt 样式表的应用中的样式。 QStyle是 Qt 中所有样式的基类,它封装了 Qt 用户界面的样式。 涵盖QStyle类不在本书的讨论范围内,但仍应注意,创建QStyle的子类并在其中实现不同的样式函数最终是更改外观的最有效方法。 Qt 应用。 但是,Qt 还提供样式表来样式应用。 Qt 样式表的语法几乎与 HTML CSS(级联样式表)相同,这是网页样式中不可分割的一部分。
CSS 是一种样式语言,可用于定义用户界面上对象的外观。 通常,使用 CSS 文件有助于将网页的样式与基础实现分开。 Qt 在其样式表中使用了一种非常相似的方法来描述小部件的外观。 如果您熟悉 CSS 文件,那么 Qt 样式表对您来说将是小菜一碟; 但是,即使您是第一次接触该概念,也请放心,这是一种易于,简单且快速学习的方法。
让我们通过一个简单的示例来确切地了解什么是样式表以及如何在 Qt 中使用它。 让我们再次回到我们的Hello_Qt_OpenCV项目。 打开项目并转到设计器。 选择窗口上的任何窗口小部件,或单击一个空的位置以选择窗口窗口小部件本身,然后可以找到一个名为styleSheet的属性。 基本上,每个 Qt 窗口小部件(或QWidget子类)都包含一个styleSheet属性,可以将其设置为定义每个窗口小部件的外观。
单击inputPushButton小部件,并将其styleSheet属性设置为以下内容:
border: 2px solid #222222;
border-radius: 10px;
background-color: #9999ff;
min-width: 80px;
min-height: 35px;
用outputPushButton做同样的事情; 但是,这次,在styleSheet属性中使用以下命令:
border: 2px solid #222222;
border-radius: 10px;
background-color: #99ff99;
min-width: 80px;
min-height: 35px;
在设计器中设置这些样式表时,您将看到两个按钮的新外观。 这就是 Qt 中简单的样式。 唯一需要做的就是知道可以将哪种样式更改应用于任何特定的窗口小部件类型。 在前面的示例中,我们可以更改边框的外观,背景色和QPushButton的最小可接受大小。 要大致了解可以将哪种样式应用于任何窗口小部件,可以在 Qt 帮助模式下阅读《Qt 样式表参考》。 它应该已经在您的计算机上,您可以随时从“帮助”索引中脱机访问它。 在那里,您将找到 Qt 小部件的所有可能样式,其中包含清晰的示例,您可以根据自己的需求进行复制和修改,以及想要在应用中拥有什么样的外观。 这是我们刚刚使用的两个简单样式表的结果。 如您所见,我们现在的浏览按钮外观有所不同:

在前面的示例中,我们还避免在样式表中设置适当的样式规则。 Qt 样式表中的样式规则由选择器和声明组成。 选择器指定将使用样式的小部件,而声明只是样式本身。 同样,在前面的示例中,我们仅使用了一个声明,并且选择器是(隐式地)获得样式表的小部件。 这是一个例子:
QPushButton
{
border: 2px solid #222222;
border-radius: 10px;
background-color: #99ff99;
min-width: 80px;
min-height: 35px;
}
在这里,QPushButton(或者实际上是{之前的所有内容)是选择器,并且{和}之间的代码部分是声明。
现在,让我们看一下在 Qt 中设置样式表时的一些重要概念。
选择器类型
以下是可以在 Qt 样式表中使用的选择器类型。 明智而有效地使用它们可以极大地减少样式表所需的代码量,并改变 Qt 应用的外观:
| 选择器类型 | 范例 | 说明 |
|---|---|---|
| 通用 | * |
这些都是小部件 |
| 类型 | QPushButton |
这些是指定类型及其子类的窗口小部件 |
| 属性 | [text='Browse'] |
这些是具有指定属性设置为指定值的小部件 |
| 类 | .QPushButton |
这些是具有指定类型的小部件,但不是其子类 |
| ID | #inputPushButton |
这些是具有指定类型和objectName的小部件 |
| 后继 | QDialog QPushButton |
这些小部件是另一个小部件的后代(子代) |
| 子项 | QDialog > QPushButton |
这些小部件是另一个小部件的直接子代 |
子小部件
更好的是,子小部件是复杂小部件内的子小部件。 一个示例是QPinBox小部件上的向下和向上箭头按钮。 可以使用::运算符选择它们,如以下示例所示:
QSpinBox::down-button
始终记得参考 Qt Creator 帮助模式下的“Qt 样式表参考”文章,以获取(或多或少)每个小部件的子控件的完整列表。 Qt 是一个不断发展的框架,并且会定期添加新功能,因此没有比它自己的文档更好的参考了。
伪状态
每个窗口小部件都可以具有某些伪状态,例如,悬停,按下等。 可以使用:运算符在样式表中选择它们,如以下示例所示:
QRadioButton:!hover { color: black }
就像子控件一样,请始终参考 Qt Creator 帮助模式下的 Qt 样式表参考,以获取每个小部件的适用伪状态列表。
级联
您可以为整个应用,父窗口小部件或子窗口小部件设置样式表。 在前面的示例中,我们仅设置了两个子小部件的样式表。 每个窗口小部件的样式都将取决于级联规则,这意味着每个窗口小部件还将获得在父窗口小部件或应用中设置的样式规则(如果为其设置了样式表)。 我们可以利用这一事实来避免重复设置整个应用或每个小部件中特定窗口通用的样式规则。
现在,让我们尝试MainWindow中的以下样式表,它在一个简单的示例中结合了您学到的所有知识。 确保删除所有先前设置的样式表(对于两个“浏览”按钮),并在窗口小部件的stylesheet属性中简单地使用以下内容:
*
{
font: 75 11pt;
background-color: rgb(220, 220, 220);
}
QPushButton, QLineEdit, QGroupBox
{
border: 2px solid rgb(0, 0, 0);
border-radius: 10px;
min-width: 80px;
min-height: 35px;
}
QPushButton
{
background-color: rgb(0, 255, 0);
}
QLineEdit
{
background-color: rgb(0, 170, 255);
}
QPushButton:hover, QRadioButton:hover, QCheckBox:hover
{
color: red;
}
QPushButton:!hover, QRadioButton:!hover, QCheckBox:!hover
{
color: black;
}
如果立即运行应用,则可以看到外观上的更改。 您还将注意到,即使“关闭确认”对话框小部件的样式也已更改,其原因仅仅是因为我们在其父窗口中设置了样式表。 这是屏幕截图:

不用说,您可以通过将样式表保存在文本文件中,然后在运行时加载并设置样式表来执行相同的操作,就像我们在本章后面为综合计算机视觉应用的基础构建时所做的那样。 如本章前面所述,您甚至可以在应用中存储默认样式表(请参阅 Qt 资源系统),并默认加载它,如果在自定义文件的特定位置存储了自定义文件,则可以跳过该样式表。 电脑。 这样,您可以轻松拥有可定制的应用。 您甚至可以拆分任务,并要求专业设计师为您提供样式表,以便您可以在应用中使用它。 这实质上就是 Qt 应用中样式的简单程度。
要获得更多特定于样式表的语法和帮助,最好始终关注 Qt Creator 帮助模式下的样式表语法文章,因为 Qt 样式表基本上是特定于 Qt 的,并且在某些情况下与标准 CSS 有所不同。
多国语言支持
在本节中,您将学习如何使用 Qt 框架创建支持多种语言的应用。 实际上,所有这些都归结为非常易于使用的单个类。 QTranslator类是主要的 Qt 类,负责处理输出(显示)文本的国际化。 您只需要确保以下几点:
- 在构建项目时,请使用默认语言(例如英语)。 这意味着,只需对显示的所有内容使用默认语言的句子和单词即可。
- 确保
tr()函数中包含代码中的所有文字语句,或者具体来说,选择其他语言时需要翻译的所有文字语句。
例如,在代码中,如果您需要编写诸如Open Input Image之类的文字语句(就像在Hello_Qt_OpenCV示例中所做的那样),只需将其传递给tr函数并编写tr("Open Input Image")。 设计器不是这种情况,仅适用于代码内文字字符串。 在设计器中设置属性时,只需使用文字字符串即可。
- 确保在项目文件中指定翻译文件。 为此,您需要使用
TRANSLATIONS来指定它们,就像项目文件中的SOURCES和HEADERS一样。
例如,如果要在应用中使用德语和土耳其语翻译,请将以下内容添加到项目(*.PRO)文件中:
TRANSLATIONS = translation_de.ts translation_tr.ts
确保对每个翻译文件始终使用清晰的名称。 即使您可以随意命名,也最好使用包含的语言代码来命名它们(tr表示土耳其语,de表示德语,等等),如前面的示例所示。 这也有助于 Qt Linguist 工具(您将在后面学习)了解翻译的目标语言。
- 使用 Qt 的
lupdate工具创建 TS 文件(如果已经存在,则更新它们)。lupdate是 Qt 工具,可在所有源代码和 UI 文件中搜索可翻译的文本,然后创建或更新上一步中提到的 TS 文件。 负责翻译应用的人员可以使用 Qt Linguist 工具轻松打开 TS 文件,并专注于通过简单的用户界面来翻译应用。
lupdate位于 Qt 安装的bin文件夹内。 例如,在 Windows OS 上,它的路径类似于此:
C:\Qt\Qt5.9.1\5.9.1\msvc2015\bin
您可以从 Qt Creator 中在项目中执行lupdate,只需在主菜单中单击“工具/外部/语言学家/更新翻译”(lupdate)。 Windows 用户的重要注意事项:如果在运行lupdate后遇到任何问题,则可能是由于 Qt 安装故障所致。 要解决此问题,只需使用开发环境的命令提示符运行lupdate。 如果按照本书中的说明进行操作,则可以从开始菜单执行Developer Command Prompt for VS2015,然后使用cd命令切换到项目文件夹,然后运行lupdate,如此处所示(示例案例为Hello_Qt_OpenCV 我们之前创建的项目):
C:\Qt\Qt5.9.1\5.9.1\msvc2015\bin\lrelease.exe Hello_Qt_OpenCV.pro
运行此命令后,如果进入项目文件夹,您会注意到现在已经创建了先前在项目文件中指定的 TS 文件。 随着您的应用越来越大,定期运行lupdate非常重要,它可以提取需要翻译的新字符串,并进一步扩展多语言支持。
- 使用 Qt Linguist 工具翻译所有必需的字符串。 它已安装在您的计算机上,因为它是默认 Qt 安装的一部分。 只需选择文件/打开,然后从
Project文件夹中选择所有 TS 文件(刚创建的)并打开它们。 如果到目前为止,您已经遵循了有关Hello_Qt_OpenCV项目的所有说明,那么在 Qt Linguist 中打开 TS 文件后,您应该看到以下屏幕:

Qt Linguist 允许快速,轻松地翻译项目中所有可翻译的元素。 只需使用显示的所有语言为每个项目编写翻译,然后使用顶部的工具栏将其标记为Done。 在退出 Qt Linguist 工具之前,请确保进行保存。
- 使用转换后的 TS 文件创建压缩的 QM 文件和二进制 Qt 语言文件。 为此,您需要使用 Qt
lrelease工具。
使用lrelease与lupdate相同,您在前面的步骤中了解到。 只需将所有lupdate命令替换为lrelease,就可以了。
- 将 QM 文件(二进制语言文件)添加到您的应用资源中。
您已经学习了如何使用 Qt 资源系统。 只需创建一个称为Translations的新前缀,然后在该前缀下添加新创建的 QM 文件。 如果正确完成,则项目中应包含以下内容:

- 现在,您可以开始使用
QTranslator类在您的应用中使用多种语言,还可以在运行时在各种语言之间切换。 让我们再次回到示例项目Hello_Qt_OpenCV。 在应用中使用翻译器有多种方法,但是,现在,我们将从最简单的方法开始。 首先将QTranslator包含文件添加到mainwindow.h文件中,并在MainWindow类中定义两个私有QTranslator对象,如下所示:
QTranslator *turkishTranslator;
QTranslator *germanTranslator;
- 在调用
loadSettings函数之后,将以下内容添加到MainWindow构造器代码中:
turkishTranslator = new QTranslator(this);
turkishTranslator
->load(":/translations/translation_tr.qm");
germanTranslator = new QTranslator(this);
germanTranslator
->load(":/translations/translation_de.qm");
- 现在,是时候向我们的
Hello_Qt_OpenCV项目添加一个主菜单,并允许用户在这些语言之间进行切换了。 您只需在 Qt Creator 设计模式下右键单击窗口,然后选择“创建菜单栏”即可。 然后,在顶部菜单栏中添加一个名为Language的项目。 只需单击并键入以下内容,即可向其中添加三个子项目:
现在,您应该有一个类似于以下内容的主菜单:

在设计器的底部,您可以找到动作编辑器。 显然,您现在在此处有三个条目,这些条目是在创建主菜单时自动创建的。 它们每个都对应于您在主菜单中输入的每种语言名称。
- 右键单击“土耳其语”,然后选择“转到插槽”,然后从列表中选择
trigger(),然后单击“确定”。 换句话说(如您在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中所了解的),为actionTurkish对象的触发插槽编写以下代码行:
void MainWindow::on_actionTurkish_triggered()
{
qApp->installTranslator(turkishTranslator);
}
- 为
actionGerman对象添加以下行。 基本上,重复说明,但将其适应于actionTurkish对象:
void MainWindow::on_actionGerman_triggered()
{
qApp->installTranslator(germanTranslator);
}
- 并对
actionEnglish对象执行相同的操作。 这次,您需要从应用中删除翻译器,因为英语是我们应用的默认语言:
void MainWindow::on_actionEnglish_triggered()
{
qApp->removeTranslator(turkishTranslator);
qApp->removeTranslator(germanTranslator);
}
- 好了,我们现在就可以在我们的 Qt 应用中找到所有与翻译有关的内容,除了我们需要确保屏幕上的项目已重新翻译并基本上已重新加载。 为此,我们需要使用
QMainWindow类的changeEvent。 每次使用前面的installTranslator和removeTranslator函数安装或移除翻译器时,Language Change事件都会发送到应用中的所有窗口。 要捕获此事件,并确保在语言更改时重新加载窗口,我们需要在程序中覆盖changeEvent函数。 只需将以下代码行添加到mainwindow.h文件中MainWindow类的受保护成员,就在您先前定义closeEvent的位置之后:
void changeEvent(QEvent *event);
- 现在,将以下代码片段添加到您的
mainwindow.cpp文件中:
void MainWindow::changeEvent(QEvent *event)
{
if(event->type() == QEvent::LanguageChange)
{
ui->retranslateUi(this);
}
else
{
QMainWindow::changeEvent(event);
}
}
前面的代码只是意味着,如果change事件是Language Change,则重新转换窗口,否则,一切都应照常进行。 retranslateUi函数是使用UIC生成的(请参阅本章的UIC部分),它只是根据应用中最新安装的QTranslator对象设置正确翻译的字符串。
而已。 现在,您可以运行您的应用,并尝试在两种语言之间切换。 我们已经完成了第一个真正的多语言应用。 重要的是要注意,您在本节中学到的内容基本上适用于每个 Qt 应用,并且是制作多语言应用的标准方法。 在应用中使用不同语言的更为定制的方式将遵循几乎相同的指令集,但是如果不使用资源文件将语言文件内置到应用中,则如果从磁盘上的某个位置加载语言会更好。 。 这具有更新翻译甚至添加新语言(具有更多代码)的优势,而不必重建应用本身。
创建和使用插件
在应用中使用插件是扩展应用的最强大方法之一,人们日常生活中使用的许多应用都受益于插件的功能。 插件只是一个库(Windows 上为*.dll,Linux 上为*.so等),可以在运行时加载并使用它来执行特定任务,但是当然,它不能像独立应用一样执行,这取决于使用它的应用。 在本书中,我们还将使用插件来扩展计算机视觉应用。
在本节中,我们将学习如何创建一个示例应用(称为Image_Filter),该应用简单地加载和使用计算机上指定文件夹中的插件。 但是,在此之前,我们将学习如何在 Qt 中创建同时使用 Qt 和 OpenCV 框架的插件,因为我们的插件很可能需要使用 OpenCV 库来做一些计算机视觉魔术。 所以,让我们开始吧。
首先,我们需要定义一组接口,以便我们的应用与插件对话。 与 C++ 中的接口等效的是带有纯虚函数的类。 因此,我们基本上需要一个接口,其中包含我们期望在插件中提供的所有功能。 通常,这就是创建插件的方式,这也是第三方开发人员可以为他人开发的应用编写插件的方式。 是的,他们知道插件的接口,只需要用实际执行某些功能的实际代码填充即可。
接口
该接口比乍看之下要重要得多。 是的,它基本上是一个什么都不做的类,但是,它列出了我们的应用在所有时间所需的所有插件的草图。 因此,我们需要确保从一开始就将所有必需的功能都包含在插件接口中,否则,以后几乎不可能添加,删除或修改功能。 由于目前我们正在处理一个示例项目,因此看上去可能并不那么严重,但是在实际项目中,这些通常是决定应用可扩展性的一些关键因素。 因此,既然我们知道了接口的重要性,就可以开始为示例项目创建一个接口。
打开 Qt Creator,并确保没有打开任何项目。 现在,从主菜单中选择“文件/新文件”或“项目”。 在出现的窗口中,从左侧列表(底部的列表)中选择 C++,然后选择C++ Header File。 输入cvplugininterface作为文件名,然后继续进行操作,直到进入代码编辑器模式。 将代码更改为以下内容:
#ifndef CVPLUGININTERFACE_H
#define CVPLUGININTERFACE_H
#include <QObject>
#include <QString>
#include "opencv2/opencv.hpp"
class CvPluginInterface
{
public:
virtual ~CvPluginInterface() {}
virtual QString description() = 0;
virtual void processImage(const cv::Mat &inputImage,
cv::Mat &outputImage) = 0;
};
#define CVPLUGININTERFACE_IID "com.amin.cvplugininterface"
Q_DECLARE_INTERFACE(CvPluginInterface, CVPLUGININTERFACE_IID)
#endif // CVPLUGININTERFACE_H
您可能已经注意到,类似于以下代码的行会自动添加到使用 Qt Creator 创建的任何头文件中:
#ifndef CVPLUGININTERFACE_H
#define CVPLUGININTERFACE_H
...
#endif // CVPLUGININTERFACE_H
这些只是确保在应用编译期间每个头文件仅被包含(并被处理)一次。 基本上,还有许多其他方法可以在 C++ 中实现相同的目标,但这是最广泛接受和使用的方法,尤其是 Qt 和 OpenCV 框架都获得了最高程度的跨平台支持。 与 Qt Creator 一起使用时,始终会自动将其添加到头文件中,而无需执行任何其他工作。
前面的代码基本上是 Qt 中插件接口所需的一切。 在示例接口中,我们只有两种简单类型的函数,我们需要插件来支持,但是正如我们稍后将要看到的那样,为了支持参数,语言等,我们需要的还不止这些。 但是,对于我们的示例,这应该足够了。
对于一般的 C++ 开发人员来说,一个非常重要的注意事项是前一个接口中的第一个公共成员,它在 C++ 中被称为虚拟析构器,它是许多人忘记包括而又不太注意的最重要方法之一, 因此,最好查看一下它的真实含义并记住它以避免内存泄漏,尤其是在使用 Qt 插件时:
virtual ~CvPluginInterface() {}
基本上,任何具有虚拟方法并打算多态使用的 C++ 基类都必须包含虚拟析构器。 这有助于确保即使使用基类的指针访问子类中的析构器也可以调用它们(多态)。 不幸的是,对于大多数 C++ 编译器,在犯此常见 C++ 编程错误时,您甚至都不会收到警告。
因此,我们的插件接口包含一个名为description()的函数,该函数旨在返回任何插件的描述以及有关该插件的有用信息。 它还包含一个名为processImage的函数,该函数将OpenCV Mat类作为输入,并返回一个作为输出。 显然,在此函数中,我们希望每个插件执行某种图像处理,过滤等操作,然后将结果提供给我们。
之后,我们使用Q_DECLARE_INTERFACE宏将类定义为接口。 如果不包含此宏,Qt 将无法将我们的类识别为插件接口。 CVPLUGININTERFACE_IID应该是具有类似包名称格式的唯一字符串,但是您基本上可以根据自己的喜好进行更改。
确保将cvplugininterface.h文件保存到您选择的任何位置,然后将其关闭。 现在,我们将创建一个使用此接口的插件。 让我们使用先前在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”时看到的 OpenCV 函数:medianBlur。
插件
现在,我们将创建一个名为median_filter_plugin的插件,该插件使用我们的CvPluginInterface接口类。 首先从主菜单中选择“文件”,然后选择“新建文件”或“项目”。 然后,选择“库”和“C++ 库”,如以下屏幕截图所示:

确保选择“共享库”作为“类型”,然后输入median_filter_plugin作为名称,然后单击“下一步”。 选择套件类型为桌面,然后单击前进。 在“选择所需的模块”页面中,确保仅选中QtCore,然后继续单击“下一步”(最后是“完成”),而不更改任何选项,直到最终进入 Qt Creator 的代码编辑器。
我们基本上创建了一个 Qt 插件项目,并且您可能已经注意到,插件项目的结构与到目前为止我们尝试过的所有应用项目非常相似(除了它没有 UI 文件),这是因为插件实际上不能与应用相同,只是它不能单独运行。
现在,将我们在上一步中创建的cvplugininterface.h文件复制到新创建的插件项目的文件夹中。 然后,只需在“项目”窗格中右键单击项目文件夹,然后从弹出菜单中选择“添加现有文件”,即可将其添加到项目中,如下所示:

我们需要告诉 Qt 这是一个插件,而不仅仅是任何库。 为此,我们需要将以下内容添加到我们的*.PRO文件中。 您可以将其添加到项目中的任何位置,但是将其添加到TEMPLATE = lib行是一个好习惯:
CONFIG += plugin
现在,我们需要将 OpenCV 添加到我们的插件项目中。 到现在为止,这对您来说应该是小菜一碟。 只需将以下内容添加到插件的*.PRO文件中,就像之前使用Hello_Qt_OpenCV项目所做的那样:
win32: {
include("c:/dev/opencv/opencv.pri")
}
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
unix: macx{
INCLUDEPATH += "/usr/local/include"
LIBS += -L"/usr/local/lib" \
-lopencv_world
}
当您向*.PRO文件中添加一些代码,或者使用 Qt Creator 主菜单(和其他用户界面快捷方式)添加新的类或 Qt 资源文件时,手动运行qmake是一个很好的习惯。 如果您发现 Qt Creator 与项目内容不同步。 您可以通过从“项目”窗格的右键菜单中选择“运行qmake”来轻松完成此操作,如以下屏幕截图所示:

好了,场景已经设定好了,我们可以开始为第一个 Qt + OpenCV 插件编写代码了。 正如您将在接下来的章节中看到的那样,我们将使用插件向我们的应用添加类似的功能; 这样,我们将只关注开发插件,而不用为添加的每个功能修改整个应用。 因此,熟悉和熟悉此过程的这一步骤非常重要。
首先打开median_filter_plugin.h文件并进行如下修改:
#ifndef MEDIAN_FILTER_PLUGIN_H
#define MEDIAN_FILTER_PLUGIN_H
#include "median_filter_plugin_global.h"
#include "cvplugininterface.h"
class MEDIAN_FILTER_PLUGINSHARED_EXPORT Median_filter_plugin:
public QObject, public CvPluginInterface
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "com.amin.cvplugininterface")
Q_INTERFACES(CvPluginInterface)
public:
Median_filter_plugin();
~Median_filter_plugin();
QString description();
void processImage(const cv::Mat &inputImage,
cv::Mat &outputImage);
};
#endif // MEDIAN_FILTER_PLUGIN_H
创建median_filter_plugin项目时,大多数上述代码是自动生成的。 这就是基本 Qt 库类定义的外观。 但是,正是我们的添加使它变成了一个有趣的插件。 让我们回顾一下前面的代码,看看真正添加到该类中的内容:
-
首先,我们包含了
cvplugininterface.h头文件。 -
然后,我们确保
Median_filter_plugin类继承了QObject和CvPluginInterface。 -
之后,我们添加了 Qt 所需的宏,以便将我们的库识别为插件。 这仅表示以下三行代码,它们对应于您在本章之前学过的第一个
Q_OBJECT宏,并且默认情况下应存在于任何 Qt 类中以允许 Qt 特定的功能(例如信号和插槽)。 下一个是Q_PLUGIN_METADATA,它需要在插件的源代码中恰好出现一次,用于添加有关插件的元数据; 最后一个Q_INTERFACES需要声明在插件中实现的接口。 这是必需的宏:
Q_OBJECT
Q_PLUGIN_METADATA
Q_INTERFACES
- 然后,我们在类中添加了
description和processImage函数的定义。 在这里,我们将真正定义插件的功能,而不是仅拥有声明而不是实现的接口类。 - 最后,我们可以将所需的更改和实际实现添加到
median_filter_plugin.cpp文件中。 确保将以下三个函数添加到median_filter_plugin.cpp文件的底部:
Median_filter_plugin::~Median_filter_plugin()
{}
QString Median_filter_plugin::description()
{
return "This plugin applies median blur filters to any image."
" This plugin's goal is to make us more familiar with the"
" concept of plugins in general.";
}
void Median_filter_plugin::processImage(const cv::Mat &inputImage,
cv::Mat &outputImage)
{
cv::medianBlur(inputImage, outputImage, 5);
}
我们刚刚添加了类析构器的实现:description和processImage函数。 如您所见,description函数返回有关插件的有用信息,在这种情况下,它没有复杂的帮助页面,而只是几句话。 processImage函数仅将medianBlur应用于图像,您已经(简要地)在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中使用了该图像。
现在,您可以在右键单击项目后或从主菜单上的“生成”条目中单击“重建”。 这将创建一个插件文件,我们将在下一节中使用它,通常在与项目处于同一级别的文件夹下。 这是Build文件夹,在第 2 章,“创建我们的第一个 Qt 和 OpenCV 项目”中引入了该文件夹。
取决于操作系统,插件文件的扩展名可以不同。 例如,在 Windows 上应为.dll,在 MacOS 和 Linux 上应为.dylib或.so,依此类推。
插件加载器和用户
现在,我们将使用在本书上一节中创建的插件。 让我们从创建一个新的 Qt Widgets 应用项目开始。 我们命名为Plugin_User。 创建项目后,首先将 OpenCV 框架添加到*.PRO文件中(您已经足够了解此内容),然后继续创建类似于以下内容的用户界面:
- 显然,您需要修改
mainwindow.ui文件,对其进行设计以使其看起来像这样,并设置所有对象名称,如以下屏幕快照所示:

确保使用与上图中相同的布局类型。
-
接下来,将
cvplugininterface.h文件添加到该项目的文件夹中,然后使用“添加现有文件”选项将其添加到项目中,就像创建插件时一样。 -
现在,我们可以开始为用户界面编写代码,以及加载,检查和使用插件所需的代码。 首先,将所需的标头添加到
mainwindow.h文件,如下所示:
#include <QDir>
#include <QFileDialog>
#include <QMessageBox>
#include <QPluginLoader>
#include <QFileInfoList>
#include "opencv2/opencv.hpp"
#include "cvplugininterface.h"
- 然后,在
};之前,向MainWindow类的私有成员添加一个函数,这似乎是个好地方:
void getPluginsList();
- 现在,切换到
mainwindow.cpp并将以下定义添加到文件顶部,紧接在任何现有#include行之后:
#define FILTERS_SUBFOLDER "/filter_plugins/"
- 然后,将以下函数添加到
mainwindow.cpp,这基本上是getPluginsList函数的实现:
void MainWindow::getPluginsList()
{
QDir filtersDir(qApp->applicationDirPath() +
FILTERS_SUBFOLDER);
QFileInfoList filters = filtersDir.entryInfoList(
QDir::NoDotAndDotDot |
QDir::Files, QDir::Name);
foreach(QFileInfo filter, filters)
{
if(QLibrary::isLibrary(filter.absoluteFilePath()))
{
QPluginLoader pluginLoader(
filter.absoluteFilePath(),
this);
if(dynamic_cast<CvPluginInterface*>(
pluginLoader.instance()))
{
ui->filtersList->addItem(
filter.fileName());
pluginLoader
.unload(); // we can unload for now
}
else
{
QMessageBox::warning(
this, tr("Warning"),
QString(tr("Make sure %1 is a correct"
" plugin for this application<br>"
"and it's not in use by some other"
" application!"))
.arg(filter.fileName()));
}
}
else
{
QMessageBox::warning(this, tr("Warning"),
QString(tr("Make sure only plugins"
" exist in plugins folder.<br>"
"%1 is not a plugin."))
.arg(filter.fileName()));
}
}
if(ui->filtersList->count() <= 0)
{
QMessageBox::critical(this, tr("No Plugins"),
tr("This application cannot work without plugins!"
"<br>Make sure that filter_plugins folder exists "
"in the same folder as the application<br>and that "
"there are some filter plugins inside it"));
this->setEnabled(false);
}
}
首先让我们看看这个函数的作用。 前面的函数,我们将在MainWindow类的构造器中调用该函数:
- 首先,假设在名为
filter_plugins的子文件夹中存在插件,并且该子文件夹与应用可执行文件位于同一文件夹中。 (稍后,我们需要在该项目的build文件夹内手动创建此文件夹,然后将在上一步中构建的插件复制到该新创建的文件夹中。)以下是用于获取过滤器插件的直接路径的信息。 子文件夹:
qApp->applicationDirPath() + FILTERS_SUBFOLDER
- 接下来,它使用
QDir类的entryInfoList函数从文件夹中提取QFileInfoList。QFileInfoList类本身基本上是一个QList类,其中包含QFileInfo项(QList<QFileInfo>),每个QFileInfo项都提供有关磁盘上文件的信息。 在这种情况下,每个文件都是一个插件。 - 之后,通过遍历
foreach循环中的文件列表,它检查plugins文件夹中的每个文件,以确保仅接受插件(库)文件,请使用以下函数:
QLibrary::isLibrary
- 然后检查通过上一步的每个库文件,以确保它与我们的插件接口兼容。 我们不仅会允许任何库文件作为插件被接受,因此我们将以下代码用于此目的。
dynamic_cast<CvPluginInterface*>(pluginLoader.instance())
- 如果库在上一步中通过了测试,则认为该库是正确的插件(与
CvPluginInterface兼容),已添加到我们窗口的列表小部件中,然后被卸载。 我们可以在需要时简单地重新加载和使用它。 - 在每个步骤中,如果有问题,则使用
QMessageBox向用户显示有用的信息。 同样,最后,如果列表为空,则意味着没有插件可使用,窗口上的窗口小部件被禁用,应用不可用。
- 不要忘记在
setupUi调用之后从MainWindow构造器调用此函数。 - 我们还需要编写
inputImgButton的代码,该代码用于打开图像文件。 这里是:
void MainWindow::on_inputImgButton_pressed()
{
QString fileName =
QFileDialog::getOpenFileName(
this,
tr("Open Input Image"),
QDir::currentPath(),
tr("Images") + " (*.jpg *.png *.bmp)");
if(QFile::exists(fileName))
{
ui->inputImgEdit->setText(fileName);
}
}
我们之前已经看过这段代码,不需要解释。 它只是允许您打开图像文件并确保已正确选择它。
- 现在,我们将为
helpButton编写代码,该代码将在插件中显示description函数的结果:
void MainWindow::on_helpButton_pressed()
{
if(ui->filtersList->currentRow() >= 0)
{
QPluginLoader pluginLoader(
qApp->applicationDirPath() +
FILTERS_SUBFOLDER +
ui->filtersList->currentItem()->text());
CvPluginInterface *plugin =
dynamic_cast<CvPluginInterface*>(
pluginLoader.instance());
if(plugin)
{
QMessageBox::information(this, tr("Plugin Description"),
plugin->description());
}
else
{
QMessageBox::warning(this, tr("Warning"),
QString(tr("Make sure plugin %1" " exists and is usable."))
.arg(ui->filtersList->currentItem()->text()));
}
}
else
{
QMessageBox::warning(this, tr("Warning"), QString(tr("First
select a filter" " plugin from the list.")));
}
}
我们使用QPluginLoader类从列表中正确加载插件,然后使用instance函数获取其实例,最后,我们将通过接口在插件中调用该函数。
- 相同的逻辑也适用于
filterButton。 唯一的区别是这一次,我们将调用实际的过滤函数,如下所示:
void MainWindow::on_filterButton_pressed()
{
if(ui->filtersList->currentRow() >= 0 &&
!ui->inputImgEdit->text().isEmpty())
{
QPluginLoader pluginLoader(qApp->applicationDirPath() +
FILTERS_SUBFOLDER +
ui->filtersList->currentItem()->text());
CvPluginInterface *plugin =
dynamic_cast<CvPluginInterface*>(
pluginLoader.instance());
if(plugin)
{
if(QFile::exists(ui->inputImgEdit->text()))
{
using namespace cv;
Mat inputImage, outputImage;
inputImage = imread(ui->inputImgEdit->
text().toStdString());
plugin->processImage(inputImage, outputImage);
imshow(tr("Filtered Image").toStdString(),
outputImage);
}
else
{
QMessageBox::warning(this,
tr("Warning"),
QString(tr("Make sure %1 exists."))
.arg(ui->inputImgEdit->text()));
}
}
else
{
QMessageBox::warning(this, tr("Warning"),
QString(tr(
"Make sure plugin %1 exists and is usable." ))
.arg(ui->filtersList->currentItem()->text()));
}
}
else
{
QMessageBox::warning(this, tr("Warning"),
QString(tr( "First select a filter plugin from the list." )));
}
}
使用QMessageBox或其他类型的信息提供功能,始终让用户了解正在发生的事情以及可能发生的问题,这一点很重要。 如您所见,它们通常甚至比正在执行的实际任务花费更多的代码,但这对于避免应用崩溃至关重要。 默认情况下,Qt 不支持异常处理,并相信开发人员将使用足够的if和else指令来处理所有可能的崩溃情况。 关于前面的代码示例的另一个重要说明是tr函数。 请记住,始终将其用于文字字符串。 这样,以后您可以轻松地使您的应用成为多语言。 即使您不打算支持多种语言,也要习惯于在文字字符串中添加tr函数,这是一个好习惯。 它不会造成任何伤害。
现在,我们准备运行我们的Plugin_User应用。 如果现在运行它,我们将看到一条错误消息(将其放置在自己的位置),并且将被警告没有适当的插件。 为了能够使用我们的Plugin_User应用,我们需要执行以下操作:
- 在
Plugin_User项目的生成文件夹中创建一个名为filter_plugins的文件夹。 这是创建项目可执行文件的文件夹。 - 复制我们构建的插件文件(该文件将是
median_filter_plugin项目的build文件夹内的库文件),然后将其粘贴到第一步中的filter_plugins文件夹中。 如前所述,取决于操作系统,诸如可执行程序之类的插件文件将具有扩展名。
现在,尝试运行Plugin_User,一切都会正常。 您应该能够在列表中看到单个插件,将其选中,单击帮助按钮以获取有关它的信息,然后单击过滤器按钮以将插件中的过滤器应用于图像。 如以下屏幕截图所示:

尝试创建另一个名为gaussian_filter_plugin的插件,并遵循与median_filter_plugin完全相同的指令集,仅这次,使用在第 2 章,“Qt 和 OpenCV 项目”中看到的gaussianBlur函数。 然后构建它并将其放在filter_plugins文件夹中,然后再次运行Plugin_User应用。 另外,请尝试放置一些随机库文件(和其他非库文件)以测试我们在这些情况下编写的应用。
这里要注意的一个非常重要的事情是,您必须确保不要将以调试模式构建的插件与以发布模式构建的应用一起使用,反之亦然。 还有其他重要规则适用于加载插件,例如,使用 Qt 较高版本构建的插件不能与使用 Qt 较低版本构建的应用一起使用。 使用较低的 Qt 主版本号构建的插件不能与使用较高的 Qt 主版本号构建的应用一起使用。 有关插件及其用法的更新信息,请始终参阅 Qt 文档或 Qt Creator 帮助模式下的“部署插件”文章。
建立基础
您在本章中学到的所有内容都旨在使您准备开始构建全面的计算机视觉应用,该应用将执行以下操作:
- 使用插件扩展其功能
- 使用 Qt 样式表自定义其外观
- 支持多种语言
因此,从现在开始,我们将考虑您在本章,前几章中学到的所有事实,为应用奠定基础,例如:
- 我们的应用将能够保存和加载所有用户首选项和设置。 我们将通过使用
QSettings类来实现它,您已经学习了如何使用它。 - 最好有一个集中的单一 Qt 样式表来照顾我们应用的整体外观,并且更好的是从磁盘加载而不是将其嵌入应用本身。
为此,除非用户从应用的设置页面中手动选择主题,否则我们将简单地假定应用具有本机外观。 主题将是 Qt 样式表,保存在应用可执行文件所在的同一文件夹中的themes文件夹中,样式表文件的扩展名将为thm。 所选主题(或准确地说,样式表)将在运行时从磁盘加载。
- 支持多种语言至关重要。 我们将创建一个应用,该应用无需扩展应用即可扩展支持的语言。
这可以通过将 Qt 二进制语言文件放在应用可执行文件所在的文件夹中的languages文件夹中来完成。 我们可以使用系统默认语言并加载用户的语言(如果我们有其翻译和二进制语言文件); 否则,我们可以加载默认语言,例如英语。 我们还可以允许用户在运行时通过从设置页面中选择应用来更改其语言。
- 我们将构建一个支持处理单个图像和视频帧的计算机视觉应用。
为了实现这一点,我们需要有一个与本章中所看到的非常相似的插件接口(CvPluginInterface),该接口将图像作为输入并产生输出。 然后,我们将需要以本章中所看到的确切方式加载和使用这些插件。 我们将假定插件位于名为cvplugins的文件夹中,该文件夹将存在于我们的应用可执行文件所在的文件夹中。
除此之外,我们需要考虑计算机视觉应用中即将出现的一些障碍。 在构建应用时始终遥遥领先,这一点很重要; 否则,您将陷入尴尬的境地而无路可走。 因此,它们是:
- 在我们的应用中,我们将不断处理图像和视频。 不仅来自文件,还来自摄像机或来自网络(例如互联网)的供稿通道的输入。 我们将在第 4 章,“
Mat和QImage”中介绍所有内容。 - 没有适当的工具来查看和播放图像,涉及计算机视觉的应用将一无所获。 本主题和所有相关主题将在第 5 章,“图形视图框架”中介绍。
- 稍后,在第 9 章“视频分析”中,我们的计算机视觉应用将需要处理和处理视频,这不仅意味着单张图像,而且连续的一组图像(或帧)也会影响过程的结果。 这显然不能通过本章中看到的插件接口来实现,我们将需要创建在单独线程中工作的插件。 我们将在第 8 章,“多线程”中保留该主题,您将在这里学习 Qt 中的并行处理机制。 之后,我们将能够创建适合视频处理的新插件接口,然后在我们的应用中使用它。
现在,您可以使用 Qt Creator 创建一个 Qt Widgets 应用并将其命名为Computer_Vision。 我们将扩展此应用,直到第 9 章,“视频分析”结束,然后我们将逐步介绍所有新概念。 从本章,前几章中学到的知识中,您应该能够自己创建上述基础列表中的前三项(对主题,语言和插件的支持),强烈建议您至少尝试这样做; 但是,在接下来的两章中,我们将扩展此基础应用。 稍后,在第 5 章,“图形视图框架”结束时,将为您提供下载Computer_Vision的完整基础项目的链接。 。 该基础项目将包含一个MainWindow类,该类能够加载和显示其中包含 GUI 的插件。 在该项目中,您还将找到一个插件接口(类似于本章中看到的接口),但是具有更多的功能,可以实现以下功能:
- 获取插件标题
- 获取插件的描述(帮助)信息
- 获取插件特有的 GUI(Qt 容器小部件)
- 获取插件的类型,无论是处理并返回任何图像还是仅在其 GUI 中显示信息
- 将图像传递到插件并获得结果
该基础项目源代码将包含与您在本章中看到的功能类似的功能,以设置样式,更改语言等。
总结
在您作为开发人员的职业生涯或研究工作期间,您会经常遇到可持续这个词。 本章的目的是向您介绍一般创建可持续应用的基本概念,尤其是使用 Qt 和 OpenCV 创建计算机视觉应用的基本概念。 现在,您已经熟悉创建插件,这意味着您可以创建一个应用,该应用可以由第三方开发人员(当然,也可以是您自己)创建的可重用库进行扩展,而无需重建核心应用。 在本章中,您还学习了自定义 Qt 应用的外观和感觉以及如何创建多语言 Qt 应用。
这是一个漫长但充满希望的章节。 如果您已完成所有示例和分步说明,那么现在您应该已经熟悉使用 Qt 框架进行跨平台应用开发的一些最关键的技术。 在本章中,您了解了 Qt 中的样式表和样式表,以及它提供的一些重要功能来开发美观的应用。 然后,我们继续学习创建多语言应用。 在全球社区中,以及在我们的应用可访问世界每个角落(由于在线应用商店等)的时代,构建支持多种语言的应用是必须要做的,而不仅仅是在大多数情况下都是首选情况。 在学习了多语言应用开发之后,我们进入了插件主题,并通过动手实例和经验学习了其所有基础知识。 我们创建的项目看起来很简单,它包含构建插件以及使用它们的应用的所有重要方面。
在第 4 章,“Mat和QImage”中,您将了解 OpenCV Mat和 Qt QImage类(以及相关的类),这两个框架都可以处理图像数据。 您将了解所有不同的读取方法(从文件,照相机等)以及写入图像,将它们相互转换并最终在 Qt 应用中显示它们的方法。 到目前为止,我们使用 OpenCV 中的imshow函数仅在默认窗口中显示结果图像。 在第 4 章,“Mat和QImage”中,这将成为历史,因为您将学习如何将 OpenCV Mat转换为QImage类,然后将其正确显示在 Qt 小部件。
四、Mat和QImage
在第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们了解了创建全面且可持续的应用的基本规则,这些应用看起来很吸引人,支持多种语言,并且通过使用 Qt 中的插件系统易于扩展。 现在,我们将通过学习负责处理计算机视觉数据类型的类和结构,来进一步扩展有关计算机视觉应用基础的知识库。 了解 OpenCV 和 Qt 框架所需的基本结构和数据类型是了解在应用中执行时处理它们的基础计算机视觉功能如何执行的第一步。 OpenCV 是旨在提高速度和性能的计算机视觉框架。 另一方面,Qt 是一个不断发展的应用开发框架,具有大量的类和函数。 这就是为什么他们俩都需要一组定义明确的类和结构来处理要在计算机视觉应用中处理,显示甚至保存或打印的图像数据的原因。 始终使自己熟悉有关 Qt 和 OpenCV 中现有结构的有用详细信息,这始终是一个好习惯。
您已经使用过 OpenCV 的Mat类来简要地读取和处理图像。 正如您将在本章中了解到的,即使Mat是负责处理 OpenCV 中图像数据的主要类(至少在传统上是这样),Mat类也有一些变体, 实用的功能,其中某些功能甚至是特定功能所必需的,您将在接下来的章节中学习这些功能。 在 Qt 框架的情况下,即使QImage是 Qt 中用于处理图像数据的主要类,也没有太大不同,还有更多的类(有时具有惊人的相似名称)用于支持计算机。 视觉并处理图像数据,视频等。
在本章中,我们将从最关键的 OpenCV 类Mat开始,然后继续研究不同的变体(其中有些是Mat的子类),最后向您介绍新的UMat类, 这是该框架的 OpenCV 3 补充。 我们将学习使用新的UMat类(实际上是Mat兼容)而不是Mat类的优点。 然后,我们将进入 Qt 的QImage类,并学习如何通过将这两种数据类型相互转换在 OpenCV 和 Qt 之间传递图像数据。 我们还将学习QPixmap,QPainter以及许多其他 Qt 类,对于要从事计算机视觉领域的任何人,所有这些都是必不可少的类。
最后,我们将学习 OpenCV 和 Qt 框架从文件,摄像机,网络订阅源等读取,写入和显示图像和视频的多种方式。 正如您将在本章末了解到的那样,始终最好根据所需的计算机视觉任务来决定哪种类最适合我们,因此在处理图像数据输入或输出时,我们应该对手头的各种选项有足够的了解 。
我们将在本章中介绍的主题包括:
Mat类,其子类和新的UMat类的简介QImage简介和计算机视觉中使用的主要 Qt 类- 如何读取,写入和显示图像和视频
- 如何在 Qt 和 OpenCV 框架之间传递图像数据
- 如何在 Qt 中创建自定义小部件并使用
QPainter对其进行绘制
关于Mat类
在前面的章节中,您非常简要地体验了 OpenCV 框架的Mat类,但是现在我们将更深入地进行研究。 从矩阵中借用其名称的Mat类是n维数组,能够在单个或多个通道中存储和处理不同的数学数据类型。 为了进一步简化,让我们看一下计算机视觉中的图像。 计算机视觉中的图像是像素矩阵(因此为二维数组),具有指定的宽度(矩阵中的列数)和高度(矩阵中的行数)。 此外,灰度图像中的像素可以用一个数字(因此是单个通道)表示,最小值(通常为 0)表示黑色,而最大值(通常为 255)是一个字节可能出现的最大值 )代表白色,而介于两者之间的所有值则对应于不同的灰色强度。 请看以下示例图像,它们只是较大的灰度图像的放大部分。 每个像素都标记有我们刚才提到的强度值:

同样,标准 RGB 彩色图像中的像素具有三个不同的元素,而不是一个(因此具有三个通道),分别对应于红色,蓝色和绿色值。 看下面的图像例如:

如上图所示,在简单的图像查看器程序中,放大(缩放)的图像可以显示出负责图像的像素。 考虑每个Mat类中的单个元素,可以直接访问,修改和使用它们。 图像的这种矩阵状表示使某些功能最强大的计算机视觉算法可以轻松地处理图像,测量所需值甚至生成新图像。
这是上一个示例图片中放大的区域的另一种表示形式。 每个像素都标有基础的红色,绿色和蓝色值:

考虑图像数据和像素的大小写有助于理解Mat类,并且正如我们稍后将要看到的,Mat类和一般的 OpenCV 中的大多数函数都假定Mat是图像, 但是,必须注意Mat可以包含任何数据(不仅是图像),并且实际上在 OpenCV 中有Mat用于传递图像以外的数据数组的情况。 我们将在第 6 章,“OpenCV 中的图像处理”中学习一些相关示例。
由于一般而言,目前Mat类和 OpenCV 函数的数学细节并不符合我们的最大兴趣,因此,我们将以给定的介绍就足够了,并着重于Mat类及其在 OpenCV 中的基础方法。
构造器,属性和方法
构造Mat类的方法很多。 在撰写本书时,Mat类具有二十多种不同的构造器。 其中一些只是便捷的构造器,但是例如为了创建三个或更多维数组,还需要其他一些函数。 以下是一些使用最广泛的构造器以及如何使用它们的示例:
创建一个10x10矩阵,每个元素一个通道 8 位无符号整数(或字节):
Mat matrix(10, 10, CV_8UC(1));
创建相同的矩阵,并使用0的值初始化其所有元素:
Mat matrix(10, 10, CV_8UC(1), Scalar(0);
前面代码中所示的构造器中的第一个参数是行数,第二个参数是矩阵中的列数。 但是,第三个参数非常重要,它将类型,位数和通道数混合到一个宏中。 这是宏的模式和可以使用的值:
CV_<bits><type>C(<channels>)
让我们看看宏的每个部分都用于:
<bits>可以替换为:
8:用于无符号和有符号整数16:用于无符号和有符号整数32:用于无符号和有符号整数以及浮点数64:用于无符号和有符号浮点数
<type>可以替换为:
U:用于无符号整数S:用于有符号整数F:用于带符号的浮点数
从理论上讲,<channels>可以用任何值代替,但是对于一般的计算机视觉函数和算法,它不会高于 4。
如果使用的通道数不超过四个,则可以省略<channels>参数的左括号和右括号。 如果通道数仅为一个,则也可以完全省略<channels>和前面的C。 为了获得更好的可读性和一致性,最好使用在前面和后面的示例中使用的标准模式,并且在使用这种广泛使用的宏的方式中保持一致也是一种良好的编程习惯。
创建一个边长为 10 且具有两个double类型(64 位)通道元素的多维数据集(三维数组),并使用1.0的值初始化所有值。 显示如下:
int sizes[] = {10, 10, 10};
Mat cube(3, sizes, CV_64FC(2), Scalar::all(1.0));
您还可以稍后使用Mat类的create方法来更改其大小和类型。 这是一个例子:
Mat matrix;
// ...
matrix.create(10, 10, CV_8UC(1));
Mat类的先前内容无关紧要。 基本上,将全部删除(安全清理,并将分配的内存分配回操作系统),并创建一个新的Mat类。
您可以创建一个Mat类,该类是另一个Mat类的一部分。 这称为感兴趣的区域(ROI),当我们需要访问图像的一部分,就好像它是独立图像时,它特别有用。 例如,当我们只想过滤图像的一部分时。 这是创建 ROI Mat类的方法,该类包含一个50x50像素宽的正方形,从图像的(X = 25, Y = 25)位置开始:
Mat roi(image, Rect(25,25,50,50));
在OpenCV中指定Mat的大小时,通常以行和列(高度和宽度)为单位,这有时会使习惯于首先看到宽度的人感到困惑,因为其他框架在很多情况下就是这样。 原因仅仅是OpenCV中图像的矩阵方法。 如果您更喜欢后者,则可以在创建Mat类时在 OpenCV 中使用Size类。
在本节的示例中,除非另有明确说明,否则假设使用imread函数使用前面章节中的测试图像获取Mat类型的image变量。 这将有助于我们获取Mat类所需的信息,但是,本章稍后将在imread和类似函数中看到更多信息。
让我们看一下下图,以更好地理解 OpenCV Mat类中 ROI,大小和位置的概念。 如下图所示,图像的左上角被视为图像中坐标系的原点。 因此,原点的位置是(0, 0)。 类似地,图像的右上角具有位置值(width - 1, 0),其中宽度可以用列数代替。 考虑到这一点,图像的右下角将具有(width-1,height-1)的位置值,依此类推。 现在,让我们考虑基于如下所示的区域创建一个Mat类。 我们可以使用前面看到的方法,但是我们需要使用Rect类提供 ROI 的左上角及其宽度和高度:

重要的是要注意,使用先前的方法创建 ROI Mat类时,对 ROI 像素的所有更改都会影响原始图像,因为创建 ROI 不会执行原始Mat类内容的深层复制 。 如果出于任何原因想要将Mat类复制到新的(且完全独立的)Mat中,则需要使用clone函数,如以下示例所示:
Mat imageCopy = image.clone();
假设Mat图像包含先前的图像(来自先前的章节),您可以使用以下示例代码选择图像中看到的 ROI,并使突出显示区域中的所有像素均为黑色:
4: Mat roi(image, Rect(500, 138, 65, 65));
roi = Scalar(0);
您还可以选择Mat中的一个或多个行或列,其方式与我们对 ROI 进行的方式非常相似,除了需要使用row,rowRange,column或colRange在Mat类中起作用。 这是如何做:
Mat r = image.row(0); // first row
Mat c = image.row(0); // first column
这是另一个使用rowRange和colRange函数的示例,这些函数可用于选择一系列行和列,而不是仅选择一行。 以下示例代码将在图像的中心产生一个+符号,其厚度为 20 个像素:
Mat centralRows = image.rowRange(image.rows/2 - 10,
image.rows/2 + 10);
Mat centralColumns = image.colRange(image.cols/2 - 10,
image.cols/2 + 10);
centralRows = Scalar(0);
centralColumns = Scalar(0);
这是在我们的测试图像上执行的结果:

当使用前面提到的方法提取 ROI 并将其存储在新的Mat类中时,可以使用locateROI函数获取父图像的大小和 ROI 在父对象内部的图片的左上角位置。 这是一个例子:
Mat centralRows = image.rowRange(image.rows/2 - 10,
image.rows/2 + 10);
Size parentSize;
Point offset;
centralRows.locateROI(parentSize, offset);
int parentWidth = parentSize.width;
int parentHeight = parentSize.height;
int x = offset.x;
int y = offset.y;
执行此代码后,parentWidth将包含图像的宽度,parentHeight将包含图像的高度,x和y将包含centralRows在父对象中的左上位置 Mat或换句话说就是图像。
Mat类还包含许多有用的属性和函数,可用于获取有关任何单个Mat类实例的信息。 信息丰富的意思是指提供有关每个像素,通道,颜色深度,宽度和高度的详细信息的成员,以及更多类似的成员。 这些成员包括:
depth:包含Mat类的深度。 深度值对应于Mat类的类型和位数。 因此,它可以是以下值之一:CV_8U:8 位无符号整数CV_8S:8 位有符号整数CV_16U:16 位无符号整数CV_16S:16 位有符号整数CV_32S:32 位有符号整数CV_32F:32 位浮点数CV_64F:64 位浮点数
channels:它仅包含Mat类的每个元素中的通道数。 对于标准图像,该值通常为三个通道。type:这将包含Mat类的类型。 这与本章前面创建Mat类所使用的类型常量相同。cols:这对应于Mat类中的列数或图像宽度。rows:这对应于Mat类中的行数或图像高度。elemSize:可用于获取Mat类中每个元素的大小(以字节为单位)。elemSize1:无论通道数如何,均可用于获取Mat类中每个元素的大小(以字节为单位)。 例如,在三通道图像中,elemSize1将包含elemSize的值除以三。empty:如果Mat类中没有元素,则返回true,否则返回false。isContinuous:可用于检查Mat的元素是否以连续方式存储。 例如,只有一个单行的Mat类始终是连续的。
使用create函数创建的Mat类始终是连续的。 重要的是要注意,在这种情况下,Mat类的二维表示是使用step值来处理的。 这意味着在连续的元素数组中,每步数的元素对应于二维表示中的一行。
isSubmatrix:如果Mat类是另一个Mat类的子矩阵,则返回true。 在前面的示例中,在所有使用其他图像创建 ROI 的情况下,此属性将返回true,并且在父Mat类中为false。total:这将返回Mat类中的元素总数。 例如,在图像中,此值等于宽度乘以图像的高度。step:返回与Mat类中的一个步骤相对应的元素数。 例如,在标准图像(非连续存储的图像)中,step包含Mat类的宽度(或cols)。
除了提供信息的成员之外,Mat类还包含许多用于访问其单个元素(或像素)(并对其执行操作)的函数。 它们包括:
at:这是一个模板函数,可用于访问Mat类中的元素。 访问图像中的元素(像素)特别有用。 这是一个例子。 假设我们在名为image的Mat类中加载了标准的三通道彩色图像。 这意味着image的类型为CV_8UC(3),然后我们可以简单地编写以下内容以访问位置X,Y的像素,并将其颜色值设置为C:
image.at<Vec3b>(X,Y) = C;
OpenCV 提供Vec(向量)类及其变体,以便于数据访问和处理。 您可以使用以下typedef创建和命名自己的Vec类型:
typedef Vec<Type, C> NewType;
例如,在前面的代码中,您可能已经定义了自己的 3 字节向量(例如 QCvVec3B),并用它代替Vec3b,并使用以下代码:
typedef Vec<quint8,3> QCvVec3B;
不过,您可以使用at函数:
typedef Vec<uchar, 2> Vec2b;
typedef Vec<uchar, 3> Vec3b;
typedef Vec<uchar, 4> Vec4b;
typedef Vec<short, 2> Vec2s;
typedef Vec<short, 3> Vec3s;
typedef Vec<short, 4> Vec4s;
typedef Vec<ushort, 2> Vec2w;
typedef Vec<ushort, 3> Vec3w;
typedef Vec<ushort, 4> Vec4w;
typedef Vec<int, 2> Vec2i;
typedef Vec<int, 3> Vec3i;
typedef Vec<int, 4> Vec4i;
typedef Vec<int, 6> Vec6i;
typedef Vec<int, 8> Vec8i;
typedef Vec<float, 2> Vec2f;
typedef Vec<float, 3> Vec3f;
typedef Vec<float, 4> Vec4f;
typedef Vec<float, 6> Vec6f;
typedef Vec<double, 2> Vec2d;
typedef Vec<double, 3> Vec3d;
typedef Vec<double, 4> Vec4d;
typedef Vec<double, 6> Vec6d;
begin和end:它们可用于使用类似 C++ STL 的迭代器来检索和访问Mat类中的元素。forEach:可用于在Mat类的所有元素上并行运行函数。 该函数需要提供一个函数对象,函数指针或 Lambda。
Lambda 仅在 C++ 11 和更高版本上可用,如果您还没有这样做,它们就是切换到 C++ 11 和更高版本的重要原因。
以下三个示例代码使用前面的代码中提到的访问方法实现了相同的目标,它们都通过将每个像素值除以5使图像更暗。 首先,使用at函数:
for(int i=0; i<image.rows; i++)
{
for(int j=0; j<image.cols; j++)
{
image.at<Vec3b>(i, j) /= 5;
}
}
接下来,使用具有begin和end函数的类 STL 迭代器:
MatIterator_<Vec3b> it_begin = image.begin<Vec3b>();
MatIterator_<Vec3b> it_end = image.end<Vec3b>();
for( ; it_begin != it_end; it_begin++)
{
*it_begin /= 5;
}
最后,使用[lambda]提供的forEach函数:
image.forEach<Vec3b>([](Vec3b &p, const int *)
{
p /= 5;
});
这是生成的较暗的图像,对于所有前面的三个代码来说都是相同的:

正如您已经注意到的,Mat是具有许多方法的类,并且很显然,因为它是使用 OpenCV 和图像时的基本构建块。 除了函数和属性,您之前已经看过,在继续进行下一部分之前,我们还需要了解一些其他函数。 他们来了:
adjustROI:此函数可用于轻松更改子矩阵(或准确地说是 ROI 矩阵)的大小。clone:这是创建Mat类的深层副本的广泛使用的函数。 一个示例情况是,您可能希望过滤或处理图像,但仍保留原始图像的副本以供以后比较。convertTo:可用于更改Mat类的数据类型。 此函数还可以选择缩放图像。copyTo:此函数可用于将全部(或图像的一部分)复制到另一个Mat。ptr:可用于获取指针并访问Mat中的图像数据。 根据重载的版本,您可以获得一个指向特定行或图像中任何其他位置的指针。release:此函数在Mat析构器中调用,并且基本上负责Mat类所需的内存清理任务。reserve:可用于为许多指定的行保留存储空间。reserveBuffer:类似于reserve,但是它为多个指定字节保留了存储空间。reshape:当我们需要更改通道数以获取矩阵数据的不同表示形式时,这很有用。 一个示例情况是将具有单个通道且每个元素中的每个具有三个字节的Mat(例如Vec3b)转换为具有每个元素中的每个字节具有一个字节的三通道Mat。 显然,这样的转换(或精确地调整形状)将导致目标Mat中的行计数乘以三。 之后,可以使用所得矩阵的转置在行和列之间进行切换。 稍后,您将学习t或转置函数。resize:可用于更改Mat类中的行数。setTo:可用于将矩阵中的所有或某些元素设置为指定值。
最后但并非最不重要的一点是,Mat类提供了一些方便的方法来处理矩阵运算,例如:
cross:计算两个三元素矩阵的叉积。diag:从矩阵中提取对角线。dot:计算两个矩阵的点积。eye:这是一个静态函数,可用于创建单位矩阵。inv:创建逆矩阵。mul:计算两个矩阵的逐元素乘法或除法。ones:这是另一个静态函数,可用于创建一个矩阵,其所有元素都持有值1。t:此函数可用于获取Mat类的转置矩阵。 有趣的是,该函数等同于镜子和图像旋转 90 度。 有关更多信息,请参见后续图像。zeroes:可用于创建一个矩阵,其所有元素的值为零。 这等于给定宽度,高度和类型的全黑图像。
在以下屏幕截图中,左侧的图像是原始图像,而右侧的图像是生成的转置图像。 由于转置矩阵的转置与原始矩阵相同,因此我们也可以说左侧的图像是右侧的图像的转置结果。 这是在图像上执行Mat类的t函数的示例结果:

同样重要的是要注意,Mat类也可以进行所有标准算术运算。 例如,与其像前面的例子中讨论Mat类中的访问方法那样,不将所有像素一一分割,我们可以编写以下代码:
Mat darkerImage = image / 5; // or image * 0.2
在这种情况下,矩阵中的每个元素(或图像,如果需要)将进行完全相同的操作。
Mat_<_Tp>类
Mat_<_Tp>类是具有相同成员的Mat类(和模板类)的子类,但是当在<indexentry content="Mat class:Mat_ class" dbid="256603" state="mod">编译时已知矩阵(或图像中的元素)的类型时,它会非常有用。 与Mat类的at函数相比,它还提供了一种更好的访问方法(可以说更具可读性)。 这是一个简短的示例:
Mat_<Vec3b> imageCopy(image); // image is a Mat class
imageCopy(10, 10) = Vec3b(0,0,0); // imageCopy can use ()
如果您对类型小心,可以将Mat_<_Tp>类传递给接受Mat类的任何函数,而不会出现任何问题。
Matx<_Tp, m, n>
Matx类仅用于在编译时具有已知类型,宽度和高度的<indexentry content="Mat class:Matx class" dbid="256603" state="mod">小矩阵的情况。 它具有类似于Mat的方法,并提供矩阵运算,再次类似于Mat。 通常,您可以使用刚刚学习的相同Mat类代替Matx,因为它提供了更多的灵活性和可使用的功能。
UMat类
UMat类是新引入的Mat类,在 3.0 之前的 OpenCV 版本中不可用。 使用新的UMat类(或统一的Mat类)的优势主要取决于运行它的平台上是否存在OpenCL层。 我们不会详细介绍这些细节,但是应该足以注意到OpenCL(带有 L,不要与我们自己的 OpenCV 混淆)是一个允许 CPU,GPU 和系统上的其他计算资源一起工作(有时甚至是并行工作)的框架,来实现共同的计算目标。 因此,简单地说,如果它存在于平台上,则将UMat类传递给 OpenCV 函数将导致调用基础OpenCL指令(假设它们在特定功能中实现),从而获得计算机视觉应用的更高性能。 否则,将UMat简单地转换为Mat类,并调用标准的仅 CPU 实现。 与以前版本的 OpenCV 不同,所有的OpenCL实现都位于ocl命名空间中,并且与ocl命名空间完全分离,这允许统一的抽象(这是U的来源),并使其更易于使用更快的OpenCL实现。 标准实现。
因此,最好始终使用UMat类而不是Mat类,尤其是在具有底层OpenCL实现的 CPU 密集型函数中。 只要我们不使用较旧的 OpenCV 版本,就不会有问题。 只是请注意,在需要在Mat和UMat之间进行显式转换的情况下(正如您稍后将看到的,在某些情况下是必需的),每个类都提供了一个可用于转换它的函数。 到另一个:
Mat::getUMat
UMat::getMat
对于这两个函数,都需要一个访问标志,该标志可以是:
ACCESS_READACCESS_WRITEACCESS_RWACCESS_FAST
在本书的整个过程中,我们将尽可能地交替使用Mat和UMat类。 UMat和OpenCL实现是一种日益增长的 OpenCV 现象,习惯使用它具有巨大的优势。
InputArray,OutputArry,InputOutputArray
您会注意到,大多数 OpenCV 函数都接受这些类型的参数,而不是Mat及其类似的数据类型。 这些是用于提高可读性和数据类型支持的代理数据类型。 这只是意味着您可以将以下任何数据类型传递给 OpenCV 函数,除了InputArray,OutputArray或InputOutputArray数据类型:
MatMat_<T>Matx<T, m, n>std::vector<T>std::vector<std::vector<T> >std::vector<Mat>std::vector<Mat_<T> >UMatstd::vector<UMat>double
注意,OpenCV 像Mat或类似的类一样对待标准 C++ 向量(std::vector)。 或多或少显而易见的原因是它们的基础数据结构或多或少都是相同的。
永远不要显式创建InputArray,OutputArry或InputOutputArray。 只需传递前面提到的一种类型,一切都会好起来的。
使用 OpenCV 读取图像
既然我们已经了解了 OpenCV 中的Mat类,我们可以继续学习如何读取图像并为图像填充Mat类以进一步处理它。 正如您在前几章中简要看到的那样,imread函数可用于从磁盘读取图像。 这是一个例子:
Mat image = imread("c:/dev/test.jpg", IMREAD_GRAYSCALE |
IMREAD_IGNORE_ORIENTATION);
imread仅将 C++ std::string类作为第一个参数,将ImreadModes标志作为第二个参数。 如果由于某种原因无法读取图像,则返回空的Mat类(data == NULL),否则,将返回Mat类,其中填充了具有第二个参数中指定的类型和颜色的图像像素。 根据平台中某些图像类型的可用性,imread可以读取以下图像类型:
- Windows 位图:
\*.bmp,\*.dib - JPEG 文件:
\*.jpeg,\*.jpg,\*.jpe - JPEG 2000 文件:
\*.jp2 - 便携式网络图形:
\*.png - WebP:
\*.webp - 便携式图像格式:
\*.pbm,\*.pgm,\*.ppm,\*.pxm,\*.pnm - SUN 栅格:
\*.sr,\*.ras - TIFF 文件:
\*.tiff,\*.tif - OpenEXR 图像文件:
\*.exr - Radius HDR:
\*.hdr,\*.pic - Gdal 支持的栅格和向量地理空间数据
您可以看到ImreadModes枚举表示可以传递给imread函数的可能标志。 在我们的示例中,我们使用了以下内容:
IMREAD_GRAYSCALE | IMREAD_IGNORE_ORIENTATION
这意味着我们希望将图像加载为灰度图像,并且还希望忽略存储在图像文件的 EXIF 数据部分中的方向信息。
OpenCV 还支持读取多页图像文件。 因此,您需要使用imreadmulti函数。 这是一个简单的例子:
std::vector<Mat> multiplePages;
bool success = imreadmulti("c:/dev/multi-page.tif", multiplePages,
IMREAD_COLOR);
除了imread和imreadmulti,OpenCV 还支持使用imdecode函数从存储缓冲区读取图像。 如果图像未存储在磁盘上或需要从网络读取数据流中,则此函数特别有用。 用法与imread函数几乎相同,除了需要为其提供数据缓冲区而不是文件名。
使用 OpenCV 写入图像
OpenCV 中的imwrite函数可用于将图像写入磁盘上的文件。 它使用文件名的扩展名来确定图像的格式。 要在imwrite函数中自定义压缩率和类似设置,您需要使用ImwriteFlags,ImwritePNGFlags等。 这是一个简单的示例,展示了如何在设置了渐进模式且质量相对较低(较高的压缩率)的情况下将图像写入 JPG 文件:
std::vector<int> params;
params.push_back(IMWRITE_JPEG_QUALITY);
params.push_back(20);
params.push_back(IMWRITE_JPEG_PROGRESSIVE);
params.push_back(1); // 1 = true, 0 = false
imwrite("c:/dev/output.jpg", image, params);
如果要使用默认设置,则可以完全省略params并只需输入:
imwrite("c:/dev/output.jpg", image, params);
有关imwrite函数中支持的文件类型的相同列表,请参见上一节中的imread函数。
除了imwrite,OpenCV 还支持使用imencode函数将图像写入内存缓冲区。 与imdecode相似,在图像需要传递到数据流而不是保存到文件中的情况下,这尤其有用。 用法与imwrite函数几乎相同,除了需要为其提供数据缓冲区而不是文件名。 在这种情况下,由于未指定文件名,因此imdecode还需要扩展图像以决定输出格式。
在 OpenCV 中读写视频
OpenCV 提供了一个简单易用的类,称为VideoCapture,可从磁盘上保存的文件,捕获设备,摄像机或网络视频流(例如,RTSP 上的 RTSP 地址)读取视频(或图像序列)。 互联网)。 您可以简单地使用open函数来尝试从任何提到的源类型打开视频,然后使用read函数将传入的视频帧捕获为图像。 这是一个例子:
VideoCapture video;
video.open("c:/dev/test.avi");
if(video.isOpened())
{
Mat frame;
while(true)
{
if(video.read(frame))
{
// Process the frame ...
}
else
{
break;
}
}
}
video.release();
如果要加载图像序列,只需要将文件名替换为文件路径模式。 例如,image_%02d.png将读取文件名如image_00.png,image_01.png和image_02.png等的图像。
对于来自网络 URL 的视频流,只需提供 URL 作为文件名即可。
关于我们的示例要注意的另一重要事项是,它不是一个完整且可以立即使用的示例。 您会发现,如果尝试一下,无论何时程序进入while循环,都将阻止 GUI 更新,并且您的应用甚至可能崩溃。 使用 Qt 时,对此的快速解决方案是通过在循环内添加以下代码来确保还处理了 GUI(和其他)线程:
qApp->processEvents();
稍后,我们将在第 8 章,“多线程”和第 9 章,“视频分析”中了解有关此问题的更正确的解决方法。
除了我们学到的知识外,VideoCapture类还提供两个重要函数,即set和get。 这些可用于配置该类的众多参数。 有关可配置参数的完整列表,请参考VideoCaptureProperties枚举。
这是一个永不过时的技巧。 您也可以使用 Qt Creator 代码完成功能,并只需编写CAP_PROP_,因为所有相关参数均以此开头。 基本上,该技巧也适用于查找任何函数,枚举等。 在不同的 IDE 中使用这些技巧通常不会在书中讨论,但在某些情况下可能意味着节省大量时间。 以前面提到的内容为例,例如,您可以在 Qt Creator 代码编辑器中编写VideoCaptureProperties,然后按住Ctrl按钮并单击。 这将带您到枚举的源,并且您可以查看所有可能的枚举,并且如果幸运的话,源代码中的文档正在等待着您。
这是一个简单的示例,可读取视频中的帧数:
double frameCount = video.get(CAP_PROP_FRAME_COUNT);
这是另一个将视频中抓帧器的当前位置设置为帧号100的示例:
video.set(CAP_PROP_POS_FRAMES, 100);
在使用上与VideoCapture类几乎相同,您可以使用VideoWriter类将视频和图像序列写入磁盘。 但是,在编写带有VideoWriter类的视频时,需要更多一些参数。 这是一个例子:
VideoWriter video;
video.open("c:/dev/output.avi", CAP_ANY, CV_FOURCC('M','P', 'G',
'4'), 30.0, Size(640, 480), true);
if(video.isOpened())
{
while(framesRemain())
{
video.write(getFrame());
}
}
video.release();
在此示例中,framesRemain和getFrame函数是虚构函数,用于检查是否还有要写入的剩余函数,并获取帧(Mat)。 如示例所示,在这种情况下需要提供一个捕获 API(由于它是可选的,因此我们在VideoCapture中将其省略)。 此外,在打开用于写入的视频文件时,必须具有FourCC代码, FPS(每秒帧)和帧大小。 可以使用OpenCV中定义的CV_FOURCC宏输入FourCC代码。
有关可能的FourCC代码的列表,请参见这里。 请务必注意,某些FourCC代码及其相应的视频格式可能在平台上不可用。 在将应用部署到客户时,这一点很重要。 您需要确保您的应用可以读写需要支持的视频格式。
OpenCV 中的 HighGUI 模块
OpenCV 中的 HighGUI 模块负责制作快速简单的 GUI。 在本书的第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们已经使用了模块imshow中广泛使用的函数之一来快速显示图像。 但是,当我们要了解 Qt 和用于处理 GUI 创建的更复杂的框架时,我们将完全跳过此模块,而转至 Qt 主题。 但是,在此之前,值得引用 OpenCV 文档中 HighGUI 模块的当前介绍:
“虽然 OpenCV 设计用于全面应用,并且可以在功能丰富的 UI 框架(例如 Qt,WinForms 或 Cocoa)中使用,或者根本不使用任何 UI,但有时需要快速尝试功能并可视化结果。这就是 HighGUI 模块的设计目标。”
正如您将在本章稍后了解的那样,我们还将停止使用imshow函数,并坚持使用 Qt 功能以正确,一致地显示图像。
Qt 中的图像和视频处理
Qt 使用几种不同的类来处理图像数据,视频,照相机和相关的计算机视觉主题。 在本节中,我们将学习它们,并学习如何在 OpenCV 和 Qt 类之间进行链接,以获得更灵活的计算机视觉应用开发体验。
QImage类
Qt QImage可能是 Qt 中最重要的与计算机视觉相关的类,它是处理图像数据的主要 Qt 类,它提供对图像的像素级访问,以及许多其他处理图像数据的函数。 我们将介绍其构造器和函数的最重要子集,尤其是使用OpenCV时最重要的子集。
QImage包含许多不同的构造器,这些构造器允许从文件或原始图像数据或空白图像创建和处理其像素的QImage。 我们可以创建一个具有给定大小和格式的空QImage类,如以下示例所示:
QImage image(320, 240, QImage::Format_RGB888);
这将创建一个320x240像素(宽度和高度)的标准 RGB 彩色图像。 您可以参考QImage::Format枚举(使用QImage类文档)以获取受支持格式的完整列表。 我们还可以传递QSize类而不是值,并编写以下代码:
QImage image(QSize(320, 240), QImage::Format_RGB888);
下一个构造器也是从 OpenCV Mat类创建QImage的方法之一。 这里要注意的重要一点是,OpenCV Mat类中的数据格式应与QImage类中的数据格式兼容。 默认情况下,OpenCV 以 BGR 格式(不是 RGB)加载彩色图像,因此,如果我们尝试使用该格式构建QImage,则会在通道数据中输入错误。 因此,我们首先需要将其转换为 RGB。 这是一个例子:
Mat mat = imread("c:/dev/test.jpg");
cvtColor(mat, mat, CV_BGR2RGB);
QImage image(mat.data,
mat.cols,
mat.rows,
QImage::Format_RGB888);
在此示例中,cvtColor函数是 OpenCV 函数,可用于更改Mat类的色彩空间。 如果我们省略该行,我们将得到一个QImage,它的蓝色和红色通道已互换。
可以使用我们将要看到的下一个QImage构造器来创建先前代码的正确版本(以及将Mat转换为QImage的推荐方法)。 它还需要一个bytesPerLine参数,这是我们在Mat类中了解的step参数。 这是一个例子:
Mat mat = imread("c:/dev/test.jpg");
cvtColor(mat, mat, CV_BGR2RGB);
QImage image(mat.data,
mat.cols,
mat.rows,
mat.step,
QImage::Format_RGB888);
使用此构造器和bytesPerLine参数的优点是我们还可以转换连续存储在内存中的图像数据。
下一个构造器也是从磁盘上保存的文件读取QImage的方法。 这是一个例子:
QImage image("c:/dev/test.jpg");
请注意,Qt 和 OpenCV 支持的文件类型彼此独立。 这仅表示一个提到的框架可能根本不支持文件类型,在这种情况下,读取特定文件类型时需要选择另一个框架。 默认情况下,Qt 支持读取以下图像文件类型:
| 格式 | 说明 | 支持 |
|---|---|---|
| BMP | Windows 位图 | 读/写 |
| GIF | 图形交换格式(可选) | 读 |
| JPG | 联合摄影专家组 | 读/写 |
| JPEG | 联合摄影专家组 | 读/写 |
| PNG | 便携式网络图形 | 读/写 |
| PBM | 便携式位图 | 读 |
| PGM | 便携式灰度图 | 读 |
| PPM | 便携式像素图 | 读/写 |
| XBM | X11 位图 | 读/写 |
| XPM | X11 像素图 | 读/写 |
供参考的表源:位于这个页面的QImage类文档。
除了所有构造器之外,QImage包括以下成员,这些成员在处理图像时非常方便:
allGray:可以用来检查图像中的所有像素是否都是灰色阴影。 这基本上检查所有像素在各自通道中是否具有相同的 RGB 值。bits和constBits(仅是bits的const版本):这些可用于访问QImage中的基础图像数据。 这可以用于将QImage转换为Mat以便在OpenCV中进行进一步处理。 与将Mat转换为QImage时所看到的相同,在这里我们也需要确保它们与格式兼容。 为确保这一点,我们可以添加convertToFormat函数,以确保我们的QImage是标准的三通道 RGB 图像。 这是一个例子:
QImage image("c:/dev/test.jpg");
image = image.convertToFormat(QImage::Format_RGB888);
Mat mat = Mat(image.height(),
image.width(),
CV_8UC(3),
image.bits(),
image.bytesPerLine());
极其重要的是要注意,当像这样传递数据时,以及像将Mat转换为QImage时所看到的那样,在 Qt 和 OpenCV 中的类之间传递了相同的内存空间。 这意味着,如果您在前面的示例中修改了Mat类,则实际上是在修改图像类,因为您只是将其数据指针传递给了Mat类。 同时这非常有用(更容易操作图像)和危险(应用崩溃),并且像这样使用 Qt 和 OpenCV 时需要小心。 如果要确保QImage和Mat类具有完全独立的数据,则可以使用Mat类中的clone函数或QImage中的copy函数。
byteCount:这将返回图像数据占用的字节数。bytesPerLine:类似于Mat类中的step参数。 它为图像中每条扫描线提供字节数。 这基本上与width相同,或者更好的是byteCount/height。convertToFormat:可用于将图像转换为另一种格式。 在前面的bits函数示例中,我们已经看到了一个示例。copy:可用于将图像的部分(或全部)复制到另一个QImage类。depth:这将返回图像的深度(或每像素位数)。fill:此函数可用于填充相同颜色的图像中的所有像素。
像这样的函数以及 Qt 框架中的许多其他类似函数,可以使用QColor,Qt::GlobalColor这三种颜色类型,最后是与像素中的位相对应的整数值。 即使它们非常易于使用,在继续之前,花一些时间以 Qt Creator Help模式阅读其文档页面也是明智的。
format:可用于获取QImage中图像数据的当前格式。 正如我们在前面的示例中看到的,QImage::Format_RGB888是在 Qt 和OpenCV之间传递图像数据时最兼容的格式。hasAlphaChannel:如果图像具有 Alpha 通道,则返回true。 Alpha 通道用于确定像素的透明度。height,width和size:这些可用于获取图像的高度,宽度和尺寸。isNull:如果没有图像数据,则返回true,否则返回false。load,loadFromData和fromData:它们可用于从磁盘或从缓冲区中存储的数据中检索图像(类似于OpenCV中的imdecode)。mirrored:这实际上是一种图像处理函数,可用于垂直,水平或同时镜像(翻转)图像。pixel:类似于Mat类中的at函数,pixel可用于检索像素数据。pixelColor:类似于pixel,但此返回一个QColor。rect:这将返回一个QRect类,其中包含图像的边界矩形。rgbSwapped:这是一个非常方便的函数,尤其是在使用 OpenCV 并显示图像时。 它在不更改实际图像数据的情况下交换蓝色和红色通道。 正如我们将在本章稍后看到的那样,这是在 Qt 中正确显示Mat类并避免 OpenCVcvtColor函数调用所必需的。save:这些可用于将图像内容保存到文件中。scaled,scaledToHeight和scaledToWidth:提及的所有三个函数均可用于调整图像大小以适合给定大小。 (可选)调用此函数时,可以使用以下常量之一来解决任何长宽比问题。 我们将在接下来的章节中看到更多有关此的内容。Qt::IgnoreAspectRatioQt::KeepAspectRatioQt::KeepAspectRatioByExpanding
setPixel和setPixelColor:这些可用于设置图像中单个像素的内容。setText:可用于以支持它的图像格式设置文本值。text:可用于检索设置到图像的文本值。transformed:顾名思义,此函数用于转换图像。 它采用QMatrix或QTransform类并返回转换后的图像。 这是一个简单的例子:
QImage image("c:/dev/test.jpg");
QTransform trans;
trans.rotate(45);
image = image.transformed(trans);
trueMatrix:可用于检索用于变换图像的变换矩阵。valid:这将获取一个点(X, Y),如果给定点是图像内的有效位置,则返回true,否则返回false。
QPixmap类
QPixmap类在某些方面类似于QImage,但是当我们需要在屏幕上显示图像时,可以使用QPixmap类。 QPixmap可用于加载和保存图像(就像QImage一样),但它不提供处理图像数据的灵活性,我们也仅在执行所有的修改,处理和操作之后,在需要显示任何图像时再使用它。 大多数QPixmap方法与QImage方法同名,并且基本上以相同的方式使用。 对我们来说很重要且在QImage中不存在的两个函数如下:
convertFromImage:此函数可用于用来自QImage的图像数据填充QPixmap数据fromImage:这是一个静态函数,基本上与convertFromImage相同
现在,我们将创建一个示例项目,以利用我们到目前为止所学的知识进行实践。 没有真正的动手项目,本章中学习的所有令人兴奋的技术都将浪费掉,因此让我们从我们的图像查看示例应用开始:
- 首先在 Qt Creator 中创建一个新的 Qt Widgets 应用并将其命名为
ImageViewer。 - 然后选择
mainwindow.ui,然后使用设计器删除菜单栏,状态栏和工具栏,然后在窗口上放置一个标签小部件(QLabel)。 单击窗口上的空白区域,然后按Ctrl + G将所有内容(只有标签的小部件)布置为网格。 这将确保始终调整所有大小以适合窗口。 - 现在,将
label的alignment/Horizontal属性更改为AlignHCenter。 然后将其Horizontal和VerticalsizePolicy属性都更改为Ignored。 接下来,将以下include语句添加到mainwindow.h文件中:
#include <QPixmap>
#include <QDragEnterEvent>
#include <QDropEvent>
#include <QMimeData>
#include <QFileInfo>
#include <QMessageBox>
#include <QResizeEvent>
- 现在,使用代码编辑器将以下受保护的函数添加到
mainwindow.h中的MainWindow类定义中:
protected:
void dragEnterEvent(QDragEnterEvent *event);
void dropEvent(QDropEvent *event);
void resizeEvent(QResizeEvent *event);
- 另外,将私有的
QPixmap添加到您的mainwindow.h中:
QPixmap pixmap;
- 现在,切换到
mainwindow.cpp并将以下内容添加到MainWindow构造器中,以便在程序开始时立即对其进行调用:
setAcceptDrops(true);
- 接下来,在
mainwindow.cpp文件中添加以下函数:
void MainWindow::dragEnterEvent(QDragEnterEvent *event)
{
QStringList acceptedFileTypes;
acceptedFileTypes.append("jpg");
acceptedFileTypes.append("png");
acceptedFileTypes.append("bmp");
if (event->mimeData()->hasUrls() &&
event->mimeData()->urls().count() == 1)
{
QFileInfo file(event->mimeData()->urls().at(0).toLocalFile());
if(acceptedFileTypes.contains(file.suffix().toLower()))
{
event->acceptProposedAction();
}
}
}
- 应添加到
mainwindow.cpp的另一个函数如下:
void MainWindow::dropEvent(QDropEvent *event)
{
QFileInfo file(event->mimeData()->urls().at(0).toLocalFile());
if(pixmap.load(file.absoluteFilePath()))
{
ui->label->setPixmap(pixmap.scaled(ui->label->size(),
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
}
else
{
QMessageBox::critical(this,
tr("Error"),
tr("The image file cannot be read!"));
}
}
- 最后,将以下函数添加到
mainwindow.cpp,我们准备执行我们的应用:
void MainWindow::resizeEvent(QResizeEvent *event)
{
Q_UNUSED(event);
if(!pixmap.isNull())
{
ui->label->setPixmap(pixmap.scaled(ui->label->width()-5,
ui->label->height()-5,
Qt::KeepAspectRatio,
Qt::SmoothTransformation));
}
}
您猜对了,我们只是编写了一个显示在其中拖放图像的应用。 通过向MainWindow添加dragEnterEvent函数,我们可以检查所拖动的对象是否是文件,尤其是它是否是单个文件。 然后,我们检查了图像类型以确保它受支持。
在dropEvent函数中,我们只需将图像文件加载到QPixmap中,然后将其拖放到应用窗口中即可。 然后,将QLabel类的pixmap属性设置为pixmap。
最后,在resizeEvent函数中,我们确保无论窗口大小如何,我们的图像始终会缩放以适合具有正确纵横比的窗口。
忘记上面描述的一个简单步骤,您将面临 Qt 中的拖放编程技术的问题。 例如,没有下面的行,即在我们的MainWindow类的构造器中,则无论将什么函数添加到MainWindow类,都不会接受任何删除:
setAcceptDrops(true);
这是生成的应用的屏幕截图。 尝试将不同的图像拖放到应用窗口中,以查看会发生什么。 您甚至可以尝试拖放非图像文件以确保不接受它们:

这基本上是一本有关如何在 Qt 中显示图像以及如何在 Qt 应用中添加拖放功能的教程。 正如我们在前面的示例中看到的那样,可以将QPixmap与QLabel小部件一起轻松显示。 QLabel小部件名称有时可能会引起误解,但实际上,它不仅可以用于显示纯文本,而且可以用于显示富文本,象素图甚至电影(使用QMovie类)。 由于我们已经知道如何将Mat转换为QImage(反之亦然),以及如何将QImage转换为QPixmap,因此我们可以编写如下内容,以使用OpenCV,使用一些计算机视觉算法对其进行处理(我们将在第 6 章,“OpenCV 的图像处理”和之后章节中对此进行详细了解),然后将其转换为QImage 然后到QPixmap,最后在QLabel上显示结果,如以下示例代码所示:
cv::Mat mat = cv::imread("c:/dev/test.jpg");
QImage image(mat.data,
mat.cols,
mat.rows,
mat.step,
QImage::Format_RGB888);
ui->label->setPixmap(QPixmap::fromImage(image.rgbSwapped()));
QImageReader和QImageWriter类
QImageReader和QImageWriter类可用于对图像读写过程进行更多控制。 它们支持与QImage和QPixmap相同的文件类型,但具有更大的灵活性,当图像读取或写入过程出现问题时会提供错误消息,并且在以下情况下,您还可以设置并获得更多图像属性: 您使用QImageReader和QImageWriter类。 正如您将在接下来的章节中看到的那样,我们将在全面的计算机视觉应用中使用这些相同的类,以更好地控制图像的读写。 现在,我们只需简短的介绍就可以继续进行下一部分。
QPainter类
QPainter类可用于在作为QPaintDevice类子类的任何 Qt 类上进行绘制(基本上是绘画)。 这意味着什么? 基本上,这意味着包括 Qt 小部件在内的所有东西,都具有视觉效果,可以在其上绘制某些东西。 因此,仅举几例,QPainter可用于绘制QWidget类(基本上意味着所有现有和定制的 Qt 小部件),QImage,QPixmap和许多其他 Qt 类。 您可以在 Qt Creator 帮助模式下查看QPaintDevice类文档页面,以获取继承QPaintDevice的 Qt 类的完整列表。 QPainter具有众多函数,其中许多名称以draw开头,而涵盖所有这些函数本身将需要整整一章,但是我们将看到一个基本示例,说明如何将其与QWidget和QImage一起使用。 基本上,相同的逻辑适用于可以与QPainter一起使用的所有类。
因此,正如您所说,您可以自己创建一个自定义 Qt 小部件,并使用QPainter创建(或绘制)其可视面。 实际上,这是用于创建新的 Qt 小部件的一种方法(也是一种流行的方法)。 让我们通过一个示例来帮助它沉入。我们将创建一个新的 Qt 小部件,该小部件仅显示一个闪烁的圆圈:
- 首先创建一个名为
Painter_Test的 Qt Widgets 应用。 - 然后从主菜单中选择文件/新文件或项目。
- 在“新建文件或项目”窗口中,选择“C++ 和 C++ 类”,然后按“选择”。
- 在出现的窗口中,确保将“类名”设置为
QBlinkingWidget,并将“基类”选择为QWidget。 确保选中“包括QWidget”复选框,并保留其余选项,如以下屏幕截图所示:

- 现在按下一步,然后按完成。 这将创建一个带有标题和源文件的新类,并将其添加到您的项目中。
- 现在,您需要覆盖
QBlinkingWidget的paintEvent方法,并使用QPainter进行一些绘制。 因此,首先将以下include语句添加到qblinkingwidget.h文件中:
#include <QPaintEvent>
#include <QPainter>
#include <QTimer>
- 现在,将以下受保护的成员添加到
QBlinkingWidget类中(例如,将其添加到现有的公共成员之后):
protected:
void paintEvent(QPaintEvent *event);
- 您还需要向此类添加一个专用插槽。 因此,在先前受保护的
paintEvent函数之后添加以下内容:
private slots:
void onBlink();
- 最后要添加到
qblinkingwidget.h文件中,添加以下我们将在小部件中使用的私有成员:
private:
QTimer blinkTimer;
bool blink;
- 现在,切换到
qblinkingwidget.cpp并在自动创建的构造器中添加以下代码:
blink = false;
connect(&blinkTimer,
SIGNAL(timeout()),
this,
SLOT(onBlink()));
blinkTimer.start(500);
- 接下来,将以下两种方法添加到
qblinkingwidget.cpp中:
void QBlinkingWidget::paintEvent(QPaintEvent *event)
{
Q_UNUSED(event);
QPainter painter(this);
if(blink)
painter.fillRect(this->rect(),
QBrush(Qt::red));
else
painter.fillRect(this->rect(),
QBrush(Qt::white));
}
void QBlinkingWidget::onBlink()
{
blink = !blink;
this->update();
}
- 现在,通过打开
mainwindow.ui切换到设计模式,然后将小部件添加到MainWindow类。Widget的确切含义是Widget,它是一个空的,当您将其添加到MainWindow时会注意到。 请参见以下屏幕截图:

- 现在,右键单击添加的
QWidget类的空窗口小部件,然后从弹出菜单中选择“升级到”:

- 在将打开的新窗口中,称为
Promoted Widgets窗口,将Promoted类名设置为QBlinkingWidget,然后按Add按钮:

- 最后,按
Promote。 您的应用和自定义小部件已准备就绪,可以运行。 应用启动后,您将看到它每 500 毫秒(半秒)闪烁一次。
实际上,这是在 Qt 中创建自定义窗口小部件的通用方法。 要创建一个新的自定义 Qt 小部件并在您的 Qt 小部件应用中使用它,您需要:
- 创建一个继承
QWidget的新类。 - 覆盖其
paintEvent函数。 - 使用
QPainter类在其上进行绘制。 - 在窗口中添加
QWidget(小部件)。 - 将其升级到新创建的小部件。
实际上,将QWidget提升为定制的小部件也是从第三方开发人员(或也许是从互联网)获得小部件并希望在应用窗口中使用它时使用的方法。
在前面的示例中,我们根据闪烁变量状态使用QPainter的fillRect函数简单地每秒用红色和白色填充它。 同样,您可以使用drawArc,drawEllipse,drawImage和QPainter中的更多函数在小部件上绘制几乎所有内容。 这里要注意的重要一点是,当我们想在小部件上绘制时,我们将this传递给QPainter实例。 如果我们想使用QImage,我们只需要确保将QImage传递给它或使用begin函数来构造QPainter即可。 这是一个例子:
QImage image(320, 240, QImage::Format_RGB888);
QPainter painter;
painter.begin(&image);
painter.fillRect(image.rect(), Qt::white);
painter.drawLine(0, 0, this->width()-1, this->height()-1);
painter.end();
在这种情况下,所有绘图函数都必须包含在begin和end函数调用中。
Qt 中的摄像头和视频处理
由于我们将使用 OpenCV 接口来处理图像,摄像机和视频,因此我们不会涵盖 Qt 框架提供的用于读取,查看和处理视频的所有可能性。 但是有时候,尤其是当两个框架之一提供更好或更简单的功能实现时,避免使用它确实变得很诱人。 例如,即使 OpenCV 提供了非常强大的相机处理方法,正如我们将在第 12 章,“Qt Quick 应用”中看到的那样,对于在 Android,iOS 和移动平台上处理相机而言,Qt 还有很多话要说。 因此,让我们简要介绍一下一些用于摄像头和视频处理的重要和现有 Qt 类,并保留它们,直到在第 12 章,“Qt Quick 应用”中使用它们为止。
在 Qt Creator 帮助索引中搜索 Qt Multimedia C++ 类,以获取 Qt Multimedia 模块下可用类的完整列表和更新列表,以及文档和示例。
他们来了:
QCamera:可以访问平台上可用的摄像机。QCameraInfo:可用于获取有关平台上可用摄像机的信息。QMediaPlayer:可以用来播放视频文件和其他类型的录制媒体。QMediaRecorder:录制视频或其他媒体类型时,此类很有用。QVideoFrame:此类可用于访问相机抓取的单个帧。QVideoProbe:可用于监视来自摄像机或视频源的帧。 此类也可以用于在 Qt 中进一步处理帧。QVideoWidget:可用于显示来自摄像机或视频源的传入帧。
请注意,提到的所有类都存在于 Qt 的多媒体模块中,因此要使用它们,您需要首先通过将以下行添加到 Qt 项目 PRO 文件中,以确保多媒体模块对您的项目公开:
QT += multimedia
除了前面提到的类之外,Qt 多媒体模块还提供了许多其他类来处理视频数据。 您始终可以在 Qt Creator 帮助模式下通过在帮助索引中进行搜索来检出每个类的文档页面。 通常,新的 Qt 版本会引入新的类或对现有类的更新,因此,要成为一名真正的 Qt 开发人员,就要注意文档页面和更新,甚至可能在发现任何问题时报告错误或问题,因为 Qt 仍然是一个开源框架,它依赖于其开源用户社区的支持。
总结
本章是一个重要的里程碑,因为它介绍了将 OpenCV 和 Qt 框架链接在一起所需的概念。 在本章中,我们学习了有关Mat类及其变体的所有信息。 我们了解了 OpenCV 中新的透明 API,以及如何使用UMat类可以提高计算机视觉应用的性能。 我们还学习了读写图像和视频,还捕获了来自照相机和基于网络的视频源的视频帧。 后来,我们继续学习与计算机视觉和图像处理相关的 Qt 函数和类。 本章介绍了 Qt 中的QImage类,它与OpenCV中的Mat类等效。 我们还了解了QPixmap和QPainter类以及其他几个 Qt 类。 在这样做的同时,我们还学习了如何创建自定义 Qt 小部件并使用QPainter类绘制QImage类。 最后,我们以介绍视频和摄像机处理相关的 Qt 类作为本章的结尾。
在第 5 章,“图形视图框架”中,我们将通过引入一个非常强大的类QGraphicsScene和图形视图框架来完成 Qt 和OpenCV中的计算机视觉难题,它可用于以非常灵活的方式查看和操作图像数据。 第 5 章,“图形视图框架”将是进入计算机视觉和图像处理领域的最后一章,因为我们全面的计算机视觉应用将通过最重要的功能之一完成,那就是图像查看器和操纵器,我们将继续学习新的计算机视觉技巧,每次都向其添加新的插件,如我们在前几章中学到的那样。
五、图形视图框架
既然我们已经熟悉了 Qt 和 OpenCV 框架中计算机视觉应用的基本构建模块,那么我们可以继续学习有关计算机视觉应用中可视化部分的开发的更多信息。 谈论计算机视觉,每个用户都会立即寻找一些预览图像或视频。 以您想要的任何图像编辑器为例,它们都在用户界面上包含一个区域,该区域可立即引起注意,并且可以通过 GUI 上的其他组件轻松地通过一些边框甚至简单的线条来识别。 关于视频编辑软件以及实际上需要与视觉概念和媒体输入源配合使用的所有内容,也可以这样说。 同样,对于我们将创建的计算机视觉应用,完全相同的推理也适用。 当然,在某些情况下,过程的结果只是简单地显示为数值或通过网络发送给与过程有关的其他各方。 但是,对我们来说幸运的是,我们将看到这两种情况,因此我们需要在应用中具有类似的功能,以便用户可以预览自己打开的文件或查看生成的转换(或过滤后的)图像。 屏幕。 甚至更好,请在实时视频输出预览面板中查看某些对象检测算法的结果。 该面板基本上是一个场景,或者甚至更好,它是一个图形场景,这是本书本章要讨论的主题。
在 Qt 框架内的许多模块,类和子框架下,有一块专门用于简化图形处理的工具,称为图形视图框架。 它包含许多类,几乎所有的类都以QGraphics开头,并且所有这些类都可用于处理构建计算机视觉应用时可能遇到的大多数图形任务。 图形视图框架将所有可能的对象简单地分为三个主要类别,随之而来的架构允许轻松地添加,删除,修改以及显示图形对象。
- 场景(
QGraphicsScene类) - 视图(
QGraphicsView小部件) - 图形项目(
QGraphicsItem及其子类)
在之前的章节中,我们使用了最简单的方式同时使用 OpenCV(imshow函数)和 Qt 标签窗口小部件来可视化图像,这在处理显示的图像(例如选择它们,修改它们, 缩放它们,依此类推。 即使是最简单的任务,例如选择图形项目并将其拖动到其他位置,我们也必须编写大量代码并经历令人困惑的鼠标事件处理。 放大和缩小图像也是如此。 但是,通过使用图形视图框架中的类,可以更轻松地处理所有这些事情,并具有更高的性能,因为图形视图框架类旨在以高效的方式处理许多图形对象。
在本章中,我们将开始学习 Qt 的图形视图框架中最重要的类,并且重要的是,我们显然是指与构建全面的计算机视觉应用所需的类最相关的类。 本章学习的主题将完成Computer_Vision项目的基础,该项目是在第 3 章,“创建全面的 Qt + OpenCV 项目”的结尾创建的。 到本章末,您将能够创建一个与图像编辑软件中看到的场景相似的场景,在该场景中,您可以向场景中添加新图像,选择它们,删除它们,放大和缩小它们等等。 您还将在本章末找到Computer_Vision项目基础和基础版本的链接,我们将继续使用该链接,直到本书的最后几章。
在本章中,我们将介绍以下各章:
- 如何使用
QGraphicsScene在场景上绘制图形 - 如何使用
QGraphicsItem及其子类来管理图形项目 - 如何使用
QGraphicsView查看QGraphicsScene - 如何开发放大,缩小以及其他图像编辑和查看功能
场景-视图-项目架构
正如引言中提到的那样,Qt 中的图形视图框架(或从现在开始简称 Qt)将可能需要处理的与图形相关的对象分为三个主要类别,即场景,视图和项目。 Qt 包含名称非常醒目的类,以处理此架构的每个部分。 尽管从理论上讲,将它们彼此分开很容易,但在实践中,它们却是交织在一起的。 这意味着我们不能不提及其他人而真正深入研究其中之一。 清除架构的一部分,您将完全没有图形。 另外,再看一下架构,我们可以看到模型视图设计模式,其中模型(在本例中为场景)完全不知道如何显示或显示哪个部分。 正如在 Qt 中所说的,这是一种基于项目的模型-视图编程方法,我们将牢记这一点,同时还要简要介绍一下它们中的每一个在实践中的含义:
- 场景或
QGraphicsScene管理项目或QGraphicsItem的实例(其子类),包含它们,并将事件(例如,鼠标单击等)传播到项目中。 - 视图或
QGraphicsView小部件用于可视化和显示QGraphicsScene的内容。 它还负责将事件传播到QGraphicsScene。 这里要注意的重要一点是QGraphicsScene和QGraphicsView都具有不同的坐标系。 可以猜到,如果放大,缩小或进行不同的相似变换,则场景上的位置将不同。QGraphicsScene和QGraphicsView都提供了转换彼此适合的位置值的功能。 - 这些项目或
QGraphicsItem子类的实例是QGraphicsScene中包含的项目。 它们可以是线,矩形,图像,文本等。
让我们从一个简单的入门示例开始,然后继续详细讨论上述每个类:
-
创建一个名为
Graphics_Viewer的 Qt Widgets 应用,类似于在第 4 章,“Mat和QImage”中创建的项目,以了解有关在 Qt 中显示图像的信息。 但是,这一次只需向其中添加“图形视图”窗口小部件,而无需任何标签,菜单,状态栏等。 将其objectName属性保留为graphicsView。 -
另外,添加与以前相同的拖放功能。 如前所述,您需要在
MainWindow类中添加dragEnterEvent和dropEvent。 并且不要忘记将setAcceptDrops添加到MainWindow类的构造器中。 显然,这一次,您需要删除用于在QLabel上设置QPixmap的代码,因为该项目中没有任何标签。 -
现在,将所需变量添加到
mainwindow.h中MainWindow类的私有成员部分,如下所示:
QGraphicsScene scene;
scene基本上是我们将使用并显示在添加到MainWindow类的QGraphicsView小部件中的场景。 最有可能的是,您需要为所使用的每个类添加一个#include语句,这是代码编辑器无法识别的。 您还将获得与此相关的编译器错误,通常可以很好地提醒我们忘记将其包含在源代码中的类。 因此,从现在开始,请确保为您使用的每个 Qt 类添加一个类似于以下内容的#include指令。 但是,如果要使某个类可用就需要采取任何特殊的措施,则将在书中明确说明:
#include <QGraphicsScene>
- 接下来,我们需要确保我们的
graphicsView对象可以访问场景。 您可以通过在MainWindow构造器中添加以下行来实现。 (步骤 5 之后的行。) - 另外,您需要为
graphicsView禁用acceptDrops,因为我们希望能够保留放置在窗口各处的图像。 因此,请确保您的MainWindow构造器仅包含以下函数调用:
ui->setupUi(this);
this->setAcceptDrops(true);
ui->graphicsView->setAcceptDrops(false);
ui->graphicsView->setScene(&scene);
- 接下来,在上一个示例项目的
dropEvent函数中,我们设置标签的pixmaps属性,这一次,我们需要确保创建了QGraphicsItem并将其添加到场景中,或者准确地说是QGraphicsPixmapItem。 这可以通过两种方式完成,让我们来看第一种:
QFileInfo file(event
->mimeData()
->urls()
.at(0)
.toLocalFile());
QPixmap pixmap;
if(pixmap.load(file
.absoluteFilePath()))
{
scene.addPixmap(pixmap);
}
else
{
// Display an error message
}
在这种情况下,我们仅使用了QGraphicsScene的addPixmap函数。 另外,我们可以创建QGraphicsPixmapItem并使用addItem方法将其添加到场景中,如下所示:
QGraphicsPixmapItem *item =
new QGraphicsPixmapItem(pixmap);
scene.addItem(item);
在这两种情况下,都不必担心项目指针,因为在调用addItem时场景将拥有它的所有权,并且场景会自动从内存中清除。 当然,如果我们要手动从场景和内存中完全删除该项目,我们可以编写一个简单的delete语句来删除该项目,如下所示:
delete item;
我们的简单代码有一个大问题,乍看之下看不到,但是如果我们继续将图像拖放到窗口中,则每次将最新图像添加到先前图像的顶部并且不清除先前图像。 实际上,如果您亲自尝试一下,这是一个好主意。 但是,首先在写入addItem的行之后添加以下行:
qDebug() << scene.items().count();
您需要将以下头文件添加到mainwindow.h文件中,此文件才能起作用:
#include <QDebug>
现在,如果您运行该应用并尝试通过将其拖放到窗口中来添加图像,您会注意到,在 Qt Creator 代码编辑器屏幕底部的“应用输出”窗格中,每次放置图像时,所显示的数字增加,即scene中items的count:

如上例所示,使用qDebug()是许多 Qt 开发人员用来在开发过程中快速查看某些变量的值的技巧。 Qt 中的qDebug()是与std::cout类似的玩具,用于输出到控制台(或终端)。 我们将在第 10 章,“调试和测试”中了解更多有关测试和调试的信息,但现在,让我们记下qDebug()并使用它来快速解决以下问题。 我们使用 Qt 和 C++ 进行开发时的代码。
- 因此,要解决前面示例中提到的问题,我们显然需要先对
clear和scene进行添加。 因此,只需在调用任何addItem(或addPixmap等)之前添加以下内容:
scene.clear();
尝试再次运行您的应用,然后查看结果。 现在,将其放入我们的应用窗口后,应该只存在一个图像。 另外,记下应用的输出,您将看到显示的值始终为1,这是因为在任何时候scene中始终只保留一个图像。 在我们刚才看到的示例项目中,我们使用了 Qt 的图形视图框架中的所有现有主要部分,即场景,项目和视图。 现在,我们将详细了解这些类,同时,为我们全面的计算机视觉应用Computer_Vision项目创建强大的图形查看器和编辑器。
场景,QGraphicsScene
此类提供了处理多个图形项(QGraphicsItem)所需的几乎所有方法,即使在前面的示例中我们仅将其与单个QGraphicxPixmapItem一起使用。 在本节中,我们将回顾该类中一些最重要的函数。 如前所述,我们将主要关注用例所需的属性和方法,因为涵盖所有方法(尽管它们都很重要)对于本书而言都是徒劳的。 我们将跳过QGraphicsScene的构造器,因为它们仅用于获取场景的尺寸并相应地创建场景。 至于其余的方法和属性,就在这里,对于其中一些可能不太明显的示例,您可以找到一个简单的示例代码,可以使用本章前面创建的Graphics_Viewer项目进行尝试 :
addEllipse,addLine,addRect和addPolygon函数可以从它们的名称中猜测出来,可以用来向场景添加通用的几何形状。 它们中的一些提供了重载函数,以便于输入参数。 创建并添加到场景时,上述每个函数都会返回其对应的QGraphicsItem子类实例(如下所示)。 返回的指针可以保留,以后可用于修改,删除或以其他方式使用该项目:QGraphicsEllipseItemQGraphicsLineItemQGraphicsRectItemQGraphicsPolygonItem
这是一个例子:
scene.addEllipse(-100.0, 100.0, 200.0, 100.0,
QPen(QBrush(Qt::SolidPattern), 2.0),
QBrush(Qt::Dense2Pattern));
scene.addLine(-200.0, 200, +200, 200,
QPen(QBrush(Qt::SolidPattern), 5.0));
scene.addRect(-150, 150, 300, 140);
QVector<QPoint> points;
points.append(QPoint(150, 250));
points.append(QPoint(250, 250));
points.append(QPoint(165, 280));
points.append(QPoint(150, 250));
scene.addPolygon(QPolygon(points));
这是前面代码的结果:

-
addPath函数可用于将QPainterPath与给定的QPen和QBrush添加到场景中。QPainterPath类可用于记录绘画操作,类似于我们在QPainter中看到的操作,并在以后使用它们。 另一方面,QPen和QBrush类具有不言自明的标题,但在本章后面的示例中我们也将使用它们。addPath函数返回一个指向新创建的QGraphicsPathItem实例的指针。 -
addSimpleText和addText函数可用于将纯文本和带格式的文本添加到场景中。 它们分别返回指向QGraphicsSimpleTextItem或QGraphicsTextItem的指针。 -
在上一示例中已经使用过的
addPixmap函数可用于将图像添加到场景,并且它返回指向QGraphicsPixmapItem类的指针。 -
addItem函数仅接受任何QGraphicsItem子类并将其添加到场景中。 我们在前面的示例中也使用了此函数。 -
addWidget函数可用于将 Qt 小部件添加到场景。 除了某些特殊的小部件(即设置了Qt::WA_PaintOnScreen标志的小部件或使用外部库(例如OpenGL或Active-X绘制的小部件))之外,您还可以将其他任何小部件添加到场景中,就像将其添加到场景中一样。 一个窗口。 这为使用交互式图形项创建场景提供了巨大的力量。 您绝对可以使用它来创建简单的游戏,添加对图像执行某些操作的按钮以及许多其他功能。 我们将在Computer_Vision项目中大量使用此示例,并提供足够的示例来帮助您入门,但是现在这是一个简短的示例:
QPushButton *button = new QPushButton(Q_NULLPTR);
connect(button, SIGNAL(pressed()), this, SLOT(onAction()));
button->setText(tr("Do it!"));
QGraphicsProxyWidget* proxy = scene.addWidget(button);
proxy->setGeometry(QRectF(-200.0, -200, 400, 100.0));
前面的代码只是添加了一个标题为Do it!的按钮,并将其连接到名为onAction的插槽。 每当按下场景中的此按钮时,就会调用onAction函数。 与向窗口添加按钮时完全相同:
-
setBackgroundBrush,backgroundBrush,setForegroundBrush和foregroundBrush函数允许访问负责刷刷场景的background和foreground的QBrush类。 -
font和setFont函数可用于获取或设置QFont类,以确定场景中使用的字体。 -
当我们想要定义最小尺寸来决定某项是否适合绘制(渲染)时,
minimumRenderSize和setMinimumRenderSize函数非常有用。 -
sceneRect和setSceneRect函数可用于指定场景的边界矩形。 这基本上意味着场景的宽度和高度,以及其在坐标系上的位置。 重要的是要注意,如果未调用setSceneRect或在QGraphicsScene的构造器中未设置矩形,则调用sceneRect将始终返回可以覆盖添加到场景的所有项目的最大矩形。 始终最好设置一个场景矩形,并根据需要在场景中进行任何更改等操作,基本上根据需要手动(使用setSceneRect)再次对其进行设置。 -
stickyFocus和setStickyFocus函数可用于启用或禁用场景的粘滞聚焦模式。 如果启用了粘滞聚焦,则单击场景中的空白区域不会对聚焦的项目产生任何影响; 否则,将仅清除焦点,并且不再选择选定的项目。 -
collidingItems是一个非常有趣的功能,可用于简单地确定某项是否与其他任何项共享其区域的某个部分(或发生碰撞)。 您需要将QGraphicsItem指针与Qt::ItemSelectionMode一起传递,您将获得与项目发生冲突的QGraphicsItem实例的QList。 -
createItemGroup和destroyItemGroup函数可用于创建和删除QGraphicsItemGroup类实例。QGraphicsItemGroup基本上是另一个QGraphicsItem子类(如QGraphicsLineItem等),可用于将一组图形项分组并因此表示为单个项。 -
hasFocus,setFocus,focusItem和setFocusItem函数均用于处理图形场景中当前聚焦的项目。 -
返回与
sceneRect.width()和sceneRect.height()相同值的width和height可用于获取场景的宽度和高度。 请务必注意,这些函数返回的值的类型为qreal(默认情况下与double相同),而不是integer,因为场景坐标在像素方面不起作用。 除非使用视图绘制场景,否则将其上的所有内容都视为逻辑和非视觉对象,而不是视觉对象,这是QGraphicsView类的领域。 -
在某些情况下,与
update()相同的invalidate可用于请求全部或部分重绘场景。 类似于刷新函数。 -
itemAt函数可用于在场景中的某个位置找到指向QGraphicItem的指针。 -
item返回添加到场景的项目列表。 基本上是QGraphicsItem的QList。 -
itemsBoundingRect可用于获取QRectF类,或仅获取可包含场景中所有项目的最小矩形。 如果我们需要查看所有项目或执行类似操作,此函数特别有用。 -
mouseGrabberItem可用于获取当前单击的项目,而无需释放鼠标按钮。 此函数返回一个QGraphicsItem指针,使用它我们可以轻松地向场景添加“拖动和移动”或类似功能。 -
removeItem函数可用于从场景中删除项目。 此函数不会删除该项目,并且调用方负责任何必需的清理。 -
render可用于渲染QPaintDevice上的场景。 这只是意味着您可以使用QPainter类(如您在第 4 章,“Mat和QImage”中学习的)在QImage,QPrinter等类似对象上绘制场景,通过将QPainter类的指针传递给此函数。 (可选)您可以在QPaintDevice渲染目标类的一部分上渲染场景的一部分,并且还要注意宽高比的处理。 -
当
selectedItems,selectionArea和setSelectionArea函数结合使用时,可以帮助处理一个或多个项目选择。 通过提供Qt::ItemSelectionMode枚举,我们可以基于完全选择一个框中的项目或仅对其一部分进行选择,等等。 我们还可以为该函数提供Qt::ItemSelectionOperation枚举条目,以增加选择或替换所有先前选择的项目。 -
sendEvent函数可用于将QEvent类(或子类)发送到场景中的项目。 -
style和setStyle函数用于设置和获取场景样式。 -
update函数可用于重绘部分或全部场景。 当场景的视觉部分发生变化时,最好将此函数与QGraphicsScene类发出的变化信号结合使用。 -
views函数可用于获取QList类,其中包含用于显示(或查看)此场景的QGraphicsView小部件。
除了先前的现有方法外,QGraphicsScene提供了许多虚拟函数,可用于进一步自定义和增强QGraphicsScene类的行为以及外观。 因此,与其他任何类似的 C++ 类一样,您需要创建QGraphicsScene的子类,并只需添加这些虚拟函数的实现即可。 实际上,这是使用QGraphicsScene类的最佳方法,它为新创建的子类提供了极大的灵活性:
- 可以覆盖
dragEnterEvent,dragLeaveEvent,dragMoveEvent和dropEvent函数,以向场景添加拖放功能。 请注意,这与前面示例中将图像拖放到窗口中所做的非常相似。 这些事件中的每一个都提供足够的信息和参数来处理整个拖放过程。 - 如果我们需要在整个场景中添加自定义背景或前景,则应覆盖
drawBackground和drawForeground函数。 当然,对于简单的背景或前景绘画或着色任务,我们可以简单地调用setBackgroundBrush和setForegroundBrush函数,而跳过这些函数。 mouseDoubleClickEvent,mouseMoveEvent,mousePressEvent,mouseReleaseEvent和wheelEvent函数可用于处理场景中的不同鼠标事件。 例如,当我们在Computer_Vision项目中为场景添加放大和缩小功能时,将在本章稍后使用wheelEvent。- 可以覆盖
event以处理场景接收到的所有事件。 此函数基本上负责将事件调度到其相应的处理器,但是它也可以用于处理自定义事件或不具有便捷功能的事件,例如前面提到的所有事件。
就像到目前为止您学过的所有类一样,无论是在 Qt 还是 OpenCV 中,本书中提供的方法,属性和函数的列表都不应被视为该类各个方面的完整列表。 最好总是使用框架的文档来学习新函数和属性。 但是,本书中的描述旨在更简单,尤其是从计算机视觉开发人员的角度出发。
项目,QGraphicsItem
这是场景中绘制的所有项目的基类。 它包含各种方法和属性来处理每个项目的绘制,碰撞检测(与其他项目),处理鼠标单击和其他事件,等等。 即使您可以将其子类化并创建自己的图形项,Qt 也会提供一组子类,这些子类可用于大多数(如果不是全部)日常图形任务。 以下是这些子类,在前面的示例中已经直接或间接使用了这些子类:
QGraphicsEllipseItemQGraphicsLineItemQGraphicsPathItemQGraphicsPixmapItemQGraphicsPolygonItemQGraphicsRectItemQGraphicsSimpleTextItemQGraphicsTextItem
如前所述,QGraphicsItem提供了许多函数和属性来处理图形应用中的问题和任务。 在本节中,我们将介绍QGraphicsItem中一些最重要的成员,这些成员因此可以通过熟悉前面提到的子类来帮助我们:
acceptDrops和setAcceptDrops函数可用于使项目接受拖放事件。 请注意,这与我们在前面的示例中已经看到的拖放事件非常相似,但是这里的主要区别是项目本身可以识别拖放事件。acceptHoverEvents,setAcceptHoverEvents,acceptTouchEvents,setAcceptTouchEvents,acceptedMouseButtons和setAcceptedMouseButtons函数均处理项目交互及其对鼠标单击的响应等。 这里要注意的重要一点是,一个项目可以根据Qt::MouseButtons枚举设置来响应或忽略不同的鼠标按钮。 这是一个简单的例子:
QGraphicsRectItem *item =
new QGraphicsRectItem(0,
0,
100,
100,
this);
item->setAcceptDrops(true);
item->setAcceptHoverEvents(true);
item->setAcceptedMouseButtons(
Qt::LeftButton |
Qt::RightButton |
Qt::MidButton);
boundingRegion函数可用于获取描述图形项区域的QRegion类。 这是一项非常重要的函数,因为它可用于获取需要绘制(或重绘)项目的确切区域,并且与项目的边界矩形不同,因为简单地说,该项目可能仅覆盖其边界矩形的一部分,如直线等。 有关更多信息,请参见以下示例。- 在计算项目的
boundingRegion函数时,boundingRegionGranularity和setBoundingRegionGranularity函数可用于设置和获取粒度级别。 从这个意义上讲,粒度是0和1之间的实数,它对应于计算时的预期详细程度:
QGraphicsEllipseItem *item =
new QGraphicsEllipseItem(0,
0,
100,
100);
scene.addItem(item);
item->setBoundingRegionGranularity(g); // 0 , 0.1 , 0.75 and 1.0
QTransform transform;
QRegion region = item->boundingRegion(transform);
QPainterPath painterPath;
painterPath.addRegion(region);
QGraphicsPathItem *path = new QGraphicsPathItem(painterPath);
scene.addItem(path);
在前面的代码中,如果将g替换为0.0,0.1,0.75和1.0,则会得到以下结果。 显然,0的值(默认粒度)导致单个矩形(边界矩形),这不是准确的估计。 随着级别的增加,我们得到了覆盖图形形状和项目的更准确的区域(基本上是矩形集):

-
childItems函数可用于获取填充有QGraphicsItem类的QList,这些类是此项的子级。 将它们视为更复杂项目的子项目。 -
childrenBoundingRect,boundingRect和sceneBoundingRect函数可用于检索QRectF类,其中包含该项目的子项bounding rect,该项目本身和场景。 -
clearFocus,setFocus和hasFocus函数可用于删除,设置和获取该项目的聚焦状态。 具有焦点的项目接收键盘事件。 -
collidesWithItem,collidesWithPath和collidingItems函数可用于检查此项目是否与任何给定项目发生冲突,以及该项目与之碰撞的项目列表。 -
contains函数获取一个点的位置(准确地说是QPointF类),然后检查此项是否包含该点。 -
cursor,setCursor,unsetCursor和hasCursor函数对于设置,获取和取消设置此项的特定鼠标光标类型很有用。 您还可以在取消设置之前检查项目是否有任何设置的光标。 设置后,如果鼠标光标悬停在该项目上,则光标形状变为一组。 -
hide,show,setVisible,isVisible,opacity,setOpacity和effectiveOpacity函数均与商品的可见性(和不透明度)有关。 所有这些函数都具有不言自明的名称,唯一值得注意的是effectiveOpacity,它可能与此项的不透明度相同,因为它是基于该项及其父项的不透明度级别计算的。 最终,effectiveOpacity是用于在屏幕上绘制该项目的不透明度级别。 -
flags,setFlags和setFlag函数可用于获取或设置此项的标志。 通过标志,我们基本上是指QGraphicsItem::GraphicsItemFlag枚举中各项的组合。 这是一个示例代码:
item->setFlag(QGraphicsItem::ItemIsFocusable, true);
item->setFlag(QGraphicsItem::ItemIsMovable, false);
重要的是要注意,当我们使用setFlag函数时,所有以前的标志状态都会保留,并且此函数中只有一个标志会受到影响。 但是,当我们使用setFlags时,基本上所有标志都会根据给定的标志组合进行重置。
- 当我们想要更改从场景中获取鼠标和键盘事件的项目时,
grabMouse,grabKeyboard,ungrabMouse和ungrabKeyboard方法很有用。 显然,使用默认实现时,一次只能抓取一个项目,除非另一个抓取项目或者项目本身不变形或被删除或隐藏,否则抓取器将保持不变。 正如本章前面所看到的,我们总是可以使用QGraphicsScene类中的mouseGrabberItem函数来获取抓取器项目。 setGraphicsEffect和graphicsEffect函数可用于设置和获取QGraphicsEffect类。 这是一个非常有趣且易于使用的函数,但功能强大,可用于向场景中的项目添加过滤器或效果。QGraphicsEffect是 Qt 中所有图形效果的基类。 您可以将其子类化并创建自己的图形效果或过滤器,也可以仅使用提供的 Qt 图形效果之一。 目前,Qt 中有一些图形效果类,您可以自己尝试一下:QGraphicsBlurEffectQGraphicsColorizeEffectQGraphicsDropShadowEffectQGraphicsOpacityEffect
让我们看一个示例自定义图形效果,并使用 Qt 自己的图形效果使自己更加熟悉这个概念:
-
您可以使用我们在本章前面创建的
Graphics_Viewer项目。 只需在 Qt Creator 中打开它,然后使用主菜单中的New File或Project,选择 C++ 和 C++ 类,然后单击Choose按钮。 -
接下来,确保输入
QCustomGraphicsEffect作为类名。 选择QObject作为基类,最后选中Include QObject复选框(如果默认情况下未选中)。 单击下一步,然后单击完成按钮。 -
然后,将以下
include语句添加到新创建的qcustomgraphicseffect.h文件中:
#include <QGraphicsEffect>
#include <QPainter>
- 之后,您需要确保我们的
QCustomGraphicsEffect类继承了QGraphicsEffect而不是QObject。 确保首先更改qcustomgraphicseffect.h文件中的类定义行,如下所示:
class QCustomGraphicsEffect : public QGraphicsEffect
- 我们还需要更新该类的构造器,并确保在我们的类构造器中调用了
QGraphicsEffect构造器,否则将出现编译器错误。 因此,更改qcustomgraphics.cpp文件中的类构造器,如下所示:
QCustomGraphicsEffect::QCustomGraphicsEffect(QObject *parent)
: QGraphicsEffect(parent)
- 接下来,我们需要实现
draw函数。 基本上,这是通过实现draw函数制作所有QGraphicsEffect类的方式。 因此,将以下代码行添加到qcustomgraphicseffect.h文件中的QCustomGraphicsEffect类定义中:
protected:
void draw(QPainter *painter);
- 然后,我们需要编写实际的效果代码。 在此示例中,我们将编写一个简单的阈值过滤器,根据像素的灰度值,将其设置为完全黑色或完全白色。 尽管起初代码看起来有些棘手,但它仅使用了我们在前几章中已经学到的经验。 而且,这也是使用
QGraphicsEffect类编写新效果和过滤器的简单程度的简单示例。 如您所见,传递给draw函数的QPainter类的指针可用于在效果所需的更改之后简单地对其进行修改和绘制:
void QCustomGraphicsEffect::draw(QPainter *painter)
{
QImage image;
image = sourcePixmap().toImage();
image = image.convertToFormat(
QImage::Format_Grayscale8);
for(int i=0; i<image.byteCount(); i++)
image.bits()[i] =
image.bits()[i] < 100 ?
0
:
255;
painter->drawPixmap(0,0,QPixmap::fromImage(image));
}
- 最后,我们可以使用新的效果类。 只要确保它包含在
mainwindow.h文件中:
#include "qcustomgraphicseffect.h"
- 然后,通过调用项目的
setGraphicsEffect函数来使用它。 在我们的Graphics_Viewer项目中,我们实现了dropEvent。 您可以简单地将以下代码段添加到dropEvent函数中,因此将具有以下内容:
QGraphicsPixmapItem *item = new QGraphicsPixmapItem(pixmap);
item->setGraphicsEffect(new QCustomGraphicsEffect(this));
scene.addItem(item);
如果在运行应用并将其放置在其上的图像时所有操作均正确完成,您将注意到我们的阈值效果的结果:

在我们使用自定义图形效果的最后一步中,尝试用任何 Qt 提供的效果的类名替换QCustomGraphicsEffect,然后亲自检查结果。 如您所见,它们在图形效果和类似概念方面提供了极大的灵活性。
现在,让我们继续进行QGraphicsItem类中的其余函数和属性:
- 当我们想将一个项目添加到组中或获取包含该项目的组类时,
group和setGroup函数非常有用,只要该项目属于任何组。QGraphicsItemGroup是负责处理组的类,就像您在本章前面所学的那样。 isAncestorOf函数可用于检查该项目是否为任何给定其他项目的父项(或父项的父项,依此类推)。- 可以设置
setParentItem和parentItem并检索当前项目的父项目。 一个项目可能根本没有任何父项,在这种情况下,parentItem函数将返回零。 isSelected和setSelected函数可用于更改项目的所选模式。 这些函数与setSelectionArea和您在QGraphicsScene类中了解的类似函数密切相关。mapFromItem,mapToItem,mapFromParent,mapToParent,mapFromScene,mapToScene,mapRectFromItem,mapRectToScene,mapRectFromParent,mapRectToParent,mapRectFromScene和mapRectToScene函数 ,所有这些函数甚至都具有更多方便的重载函数,构成了一长串函数,这些函数用于从或向其进行基本映射,或者换句话说,可用于从场景,另一项或父对象到场景的坐标转换。 。 实际上,如果您考虑到每个单独的项目和场景与其他项目无关的事实,那么这很容易掌握。 首先,请看下面的图,然后让我们对其进行更详细的讨论:

因为场景包含所有项目,所以我们假设主坐标系(或世界坐标系)是场景的坐标系。 实际上,这是一个正确的假设。 因此,项目在场景中的位置值为(A, B)。 同样,父项在场景中的位置为(D, E)。 现在,这有点棘手,子项 1 在父项中的位置值为(F, G)。 类似地,子项 2 在父项中的位置值为(H, I)。 显然,如果父项和子项的数量增加,我们将拥有不同坐标系的迷宫,在这里,提到的映射函数会很有用。 这是一些示例情况。 您可以使用以下代码段自己测试它,以创建一个场景,其中包含与前面提到的场景类似的项目:
QGraphicsRectItem *item =
new QGraphicsRectItem(0,
0,
100,
100);
item->setPos(50,400);
scene.addItem(item);
QGraphicsRectItem *parentItem =
new QGraphicsRectItem(0,
0,
320,
240);
parentItem->setPos(300, 50);
scene.addItem(parentItem);
QGraphicsRectItem *childItem1 =
new QGraphicsRectItem(0,
0,
50,
50,
parentItem);
childItem1->setPos(50,50);
QGraphicsRectItem *childItem2 =
new QGraphicsRectItem(0,
0,
75,
75,
parentItem);
childItem2->setPos(150,75);
qDebug() << item->mapFromItem(childItem1, 0,0);
qDebug() << item->mapToItem(childItem1, 0,0);
qDebug() << childItem1->mapFromScene(0,0);
qDebug() << childItem1->mapToScene(0,0);
qDebug() << childItem2->mapFromParent(0,0);
qDebug() << childItem2->mapToParent(0,0);
qDebug() << item->mapRectFromItem(childItem1,
childItem1->rect());
qDebug() << item->mapRectToItem(childItem1,
childItem1->rect());
qDebug() << childItem1->mapRectFromScene(0,0, 25, 25);
qDebug() << childItem1->mapRectToScene(0,0, 25, 25);
qDebug() << childItem2->mapRectFromParent(0,0, 30, 30);
qDebug() << childItem2->mapRectToParent(0,0, 25, 25);
尝试在 Qt Creator 和 Qt Widgets 项目中运行前面的代码,您将在 Qt Creator 的应用输出窗格中看到以下内容,这基本上是qDebug()语句的结果:
QPointF(300,-300)
QPointF(-300,300)
QPointF(-350,-100)
QPointF(350,100)
QPointF(-150,-75)
QPointF(150,75)
QRectF(300,-300 50x50)
QRectF(-300,300 50x50)
QRectF(-350,-100 25x25)
QRectF(350,100 25x25)
QRectF(-150,-75 30x30)
QRectF(150,75 25x25)
让我们尝试看看产生第一个结果的指令:
item->mapFromItem(childItem1, 0,0);
item在场景中的位置为(50, 400),childItem1在场景中的(50, 50)位置。 该语句在childItem1坐标系中的位置(0, 0)并将其转换为项目的坐标系。 自己一个个地检查其他说明。 当我们要在场景中的项目周围移动或对场景中的项目进行类似的转换时,这非常简单但非常方便:
-
moveBy,pos,setPos,x,setX,y,setY,rotation,setRotation,scale和setScale函数可用于获取或设置项目的不同几何属性。 有趣的是,pos和mapToParent(0,0)返回相同的值。 检查前面的示例,然后通过将其添加到示例代码中来进行尝试。 -
transform,setTransform,setTransformOriginPoint和resetTransform函数可用于对项目应用或检索任何几何变换。 重要的是要注意,所有变换都假设一个原点(通常为(0,0)),可以使用setTransformOriginPoint对其进行更改。 -
scenePos函数可用于获取项目在场景中的位置。 与调用mapToScene(0,0)相同。 您可以自己在前面的示例中进行尝试并比较结果。 -
data和setData函数可用于设置和检索项目中的任何自定义数据。 例如,我们可以使用它来存储设置为QGraphicsPixmapItem的图像的路径,或者存储与特定项目相关的任何其他类型的信息。 -
zValue和setZValue函数可用于修改和检索项目的Z值。Z值决定应在其他项目之前绘制哪些项目,依此类推。 具有较高Z值的项目将始终绘制在具有较低Z值的项目上。
与我们在QGraphicsScene类中看到的类似,QGraphicsItem类还包含许多受保护的虚函数,这些函数可以重新实现,主要用于处理传递到场景项上的各种事件。 以下是一些重要且非常有用的示例:
contextMenuEventdragEnterEvent,dragLeaveEvent,dragMoveEvent,dropEventfocusInEvent,focusOutEventhoverEnterEvent,hoverLeaveEvent,hoverMoveEventkeyPressEvent,keyReleaseEventmouseDoubleClickEvent,mouseMoveEvent,mousePressEvent,mouseReleaseEvent,wheelEvent
视图,QGraphicsView
我们到了 Qt 中的图形视图框架的最后一部分。 QGraphicsView类是 Qt 窗口小部件类,可以将其放置在窗口上以显示QGraphicsScene,该窗口本身包含许多QGraphicsItem子类和/或窗口小部件。 与QGraphicsScene类相似,该类还提供大量函数,方法和属性来处理图形的可视化部分。 我们将审核以下列表中的一些最重要的函数,然后我们将学习如何对QGraphicsView进行子类化并将其扩展为在我们全面的计算机视觉应用中具有若干重要功能,例如放大,缩小, 项目选择等。 因此,这是我们在计算机视觉项目中需要的QGraphicsView类的方法和成员:
-
alignment和setAlignment函数可用于设置场景在视图中的对齐方式。 重要的是要注意,只有当视图可以完全显示场景并且仍然有足够的空间并且视图不需要滚动条时,这才具有可见效果。 -
dragMode和setDragMode函数可用于获取和设置视图的拖动模式。 这是视图的最重要函数之一,它可以决定在视图上单击并拖动鼠标左键时会发生什么。 在下面的示例中,我们将使用它并对其进行全面了解。 我们将使用QGraphicsView::DragMode枚举设置不同的拖动模式。 -
isInteractive和setInteractive函数允许检索和修改视图的交互行为。 交互式视图会响应鼠标和键盘(如果已实现),否则,所有鼠标和键盘事件都将被忽略,并且该视图只能用于查看并且不能与场景中的项目进行交互。 -
optimizationFlags,setOptimizationFlags,renderHints,setRenderHints,viewportUpdateMode和setViewportUpdateMode函数分别用于获取和设置与视图的性能和渲染质量有关的参数。 在下面的示例项目中,我们将在实践中看到这些函数的用例。 -
在
dragMode设置为RubberBandDrag模式的情况下,可以使用rubberBandSelectionMode和setRubberBandSelectionMode函数设置视图的项目选择模式。 可以设置以下内容,它们是Qt::ItemSelectionMode枚举中的条目:Qt::ContainsItemShapeQt::IntersectsItemShapeQt::ContainsItemBoundingRectQt::IntersectsItemBoundingRect
-
sceneRect和setSceneRect函数可用于获取和设置视图中场景的可视化区域。 显然,该值不必与QGraphicsScene类的sceneRect相同。 -
centerOn函数可用于确保特定点或项目位于视图中心。 -
ensureVisible函数可用于将视图滚动到特定区域(具有给定的边距)以确保它在视图中。 此函数适用于点,矩形和图形项目。 -
fitInView函数与centerOn和ensureVisible非常相似,但主要区别在于,该函数还使用给定的宽高比处理参数缩放视图的内容以适合视图。 以下:Qt::IgnoreAspectRatioQt::KeepAspectRatioQt::KeepAspectRatioByExpanding
-
itemAt函数可用于在视图中的特定位置检索项目。
我们已经了解到场景中的每个项目和场景中的每个项目都有各自的坐标系,我们需要使用映射函数将位置从一个位置转换到另一个位置,反之亦然。 视图也是如此。 视图还具有自己的坐标系,主要区别在于视图中的位置和矩形等实际上是根据像素进行测量的,因此它们是整数,但是场景和项目的位置使用实数,等等。 这是由于以下事实:场景和项目在视图上被查看之前都是逻辑实体,因此所有实数都将转换为整数,而整个场景(或部分场景)准备在屏幕上显示。 。 下图可以帮助您更好地理解这一点:

在上图中,视图的中心点实际上是场景右上角的某个位置。 视图提供了类似的映射函数(与我们在项目中看到的函数相同),可以将场景坐标系中的位置转换为视图坐标系,反之亦然。 这里是它们,再加上其他一些函数和方法,在继续之前,我们需要学习以下视图:
mapFromScene和mapToScene函数可用于在场景坐标系之间转换位置。 与前面提到的一致,mapFromScene函数接受实数并返回整数值,而mapToScene函数接受整数并返回实数。 稍后我们将开发视图的缩放功能时,将使用这些函数。items函数可用于获取场景中的项目列表。render函数对于执行整个视图或其一部分的渲染很有用。 该函数的用法与QGraphicsScene中的render完全相同,只是此函数在视图上执行相同的功能。rubberBandRect函数可用于获取橡皮筋选择的矩形。 如前所述,这仅在拖动模式设置为rubberBandSelectionMode时才有意义。setScene和scene函数可用于设置和获取视图场景。setMatrix,setTransform,transform,rotate,scale,shear和translate函数都可以用于修改或检索视图的几何特性。
与QGraphicsScene和QGraphicsItem类相同,QGraphicsView还提供了许多相同的受保护虚拟成员,可用于进一步扩展视图的功能。 现在,我们将扩展Graphics_Viewer示例项目,以支持更多项目,项目选择,项目删除以及放大和缩小功能,并且在此过程中,我们将概述以下项目的一些最重要用例: 我们在本章中学到的视图,场景和项目。 因此,让我们完成它:
- 首先在 Qt Creator 中打开
Graphics_Viewer项目; 然后,从主菜单中选择“新建文件”或“项目”,然后在“新建文件或项目”窗口中选择“C++ 和 C++ 类”,然后单击“选择”按钮。 - 确保输入
QEnhancedGraphicsView作为类名,然后选择QWidget作为基类。 另外,如果Include QWidget旁边的复选框尚未选中,请选中它。 然后,单击“下一步”,然后单击“完成”。 - 添加以下内容以包含
qenhancedgraphicsview.h头文件:
#include <QGraphicsView>
- 确保
QEnhancedGraphicsView类继承了qenhancedgraphicsview.h文件中的QGraphicsView而不是QWidget,如下所示:
class QEnhancedGraphicsView : public QGraphicsView
- 您必须更正
QEnhancedGraphicsView类的构造器实现,如此处所示。 显然,这是在qenhancedgraphicsview.cpp文件中完成的,如下所示:
QEnhancedGraphicsView::QEnhancedGraphicsView(QWidget
*parent)
: QGraphicsView(parent)
{
}
- 现在,将以下受保护的成员添加到
qenhancedgraphicsview.h文件中的增强型视图类定义中:
protected:
void wheelEvent(QWheelEvent *event);
- 并将其实现添加到
qenhancedgraphicsview.cpp文件,如以下代码块所述:
void QEnhancedGraphicsView::wheelEvent(QWheelEvent *event)
{
if (event->orientation() == Qt::Vertical)
{
double angleDeltaY = event->angleDelta().y();
double zoomFactor = qPow(1.0015, angleDeltaY);
scale(zoomFactor, zoomFactor);
this->viewport()->update();
event->accept();
}
else
{
event->ignore();
}
}
您需要确保QWheelEvent和QtMath包含在我们的类源文件中,否则,您将获得qPow函数和QWheelEvent类的编译器错误。 前面的代码大部分是不言自明的-它首先检查鼠标滚轮事件的方向,然后根据滚轮中的移动量在 X 和 Y 轴上都应用一个比例。 然后,它更新视口,以确保根据需要重新绘制所有内容。
-
现在,我们需要进入 Qt Creator 中的“设计”模式,以在窗口上提升
graphicsView对象(如我们先前所见)。 我们需要右键单击并从上下文菜单中选择“升级为”。 然后,输入QEnhancedGraphicsView作为升级的类名称,然后单击“添加”按钮,最后单击“升级”按钮。 (您已经在前面的示例中学习了关于提升的知识,这也不例外。)由于QGraphicsView和QEnhancedGraphicsView类是兼容的(第一个是后者的父类),因此我们可以将父代提升为子代,和/ 或将其降级(如果我们不需要)。 升级就像将小部件转换为其子小部件以支持和添加更多功能一样。 -
您需要在
mainwindow.cpp的dropEvent函数顶部添加一小段代码,以确保在加载新图像时重置缩放级别(准确地说是比例转换):
ui->graphicsView->resetTransform();
现在,您可以启动应用,并尝试使用鼠标滚轮滚动。 向上或向下旋转轮子时,您可以看到比例级别的变化。 这是放大和缩小图像时结果应用的屏幕截图:

如果再尝试一点,很快就会发现一件事,缩放功能总是朝着图像的中心起作用,这很奇怪而且不舒服。 为了能够解决此问题,我们需要利用在本章中学到的更多提示,技巧和功能:
- 首先向我们的增强型视图类添加另一个私有受保护的函数。 除了先前使用的
wheelEvent外,我们还将使用mouseMoveEvent。 因此,将以下代码行添加到qenhancedgraphicsview.h文件中的受保护成员部分:
void mouseMoveEvent(QMouseEvent *event);
- 另外,添加一个私有成员,如下所示:
private:
QPointF sceneMousePos;
- 现在,转到它的实现部分,并将以下代码行添加到
qenhancedgraphicsview.cpp文件:
void QEnhancedGraphicsView::mouseMoveEvent(QMouseEvent
*event)
{
sceneMousePos = this->mapToScene(event->pos());
}
- 您还需要稍微调整
wheelEvent函数。 确保其外观如下:
if (event->orientation() == Qt::Vertical)
{
double angleDeltaY = event->angleDelta().y();
double zoomFactor = qPow(1.0015, angleDeltaY);
scale(zoomFactor, zoomFactor);
if(angleDeltaY > 0)
{
this->centerOn(sceneMousePos);
sceneMousePos = this->mapToScene(event->pos());
}
this->viewport()->update();
event->accept();
}
else
{
event->ignore();
}
您只需关注函数名称,就可以很容易地看到这里发生的事情。 我们实现了mouseMoveEvent来拾取鼠标的位置(在场景坐标中,这非常重要); 然后我们确保在放大(而不是缩小)之后,该视图确保所采集的点位于屏幕的中心。 最后,它会更新位置,以获得更舒适的变焦体验。 重要的是要注意,有时诸如此类的小缺陷或功能可能意味着用户可以舒适地使用您的应用,最终这是应用增长(或最坏的情况是下降)的重要参数。
现在,我们将向Graphics_Viewer应用添加更多功能。 让我们首先确保我们的Graphics_Viewer应用能够处理无限数量的图像:
- 首先,我们需要确保在将每个图像拖放到视图中(因此是场景)之后,不会清除场景,因此首先从
mainwindow.cpp的dropEvent中删除以下行:
scene.clear();
- 另外,从
dropEvent中删除以下代码行,我们先前添加了以下代码行以重置缩放比例:
ui->graphicsView->resetTransform();
- 现在,将以下两行代码添加到
mainwindow.cpp文件中dropEvent的起点:
QPoint viewPos = ui->graphicsView->mapFromParent
(event->pos());
QPointF sceneDropPos = ui->graphicsView->mapToScene
(viewPos);
- 然后,确保将项目的位置设置为
sceneDropPos,如下所示:
item->setPos(sceneDropPos);
就是这样,现在不需要其他任何东西。 启动Graphics_Viewer应用,然后尝试将图像放入其中。 在第一张图像之后,尝试缩小并添加更多图像。 (请不要通过夸大此测试来填充内存,因为如果您尝试添加大量图像,则您的应用将开始消耗过多的内存,从而导致操作系统出现问题。不用说,您的应用可能会崩溃 。)以下是在场景中各个位置拖放的一些图像的屏幕截图:

显然,该应用仍然遗漏了很多东西,但是在剩下的部分让您自己找出并发现之前,我们将在本章中介绍一些非常关键的功能。 一些非常重要的缺失功能是我们无法选择,删除项目或对其施加某些效果。 让我们一次完成一个简单但功能强大的Graphics_Viewer应用。 如您所知,稍后,我们将使用在综合计算机视觉应用(名为Computer_Vision项目)中学到的所有技术。 因此,让我们开始为Graphics_Viewer项目添加以下最终内容:
- 首先向增强的图形视图类添加另一个受保护的成员,如下所示:
void mousePressEvent(QMouseEvent *event);
- 然后,将以下专用插槽添加到相同的类定义中:
private slots:
void clearAll(bool);
void clearSelected(bool);
void noEffect(bool);
void blurEffect(bool);
void dropShadowEffect(bool);
void colorizeEffect(bool);
void customEffect(bool);
- 现在,将所有必需的实现添加到视图类源文件,即
qenhancedgraphicsview.cpp文件。 首先添加mousePressEvent的实现,如下所示:
void QEnhancedGraphicsView::mousePressEvent(QMouseEvent
*event)
{
if(event->button() == Qt::RightButton)
{
QMenu menu;
QAction *clearAllAction = menu.addAction("Clear All");
connect(clearAllAction,
SIGNAL(triggered(bool)),
this,
SLOT(clearAll(bool)));
QAction *clearSelectedAction = menu.addAction("Clear Selected");
connect(clearSelectedAction,
SIGNAL(triggered(bool)),
this,
SLOT(clearSelected(bool)));
QAction *noEffectAction = menu.addAction("No Effect");
connect(noEffectAction,
SIGNAL(triggered(bool)),
this,
SLOT(noEffect(bool)));
QAction *blurEffectAction = menu.addAction("Blur Effect");
connect(blurEffectAction,
SIGNAL(triggered(bool)),
this,
SLOT(blurEffect(bool)));
// ***
menu.exec(event->globalPos());
event->accept();
}
else
{
QGraphicsView::mousePressEvent(event);
}
}
在前面的代码中,//***对于dropShadowEffect,colorizeEffect和customEffect函数插槽基本上以相同的模式重复。 在前面的代码中,我们所做的只是简单地创建并打开一个上下文(右键单击)菜单,然后将每个动作连接到将在下一步中添加的插槽。
- 现在,添加插槽的实现,如下所示:
void QEnhancedGraphicsView::clearAll(bool)
{
scene()->clear();
}
void QEnhancedGraphicsView::clearSelected(bool)
{
while(scene()->selectedItems().count() > 0)
{
delete scene()->selectedItems().at(0);
scene()->selectedItems().removeAt(0);
}
}
void QEnhancedGraphicsView::noEffect(bool)
{
foreach(QGraphicsItem *item, scene()->selectedItems())
{
item->setGraphicsEffect(Q_NULLPTR);
}
}
void QEnhancedGraphicsView::blurEffect(bool)
{
foreach(QGraphicsItem *item, scene()->selectedItems())
{
item->setGraphicsEffect(new QGraphicsBlurEffect(this));
}
}
//***
与前面的代码相同,其余插槽遵循相同的模式。
- 在我们的应用准备好进行测试运行之前,我们需要处理一些最后的事情。 首先,我们需要确保增强的图形视图类是交互式的,并允许通过单击和拖动来选择项目。 您可以通过将以下代码段添加到
mainwindow.cpp文件中来实现。 设置场景后立即在初始化函数(构造器)中执行以下操作:
ui->graphicsView->setInteractive(true);
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
ui->graphicsView->setRubberBandSelectionMode(
Qt::ContainsItemShape);
- 最后但并非最不重要的一点是,在
mainwindow.cpp的dropEvent函数中添加以下代码行,以确保可以选择项目。 将它们添加到项目创建代码之后以及添加到场景的行之前:
item->setFlag(QGraphicsItem::ItemIsSelectable);
item->setAcceptedMouseButtons(Qt::LeftButton);
而已。 我们准备开始并测试我们的Graphics_Viewer应用,该应用现在还可以添加效果并具有更多功能。 这是显示所谓的橡皮筋选择模式行为的屏幕截图:

最后,下面是正在运行的Graphics_Viewer应用的屏幕快照,同时为场景中的图像添加了不同的效果:

而已。 现在,我们可以创建功能强大的图形查看器,并将其添加到Computer_Vision项目中,在学习新的以及更多的 OpenCV 和 Qt 技能和技术的同时,还将在接下来的章节中使用。 按照承诺,您可以从以下链接下载Computer_Vision项目的完整版本。
正如我们在前几章中反复提到的那样,该项目的目标是通过照顾每种所需的 GUI 功能,语言,主题等,帮助我们仅专注于计算机视觉主题。 。 该项目是到目前为止您学到的一切的完整示例。 该应用可以使用样式进行自定义,可以支持新语言,并且可以使用插件进行扩展。 它还将您在本章中学到的所有内容打包到一个漂亮而强大的图形查看器中,我们将在本书的其余部分中使用该图形查看器。 在继续以下各章之前,请确保下载了它。
Computer_Vision项目包含一个 Qt 多项目中的两个项目,或者更确切地说是subdirs项目类型。 第一个是mainapp,第二个是template_plugin项目。 您可以复制(克隆)并替换该项目中的代码和 GUI 文件,以创建与Computer_Vision项目兼容的新插件。 这正是我们在第 6 章,“OpenCV 中的图像处理”)中所做的工作,对于您学习的大多数 OpenCV 技能,我们将为Computer_Vision创建一个插件。 该项目还包含示例附加语言和示例附加主题,可以再次对其进行简单地复制和修改,以为应用创建新的语言和主题。 确保您查看了整个下载的源代码,并确保其中没有奥秘,并且您完全了解Computer_Vision项目源代码中的所有内容。 同样,这是为了总结您所学的所有知识并将其打包到一个单一的,全面的,可重用的示例项目中。
总结
自本书开始以来,我们已经走了很长一段路,到现在,我们已经完全掌握了许多有用的技术来承担计算机视觉应用开发的任务。 在前面的所有章节(包括我们刚刚完成的章节)中,您了解了更多有关创建强大而全面的应用所需的技能(通常,大部分情况),而不仅仅是专注于计算机视觉(准确来说是 OpenCV 技能)方面。 您学习了如何创建支持多种语言,主题和样式,插件的应用; 在本章中,您学习了如何在场景和视图中可视化图像和图形项目。 现在,我们已经拥有了深入研究计算机视觉应用开发世界所需的几乎所有东西。
在第 6 章,“OpenCV 中的图像处理”中,您将了解有关 OpenCV 以及其中可能的图像处理技术的更多信息。 对于每个学习的主题,我们仅假设我们正在创建与Computer_Vision项目兼容的插件。 这意味着我们将在Computer_Vision项目中使用模板插件,将其复制,然后简单地制作一个能够执行特定计算机视觉任务,转换过滤器或计算的新插件。 当然,这并不意味着您不能创建具有相同功能的独立应用,正如您将在接下来的章节中看到的那样,我们的插件具有 GUI,与创建应用或创建应用本质上没有什么不同。 准确地说,您在上一章中学到了所有的 Qt Widgets 应用。 但是,从现在开始,我们将继续学习更高级的主题,并且我们的重点将主要放在应用的计算机视觉方面。 您将学习如何在 OpenCV 中使用众多的过滤和其他图像处理功能,它支持的色彩空间,许多转换技术等等。
六、OpenCV 中的图像处理
它始终以未经处理的原始图像开始,这些图像是使用智能手机,网络摄像头,DSLR 相机,或者简而言之,是能够拍摄和记录图像数据的任何设备拍摄的。 但是,通常以清晰或模糊结束。 明亮,黑暗或平衡; 黑白或彩色; 以及同一图像数据的许多其他不同表示形式。 这可能是计算机视觉算法中的第一步(也是最重要的步骤之一),通常被称为图像处理(目前,让我们忘记一个事实,有时计算机视觉和图像处理可互换使用;这是历史专家的讨论。 当然,您可以在任何计算机视觉过程的中间或最后阶段进行图像处理,但是通常,用大多数现有设备记录的任何照片或视频首先都要经过某种图像处理算法。 这些算法中的某些仅用于转换图像格式,某些用于调整颜色,消除噪点,还有很多我们无法开始命名。 OpenCV 框架提供了大量功能来处理各种图像处理任务,例如图像过滤,几何变换,绘图,处理不同的色彩空间,图像直方图等,这将是本章的重点。
在本章中,您将学习许多不同的函数和类,尤其是从 OpenCV 框架的imgproc模块中。 我们将从图像过滤开始,在此过程中,您将学习如何创建允许正确使用现有算法的 GUI。 之后,我们将继续学习 OpenCV 提供的几何变换功能。 然后,我们将简要介绍一下什么是色彩空间,如何将它们彼此转换,等等。 之后,我们将继续学习 OpenCV 中的绘图函数。 正如我们在前几章中所看到的,Qt 框架还提供了相当灵活的绘图函数,它甚至还可以通过使用场景视图项目架构更轻松地处理屏幕上的不同图形项。 但是,在某些情况下,我们也会使用 OpenCV 绘图函数,这些函数通常非常快,并且可以为日常图形任务提供足够的功能。 本章将以 OpenCV 中功能最强大但最易于使用的匹配和检测方法之一结尾,即模板匹配方法。
本章将包含许多有趣的示例和动手学习材料,并且一定要确保您尝试所有这些示例,以便在工作中看到它们,并根据第一手经验而不是仅仅通过第一手经验来学习它们。 紧随本章,某些部分结尾处提供的屏幕快照和示例源代码之后。
在本章中,我们将介绍以下主题:
- 如何为
Computer_Vision项目和每个学习过的 OpenCV 技能创建新的插件 - 如何过滤图像
- 如何执行图像转换
- 颜色空间,如何将它们彼此转换以及如何应用颜色映射
- 图像阈值
- OpenCV 中可用的绘图函数
- 模板匹配以及如何将其用于对象检测和计数
图像过滤
在本入门部分,您将了解 OpenCV 中可用的不同线性和非线性图像滤波方法。 重要的是要注意,本节中讨论的所有函数都将Mat图像作为输入,并产生相同大小和相同通道数的Mat图像。 实际上,过滤器是独立应用于每个通道的。 通常,滤波方法从输入图像中获取一个像素及其相邻像素,并基于来自这些像素的函数响应来计算所得图像中相应像素的值。
这通常需要在计算滤波后的像素结果时对不存在的像素进行假设。 OpenCV 提供了许多方法来解决此问题,可以使用cv::BorderTypes枚举在几乎所有需要处理此现象的 OpenCV 函数中指定它们。 稍后我们将在本章的第一个示例中看到如何使用它,但是在此之前,让我们确保使用下图完全理解它:

如上图所示,计算(或在这种情况下为滤波函数)将区域 A 中的像素作为像素,并在处理后的所得图像(在这种情况下为过滤后的图像)中给我们像素 A。 在这种情况下没有问题,因为输入图像中像素 A 附近的所有像素都在图像内部,即区域 A。 但是,图像边缘附近的像素或 OpenCV 中称为“边界像素”的像素又如何呢? 如您所见,并非像素 B 的所有相邻像素都落入输入图像,即区域 B。 这就是我们需要做的假设,即将图像外部像素的值视为零,与边界像素相同,依此类推。 这正是cv::BorderTypes枚举的含义,我们需要在示例中使用合适的值进行指定。
现在,在开始图像过滤函数之前,让我们用第一个示例演示cv::BorderTypes的用法。 我们将借此机会还学习如何为上一章中开始的Computer_Vision项目创建新插件(或克隆现有插件)。 因此,让我们开始:
- 如果您已经完全按照本书中的示例进行了操作,那么如果您已经在第 5 章,“图形视图框架”。 为此,请从
Computer_Vision项目文件夹中的template_plugin文件夹复制(或复制并粘贴到同一文件夹中,这仅取决于您使用的 OS)。 然后,将新文件夹重命名为copymakeborder_plugin。 我们将为Computer_Vision项目创建第一个真实的插件,并通过一个真实的示例了解cv::BorderTypes的工作方式。 - 转到
copymakeborder_plugin文件夹,然后在此处重命名所有文件以匹配插件文件夹名称。 只需将文件名中的所有template词替换为copymakeborder即可。 - 您可以猜测,现在我们还需要更新
copymakeborder_plugin的项目文件。 为此,您可以简单地在标准文本编辑器中打开copymakeborder_plugin.pro文件,或将其拖放到“Qt Creator 代码编辑器”区域(而不是“项目”窗格)中。 然后,将TARGET设置为CopyMakeBorder_Plugin,如此处所示。 显然,您需要更新已经存在的类似行:
TARGET = CopyMakeBorder_Plugin
- 与上一步类似,我们还需要相应地更新
DEFINES:
DEFINES += COPYMAKEBORDER_PLUGIN_LIBRARY
- 最后,确保
pro文件中的HEADERS和SOURCES条目也已更新,如此处所示,然后保存并关闭pro文件:
SOURCES += \
copymakeborder_plugin.cpp
HEADERS += \
copymakeborder_plugin.h \
copymakeborder_plugin_global.h
- 现在,使用 Qt Creator 打开
computer_vision.pro文件。 这将打开整个Computer_Vision项目,即Qt Multi-Project。 Qt 允许在单个容器项目中处理多个项目,或者由 Qt 本身称为subdirs项目类型。 与常规 Qt Widgets 应用项目不同,subdirs项目通常(不一定)具有非常简单且简短的*.pro文件。 一行将TEMPLATE类型提到为subdirs,并列出了SUBDIRS条目,该条目列出了subdirs项目文件夹中的所有项目文件夹。 让我们在 Qt Creator 代码编辑器中打开computer_vision.pro文件,亲自了解一下:
TEMPLATE = subdirs
SUBDIRS += \
mainapp \
template_plugin
- 现在,只需将
copymakeborder_plugin添加到条目列表。 您更新的computer_vision.pro文件应如下所示:
TEMPLATE = subdirs
SUBDIRS += \
mainapp \
template_plugin \
copymakeborder_plugin
请注意,在所有qmake(基本上是所有 Qt 项目文件)定义中,如果将条目划分为多行,则需要在除最后一行之外的所有行中都添加\,如前面的代码块所示。 我们可以通过删除\并在条目之间添加空格字符来编写相同的内容。 推荐的方法不是后者,但仍然正确。
- 最后,对于这部分,我们需要更新
copymakeborder_plugin源文件和头文件的内容,因为显然,类名,包含的头文件甚至某些编译器指令都需要更新。 处理这些编程开销确实令人沮丧,因此让我们利用这一机会来了解 Qt Creator 中最有用的技巧之一,即 Qt Creator 中的“在此目录中查找...”功能。 您可以使用它从字面上查找(并替换)Qt 项目文件夹或子文件夹中的任何内容。 当我们希望避免手动浏览文件并一一替换代码段时,您将学习并使用此技术。 要使用它,您只需要从“项目”窗格中选择合适的文件夹,右键单击它,然后选择“在此目录中查找...”选项。 让我们用copymakeborder_plugin项目来完成它,如屏幕截图所示:

- 如下面的屏幕快照所示,这将打开 Qt Creator 窗口底部的“搜索结果”窗格。 在这里,您必须在“搜索:”字段中输入
TEMPLATE_PLUGIN。 另外,请确保选中区分大小写选项。 其余所有选项均保持不变,然后单击“搜索&替换”按钮:

- 这会将“搜索结果”窗格切换到“替换”模式。 用
COPYMAKEBORDER_PLUGIN填充替换为:字段,然后单击替换按钮。 显示如下:

- 在前面的步骤中,我们使用查找和替换 Qt Creator 的功能将所有
TEMPLATE_PLUGIN条目替换为COPYMAKEBORDER_PLUGIN。 使用相同的技能,并用copymakeborder_plugin替换所有template_plugin条目,并用CopyMakeBorder_Plugin替换所有Template_Plugin条目。 这样,我们的新插件项目就可以进行编程了,并最终可以在Computer_Vision项目中使用。
本章第一个示例项目中的所有上述所有步骤仅用于准备插件项目,从现在开始,无论何时需要,我们将这些步骤与克隆或(复制)模板插件来创建X插件,而在此示例中,X就是copymakeborder_plugin。 这将帮助我们避免大量重复的说明,同时,将使我们能够更加专注于学习新的 OpenCV 和 Qt 技能。 通过前面的步骤,尽可能地冗长而冗长,我们将避免处理图像读取,显示图像,选择正确的语言,选择正确的主题和样式以及许多其他任务,因为它们全都位于 Computer_Vision项目的一个子项目,称为mainapp,仅是 Qt Widgets 应用,负责处理所有与插件无关的任务,这些插件不涉及执行特定计算机视觉任务的插件。 在以下步骤中,我们将简单地填写插件的现有功能并创建其所需的 GUI。 然后,我们可以将构建的插件库文件复制到Computer_Vision可执行文件旁边的cvplugins文件夹中,并且当我们在Computer_Vision项目中运行mainapp时,每个插件都将在来自主菜单的Plugins中显示为条目,包括新添加的菜单。 本书其余部分的所有示例都将遵循相同的模式,至少在很大程度上,这意味着,除非我们需要专门更改插件或主应用的一部分,否则有关克隆和创建新版本的所有说明均应遵循。 插件(之前的步骤)将被省略。
如前几章所述,更改*.pro文件(或多个文件)后手动运行qmake始终是一个好主意。 只需在 Qt Creator 的“项目”窗格中右键单击该项目,然后单击“运行qmake”。
- 现在该为我们的插件编写代码并相应地创建其 GUI 了。 打开
plugin.ui文件,并确保其用户界面包含以下小部件。 另外,请注意小部件的objectName值。 请注意,整个PluginGui文件的布局都设置为网格布局,如下所示:

- 将
borderTypeLabel的size Policy/Horizontal Policy属性设置为Fixed。 这将确保标签根据其宽度占据固定的水平空间。 - 通过右键单击
borderTypeComboBox小部件的currentIndexChanged(int)信号添加方法,选择“转到插槽...”,选择提到的信号,然后单击“确定”按钮。 然后,在此函数的新创建函数(准确地说是插槽)中编写以下代码行:
emit updateNeeded();
该信号的目的是告诉mainapp,在组合框的所选项目发生更改之后,插件可能会产生不同的结果,并且mainapp可能希望基于此信号更新其 GUI。 您可以检查mainapp项目的源代码,您会注意到所有插件的信号都连接到mainapp中的相关插槽,该插槽仅调用插件的processImage函数。
- 现在,在
copymakeborder_plugin.cpp文件中,将以下代码添加到其setupUi函数中。setupUi函数的内容应如下所示:
ui = new Ui::PluginGui;
ui->setupUi(parent);
QStringList items;
items.append("BORDER_CONSTANT");
items.append("BORDER_REPLICATE");
items.append("BORDER_REFLECT");
items.append("BORDER_WRAP");
items.append("BORDER_REFLECT_101");
ui->borderTypeComboBox->addItems(items);
connect(ui->borderTypeComboBox,
SIGNAL(currentIndexChanged(int)),
this,
SLOT(on_borderTypeComboBox_currentIndexChanged(int)));
我们已经熟悉了与 UI 相关的启动调用,这些调用与每个 Qt Widgets 应用中的调用几乎相同,正如我们在上一章中了解到的那样。 之后,我们用相关项填充组合框,这些项只是cv::BorderTypes枚举中的条目。 如果按此顺序插入,则每个项目索引值将与其对应的枚举值相同。 最后,我们将所有信号手动连接到插件中的相应插槽。 请注意,这与常规 Qt 窗口小部件应用稍有不同,在常规应用中,您无需连接名称兼容的信号和插槽,因为它们是通过调用代码文件中的QMetaObject:: connectSlotsByName自动连接的,代码文件由 UIC 自动生成(请参阅第 3 章,“创建综合 Qt + OpenCV 项目”)。
- 最后,更新插件中的
processImage函数,如下所示:
int top, bot, left, right;
top = bot = inputImage.rows/2;
left = right = inputImage.cols/2;
cv::copyMakeBorder(inputImage,
outputImage,
top,
bot,
left,
right,
ui->borderTypeComboBox->currentIndex());
在这里,我们将调用copyMakeBorder函数,该函数也称为内部函数,该函数需要处理有关图像外部不存在的像素的假设。 我们仅假设图像顶部和底部添加的边框是图像高度的一半,而图像左侧和右侧添加的边框是图像宽度的一半。 至于borderType参数,我们只需从插件 GUI 上的选定项中获取即可。
一切都完成了,我们可以测试我们的插件了。 通过在“项目”窗格中右键单击整个Computer_Vision多项目并从菜单中选择“重建”(以确保清除并重建了所有内容),确保构建了整个Computer_Vision多项目。 然后,转到插件Build文件夹,从那里复制库文件,然后将其粘贴到mainapp可执行文件旁边的cvplugins文件夹中(在主应用Build文件夹中),最后运行mainapp 来自 Qt Creator。
mainapp启动后,您将面临一条错误消息(如果未复制插件或格式错误),或者最终将出现在Computer_Vision应用主窗口中。 然后,如果尚未选择mainapp的插件菜单,则可以选择我们刚刚构建的插件。 您可以在mainapp主窗口的组框中看到我们为插件设计的 GUI。 然后,您可以使用主菜单打开或保存图形场景的内容。 尝试打开一个文件,然后在插件组合框中的不同选项之间切换。 您也可以通过选中查看原始图像复选框来查看原始图像。 这是屏幕截图:

从组合框中选择任何其他“边框类型”,您将立即注意到结果图像的变化。 重要的是要注意BORDER_REFLECT_101也是默认的边框类型(如果您未在 OpenCV 过滤和类似函数中指定一个),则与BORDER_REFLECT十分相似,但不会重复边界之前的最后一个像素。 有关此的更多信息,请参见cv::BorderTypes的 OpenCV 文档页面。 如前所述,这是需要处理外部(不存在)像素的相似插值的每个 OpenCV 函数相同的结果:

而已。 现在,我们准备开始使用 OpenCV 中可用的过滤函数。
OpenCV 中的过滤函数
OpenCV 中的所有过滤函数均会拍摄图像,并产生尺寸和通道完全相同的图像。 如前所述,它们也都带有borderType参数,我们刚刚完成了实验和学习。 除此之外,每个过滤函数都有自己的必需参数来配置其行为。 这是可用的 OpenCV 过滤函数的列表及其说明和使用方法。 在列表的最后,您可以找到一个示例插件(称为filter_plugin)及其源代码的链接,其中包括以下列表中提到的大多数过滤器,并带有 GUI 控件以试验不同的参数和设置。 为每一个:
bilateralFilter:可用于获取图像的Bilateral Filtered副本。 根据σ值和直径,您可以获得的图像看上去可能与原始图像没有太大差异,或者获得的图像看起来像卡通图像(如果σ值足够高)。 这是bilateralFilter函数作为我们的应用的插件工作的示例代码:
bilateralFilter(inpMat,outMat,15,200,200);
这是bilateralFilter函数的屏幕截图:

blur,boxFilter,sqrBoxFilter,GaussianBlur和medianBlur:这些均用于获取输入图像的平滑版本。 所有这些函数都使用核大小参数,该参数与直径参数基本相同,并且用于确定从中计算出滤波后像素的相邻像素的直径。 (尽管我们没有了解它们的详细信息,但是这些过滤器函数与我们在本书前面各章中使用的过滤器函数相同。)GaussianBlur函数需要提供高斯核标准差(σ)参数,在X和Y方向上。 (有关这些参数的数学来源的足够信息,请参阅 OpenCV 文档。)实际上,值得注意的是,高斯过滤器中的核大小必须为奇数和正数。 同样,如果核大小也足够高,较高的σ值只会对结果产生重大影响。 以下是提到的平滑过滤器的几个示例(左侧为GaussianBlur,右侧为medianBlur),以及示例函数调用:
Size kernelSize(5,5);
blur(inpMat,outMat,kernelSize);
int depth = -1; // output depth same as source
Size kernelSizeB(10,10);
Point anchorPoint(-1,-1);
bool normalized = true;
boxFilter(inutMat,outMat,depth,
kernelSizeB,anchorPoint, normalized);
double sigma = 10;
GaussianBlur(inpMat,outMat,kernelSize,sigma,sigma);
int apertureSize = 10;
medianBlur(inpMat,outMat,apertureSize);
以下屏幕截图描绘了高斯和中值模糊的结果以及用于设置其参数的 GUI:

filter2D:此函数可用于将自定义过滤器应用于图像。 您需要为此函数提供的一个重要参数是核矩阵。 此函数非常强大,它可以产生许多不同的结果,包括与我们先前看到的模糊函数相同的结果,以及许多其他过滤器,具体取决于提供的核。 这里有几个示例核,以及如何使用它们以及生成的图像。 确保尝试使用不同的核(您可以在互联网上搜索大量有用的核矩阵),并亲自尝试使用此函数:
// Sharpening image
Matx33f f2dkernel(0, -1, 0,
-1, 5, -1,
0, -1, 0);
int depth = -1; // output depth same as source
filter2D(inpMat,outMat,depth,f2dkernel);
*****
// Edge detection
Matx33f f2dkernel(0, +1.5, 0,
+1.5, -6, +1.5,
0, +1.5, 0);
int depth = -1; // output depth same as source
filter2D(inpMat,outMat,depth,f2dkernel);
前面代码中第一个核的结果图像显示在左侧(这是图像的锐化版本),而第二个产生图像边缘检测的核在右侧可见:

Laplacian,Scharr,Sobel和spatialGradient:这些函数处理图像导数。 图像导数在计算机视觉中非常重要,因为它们可用于检测图像中具有变化或更好的是显着变化的区域(因为这是导数的用例之一)。 无需过多地讨论其理论和数学细节,可以提及的是,在实践中,它们用于处理边缘或角点检测,并且在 OpenCV 框架中被关键点提取方法广泛使用。 在前面的示例和图像中,我们还使用了导数计算核。 以下是一些有关如何使用它们以及产生的图像的示例。 屏幕截图来自Computer_Vision项目和filter_plugin,此列表后不久有一个链接。 您始终可以使用 Qt 控件(例如旋转框,刻度盘和滑块)来获取 OpenCV 函数的不同参数值,以更好地控制该函数的行为:
int depth = -1;
int dx = 1; int dy = 1;
int kernelSize = 3;
double scale = 5; double delta = 220;
Sobel(inpMat, outMat, depth,dx,dy,kernelSize,scale,delta);
以下是上述代码的输出屏幕截图:

如果我们使用以下代码:
int depth = -1;
int dx = 1; int dy = 0;
double scale = 1.0; double delta = 100.0;
Scharr(inpMat,outMat,depth,dx,dy,scale,delta);
我们最终会得到类似于以下内容:

对于以下代码:
int depth = -1; int kernelSize = 3;
double scale = 1.0; double delta = 0.0;
Laplacian(inpMat,outMat,depth, kernelSize,scale,delta);
将产生类似于以下内容:

erode和dilate:从它们的名称可以猜出这些函数,它们对于获得腐蚀和膨胀效果很有用。 这两个函数都采用一个结构元素矩阵,可以通过简单地调用getStructuringElement函数来构建它。 (可选)您可以选择多次运行该函数(或对其进行迭代),以获得越来越腐蚀或膨胀的图像。 以下是如何同时使用这两个函数及其生成的图像的示例:
erode(inputImage,
outputImage,
getStructuringElement(shapeComboBox->currentIndex(),
Size(5,5)), // Kernel size
Point(-1,-1), // Anchor point (-1,-1) for default
iterationsSpinBox->value());
以下是生成的图像:

您可以将完全相同的参数传递给dilate函数。 在前面的代码中,假设使用组合框小部件获取结构元素的形状,该小部件可以是MORPH_RECT,MORPH_CROSS或MORPH_ELLIPSE。 同样,通过使用旋转框小部件设置迭代计数,该小部件可以是大于零的数字。
让我们继续下一个函数:
morphologyEx:此函数可用于执行各种形态学操作。 它需要一个操作类型参数以及我们在dilate和erode函数中使用的相同参数。 以下是可以传递给morphologyEx函数的参数及其含义:MORPH_ERODE:产生与erode函数相同的结果。MORPH_DILATE:产生与dilate函数相同的结果。MORPH_OPEN:可用于执行打开操作。 这与对侵蚀的图像进行放大相同,对于消除图像中的细微伪影很有用。MORPH_CLOSE:可用于执行关闭操作。 它与侵蚀膨胀的图像相同,可用于消除线条中的细小断开等。MORPH_GRADIENT:此函数提供图像的轮廓,并且与同一图像的侵蚀和膨胀版本的区别相同。MORPH_TOPHAT:可用于获取图像与其打开的变形之间的差异。MORPH_BLACKHAT:这可以用来获取图像关闭和图像本身之间的差异。
这是一个示例代码,并且如您所见,该函数调用与扩散和侵蚀非常相似。 再次,我们假设使用组合框小部件选择了形态类型和形状,并使用SpinBox选择了迭代计数:
morphologyEx(inputImage,
outputImage,
morphTypeComboBox->currentIndex(),
getStructuringElement(shapeComboBox->currentIndex(),
Size(5,5)), // kernel size
Point(-1,-1), // default anchor point
iterationsSpinBox->value());
以下是不同形态学操作的结果图像:

您可以使用以下链接获取filter_plugin源代码的副本,该代码与Computer_Vision项目兼容,并且包括您在本节中学到的大多数图像过滤函数。 您可以使用同一插件来测试并生成本节中看到的大多数图像。 尝试扩展插件以控制更多参数,或者尝试向插件添加更多功能。 这是filter_plugin源代码的链接:您可以使用以下链接。
图像转换函数
在本节中,您将了解 OpenCV 中可用的图像转换函数。 通常,如果您查看 OpenCV 文档,则 OpenCV 中有两种图像转换类别,称为几何转换和其他(仅表示其他一切)转换。 在此解释其原因。
几何变换可以从其名称中猜出,主要处理图像的几何属性,例如图像的大小,方向,形状等。 注意,几何变换不会改变图像的内容,而只是根据几何变换类型通过在图像的像素周围移动来改变其形式和形状。 与我们在上一节开始时对图像进行过滤一样,几何变换函数还需要处理图像外部像素的外推,或者简单地说,在计算像素时对不存在的像素进行假设。 转换后的图像。 为此,当我们处理第一个示例copymakeborder_plugin时,可以使用本章前面学习的相同cv::BorderTypes枚举。
除此之外,除了所需的外推法之外,几何变换函数还需要处理像素的内插,因为变换后的图像中像素的计算位置将为float(或double)类型,而不是 integer,并且由于每个像素只能具有单一颜色,并且必须使用整数指定其位置,因此需要确定像素的值。 为了更好地理解这一点,让我们考虑一种最简单的几何变换,即调整图像大小,这是使用 OpenCV 中的resize函数完成的。 例如,您可以将图像调整为其大小的一半,完成后,计算出的图像中至少一半像素的新位置将包含非整数值。 位置(2,2)中的像素将位于调整大小后的图像中的位置(1,1),但是位置(3,2)中的像素将需要位于位置(1.5,1)中,依此类推。 OpenCV 提供了许多插值方法,这些方法在cv::InterpolationFlags枚举中定义,其中包括:
INTER_NEAREST:这是用于最近邻插值INTER_LINEAR:用于双线性插值INTER_CUBIC:这用于双三次插值INTER_AREA:这是用于像素区域关系重采样INTER_LANCZOS4:这是用于8x8附近的 Lanczos 插值
几乎所有的几何变换函数都需要提供cv::BorderType和cv::InterpolationFlags参数,以处理所需的外推和内插参数。
几何转换
现在,我们将从一些最重要的几何转换开始,然后学习色彩空间以及它们如何与一些广泛使用的非几何(或其他)转换相互转换。 因此,它们是:
resize:此函数可用于调整图像尺寸。 这是一个用法示例:
// Resize to half the size of input image
resize(inMat, outMat,
Size(), // an empty Size
0.5, // width scale factor
0.5, // height scale factor
INTER_LANCZOS4); // set the interpolation mode to Lanczos
// Resize to 320x240, with default interpolation mode
resize(inMat, outMat, Size(320,240));
warpAffine:此函数可用于执行仿射变换。 您需要为此函数提供适当的变换矩阵,可以使用getAffineTransform函数获得该矩阵。getAffineTransform函数必须提供两个三角形(源三角形和变换三角形),或者换句话说,提供两组三个点。 这是一个例子:
Point2f triangleA[3];
Point2f triangleB[3];
triangleA[0] = Point2f(0 , 0);
triangleA[1] = Point2f(1 , 0);
triangleA[2] = Point2f(0 , 1);
triangleB[0] = Point2f(0, 0.5);
triangleB[1] = Point2f(1, 0.5);
triangleB[2] = Point2f(0.5, 1);
Mat affineMat = getAffineTransform(triangleA, triangleB);
warpAffine(inputImage,
outputImage,
affineMat,
inputImage.size(), // output image size, same as input
INTER_CUBIC, // Interpolation method
BORDER_WRAP); // Extrapolation method
这是结果图像:

您也可以使用warpAffine函数来旋转源图像。 只需使用getRotationMatrix2D函数来获取我们在前面的代码中使用的变换矩阵,然后将其与warpAffine函数一起使用。 请注意,此方法可用于执行任意角度的旋转,而不仅仅是 90 度旋转及其乘数。 这是一个示例代码,它围绕图像的中心旋转源图像-45.0度。 您也可以选择缩放输出图像。 在此示例中,我们在旋转输出图像时将其缩放为源图像大小的一半:
Point2f center = Point(inputImage.cols/2,
inputImage.rows/2);
double angle = -45.0;
double scale = 0.5;
Mat rotMat = getRotationMatrix2D(center, angle, scale);
warpAffine(inputImage,
outputImage,
rotMat,
inputImage.size(),
INTER_LINEAR,
BORDER_CONSTANT);
以下是生成的输出屏幕截图:

warpPerspective:此函数对于执行透视变换很有用。 与warpAffine函数相似,此函数还需要可以使用findHomography函数获得的变换矩阵。findHomography函数可用于计算两组点之间的单应性变化。 这是一个示例代码,其中我们使用两组角点来计算单应性更改矩阵(或warpPerspective的变换矩阵),然后使用它执行透视更改。 在此示例中,我们还将外推颜色值(可选)设置为深灰色阴影:
std::vector<Point2f> cornersA(4);
std::vector<Point2f> cornersB(4);
cornersA[0] = Point2f(0, 0);
cornersA[1] = Point2f(inputImage.cols, 0);
cornersA[2] = Point2f(inputImage.cols, inputImage.rows);
cornersA[3] = Point2f(0, inputImage.rows);
cornersB[0] = Point2f(inputImage.cols*0.25, 0);
cornersB[1] = Point2f(inputImage.cols * 0.90, 0);
cornersB[2] = Point2f(inputImage.cols, inputImage.rows);
cornersB[3] = Point2f(0, inputImage.rows * 0.80);
Mat homo = findHomography(cornersA, cornersB);
warpPerspective(inputImage,
outputImage,
homo,
inputImage.size(),
INTER_LANCZOS4,
BORDER_CONSTANT,
Scalar(50,50,50));
以下是生成的输出屏幕截图:

remap:此函数是非常强大的几何变换函数,可用于执行从源到输出图像的像素重映射。 这意味着您可以将像素从源图像重定位到目标图像中的其他位置。 您可以模拟以前的转换和许多其他转换的相同行为,只要您创建正确的映射并将其传递给此函数即可。 这是几个示例,它们演示remap函数的功能以及使用起来的难易程度:
Mat mapX, mapY;
mapX.create(inputImage.size(), CV_32FC(1));
mapY.create(inputImage.size(), CV_32FC(1));
for(int i=0; i<inputImage.rows; i++)
for(int j=0; j<inputImage.cols; j++)
{
mapX.at<float>(i,j) = j * 5;
mapY.at<float>(i,j) = i * 5;
}
remap(inputImage,
outputImage,
mapX,
mapY,
INTER_LANCZOS4,
BORDER_REPLICATE);
从前面的代码中可以看出,除了输入和输出图像以及内插和外推参数之外,我们还需要提供映射矩阵,一个用于X方向,另一个用于Y方向。 这是从前面的代码重新映射的结果。 它只是使图像缩小了五倍(请注意,图像尺寸在remap函数中保持不变,但内容基本上被压缩为原始尺寸的五倍)。 在下面的屏幕快照中显示了该内容:

您可以尝试通过简单地替换两个for循环中的代码,并用不同的值填充mapX和mapY矩阵来尝试多种不同的图像重映射。 以下是一些重新映射的示例:
考虑第一个示例:
// For a vertical flip of the image
mapX.at<float>(i,j) = j;
mapY.at<float>(i,j) = inputImage.rows-i;
考虑以下示例:
// For a horizontal flip of the image
mapX.at<float>(i,j) = inputImage.cols - j;
mapY.at<float>(i,j) = i;
通常最好将 OpenCV 图像坐标转换为标准坐标系(笛卡尔坐标系),并以标准坐标处理X和Y,然后再将其转换回 OpenCV 坐标系。 原因很简单,就是我们在学校或任何几何书籍或课程中学习的坐标系都使用笛卡尔坐标系。 另一个原因是它还提供负坐标,这在处理转换时具有更大的灵活性。 这是一个例子:
Mat mapX, mapY;
mapX.create(inputImage.size(), CV_32FC(1));
mapY.create(inputImage.size(), CV_32FC(1));
// Calculate the center point
Point2f center(inputImage.cols/2,
inputImage.rows/2);
for(int i=0; i<inputImage.rows; i++)
for(int j=0; j<inputImage.cols; j++)
{
// get i,j in standard coordinates, thus x,y
double x = j - center.x;
double y = i - center.y;
// Perform a mapping for X and Y
x = x*x/500;
y = y;
// convert back to image coordinates
mapX.at<float>(i,j) = x + center.x;
mapY.at<float>(i,j) = y + center.y;
}
remap(inputImage,
outputImage,
mapX,
mapY,
INTER_LANCZOS4,
BORDER_CONSTANT);
这是前面的代码示例中的映射操作的结果:

remap函数的另一个(也是非常重要的)用途是校正图像中的镜头失真。 您可以使用initUndistortRectifyMap和initWideAngleProjMap函数在X和Y方向上获取所需的映射以进行失真校正,然后将它们传递给remap函数。
您可以使用以下链接获取transform_plugin的源代码副本,该代码与Computer_Vision项目兼容,并包括您在本节中学到的转换函数。 您可以使用同一插件来测试并生成本节中看到的大多数图像。 尝试扩展插件以控制更多参数,或者尝试不同的映射操作并自己尝试不同的图像。 这是transform_plugin源代码的链接。
杂项转换
杂项转换处理其他不能视为几何转换的其他任务,例如颜色空间(和格式)转换,应用颜色图,傅里叶转换等。 让我们看看它们。
颜色和色彩空间
简而言之,色彩空间是用于表示图像中像素颜色值的模型。 严格来讲,计算机视觉中的颜色由一个或多个数值组成,每个数值对应于一个通道,以 OpenCV Mat类而言。 因此,色彩空间是定义这些数值(或多个数值)如何转换为色彩的模型。 让我们以一个示例案例来更好地理解这一点。 最受欢迎的颜色空间之一(有时也称为图像格式,尤其是在 Qt 框架中)是 RGB 颜色空间,其中颜色是由红色,绿色和蓝色的组合制成的。 RGB 色彩空间已被电视,监视器,LCD 和类似的显示屏广泛使用。 另一个示例是 CMYK(或 CMYB(青色,栗色,黄色,黑色))颜色空间,可以猜到它是四通道颜色空间,并且它主要用于彩色打印机。 还有许多其他色彩空间,每个色彩空间都有各自的优势和用例,但是我们将使用给定的示例,因为我们将主要关注于将不常见的色彩空间转换为更常见的色彩空间,尤其是灰度和 BGR(请注意 B 和 R 在 BGR 中交换,否则类似于 RGB)颜色空间,这是大多数处理彩色图像的 OpenCV 函数中的默认颜色空间。
正如我们刚刚提到的,在计算机视觉科学中,因此在 OpenCV 框架中,通常需要将色彩空间相互转换,因为在某些色彩空间中,通常更容易区分图像的某些属性。 同样,正如我们在前几章中已经了解的那样,我们可以使用 Qt Widget 轻松显示 BGR 图像,但是对于其他颜色空间则无法如此。
OpenCV 框架允许使用cvtColor函数在不同的色彩空间之间进行转换。 此函数仅将输入和输出图像与转换代码(在cv::ColorConversionCodes枚举中的条目)一起使用。 以下是几个示例:
// Convert BGR to HSV color space
cvtColor(inputImage, outputImage, CV_BGR2HSV);
// Convert Grayscale to RGBA color space
cvtColor(inputImage, outputImage, CV_GRAY2RGBA);
OpenCV 框架提供了一个称为applyColorMap的函数(类似于remap函数,但本质上有很大不同),该函数可用于将输入图像的颜色映射到输出图像中的其他颜色。 您只需要为它提供cv::ColormapTypes枚举的输入图像,输出图像和颜色映射类型。 这是一个简单的例子:
applyColorMap(inputImage, outputImage, COLORMAP_JET);
以下是上述代码的输出屏幕截图:

您可以使用以下链接获取color_plugin的源代码副本,该代码与Computer_Vision项目兼容,并包括在本节中学习的由适当的用户界面控制的颜色映射函数。 使用此处提供的源代码,尝试不同的颜色映射操作并自己尝试使用不同的图像。 这是color_plugin源代码的链接。
图像阈值
在计算机视觉科学中,阈值化是图像分割的一种方法,其本身就是在强度,颜色或任何其他图像属性方面区分相关像素组的过程。 OpenCV 框架通常提供许多功能来处理图像分割。 但是,在本节中,您将了解 OpenCV 框架(以及计算机视觉)中两种最基本的(尽管已广泛使用)图像分割方法:threshold和adaptiveThreshold。 因此,在不浪费更多单词的情况下,它们是:
threshold:此函数可用于向图像应用固定级别的阈值。 尽管可以对多通道图像使用此函数,但通常在单通道(或灰度)图像上使用它来创建二进制图像,该图像具有可接受的像素和超过阈值的像素。 让我们用一个示例场景来说明这一点,您可能会遇到很多情况。 假设我们需要检测图像的最暗部分,换句话说,检测图像中的黑色。 这是我们可以使用阈值函数来仅滤除图像中像素值几乎为黑色的像素的方法:
cvtColor(inputImage, grayScale, CV_BGR2GRAY);
threshold(grayScaleIn,
grayScaleOut,
45,
255,
THRESH_BINARY_INV);
cvtColor(grayScale, outputImage, CV_GRAY2BGR);
在前面的代码中,首先,我们将输入图像转换为灰度颜色空间,然后应用阈值函数,然后将结果转换回 BGR 颜色空间。 这是生成的输出图像:

在前面的示例代码中,我们使用THRESH_BINARY_INV作为阈值类型参数; 但是,如果我们使用THRESH_BINARY,我们将得到结果的倒排版本。 threshold函数只是为我们提供了所有大于阈值参数的像素,在前面的示例中为40。
下一个是adaptiveThreshold:
adaptiveThreshold:可用于将自适应阈值应用于灰度图像。 根据传递给它的自适应方法(cv::AdaptiveThresholdTypes),此函数可用于分别自动计算每个像素的阈值。 但是,您仍然需要传递最大阈值,块大小(可以为 3、5、7 等),以及将从计算出的块平均值中减去的常数,可以是零。 这是一个例子:
cvtColor(inputImage, grayScale, CV_BGR2GRAY);
adaptiveThreshold(grayScale,
grayScale,
255,
ADAPTIVE_THRESH_GAUSSIAN_C,
THRESH_BINARY_INV,
7,
0);
cvtColor(grayScale, outputImage, CV_GRAY2BGR);
与之前一样,以及我们在阈值函数中所做的操作,我们将首先将图像色彩空间从 BGR 转换为灰度,然后应用自适应阈值,最后将其转换回。 这是前面的示例代码的结果:

使用以下链接获取segmentation_plugin的源代码副本,该代码与Computer_Vision项目兼容,并包括在本节中学习的阈值函数,并由适当的用户界面控制。
离散傅立叶变换
傅立叶变换可用于从时间函数中获取基本频率。 另一方面,离散傅里叶变换或 DFT 是一种计算采样时间函数(因此是离散的)的基础频率的方法。 那是一个纯粹的数学定义,从这个意义上来说是一个很短的定义,因此,就计算机视觉和图像处理而言,您首先需要尝试将图像(灰度图像)视为点上离散点的分布。 三维空间,其中每个离散元素的X和Y是图像中的像素位置,Z是像素的强度值。 如果您能够做到这一点,那么您还可以想象存在一个可以在空间中产生这些点的函数。 考虑到这种情况,傅立叶变换是将函数转换为其基础频率的方法。 如果您仍然感到迷路,请不要担心。 如果您不熟悉该概念,则绝对应该考虑在线阅读有关傅立叶变换的数学知识,或者甚至可以咨询您的数学教授。
在数学中,傅立叶分析是一种基于输入数据的傅立叶变换来获取信息的方法。 同样,为了使这种含义更具有计算机视觉意义,可以使用图像的 DFT 来导出最初在原始图像本身中不可见的信息。 视计算机视觉应用的目标领域而定,差异很大,但是我们将看到一个示例案例,以更好地理解 DFT 的使用方式。 因此,首先,您可以在 OpenCV 中使用dft函数来获取图像的 DFT。 请注意,由于图像(灰度)是 2D 矩阵,因此dft实际上将执行 2D 离散傅立叶变换,从而产生具有复数值的频率函数。 这是在 OpenCV 中对灰度(单通道)图像执行 DFT 的方法:
- 我们需要首先获得最佳大小来计算图像的 DFT。 在大小为 2 的幂(2、4、8、16 等)的数组上执行 DFT 变换是一个更快,更有效的过程。 对大小为
2乘积的数组执行的 DFT 转换也非常有效。 因此,使用我们刚刚提到的原理的getOptimalDFTSize用于获得大于我们图像尺寸的最小尺寸,这对于执行 DFT 是最佳的。 这是完成的过程:
int optH = getOptimalDFTSize( grayImg.rows );
int optW = getOptimalDFTSize( grayImg.cols );
- 接下来,我们需要创建具有此最佳尺寸的图像,并使用零填充添加的宽度和高度中的像素。 因此,我们可以使用本章前面了解的
copyMakeBorder函数:
Mat padded;
copyMakeBorder(grayImg,
padded,
0,
optH - grayImg.rows,
0,
optW - grayImg.cols,
BORDER_CONSTANT,
Scalar::all(0));
- 现在,我们在
padded中拥有了最佳尺寸的图像。 我们现在需要做的是形成一个适合于馈入dft函数的两通道Mat类。 这可以使用合并函数来完成。 请注意,由于dft需要浮点Mat类,因此我们还需要将最佳尺寸的图像转换为带有浮点元素的Mat类,如下所示:
Mat channels[] = {Mat_<float>(padded),
Mat::zeros(padded.size(),
CV_32F)};
Mat complex;
merge(channels, 2, complex);
- 一切准备就绪即可执行离散傅立叶变换,因此我们将其简称为此处所示。 结果也存储在
complex中,这将是一个复杂值Mat类:
dft(complex, complex);
- 现在,我们需要将复杂的结果分为真实和复杂的部分。 为此,我们可以再次使用
channels数组,如下所示:
split(complex, channels);
- 现在,我们需要使用
magnitude函数将复杂结果转换为其大小; 经过更多的转换之后,这将是适合于显示目的的结果。 由于channels现在包含复杂结果的两个通道,因此我们可以在magnitude函数中使用它,如下所示:
Mat mag;
magnitude(channels[0], channels[1], mag);
magnitude函数的结果(如果尝试查看元素)将非常大,以至于无法使用灰度图像的可能比例进行可视化。 因此,我们将使用以下代码行将其转换为更小的对数刻度:
mag += Scalar::all(1);
log(mag, mag);
- 由于我们使用最佳大小计算了 DFT,因此如果行或列的数量为奇数,我们现在需要裁剪结果。 使用以下代码片段可以轻松完成此操作。 请注意,使用
-2的按位and操作用于删除正整数中的最后一位,并使其成为偶数,或者基本上是创建带有额外像素的padded图像时所做的操作的反面:
mag = mag(Rect(
0,
0,
mag.cols & -2,
mag.rows & -2));
- 由于结果是一个频谱,显示了由 DFT 获得的频率函数所产生的波,因此我们应将结果的原点移至其中心,该中心当前位于左上角。 我们可以使用以下代码为结果的四分之四创建四个 ROI,然后将结果左上角的四分之一与右下角的四分之一交换,也将结果右上角的四分之一与左下角的四分之一交换:
int cx = mag.cols/2;
int cy = mag.rows/2;
Mat q0(mag, Rect(0, 0, cx, cy)); // Top-Left
Mat q1(mag, Rect(cx, 0, cx, cy)); // Top-Right
Mat q2(mag, Rect(0, cy, cx, cy)); // Bottom-Left
Mat q3(mag, Rect(cx, cy, cx, cy)); // Bottom-Right
Mat tmp;
q0.copyTo(tmp);
q3.copyTo(q0);
tmp.copyTo(q3);
q1.copyTo(tmp);
q2.copyTo(q1);
tmp.copyTo(q2);
- 除非我们使用
normalize函数将结果缩放到正确的灰度范围(0至255),否则尝试将结果可视化仍然是不可能的,如下所示:
normalize(mag, mag, 0, 255, CV_MINMAX);
- 使用 OpenCV 中的
imshow函数,我们已经可以查看结果了,但是为了能够在 Qt 小部件中查看结果,我们需要将其转换为正确的深度(8 位)和多个通道,因此我们需要以下内容作为最后一步:
Mat_<uchar> mag8bit(mag);
cvtColor(mag8bit, outputImage, CV_GRAY2BGR);
现在,您可以尝试在我们的测试图像上运行它。 结果将如下所示:

您在结果中看到的结果应解释为从上方直接观看的波,其中每个像素的亮度实际上是其高度的表示。 尝试在不同种类的每个图像上运行相同的过程,以查看结果如何变化。 除了通过外观检查 DFT 结果(取决于用例)之外,DFT 的一个非常特殊的用例(我们将留给您自己尝试)是在掩盖 DFT 结果的一部分后执行反向 DFT, 以获取原始图像。 此过程可以通过多种方式更改原始图像,具体取决于已过滤 DFT 结果的一部分。 这个主题在很大程度上取决于原始图像的内容,并且与 DFT 的数学特性有着深厚的联系,但是绝对值得研究和试验。 总之,您可以通过调用相同的dft函数并将附加的DCT_INVERSE参数传递给它来执行逆 DFT。 显然,这次,输入应该是图像的计算出的 DFT,输出将是图像本身。
参考:OpenCV 文档,离散傅里叶变换。
OpenCV 中的绘图
通常,当主题是 OpenCV 和计算机视觉时,就不能忽略在图像上绘制文本和形状。 由于无数原因,您将需要在输出图像上绘制(输出)一些文本或形状。 例如,您可能想编写一个在其上打印图像日期的程序。 或者,您可能需要在执行面部检测后在图像中的面部周围绘制一个正方形。 即使 Qt 框架也提供了处理这些任务的强大功能,也可以使用 OpenCV 本身来绘制图像。 在本节中,您将学习到使用 OpenCV 绘图函数的方法,它们令人惊讶的非常容易使用,以及示例代码和输出结果。
可以理解,OpenCV 中的绘图函数接受输入和输出图像,以及一些大多数参数共有的参数。 以下是 OpenCV 中提到的图形函数的常用参数,以及它们的含义和可能的值:
color:此参数只是在图像上绘制的对象的颜色。 它可以使用标量创建,并且必须采用 BGR 格式(用于彩色图像),因为它是大多数 OpenCV 函数的默认颜色格式。thickness:此参数默认设置为1,是在图像上绘制的对象轮廓的粗细。 此参数以像素为单位指定。lineType:这可以是cv::LineTypes枚举中的条目之一,它决定在图像上绘制的对象轮廓的细节。 如下图所示,LINE_AA(抗锯齿)较平滑,但绘制速度也比LINE_4和LINE_8(默认为lineType)慢。 下图描述了cv::LineTypes之间的区别:

shift:仅在提供给绘图函数的点和位置包括小数位的情况下使用此参数。 在这种情况下,首先使用以下转换函数根据移位参数对每个点的值进行移位。 对于标准整数点值,移位值将为零,这也使以下转换对结果没有影响:
Point(X , Y) = Point( X * pow(2,-shift), Y * pow(2,-shift) )
现在,让我们从实际的绘图函数开始:
line:可以通过获取线条的起点和终点来在图像上画一条线条。 以下示例代码在图像上绘制了一个X标记(两条线连接该图像的角),其厚度为3像素,并带有红色:
cv::line(img,
Point(0,0),
Point(img.cols-1,img.rows-1),
Scalar(0,0,255),
3,
LINE_AA);
cv::line(img,
Point(img.cols-1,0),
Point(0, img.rows-1),
Scalar(0,0,255),
3,
LINE_AA);
arrowedLine:用于绘制箭头线。 箭头的方向由终点(或第二个点)决定,否则,此函数的用法与line相同。 这是一个示例代码,用于从顶部到图像中心绘制一条箭头线:
cv::arrowedLine(img,
Point(img.cols/2, 0),
Point(img.cols/2, img.rows/3),
Scalar(255,255,255),
5,
LINE_AA);
rectangle:可用于在图像上绘制矩形。 您可以向其传递一个矩形(Rect类)或两个点(Point类),第一个点对应于矩形的左上角,第二个点对应于矩形的右下角。 以下是在图像中心绘制的矩形示例:
cv::rectangle(img,
Point(img.cols/4, img.rows/4),
Point(img.cols/4*3, img.rows/4*3),
Scalar(255,0,0),
10,
LINE_AA);
putText:此函数可用于在图像上绘制(或书写或放置)文本。 除了 OpenCV 绘图函数中的常规绘图参数外,您还需要为该函数提供需要在图像上绘制的文本以及字体和比例尺参数。 字体可以是cv::HersheyFonts枚举中的条目之一,而尺度是与字体有关的字体缩放。 以下代码块的示例可用于在图像中写入Computer Vision:
cv::putText(img,
"Computer Vision",
Point(0, img.rows/2),
FONT_HERSHEY_PLAIN,
2,
Scalar(255,255,255),
2,
LINE_AA);
以下屏幕截图是在测试图像上按顺序执行时在本节中看到的所有绘图示例的结果:

除了我们在本节中看到的绘图函数外,OpenCV 还提供了绘制圆,折线,椭圆等的函数。 所有这些都以与本章所述完全相似的方式使用。 尝试使用这些函数来熟悉 OpenCV 中的所有绘图函数。 您始终可以通过参考 OpenCV 的文档获取最新的绘图函数列表,可以从 OpenCV 网站的首页轻松访问。
模板匹配
OpenCV 框架提供了许多不同的方法来进行对象检测,跟踪和计数。 模板匹配是 OpenCV 中对象检测的最基本方法之一,但是,如果正确使用它并与良好的阈值结合使用,它可以用于有效检测和计数图像中的对象。 通过在 OpenCV 中使用一个称为matchTemplate函数的函数来完成此操作。
matchTemplate函数将图像作为输入参数。 考虑将要搜索的图像作为我们感兴趣的对象(或者更好的是可能包含模板的场景)。它也将模板作为第二个参数。 该模板也是一幅图像,但是它是将在第一个图像参数中搜索的模板。 此函数所需的另一个参数(也是最重要的参数和决定模板匹配方法的一个参数)是method参数,它可以是cv::TemplateMatchModes枚举中的条目之一:
TM_SQDIFFTM_SQDIFF_NORMEDTM_CCORRTM_CCORR_NORMEDTM_CCOEFFTM_CCOEFF_NORMED
如果您有兴趣,可以访问matchTemplate文档页面,以了解上述每种方法的数学计算,但是,实际上,您可以通过了解matchTemplate的一般工作原理,来了解每种方法的执行方式。
matchTemplate函数使用method参数中指定的方法,将大小为WxH的模板滑动到大小为QxS的图像上,并将模板与图像的所有重叠部分进行比较,然后存储 result Mat中的比较。 显然,图像大小(QxS)必须大于模板大小(WxH)。 重要的是要注意,所得的Mat大小实际上是Q-WxS-H,即图像高度和宽度减去模板高度和宽度。 这是由于以下事实:模板的滑动仅发生在源图像上,甚至不发生在其外部的单个像素上。
如果使用名称中带有_NORMED的方法之一进行模板匹配,则在模板匹配函数之后无需进行标准化,因为结果将在0和1之间; 否则,我们将需要使用normalize函数对结果进行归一化。 将结果归一化后,可以使用minMaxLoc函数在结果图像中定位全局最小值(图像中的最暗点)和全局最大值(图像中的最亮点)。 请记住,result Mat类包含模板和图像重叠部分之间的比较结果。 这意味着,根据所使用的模板匹配方法,result Mat类中的全局最小值或全局最大值位置实际上是最佳模板匹配。 因此,是我们检测结果的最佳候选者。 假设我们想将左侧屏幕上的图像与以下屏幕截图右侧屏幕上的图像匹配:

因此,我们可以使用matchTemplate函数。 这是一个示例案例:
matchTemplate(img, templ, result, TM_CCORR_NORMED);
在前面的函数调用中,img装载有图像本身(右侧的图像),templ装载有模板图像(左侧),TM_CCORR_NORMED被用作模板匹配方法。 如果我们在前面的代码中可视化result Mat(为简单起见,使用imshow函数),我们将得到以下输出。 注意结果图像中的最亮点:

这是模板匹配的最佳位置。 我们可以使用minMaxLoc函数找到该位置,并通过使用您在本章先前了解的绘图函数在其周围绘制一个矩形(与模板大小相同)。 这是一个例子:
double minVal, maxVal;
Point minLoc, maxLoc;
minMaxLoc(result, &minVal, &maxVal, &minLoc, &maxLoc);
rectangle(img,
Rect(maxLoc.x, maxLoc.y, templ.cols, templ.rows),
Scalar(255,255,255),
2);
通过可视化img,我们将获得以下屏幕截图:

值得注意的是matchTemplate函数不是比例不变的。 这意味着它将无法匹配图像内各种尺寸的模板,而只能匹配给该函数的模板相同的尺寸。 matchTemplate函数的另一个用例是计算图像中的对象数量。 为此,您需要确保在循环内运行matchTemplate函数,并在每次成功匹配后从源图像中删除匹配的部分,以便在下一个matchTemplate调用中找不到该部分。 尝试自己编写代码,作为一个很好的示例案例,以了解有关模板匹配及其如何用于模板计数的更多信息。
模板计数是一种广泛使用的方法,用于对生产线或平坦表面中的对象(或产品)进行计数,或对显微图像中形状和大小相似的单元进行计数,以及无数其他类似的用例和应用。
总结
现在,我们熟悉 OpenCV 框架中一些使用最广泛的函数,枚举和类。 您在本章中学到的大多数技能几乎都以一种或另一种方式用于每种计算机视觉应用中。 从图像过滤(这是计算机视觉过程中最初始的步骤之一)开始,直到图像转换方法和色彩空间转换,每个计算机视觉应用都必须有权使用这些方法,才能执行特定任务,或以某种方式优化其性能。 在本章中,您学习了有关图像过滤和几何变换的所有知识。 您学习了如何使用remap之类的函数来执行无数的图像转换。 您还了解了色彩空间以及如何将它们相互转换。 后来,我们甚至使用颜色映射函数将图像中的颜色映射到另一组颜色。 然后,您学习了图像阈值处理以及如何提取具有特定像素值的图像部分。 正如您将在整个职业生涯或计算机视觉研究中看到的那样,由于无数种原因,阈值化是无时无刻都需要和使用的。 设定阈值后,您了解了 OpenCV 框架中的绘图函数。 如前所述,Qt 框架还提供了大量接口来处理绘图任务,但是仍然不可避免地,有时我们可能需要将 OpenCV 本身用于绘图任务。 最后,我们通过学习模板匹配及其用法来完成本章。
在第 7 章,“特征和描述符”中,我们将通过学习关键点和特征描述符以及它们如何用于对象来更深入地研究计算机视觉和 OpenCV 框架。 检测和匹配。 您还将了解许多关键概念,例如直方图。 您将了解什么是直方图以及通常如何提取和使用它们。 在第 7 章,“特征和描述符”中,我们还将作为刚刚完成的那一章的补充章,在该章中,我们将使用本章中学习到的大多数技能,以及与图像特征和关键点相关的新技能,以执行图像中更复杂的匹配,比较和检测任务。
七、特征和描述符
在第 6 章,“OpenCV 中的图像处理”中,我们主要从图像内容和像素方面了解了图像处理。 我们学习了如何对它们进行过滤,转换或以一种或另一种方式处理像素值。 甚至为了匹配模板,我们仅使用原始像素内容来获取结果,并找出图像中是否存在对象。 但是,我们仍未了解使我们能够区分不同种类的对象的算法,这些算法不仅基于原始像素,而且还基于图像的特定特征来区分图像的总体含义。 识别和识别不同类型的人脸,汽车,文字以及几乎任何可见和视觉的对象,这对他们来说几乎是一件微不足道的任务,因为它们并不十分相似。 对于我们人类来说,这种情况在大多数情况下都是在我们根本没有考虑的情况下发生的。 我们甚至可以根据大脑几乎自动拾取的微小且独特的碎片来区分非常相似的人脸,并在再次看到这些人脸时再次使用它们来识别人脸。 或者,以不同汽车品牌为例。 大多数主要汽车制造商的徽标几乎都被我们的大脑所窃取。 我们的大脑很容易地使用该徽标来区分汽车模型(或制造商)。 简而言之,一直以来,我们在观察周围环境及其周围的一切时,我们的大脑在我们的眼睛的帮助下,在任何视觉对象中搜索可区分的部分(显然,在这种情况下,对象可以是任何东西) ,然后使用这些片段来识别相同或相似的视觉对象。 当然,即使是人的大脑和眼睛,也总是有出错的机会,而且事实是,我们可能会简单地忘记特定物体(或面部)的外观。
我们在引言段落中刚刚描述的内容也是创建许多用于相同目的的计算机视觉算法的基础。 在本章中,您将学习 OpenCV 框架中一些最重要的类和方法,这使我们能够在图像(或图像中的对象)中找到称为特征(或关键点)的可区分片段。 然后,我们将继续学习描述符,顾名思义,这些描述符是对找到的特征的描述。 因此,我们将学习如何检测图像中的特征,然后从特征中提取描述符。 这些描述符然后可以在计算机视觉应用中用于许多目的,包括两个图像的比较,单应性变化检测,在图像内部定位已知对象等等。 重要的是要注意,保存,处理图像的特征和描述符并基本上执行任何操作通常比尝试使用图像本身更快,更容易,因为特征和描述符只是一堆数值,尝试以一种或另一种方式描述图像,具体取决于用于检测特征和提取描述符的算法。
您可以从到目前为止所看到的内容中轻松猜出,尤其是在前几章的过程中,OpenCV 和 Qt 框架都是大量的工具,类,函数等等的集合,它们使您能够创建强大的计算机视觉应用或任何其他类型的应用。 因此,可以肯定地说的是,在一本书中涵盖所有这些框架都是不可能的,也是徒劳的。 相反,由于这两个框架都是以高度结构化的方式创建的,因此,只要我们对底层类的层次结构有清晰的了解,我们仍然可以了解我们第一次看到和使用的这些框架中的类或函数。 对于用于检测特征和提取描述符的类和方法,这几乎是完全正确的。 这就是为什么在本章中,我们将首先研究 OpenCV 中用于特征检测和描述符提取的类的层次结构,然后再深入探讨如何在实践中使用它们。
在本章中,我们将介绍以下主题:
- OpenCV 中的算法是什么?
- 如何使用现有的 OpenCV 算法
- 使用
FeatureDetector类检测特征(或关键点) - 使用
DescriptorExtractor类提取描述符 - 如何匹配描述符并将其用于检测
- 如何得出描述符匹配的结果
- 如何为我们的用例选择算法
所有算法的基础 – Algorithm类
OpenCV 中的所有算法或更好的算法,至少不是不太简短的算法,都被创建为cv::Algorithm类的子类。 与通常所期望的相反,该类不是抽象类,这意味着您可以创建它的实例,而该实例只是不执行任何操作。 即使将来可能会更改它,也不会真正影响我们访问和使用它的方式。 在 OpenCV 中使用cv::Algorithm类的方式,以及如果要创建自己的算法的推荐方法,是首先创建cv::Algorithm的子类,其中包含用于特定目的或目标的所有必需成员函数。 。 然后,可以再次对该新创建的子类进行子类化,以创建同一算法的不同实现。 为了更好地理解这一点,让我们首先详细了解cv::Algorithm类。 大致了解一下 OpenCV 源代码的外观:
class Algorithm
{
public:
Algorithm();
virtual ~Algorithm();
virtual void clear();
virtual void write(FileStorage& fs) const;
virtual void read(const FileNode& fn);
virtual bool empty() const;
template<typename _Tp>
static Ptr<_Tp> read(const FileNode& fn);
template<typename _Tp>
static Ptr<_Tp> load(const String& filename,
const String& objname=String());
template<typename _Tp>
static Ptr<_Tp> loadFromString(const String& strModel,
const String& objname=String());
virtual void save(const String& filename) const;
virtual String getDefaultName() const;
protected:
void writeFormat(FileStorage& fs) const;
};
首先,让我们看看cv::Algorithm类中使用的FileStorage和FileNode类是什么(以及许多其他 OpenCV 类),然后介绍cv::Algorithm类中的方法:
FileStorage类可用于轻松地读写 XML,YAML 和 JSON 文件。 此类在 OpenCV 中广泛使用,以存储许多算法产生或需要的各种类型的信息。 此类几乎与任何其他文件读取器/写入器类一样工作,不同之处在于它可以与所提到的文件类型一起工作。FileNode类本身是Node类的子类,用于表示FileStorage类中的单个元素。FileNode类可以是FileNode元素集合中的单个叶子,也可以是其他FileNode元素的容器。
除了上一个列表中提到的两个类之外,OpenCV 还具有另一个名为FileNodeIterator的类,顾名思义,该类可用于遍历 STL 中的节点,例如循环。 让我们看一个小的示例,该示例描述在实践中如何使用上述类:
using namespace cv;
String str = "a random note";
double d = 999.001;
Matx33d mat = {1,2,3,4,5,6,7,8,9};
FileStorage fs;
fs.open("c:/dev/test.json",
FileStorage::WRITE | FileStorage::FORMAT_JSON);
fs.write("matvalue", mat);
fs.write("doublevalue", d);
fs.write("strvalue", str);
fs.release();
OpenCV 中的此类代码将导致创建 JSON 文件,如下所示:
{
"matvalue": {
"type_id": "opencv-matrix",
"rows": 3,
"cols": 3,
"dt": "d",
"data": [ 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0 ]
},
"doublevalue": 9.9900099999999998e+02,
"strvalue": "a random note"
}
如您所见,FileStorage类几乎可以确保 JSON 文件的结构正确无误,并且以一种以后可以轻松检索的方式存储一切。 通常最好使用isOpened函数检查打开文件是否成功,为简单起见,我们跳过了该函数。 整个过程称为将类或数据结构序列化。 现在,要读回它,我们可以执行以下操作:
using namespace cv;
FileStorage fs;
fs.open("c:/dev/test.json",
FileStorage::READ | FileStorage::FORMAT_JSON);
FileNode sn = fs["strvalue"];
FileNode dn = fs["doublevalue"];
FileNode mn = fs["matvalue"];
String str = sn;
Matx33d mat = mn;
double d = dn;
fs.release();
为了便于阅读,并演示FileStorage类实际上是在读取并创建FileNode类的实例,我们将每个值都分配给FileNode,然后再分配给变量本身,但是,很显然,您已经可以将读取节点的结果直接分配给适当类型的变量。 这两个类提供的功能远远超出此范围,它们绝对值得您亲自检查一下,但这对我们来说足够了,目的是解释cv::Algorithm类如何使用它们。 因此,现在我们了解到可以使用这些类轻松地存储和检索不同类型的类,甚至包括 OpenCV 特定的类型,我们可以更深入地研究cv::Algorithm本身。
如您先前所见,cv::Algorithm类在其声明以及其实现中都使用上述类来存储和检索算法的状态,即算法的基础参数,输入或输出值,等等。 为此,它提供了我们将简要介绍的方法。
现在,不必担心它们的详细用法,因为实际上它们是在子类中重新实现的,并且它们大多数的工作方式实际上取决于实现它的特定算法; 因此,我们仅关注 OpenCV 的结构及其组织方式。
这是cv::Algorithm类提供的方法:
read:这里有一些重载版本,可用于读取算法状态。write:它类似于read,用于保存算法状态。clear:可用于清除算法状态。empty:可用于确定算法的状态是否为空。 例如,这意味着它是否正确加载(读取)。load:与read几乎相同。loadFromString:它与load和read非常相似,只是它从字符串读取并加载算法的状态。
看一下 OpenCV 网站上的cv::Algorithm文档页面(尤其是它的继承图),您会立即注意到 OpenCV 中大量实现它的类。 您可以猜测它们都具有前面提到的功能。 除此之外,它们中的每一个都提供特定于它们中每一个的方法和功能。 在重新实现cv::Algorithm的许多类中,有一个名为Feature2D的类,该类基本上是本章将要学习的类,它负责 OpenCV 中存在的所有特征检测和描述符提取算法。 此类及其子类在 OpenCV 中被称为 2D 特征框架(将其视为 OpenCV 框架的子框架),这是本章下一节的主题。
2D 特征框架
正如本章前面提到的,OpenCV 提供了类来执行由世界各地的计算机视觉研究人员创建的各种特征检测和描述符提取算法。 与 OpenCV 中实现的任何其他复杂算法一样,特征检测器和描述符提取器也通过将cv::Algorithm类子类化而创建。 该子类称为Feature2D,它包含所有特征检测和描述符提取类共有的各种函数。 基本上,任何可用于检测特征和提取描述符的类都应该是Featured2D的子类。 为此,OpenCV 使用以下两种类类型:
FeatureDetectorDescriptorExtractor
重要的是要注意,实际上这两个类都是Feature2D的不同名称,因为它们是使用以下typedef语句在 OpenCV 中创建的(我们将在本节后面讨论其原因) ):
typedef Feature2D FeatureDetector;
typedef Feature2D DescriptorExtractor;
看到Feature2D类的声明也是一个好主意:
class Feature2D : public virtual Algorithm
{
public:
virtual ~Feature2D();
virtual void detect(InputArray image,
std::vector<KeyPoint>& keypoints,
InputArray mask=noArray() );
virtual void detect(InputArrayOfArrays images,
std::vector<std::vector<KeyPoint> >& keypoints,
InputArrayOfArrays masks=noArray() );
virtual void compute(InputArray image,
std::vector<KeyPoint>& keypoints,
OutputArray descriptors );
virtual void compute( InputArrayOfArrays images,
std::vector<std::vector<KeyPoint> >& keypoints,
OutputArrayOfArrays descriptors );
virtual void detectAndCompute(InputArray image,
InputArray mask,
std::vector<KeyPoint>& keypoints,
OutputArray descriptors,
bool useProvidedKeypoints=false );
virtual int descriptorSize() const;
virtual int descriptorType() const;
virtual int defaultNorm() const;
void write( const String& fileName ) const;
void read( const String& fileName );
virtual void write( FileStorage&) const;
virtual void read( const FileNode&);
virtual bool empty() const;
};
让我们快速回顾一下Feature2D类的声明中的内容。 首先,它是cv::Algorithm的子类,正如我们之前所学。 读,写和空函数只是cv::Algorithm中存在的简单重新实现的函数。 但是,以下函数是cv::Algorithm中的新增函数,不存在,它们基本上是特征检测器和描述符提取器所需的其他函数:
detect函数可用于从一个图像或一组图像中检测特征(或关键点)。compute函数可用于从关键点提取(或计算)描述符。detectAndCompute函数可用于执行单个特征的检测和计算。descriptorSize,descriptorType和defaultNorm是算法相关的值,它们在每个能够提取描述符的Feature2D子类中重新实现。
看起来似乎很奇怪,但是有充分的理由以这种方式对特征检测器和描述符进行分类,并且只有一个类,这是因为某些算法(并非全部)都提供了特征检测和描述符提取函数。 随着我们继续为此目的创建许多算法,这将变得更加清晰。 因此,让我们从 OpenCV 2D 特征框架中现有的Feature2D类和算法开始。
检测特征
OpenCV 提供了许多类来处理图像中的特征(关键点)检测。 每个类都有自己的实现,具体取决于它实现的特定算法,并且可能需要一组不同的参数才能正确执行或具有最佳表现。 但是,它们所有的共同点就是前面提到的detect函数(因为它们都是Feature2D的子类),可用于检测图像中的一组关键点。 OpenCV 中的关键点或特征是KeyPoint类实例,其中包含为正确的关键点需要存储的大多数信息(这些术语,即关键点和特征,可以互换使用,并且很多,因此,请尝试习惯它)。 以下是KeyPoint类的成员及其定义:
pt或简单地指向:这包含关键点(X和Y)在图像中的位置。angle:这是指关键点的顺时针旋转(0 到 360 度),即,检测到关键点的算法是否能够找到它; 否则,它将被设置为-1。response:这是关键点的强度,可用于排序或过滤弱关键点,依此类推。size:这是指指定可用于进一步处理的关键点邻域的直径。octave:这是图像的八度(或金字塔音阶),从中可以检测到该特定关键点。 这是一个非常强大且实用的概念,在检测关键点或使用它们进一步检测图像上可能具有不同大小的对象时,已广泛用于实现与比例尺(比例尺不变)的独立性。 为此,可以使用相同的算法处理同一图像的不同缩放版本(仅较小版本)。 每个刻度基本上称为octave或金字塔中的一个等级。
为了方便起见,KeyPoint类提供了其他成员和方法,以便您可以自己检查一下,但是为了进一步使用它们,我们肯定经历了我们需要熟悉的所有重要属性。 现在,让我们看一下现有的 OpenCV 特征检测器类的列表,以及它们的简要说明以及如何使用它们的示例:
- 可以使用
AgastFeatureDetector(包括 AGAST(自适应和通用加速分段测试)算法的实现)来检测图像中的角。 它需要三个参数(可以省略所有参数以使用默认值)来配置其行为。 这是一个例子:
Ptr<AgastFeatureDetector> agast = AgastFeatureDetector::create();
vector<KeyPoint> keypoints;
agast->detect(inputImage, keypoints);
如此简单,我们仅将AgastFeatureDetector与默认参数集一起使用。 在深入研究上述操作的结果之前,让我们首先看一下代码本身,因为其中使用了 OpenCV 中最重要和最实用的类之一(称为Ptr)。 如前面的代码所示,我们使用了Ptr类,它是 OpenCV 共享指针(也称为智能指针)的实现。 使用智能指针的优点之一是,您不必担心在使用完该类后释放为该类分配的内存。 另一个优点以及被称为共享指针的原因是,多个Ptr类可以使用(共享)单个指针,并且该指针(分配的内存)仅保留到Ptr指向的最后一个实例被摧毁为止。 在复杂的代码中,这可能意味着极大的简化。
接下来,请务必注意,您需要使用静态create函数来创建AgastFeatureDetector类的共享指针实例。 您将无法创建此类的实例,因为它是抽象类。 其余代码并不是新内容。 我们只需创建KeyPoint的std::vector,然后使用 AGAST 的基础算法检测输入Mat图像中的关键点。
编写相同代码的另一种方法(也许是更灵活的方法)是使用多态和Feature2D类。 因为AgastFeatureDetector实际上是Feature2D的子类,所以我们可以编写相同的代码,如下所示:
Ptr<Feature2D> fd = AgastFeatureDetector::create();
vector<KeyPoint> keypoints;
fd->detect(inputImage, keypoints);
当然,只有在我们希望在不同的特征检测算法之间切换而不创建和传递许多类的许多实例的情况下,这才证明是有用的。 这是一个示例,其中根据alg的值(可以是我们定义的枚举的条目,并且包括可能的算法的名称),可以使用 AGAST 或 AKAZE 算法来检测关键点 (我们将在本章后面看到):
Ptr<Feature2D> fd;
switch(alg)
{
case AGAST_ALG:
fd = AgastFeatureDetector::create();
break;
case AKAZE_ALG:
fd = AKAZE::create();
break;
}
vector<KeyPoint> keypoints;
fd->detect(inputImage, keypoints);
在讨论 AGAST 算法的参数之前,还有一个提示,即可以通过迭代检测到的关键点和绘制点(实际上是圆圈,但是它们与点一样小)来绘制检测到的关键点,如此处所示 :
inputImage.copyTo(outputImage);
foreach(KeyPoint kp, keypoints)
circle(outputImage, kp.pt, 1, Scalar(0,0,255), 2);
或者,甚至更好的是,使用 OpenCV 2D 特征框架中专用于此目的的drawKeypoints函数。 它的优点是您无需将图像复制到输出图像,并且还可以确保对关键点进行着色以使其更加可区分。 这是一个例子; 实际上,这是使用 OpenCV 中的 AGAST 算法检测和绘制关键点的完整代码:
Ptr<AgastFeatureDetector> agast = AgastFeatureDetector::create();
vector<KeyPoint> keypoints;
agast->detect(inputImage, keypoints);
drawKeypoints(inputImage, keypoints, outputImage);
我们将在示例中使用简单且非多态的方法,但是,如本章前面所述,使用多态并在适用于不同情况的不同算法之间进行切换始终更为实用。
假设左侧的图像是我们的原始测试图像,执行前面的代码将在右侧产生结果图像,如下所示:

这是结果图像的局部放大图:

如您所见,所有检测到的关键点都绘制在结果图像上。 同样,在运行任何特征检测功能之前运行某种模糊过滤器(如果图像太清晰)总是更好的选择。 这有助于减少不必要的(和不正确的)关键点。 其原因是,在清晰的图像中,即使是图像的最细微的点点也可以检测为边缘或拐角点。
在前面的示例中,我们仅使用了默认参数(省略了默认参数),但是为了更好地控制 AGAST 算法的行为,我们需要注意以下参数:
- 默认情况下设置为
10的threshold值用于基于像素与围绕它的圆上的像素之间的强度差传递特征。 阈值越高意味着检测到的特征数量越少,反之亦然。 NonmaxSuppression可用于对检测到的关键点应用非最大抑制。 默认情况下,此参数设置为true,可用于进一步过滤掉不需要的关键点。- 可以将
type参数设置为以下值之一,并确定 AGAST 算法的类型:AGAST_5_8AGAST_7_12dAGAST_7_12sOAST_9_16(默认值)
您可以使用适当的 Qt 小部件从用户界面获取参数值。 这是 AGAST 算法的示例用户界面以及其底层代码。 另外,您可以下载本节末尾提供的完整keypoint_plugin源代码,其中包含该源代码以及以下特征检测示例,它们全部集成在一个插件中,与我们全面的computer_vision项目兼容:

请注意,当我们更改阈值并选择其他类型的 AGAST 算法时,检测到的关键点数量会发生变化。 在以下示例代码中,agastThreshSpin是旋转框小部件的objectName,agastNonmaxCheck是复选框的objectName,并且agastTypeCombo是用于选择类型的组合框的objectName:
Ptr<AgastFeatureDetector> agast =
AgastFeatureDetector::create();
vector<KeyPoint> keypoints;
agast->setThreshold(ui->agastThreshSpin->value());
agast->setNonmaxSuppression(ui->agastNonmaxCheck->isChecked());
agast->setType(ui->agastTypeCombo->currentIndex());
agast->detect(inputImage,
keypoints);
drawKeypoints(inputImage,
keypoints,
outputImage);
OpenCV 提供了一种便捷函数,可用于在不使用AgastFeatureDetector类的情况下直接在灰度图像上调用 AGAST 算法。 此函数称为AGAST(如果考虑名称空间,则称为cv::AGAST),并且通过使用它,我们可以编写相同的代码,如下所示:
vector<KeyPoint> keypoints;
AGAST(inputImage,
keypoints,
ui->agastThreshSpin->value(),
ui->agastNonmaxCheck->isChecked(),
ui->agastTypeCombo->currentIndex());
drawKeypoints(inputImage,
keypoints,
outputImage);
在本节中看到的算法以及在 OpenCV 中实现的几乎所有其他算法,通常都是基于研究研究和来自世界各地的已发表论文。 值得一看的是每种算法的相关论文,以清楚地了解其基本实现方式以及参数的确切效果以及如何有效使用它们。 因此,在每个示例的末尾,并且在研究了每种算法之后,如果您有兴趣,还将与您共享其参考文献(如果有)以供进一步研究。 第一个用于 AGAST 算法,出现在此信息框之后。
参考:Elmar Mair, Gregory D. Hager, Darius Burschka, Michael Suppa, and Gerhard Hirzinger. Adaptive and generic corner detection based on the accelerated segment test. In European Conference on Computer Vision (ECCV'10), September 2010.
让我们继续我们的特征检测算法列表。
KAZE 和 AKAZE
KAZE和AKAZE(加速 KAZE)类可用于使用 KAZE 算法(其加速版本)检测特征。 有关 KAZE 和 AKAZE 算法的详细信息,请参考以下参考文献列表中提到的文件。 类似于我们在 AGAST 中看到的那样,我们可以使用默认参数集并简单地调用detect函数,或者我们可以使用适当的 Qt 小部件获取所需的参数并进一步控制算法的行为。 这是一个例子:

AKAZE 和 KAZE 中的主要参数如下:
nOctaves或八度的数量(默认为 4)可用于定义图像的最大八度。nOctaveLayers或八度级别的数量(默认为 4)是每个八度(或每个比例级别)的子级别数。- 扩散率可以采用下列项之一,它是 KAZE 和 AKAZE 算法使用的非线性扩散方法(如稍后在此算法的参考文献中所述):
DIFF_PM_G1DIFF_PM_G2DIFF_WEICKERTDIFF_CHARBONNIER
- 阈值是接受关键点的响应值(默认为 0.001000)。 阈值越低,检测到的(和接受的)关键点数量越多,反之亦然。
- 描述符类型参数可以是以下值之一。 请注意,此参数仅存在于 AKAZE 类中:
DESCRIPTOR_KAZE_UPRIGHTDESCRIPTOR_KAZEDESCRIPTOR_MLDB_UPRIGHT
descriptor_size用于定义描述符的大小。 零值(也是默认值)表示完整尺寸的描述符。descriptor_channels可用于设置描述符中的通道数。 默认情况下,此值设置为 3。
现在,不要理会与描述符相关的参数,例如描述符类型和大小以及通道数,我们将在后面看到。 这些相同的类也用于从特征中提取描述符,并且这些参数将在其中起作用,而不必检测关键点,尤其是detect函数。
这是前面示例用户界面的源代码,其中,根据我们前面示例用户界面中Accelerated复选框的状态,选择了 KAZE(未选中)或 AKAZE(加速):
vector<KeyPoint> keypoints;
if(ui->kazeAcceleratedCheck->isChecked())
{
Ptr<AKAZE> akaze = AKAZE::create();
akaze->setDescriptorChannels(3);
akaze->setDescriptorSize(0);
akaze->setDescriptorType(
ui->akazeDescriptCombo->currentIndex() + 2);
akaze->setDiffusivity(ui->kazeDiffCombo->currentIndex());
akaze->setNOctaves(ui->kazeOctaveSpin->value());
akaze->setNOctaveLayers(ui->kazeLayerSpin->value());
akaze->setThreshold(ui->kazeThreshSpin->value());
akaze->detect(inputImage, keypoints);
}
else
{
Ptr<KAZE> kaze = KAZE::create();
kaze->setUpright(ui->kazeUprightCheck->isChecked());
kaze->setExtended(ui->kazeExtendCheck->isChecked());
kaze->setDiffusivity(ui->kazeDiffCombo->currentIndex());
kaze->setNOctaves(ui->kazeOctaveSpin->value());
kaze->setNOctaveLayers(ui->kazeLayerSpin->value());
kaze->setThreshold(ui->kazeThreshSpin->value());
kaze->detect(inputImage, keypoints);
}
drawKeypoints(inputImage, keypoints, outputImage);
参考文献:
KAZE Features. Pablo F. Alcantarilla, Adrien Bartoli and Andrew J. Davison. In European Conference on Computer Vision (ECCV), Fiorenze, Italy, October 2012.
Fast Explicit Diffusion for Accelerated Features in Nonlinear Scale Spaces. Pablo F. Alcantarilla, Jesús Nuevo and Adrien Bartoli. In British Machine Vision Conference (BMVC), Bristol, UK, September 2013.
BRISK类
BRISK类可用于使用 BRISK(二进制鲁棒不变可缩放关键点)算法检测图像中的特征。 请确保参考以下文章,以获取有关其工作方式以及 OpenCV 中基础实现的详细信息。 不过,用法与我们在 AGAST 和 KAZE 中看到的用法非常相似,其中使用create函数创建了类,然后设置了参数(如果我们不使用默认值),最后是detect函数被调用。 这是一个简单的例子:

以下是此类用户界面的源代码。 小部件名称很容易猜到,每个小部件名称对应 BRISK 算法所需的三个参数之一,它们是thresh(类似于AGAST类中的阈值,因为 BRISK 在内部使用了类似的方法),octaves(类似于 KAZE 和 AKAZE 类中的八度数)和patternScale(这是 BRISK 算法使用的可选模式缩放参数),默认情况下设置为 1:
vector<KeyPoint> keypoints;
Ptr<BRISK> brisk =
BRISK::create(ui->briskThreshSpin->value(),
ui->briskOctaveSpin->value(),
ui->briskScaleSpin->value());
drawKeypoints(inputImage, keypoints, outputImage);
参考文献:Stefan Leutenegger, Margarita Chli, and Roland Yves Siegwart. Brisk: Binary robust invariant scalable keypoints. In Computer Vision (ICCV), 2011 IEEE International Conference on, pages 2548-2555. IEEE, 2011.
FAST
FastFeatureDetector类可用于使用FAST方法检测图像中的特征(“加速段测试中的特征”)。 FAST 和 AGAST 算法共享很多,因为它们都是使用加速段测试的方法,即使在 OpenCV 实现以及此类的使用方式中,这也是显而易见的。 确保参考该算法的文章以了解更多有关它的详细信息。 但是,我们将在另一个示例中重点介绍如何使用它:

并且,这是使用 FAST 算法从图像中检测关键点的此类用户界面的源代码。 所有三个参数的含义与 AGAST 算法的含义相同,不同之处在于类型可以是以下类型之一:
TYPE_5_8TYPE_7_12TYPE_9_16
参考文献:Edward Rosten and Tom Drummond. Machine learning for high-speed corner detection. In Computer Vision-ECCV 2006, pages 430-443. Springer, 2006.
GFTT
GFTT(需要跟踪的良好特征)仅是特征检测器。 GFTTDetector可用于使用 Harris(以创建者命名)和 GFTT 角检测算法检测特征。 因此,是的,该类别实际上是将两种特征检测方法组合在一起的一个类别,原因是 GFTT 实际上是哈里斯算法的一种修改版本,使用的哪一种将由输入参数决定。 因此,让我们看看如何在示例案例中使用它,然后简要介绍一下参数:

这是此用户界面的相关源代码:
vector<KeyPoint> keypoints;
Ptr<GFTTDetector> gftt = GFTTDetector::create();
gftt->setHarrisDetector(ui->harrisCheck->isChecked());
gftt->setK(ui->harrisKSpin->value());
gftt->setBlockSize(ui->gfttBlockSpin->value());
gftt->setMaxFeatures(ui->gfttMaxSpin->value());
gftt->setMinDistance(ui->gfttDistSpin->value());
gftt->setQualityLevel(ui->gfttQualitySpin->value());
gftt->detect(inputImage, keypoints);
drawKeypoints(inputImage, keypoints, outputImage);
以下是GFTTDetector类的参数及其定义:
- 如果将
useHarrisDetector设置为true,则将使用 Harris 算法,否则将使用 GFTT。 默认情况下,此参数设置为false。 blockSize可用于设置块大小,该块大小将用于计算像素附近的导数协方差矩阵。 默认为 3。K是 Harris 算法使用的常数参数值。- 可以设置
maxFeatures或maxCorners来限制检测到的关键点数量。 默认情况下,它设置为 1000,但是如果关键点的数量超过该数量,则仅返回最强的响应。 minDistance是关键点之间的最小可接受值。 默认情况下,此值设置为 1,它不是像素距离,而是欧几里得距离。qualityLevel是阈值级别的值,用于过滤质量指标低于特定级别的关键点。 请注意,实际阈值是通过将该值与图像中检测到的最佳关键点质量相乘得出的。
参考文献:
Jianbo Shi and Carlo Tomasi. Good features to track. In Computer Vision and Pattern Recognition, 1994. Proceedings CVPR'94., 1994 IEEE Computer Society Conference on, pages 593-600. IEEE, 1994.
C. Harris and M. Stephens (1988). A combined corner and edge detector. Proceedings of the 4th Alvey Vision Conference. pp. 147-151.
ORB
最后,ORB 算法,这是我们将在本节中介绍的最后一个特征检测算法。
ORB类可用于使用 ORB(二进制鲁棒独立基本特征)算法检测图像中的关键点。 此类封装了我们已经看到的一些方法(例如 FAST 或 Harris)来检测关键点。 因此,在类构造器中设置或使用设置器函数设置的某些参数与描述符提取有关,我们将在后面学习; 但是,ORB 类可用于检测关键点,如以下示例所示:

这是此类用户界面所需的源代码。 同样,小部件的objectName属性几乎是不言自明的,如上图所示,但让我们先看一下代码,然后详细查看参数:
vector<KeyPoint> keypoints;
Ptr<ORB> orb = ORB::create();
orb->setMaxFeatures(ui->orbFeaturesSpin->value());
orb->setScaleFactor(ui->orbScaleSpin->value());
orb->setNLevels(ui->orbLevelsSpin->value());
orb->setPatchSize(ui->orbPatchSpin->value());
orb->setEdgeThreshold(ui->orbPatchSpin->value()); // = patch size
orb->setWTA_K(ui->orbWtaSpin->value());
orb->setScoreType(ui->orbFastCheck->isChecked() ?
ORB::HARRIS_SCORE
:
ORB::FAST_SCORE);
orb->setPatchSize(ui->orbPatchSpin->value());
orb->setFastThreshold(ui->orbFastSpin->value());
orb->detect(inputImage, keypoints);
drawKeypoints(inputImage, keypoints, outputImage);
该序列与到目前为止我们看到的其他算法完全相同。 让我们看看设置了哪些参数:
MaxFeatures参数只是应该检索的最大关键点数。 请注意,检测到的关键点数量可能比此数量少很多,但永远不会更高。ScaleFactor或金字塔抽取比率,与我们先前算法中看到的八度参数有些相似,用于确定金字塔每个级别的尺度值,这些尺度将用于检测关键点并从不同尺度提取同一张图片的描述符。 这就是在 ORB 中实现尺度不变性的方式。NLevels是金字塔的等级数。PatchSize是 ORB 算法使用的补丁的大小。 有关此内容的详细信息,请确保参考以下参考文献,但是对于简短说明,补丁大小决定了要提取描述的关键点周围的区域。 请注意,PatchSize和EdgeThreshold参数需要大约相同的值,在前面的示例中也将其设置为相同的值。EdgeThreshold是在关键点检测期间将忽略的以像素为单位的边框。WTA_K或 ORB 算法内部使用的 WTA 哈希的 K 值是一个参数,用于确定将用于在 ORB 描述符中创建每个元素的点数。 我们将在本章稍后看到更多有关此的内容。- 可以设置为以下值之一的
ScoreType决定 ORB 算法使用的关键点检测方法:ORB::HARRIS_SCORE用于哈里斯角点检测算法ORB::FAST_SCORE用于 FAST 关键点检测算法
FastThreshold只是 ORB 在关键点检测算法中使用的阈值。
参考文献:
Ethan Rublee, Vincent Rabaud, Kurt Konolige, and Gary Bradski. Orb: an efficient alternative to sift or surf. In Computer Vision (ICCV), 2011 IEEE International Conference on, pages 2564-2571. IEEE, 2011.
Michael Calonder, Vincent Lepetit, Christoph Strecha, and Pascal Fua, BRIEF: Binary Robust Independent Elementary Features, 11th European Conference on Computer Vision (ECCV), Heraklion, Crete. LNCS Springer, September 2010.
而已。 现在,我们熟悉如何使用 OpenCV 3 中可用的各种算法检测关键点。当然,除非我们从这些关键点中提取描述符,否则这些关键点(或特征)几乎没有用; 因此,在下一节中,我们将学习从关键点提取描述符的方法,这将因此使我们获得 OpenCV 中的描述符匹配功能,在这里我们可以使用在本节中了解到的类来识别,检测,跟踪对象和对图像进行分类。 请注意,对于我们了解的每种算法,最好阅读本文以了解其所有细节,尤其是如果您打算构建自己的自定义关键点检测器,而只是按原样使用时,就像之前提到的,对它们的目的有一个清晰的认识就足够了。
提取和匹配描述符
计算机视觉中的描述符是一种描述关键点的方式,该关键点完全依赖于用于提取关键点的特定算法,并且与关键点(在KeyPoint类中定义的)不同,描述符没有共同的结构 ,除了每个描述符都代表一个关键点这一事实外。 OpenCV 中的描述符存储在Mat类中,其中生成的描述符Mat类中的每一行都引用关键点的描述符。 正如我们在上一节中了解到的,我们可以使用任何FeatureDetector子类的detect函数从图像上基本上检测出一组关键点。 同样,我们可以使用任何DescriptorExtractor子类的compute函数从关键点提取描述符。
由于特征检测器和描述符提取器在 OpenCV 中的组织方式(这都是Feature2D子类,正如我们在本章前面了解的那样),令人惊讶的是,将它们结合使用非常容易。 在本节中看到,我们将使用完全相同的类(或更确切地说,也提供描述符提取方法的类)从我们在上一节中使用各种类发现的关键点中提取特征描述符,以在场景图像中找到对象。 重要的是要注意,并非所有提取的关键点都与所有描述符兼容,并且并非所有算法(在这种情况下为Feature2D子类)都提供detect函数和compute函数。 不过,这样做的人还提供了detectAndCompute函数,可以一次性完成关键点检测和特征提取,并且比分别调用这两个函数要快。 让我们从第一个示例案例开始,以便使所有这些变得更加清晰。 这也是匹配两个单独图像的特征所需的所有步骤的示例,这些图像可用于检测,比较等:
- 首先,我们将使用 AKAZE 算法(使用上一节中学习的
AKAZE类)从以下图像中检测关键点:

我们可以使用以下代码从两个图像中提取关键点:
using namespace cv;
using namespace std;
Mat image1 = imread("image1.jpg");
Mat image2 = imread("image2.jpg");
Ptr<AKAZE> akaze = AKAZE::create();
// set AKAZE params ...
vector<KeyPoint> keypoints1, keypoints2;
akaze->detect(image1, keypoints1);
akaze->detect(image2, keypoints2);
- 现在我们有了两个图像的特征(或关键点),我们可以使用相同的
AKAZE类实例从这些关键点提取描述符。 这是完成的过程:
Mat descriptor1, descriptor2;
akaze->compute(image1, keypoints1, descriptor1);
akaze->compute(image2, keypoints2, descriptor2);
- 现在,我们需要匹配两个图像,这是两个图像中关键点的描述符。 为了能够执行描述符匹配操作,我们需要在 OpenCV 中使用一个名为
DescriptorMatcher(非常方便)的类。 需要特别注意的是,此匹配器类需要设置为正确的类型,否则,将不会得到任何结果,或者甚至可能在运行时在应用中遇到错误。 如果在本示例中使用 AKAZE 算法来检测关键点并提取描述符,则可以在DescriptorMatcher中使用FLANNBASED类型。 这是完成的过程:
descMather = DescriptorMatcher::create(
DescriptorMatcher::FLANNBASED);
请注意,您可以将以下值之一传递给DescriptorMatcher的创建函数,并且这完全取决于您用来提取描述符的算法,显然,因为将对描述符执行匹配。 您始终可以参考每种算法的文档,以了解可用于任何特定描述符类型的算法,例如AKAZE和KAZE之类的算法具有浮点类型描述符,因此可以将FLANNBASED与他们一起使用; 但是,具有String类型的描述符(例如ORB)将需要与描述符的汉明距离匹配的匹配方法。 以下是可用于匹配的现有方法:
FLANNBASEDBRUTEFORCEBRUTEFORCE_L1BRUTEFORCE_HAMMINGBRUTEFORCE_HAMMINGLUTBRUTEFORCE_SL2
当然,最坏的情况是,当您不确定不确定时,尝试为每种特定的描述符类型找到正确的匹配算法时,只需简单地尝试每个。
- 现在,我们需要调用
DescriptorMatcher的match函数,以尝试将第一张图像(或需要检测的对象)中找到的关键点与第二张图像(或可能包含我们的对象的场景)中的关键点进行匹配。match函数将需要一个DMatch向量,并将所有匹配结果填充其中。 这是完成的过程:
vector<DMatch> matches;
descMather->match(descriptor1, descriptor2, matches);
DMatch类是简单类,仅用作保存匹配结果数据的结构:
- 在深入研究如何解释匹配操作的结果之前,我们将学习如何使用
drawMatches函数。 与drawKeypoints函数相似,drawMatches可用于自动创建适合显示的输出结果。 这是如何做:
drawMatches(image1,
keypoints1,
image2,
keypoints2,
matches,
dispImg);
在前面的代码中,dispImg显然是可以显示的Mat类。 这是结果图像:

如您所见,drawMatches函数将获取第一张和第二张图像及其关键点以及匹配结果,并会处理绘制适当结果所需的一切。 在此示例中,我们仅提供了必需的参数,这会导致颜色随机化并绘制所有关键点和匹配的关键点(使用将它们连接在一起的线)。 当然,还有一些其他参数可用于进一步修改其工作方式。 (可选)您可以设置关键点和线条的颜色,还可以决定忽略不匹配的关键点。 这是另一个例子:
drawMatches(image1,
keypoints1,
image2,
keypoints2,
matches,
dispImg,
Scalar(0, 255, 0), // green for matched
Scalar::all(-1), // unmatched color (default)
vector<char>(), // empty mask
DrawMatchesFlags::NOT_DRAW_SINGLE_POINTS);
这将产生以下结果:

现在,颜色更适合我们在这里使用的颜色。 还应注意一些很不正常的不正确匹配,可以通过修改 KAZE 算法的参数甚至使用其他算法来解决。 现在让我们看看如何解释匹配结果。
- 解释匹配结果完全取决于用例。 例如,如果我们要匹配两个具有相同大小和相同内容类型的图像(例如人脸,相同类型的对象,指纹等),则我们可能需要考虑距离值高于某个阈值的,匹配的关键点数量。 或者,就像在当前示例中一样,我们可能希望使用匹配来检测场景中的对象。 这样做的一种常见方法是尝试找出匹配关键点之间的单应性变化。 为此,我们需要执行以下三个操作:
- 首先,我们需要过滤出匹配结果,以消除较弱的匹配,换句话说,仅保留良好的匹配; 同样,这完全取决于您的场景和对象,但是通常,通过几次尝试和错误,您可以找到最佳阈值
- 接下来,我们需要使用
findHomography函数来获得好关键点之间的单应性变化 - 最后,我们需要使用
perspectiveTransform将对象边界框(矩形)转换为场景
您已了解findHomography和perspectiveTransform以及如何在第 6 章,“OpenCV 中的图像处理”中使用它们。
这是我们可以过滤掉不需要的匹配结果以获得良好匹配的方法。 请注意,匹配阈值的0.1值是通过反复试验得出的。 通常在匹配集中找到最小和最大距离,然后仅接受距离小于与最小距离相关的某个值的匹配,尽管这不是我们在此处所做的:
vector<DMatch> goodMatches;
double matchThresh = 0.1;
for(int i=0; i<descriptor1.rows; i++)
{
if(matches[i].distance < matchThresh)
goodMatches.push_back(matches[i]);
}
在需要微调阈值的情况下,可以使用 Qt 框架和用户界面的功能。 例如,您可以使用 Qt 滑块小部件快速轻松地微调并找到所需的阈值。 只要确保将matchThresh替换为滑块小部件的值即可。
现在,我们可以使用良好的匹配找到单应性变化。 为此,我们首先需要根据匹配项过滤关键点,然后将这些过滤后的关键点(仅这些点)馈送到findHomography函数,以获得所需的变换矩阵或单应性更改。 这里是:
vector<Point2f> goodP1, goodP2;
for(int i=0; i<goodMatches.size(); i++)
{
goodP1.push_back(keypoints1[goodMatches[i].queryIdx].pt);
goodP2.push_back(keypoints2[goodMatches[i].trainIdx].pt);
}
Mat homoChange = findHomography(goodP1, goodP2);
最后,我们可以使用刚刚发现的单应性变化矩阵将透视变换应用于匹配点。 为此,首先,我们需要构造与第一个图像的四个角相对应的四个点,然后应用变换,最后,简单地绘制连接四个结果点的四条线。 方法如下:
vector<Point2f> corners1(4), corners2(4);
corners1[0] = Point2f(0,0);
corners1[1] = Point2f(image1.cols-1, 0);
corners1[2] = Point2f(image1.cols-1, image1.rows-1);
corners1[3] = Point2f(0, image1.rows-1);
perspectiveTransform(corners1, corners2, homoChange);
image2.copyTo(dispImage);
line(dispImage, corners2[0], corners2[1], Scalar::all(255), 2);
line(dispImage, corners2[1], corners2[2], Scalar::all(255), 2);
line(dispImage, corners2[2], corners2[3], Scalar::all(255), 2);
line(dispImage, corners2[3], corners2[0], Scalar::all(255), 2);
这是此操作的结果:

这实际上不是对这种方法功能强大程度的测试,因为对象基本上是从同一图像上剪切下来的。 这是运行相同过程的结果,但是这次第二张图像发生了旋转和透视图变化,甚至出现了一些噪点(这是使用智能手机从屏幕上拍摄的图像)。 即使第一张图片的一小部分在视图之外,结果也几乎是正确的:

出于参考目的,使用具有DESCRIPTOR_KAZE描述符类型,0.0001阈值,4八度,4八度和DIFF_PM_G1扩散率参数的 AKAZE 算法进行匹配和检测。 自己尝试具有不同照明条件和图像的不同参数。
我们还可以将drawMatches结果与检测结果结合起来,这意味着,我们可以在匹配结果图像上方绘制检测边界框,这可能会更有帮助,尤其是在微调参数或出于其他任何信息目的时 。 为此,您需要确保首先调用drawMatches函数以创建输出图像(在我们的示例中为dispImg变量),然后添加所有具有偏移值的点,因为drawMatches也将第一个图片在左侧输出。 此偏移量仅有助于将我们生成的边界框移到右侧,或者换句话说,将第一张图像的宽度添加到每个点的X成员。 这是完成的过程:
Point2f offset(image1.cols, 0);
line(dispImage, corners2[0] + offset,
corners2[1] + offset, Scalar::all(255), 2);
line(dispImage, corners2[1] + offset,
corners2[2] + offset, Scalar::all(255), 2);
line(dispImage, corners2[2] + offset,
corners2[3] + offset, Scalar::all(255), 2);
line(dispImage, corners2[3] + offset,
corners2[0] + offset, Scalar::all(255), 2);
这是结果图像:

在前面的示例中,如您在结果中所看到的,图像以多种方式失真(例如比例,方向等),但是该算法仍然可以在一组正确的输入参数下表现良好。 从理论上讲,理想情况下,我们一直在寻找一种即用型算法,并且希望它在所有可能的情况下都能表现出色; 但是,不幸的是,在实践中,这种情况根本不会发生或很少发生。 在下一节中,我们将学习如何为用例选择最佳算法。
如何选择算法
如前所述,没有一种算法可以轻松地用于所有开箱即用的情况,其主要原因是与软件和硬件相关的因素种类繁多。 一个算法可能非常准确,但是同时,它可能需要大量资源(例如内存或 CPU 使用率)。
另一种算法可能需要较少的参数(几乎总是缓解),但同样,它可能无法达到其最高性能。 我们甚至无法开始列出影响选择最佳Feature2D(或特征检测器和描述符提取器)算法或最佳匹配算法的所有可能因素,但我们仍可以考虑一些主要且更知名的因素,这也是从结构上来讲,按原样创建 OpenCV 和大多数计算机视觉算法的原因。 以下是这些因素:
- 准确率
- 速度
- 资源使用情况(内存,磁盘空间等)
- 可用性
请注意,表现一词通常是指准确率,速度和资源使用情况的组合。 因此,我们所寻找的本质上是一种表现达到我们要求的算法,并且该算法可用于需要我们的应用进行工作的一个或多个平台。 值得一提的是,您作为工程师还可以影响这些参数,特别是通过将用例缩小到恰好需要的范围。 让我们通过刚才提到的因素来解释这一点。
准确率
首先,准确率非常容易引起误解,因为一旦看到准确率下降,我们通常会倾向于放弃算法,但是正确的方法是首先弄清楚用例的准确率要求。 查看由非常著名的公司生产的基于计算机视觉的机器的数据表,您会立即发现诸如 95% 以上的东西,依此类推。 这并不意味着机器不完美。 相反,这意味着机器的精度是明确定义的,用户可以期望达到一定的精度,同时,他们可以承受一定的低误差。 话虽这么说,它总是很好,建议目标是 100% 的准确率。
除了浏览该算法的论文和参考文献,还有更好的办法,亲自尝试一下,没有比这更好的方法来为您的用例选择一种准确的算法。 确保使用 Qt 中的适当小部件创建用户界面,以便您可以轻松地尝试现有(甚至可能是您自己的)算法。 创建基准并确保您完全了解更改阈值或其他参数时任何特定算法的行为。
另外,请确保根据比例和旋转独立性方面的需要选择算法。 例如,在 AKAZE 中,使用标准 AKAZE 描述符类型(非直立),该算法允许旋转独立,因此您的匹配甚至可以与旋转对象一起使用。 或者,使用较高的八度(或金字塔等级)数字,因为这可以帮助匹配不同大小的图像,从而实现比例独立。
速度
如果您正在开发实时应用,其中 FPS(每秒帧或帧速率)值必须尽可能高,则算法执行的速度尤为重要。 因此,与准确率一样,您也需要小心以澄清此要求。 如果您匹配两幅图像并向用户显示一些匹配结果,则即使半秒(500 毫秒)的延迟仍然可以接受,但是当使用高 FPS 值时,每帧的半秒延迟会非常高 。
您可以在 OpenCV 中使用TickMeter类或getTickFrequency和getTickCount函数来测量计算机视觉进程(或与此相关的任何进程)的执行时间。 首先,让我们看看旧方法的工作原理:
double freq = cv::getTickFrequency();
double tick = cv::getTickCount();
processImage(); // Any process
double dur = (cv::getTickCount() - tick) / freq;
getTickFrequency函数可用于以秒(或频率)为单位获取 CPU 滴答计数。 同样,getTickCount可用于获取自启动以来传递的 CPU 滴答声数量。 因此,很明显,在前面的示例代码中,我们将获得执行processImage函数的持续时间(以秒为单位)。
但是TickMeter类提供了更大的灵活性,并且更易于使用。 您只需在任何过程之前启动它,然后在该过程之后停止它。 这是完成的过程:
cv::TickMeter meter;
meter.start();
processImage(); // Any process
meter.stop();
meter.getTimeMicro();
meter.getTimeMilli();
meter.getTimeSec();
meter.getTimeTicks();
在满足您精度要求的不同算法之间进行切换,并使用此技术测量其速度,然后选择最适合您的算法。 尝试远离经验法则,例如 ORB 更快,或者 BRISK 更准确,等等。 即使具有String类型的描述符(例如 ORB)通常在匹配方面也更快(因为它们使用汉明距离); 最新的算法(例如 AKAZE)可以使用 GPU 和 OpenCV UMat(请参阅第 4 章,“Mat和QImage”,以了解有关UMat类的更多信息) 。 因此,请尝试使用您的度量或任何受信任的度量参考作为经验法则的来源。
您也可以使用 Qt 的QElapsedTimer类,类似于 OpenCV 的TickMeter类,来测量任何进程的执行时间。
资源使用
尤其是对于较新的高端设备和计算机,这通常不是什么大问题,但是对于磁盘和内存空间有限的计算机(例如嵌入式计算机),这仍然可能是个问题。 为此,请尝试使用操作系统随附的资源监视器应用。 例如,在 Windows 上,您可以使用任务管理器应用查看已使用的资源,例如内存。 在 MacOS 上,您可以使用“活动监视器”应用甚至查看每个程序使用的电池电量(能量)以及内存和其他资源使用情况信息。 在 Linux 上,您可以使用多种工具(例如系统监视器)来实现完全相同的目的。
可用性
即使 OpenCV 和 Qt 都是跨平台框架,算法(甚至是类或函数)仍然可以依赖于平台特定的功能,尤其是出于性能方面的考虑。 重要且显而易见的是,您需要确保使用的算法在旨在发布您的应用的平台上可用。最好的来源通常是 OpenCV 和 Qt 框架中基础类的文档页面。 。
您可以从以下链接下载用于关键点检测,描述符提取和描述符匹配的完整源代码。 您可以使用同一插件在准确率和速度方面比较不同的算法。 不用说,此插件与我们在整本书中一直构建的computer_vision项目兼容。
总结
特征检测,描述和匹配可能是计算机视觉中最重要和最热门的主题,仍在不断发展和改进中。 本章介绍的算法只是世界上现有算法的一小部分,我们之所以选择介绍它们,是因为它们或多或少都可供公众免费使用,以及它们默认包含在feature2d模块下的 OpenCV 中。 如果您有兴趣了解更多算法,还可以查看额外 2D 特征框架(xfeature2d),其中包含非自由算法,例如 SURF 和 SIFT,或其他仍处于实验状态的算法。 当然,在构建它们以将其功能包括在 OpenCV 安装中之前,您需要单独下载它们并将其添加到 OpenCV 源代码中。 也建议。 但是,还要确保使用不同的图像和各种参数来尝试使用本章中学习的算法,以熟悉它们。
通过完成本章,您现在可以使用与特征和描述符相关的算法来检测关键点并提取特征并进行匹配以检测对象或相互比较图像。 使用本章介绍的类,您现在可以正确显示匹配操作的结果,还可以测量每个过程的性能来确定哪个更快。
在第 8 章,“多线程”中,我们将了解 Qt 中的多线程和并行处理(及其在 OpenCV 中的应用)以及如何从应用的主线程中,有效创建和使用分别存在的线程和进程。 利用下一章的知识,我们将准备处理继续在视频文件或摄像机帧中的连续帧上执行的视频处理和计算机视觉任务。
八、多线程
不久前,计算机程序的设计和构建是一个接一个地运行一系列指令。 实际上,这种方法非常易于理解和实现,即使在今天,我们也使用相同的方法来编写脚本和简单程序,这些脚本和简单程序以串行方式处理所需的任务。 但是,随着时间的推移,尤其是随着功能更强大的处理器的兴起,多任务成为主要问题。 期望计算机一次执行多个任务,因为它们足够快地执行多个程序所需的指令,并且仍然有一些空闲时间。 当然,随着时间的流逝,甚至会编写更复杂的程序(游戏,图形程序等),并且处理器必须公平地管理不同程序所使用的时间片,以使所有程序继续正确运行。 程序(或过程,在这种情况下使用更合适的词)被分成称为线程的较小片段。 直到现在,这种方法(或多线程)已经帮助创建了可以与相似或完全不相关的进程一起运行的快速响应进程,从而带来了流畅的多任务处理体验。
在具有单个处理器(和单个内核)的计算机上,每个线程都有一个时间片,并且处理器显然一次只能处理一个线程,但是多个线程之间的切换通常是如此之快,以至于从用户需求的角度来看,似乎是真正的并行性。 但是,如今,即使人们随身携带的大多数智能手机中的处理器也具有使用处理器中的多个内核处理多个线程的能力。
为确保我们对线程以及如何使用它们有清晰的了解,以及为什么不使用线程就无法编写功能强大的计算机视觉程序,我们来看看进程与线程之间的主要区别:
- 进程类似于单个程序,它们直接由操作系统执行
- 线程是进程的子集,换句话说,一个进程可以包含多个线程
- 一个进程(通常)独立于任何其他进程,而线程彼此共享内存和资源(请注意,进程可以通过操作系统提供的方法相互交互)
根据设计的方式,每个进程可能会也可能不会创建和执行不同的线程,以实现最佳性能和响应能力。 另一方面,每个线程将执行该进程所需的特定任务。 Qt 和 GUI 编程中的典型示例是进度信息。 运行复杂且耗时的过程时,通常需要显示有关进度的阶段和状态的信息,例如剩余的工作百分比,完成的剩余时间等等。 最好通过将实际任务和 GUI 更新任务分成单独的线程来完成。 在计算机视觉中非常常见的另一个示例是视频(或摄像机)处理。 您需要确保在需要时正确阅读,处理和显示了视频。 在学习 Qt 框架中的多线程功能时,这以及此类示例将成为本章的重点。
在本章中,我们将介绍以下主题:
- Qt 中的多线程方法
- 如何在 Qt 中使用
QThread和多线程类 - 如何创建响应式 GUI
- 如何处理多张图像
- 如何处理多个摄像机或视频
Qt 中的多线程
Qt 框架提供了许多不同的技术来处理应用中的多线程。 QThread类用于处理各种多线程功能,正如我们将在本章中看到的那样,使用它也是 Qt 框架中处理线程的最强大,最灵活的方式。 除了QThread,Qt 框架还提供了许多其他名称空间,类和函数,可满足各种多线程需求。 在我们查看如何使用它们的示例之前,以下是它们的列表:
QThread:此类是 Qt 框架中所有线程的基础。 可以将其子类化以创建新线程,在这种情况下,您需要覆盖run方法,或者可以创建该方法的新实例,然后通过调用 Qt 对象(QObject子类)将其移至新线程中。moveToThread函数。QThreadPool:通过允许将现有线程重新用于新用途,可用于管理线程并帮助降低线程创建成本。 每个 Qt 应用都包含一个全局QThreadPool实例,可以使用QThreadPool::globalInstance()静态函数对其进行访问。 此类与QRunnable类实例结合使用,以控制,管理和回收 Qt 应用中的可运行对象。QRunnable:这提供了另一种创建线程的方法,它是 Qt 中所有可运行对象的基础。 与QThread不同,QRunnable不是QObject子类,并且用作需要运行的一段代码的接口。 您需要继承并覆盖run函数,才能使用QRunnable。 如前所述,QRunnable实例由QThreadPool类管理。QMutex,QMutexLocker,QSemaphore,QWaitCondition,QReadLocker,QWriteLocker和QWriteLocke:这些类用于处理线程间同步任务。 根据情况,可以使用这些类来避免出现以下问题:线程覆盖彼此的计算,试图读取或写入一次只能处理一个线程的设备的线程以及许多类似的问题。 创建多线程应用时,通常需要手动解决此类问题。QtConcurrent:此命名空间可用于使用高级 API 创建多线程应用。 它使编写多线程应用变得更加容易,而无需处理互斥量,信号量和线程间同步问题。QFuture,QFutureWatcher,QFututeIterator和QFutureSynchronizer:这些类都与QtConcurrent命名空间结合使用,以处理多线程和异步操作结果。
通常,在 Qt 中有两种不同的多线程方法。 第一种基于QThread的方法是低级方法,它提供了很多灵活性和对线程的控制,但是需要更多的编码和维护才能完美地工作。 但是,有很多方法可以使用QThread来制作多线程应用,而工作量却少得多,我们将在本章中学习它们。 第二种方法基于QtConcurrent命名空间(或 Qt 并发框架),这是在应用中创建和运行多个任务的高级方法。
使用QThread的低级多线程
在本节中,我们将学习如何使用QThread及其关联类创建多线程应用。 我们将通过创建一个示例项目来完成此过程,该项目将使用单独的线程处理并显示视频源的输入和输出帧。 这有助于使 GUI 线程(主线程)保持空闲和响应状态,而第二个线程处理更密集的进程。 正如前面提到的,我们将主要关注计算机视觉和 GUI 开发的通用用例。 但是,可以将相同(或非常相似)的方法应用于任何多线程问题。
我们将使用此示例项目来使用 Qt 中提供的两种不同方法(用于QThread类)来实现多线程。 首先,子类化并覆盖run方法,其次,使用所有 Qt 对象中可用的moveToThread函数,或者换句话说,使用QObject子类。
子类化QThread
让我们首先在 Qt Creator 中创建一个名为MultithreadedCV的示例 Qt Widgets 应用。 以我们在本书开始章节中学到的相同方式将 OpenCV 框架添加到该项目中:在MultithreadedCV.pro文件中包含以下代码(请参见第 2 章,“第一个 Qt 和 OpenCV 项目”或第 3 章,“创建一个综合的 Qt + OpenCV 项目”,以了解更多信息):
win32: {
include("c:/dev/opencv/opencv.pri")
}
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
unix: macx{
INCLUDEPATH += /usr/local/include
LIBS += -L"/usr/local/lib" \
-lopencv_world
}
然后,将两个标签窗口小部件添加到mainwindow.ui文件,如下所示。 我们将使用以下标签在计算机上显示来自默认摄像头的原始视频和经过处理的视频:

确保将左侧标签的objectName属性设置为inVideo,将右侧标签的objectName属性设置为outVideo。 另外,将其alignment/Horizontal属性设置为AlignHCenter。 现在,通过右键单击项目 PRO 文件并从菜单中选择“新建”,创建一个名为VideoProcessorThread的新类。 然后,选择“C++ 类”,并确保新类向导中的组合框和复选框如下图所示:

创建类后,项目中将有两个名为videoprocessorthread.h和videoprocessor.cpp的新文件,其中将实现一个视频处理器,该处理器在与mainwindow文件和 GUI 线程不同的线程中工作。 首先,通过添加相关的包含行和类继承来确保此类继承了QThread,如下所示(只需在头文件中将QObject替换为QThread)。 另外,请确保您包含 OpenCV 标头:
#include <QThread>
#include "opencv2/opencv.hpp"
class VideoProcessorThread : public QThread
您需要类似地更新videoprocessor.cpp文件,以便它调用正确的构造器:
VideoProcessorThread::VideoProcessorThread(QObject *parent)
: QThread(parent)
现在,我们需要向videoprocessor.h文件中添加一些必需的声明。 将以下行添加到您的类的private成员区域:
void run() override;
然后,将以下内容添加到signals部分:
void inDisplay(QPixmap pixmap);
void outDisplay(QPixmap pixmap);
最后,将以下代码块添加到videoprocessorthread.cpp文件:
void VideoProcessorThread::run()
{
using namespace cv;
VideoCapture camera(0);
Mat inFrame, outFrame;
while(camera.isOpened() && !isInterruptionRequested())
{
camera >> inFrame;
if(inFrame.empty())
continue;
bitwise_not(inFrame, outFrame);
emit inDisplay(
QPixmap::fromImage(
QImage(
inFrame.data,
inFrame.cols,
inFrame.rows,
inFrame.step,
QImage::Format_RGB888)
.rgbSwapped()));
emit outDisplay(
QPixmap::fromImage(
QImage(
outFrame.data,
outFrame.cols,
outFrame.rows,
outFrame.step,
QImage::Format_RGB888)
.rgbSwapped()));
}
}
run函数被覆盖,并已执行以执行所需的视频处理任务。 如果您试图在mainwindow.cpp代码中循环执行相同的操作,则会注意到您的程序无响应,最终必须终止它。 但是,使用这种方法,现在相同的代码位于单独的线程中。 您只需要确保通过调用start函数而不是run启动此线程即可! 注意run函数是在内部调用的,因此您只需要重新实现它即可,如本示例所示; 但是,要控制线程及其执行行为,您需要使用以下函数:
-
start:如果尚未启动线程,则可用于启动该线程。 该函数通过调用我们实现的run函数开始执行。 您可以将以下值之一传递给start函数,以控制线程的优先级:QThread::IdlePriority(在没有其他线程在运行时调度)QThread::LowestPriorityQThread::LowPriorityQThread::NormalPriorityQThread::HighPriorityQThread::HighestPriorityQThread::TimeCriticalPriority(尽可能安排此时间)QThread::InheritPriority(这是默认值,它仅从父级继承优先级)
-
terminate:此函数仅在极端情况下使用(意味着永远不会,希望如此),将强制线程终止。 -
setTerminationEnabled:可用于启用或禁用terminate函数。 -
wait:此函数可用于阻塞线程(强制等待),直到线程完成或达到超时值(以毫秒为单位)为止。 -
requestInterruption和isRequestInterrupted:这些函数可用于设置和获取中断请求状态。 使用这些函数是确保线程在可能永远持续的进程中间安全停止的一种有用方法。 -
isRunning和isFinished:这些函数可用于请求线程的执行状态。
除了我们在此处提到的函数之外,QThread包含其他可用于处理多线程的函数,例如quit,exit和idealThreadCount等。 最好亲自检查一下并考虑其中每个用例。 QThread是一个功能强大的类,可以帮助您最大化应用的效率。
让我们继续我们的示例。 在run函数中,我们使用 OpenCV VideoCapture类读取视频帧(永久),并将简单的bitwise_not运算符应用于Mat帧(此时我们可以进行任何其他图像处理,因此 bitwise_not只是一个例子,是一个相当简单的解释我们的观点),然后通过QImage将其转换为QPixmap,然后使用两个信号发送原始帧和修改后的帧。 请注意,在永远持续的循环中,我们将始终检查摄像头是否仍处于打开状态,并还会检查对此线程是否有中断请求。
现在,让我们在MainWindow中使用我们的线程。 首先将其头文件包含在mainwindow.h文件中:
#include "videoprocessorthread.h"
然后,将以下行添加到mainwindow.h文件中MainWindow的private成员部分:
VideoProcessorThread processor;
现在,在setupUi行之后,将以下代码添加到MainWindow构造器中:
connect(&processor,
SIGNAL(inDisplay(QPixmap)),
ui->inVideo,
SLOT(setPixmap(QPixmap)));
connect(&processor,
SIGNAL(outDisplay(QPixmap)),
ui->outVideo,
SLOT(setPixmap(QPixmap)));
processor.start();
然后将以下行添加到delete ui;行之前的MainWindow析构器中:
processor.requestInterruption();
processor.wait();
我们只需将VideoProcessorThread类的两个信号连接到我们添加到MainWindow GUI 的两个标签,然后在程序启动后立即启动线程。 我们还要求线程在MainWindow关闭后立即删除,并且在删除 GUI 之前。 在继续执行删除指令之前,wait函数调用可确保等待线程清理并安全完成执行。 尝试运行此代码以自行检查。 程序启动后,您应该会看到类似于下图的内容:

程序启动后,计算机上默认摄像头中的视频应立即开始播放,关闭程序后,该视频将停止播放。 尝试通过向其中传递摄像机索引号或视频文件路径来扩展VideoProcessorThread类。 您可以根据需要实例化许多VideoProcessorThread类。 您只需要确保将信号连接到 GUI 上的正确小部件,就可以通过这种方式在运行时动态处理和显示多个视频或摄像机。
使用moveToThread函数
如前所述,您还可以使用任何QObject子类的moveToThread函数来确保它在单独的线程中运行。 为了确切地了解它是如何工作的,让我们通过创建完全相同的 GUI 来重复相同的示例,然后创建一个新的 C++ 类(与以前相同),但是这次将其命名为VideoProcessor。 但是,这一次,在创建类之后,您无需从QThread继承它,而将其保留为QObject(原样)。 只需将以下成员添加到videoprocessor.h文件中:
signals:
void inDisplay(QPixmap pixmap);
void outDisplay(QPixmap pixmap);
public slots:
void startVideo();
void stopVideo();
private:
bool stopped;
signals与以前完全相同。 stopped是一个标志,我们将用来帮助我们停止视频,以使视频不会永远播放下去。 startVideo和stopVideo是我们用来启动和停止来自默认网络摄像头的视频处理的功能。 现在,我们可以切换到videoprocessor.cpp文件并添加以下代码块。 与以前非常相似,但明显的区别是我们不需要实现run函数,因为它不是QThread子类,并且我们按自己的喜好命名了函数:
void VideoProcessor::startVideo()
{
using namespace cv;
VideoCapture camera(0);
Mat inFrame, outFrame;
stopped = false;
while(camera.isOpened() && !stopped)
{
camera >> inFrame;
if(inFrame.empty())
continue;
bitwise_not(inFrame, outFrame);
emit inDisplay(
QPixmap::fromImage(
QImage(
inFrame.data,
inFrame.cols,
inFrame.rows,
inFrame.step,
QImage::Format_RGB888)
.rgbSwapped()));
emit outDisplay(
QPixmap::fromImage(
QImage(
outFrame.data,
outFrame.cols,
outFrame.rows,
outFrame.step,
QImage::Format_RGB888)
.rgbSwapped()));
}
}
void VideoProcessor::stopVideo()
{
stopped = true;
}
现在我们可以在MainWindow类中使用它。 确保为VideoProcessor类添加include文件,然后将以下内容添加到MainWindow的私有成员部分:
VideoProcessor *processor;
现在,将以下代码段添加到mainwindow.cpp文件中的MainWindow构造器中:
processor = new VideoProcessor();
processor->moveToThread(new QThread(this));
connect(processor->thread(),
SIGNAL(started()),
processor,
SLOT(startVideo()));
connect(processor->thread(),
SIGNAL(finished()),
processor,
SLOT(deleteLater()));
connect(processor,
SIGNAL(inDisplay(QPixmap)),
ui->inVideo,
SLOT(setPixmap(QPixmap)));
connect(processor,
SIGNAL(outDisplay(QPixmap)),
ui->outVideo,
SLOT(setPixmap(QPixmap)));
processor->thread()->start();
在前面的代码片段中,首先,我们创建了VideoProcessor的实例。 请注意,我们没有在构造器中分配任何父对象,并且还确保将其定义为指针。 当我们打算使用moveToThread函数时,这非常重要。 具有父对象的对象无法移到新线程中。 此代码段中的第二个非常重要的教训是,我们不应该直接调用VideoProcessor的startVideo函数,而只能通过将适当的信号连接到它来调用它。 在这种情况下,我们使用了自己线程的启动信号。 但是,您可以使用具有相同签名的任何其他信号。 剩下的全部都是关于连接的。
在MainWindow析构器中,添加以下行:
processor->stopVideo();
processor->thread()->quit();
processor->thread()->wait();
这是很不言自明的,但是,为了清楚起见,让我们在这里再做一个说明,那就是在这样的线程启动之后,必须通过调用quit函数来停止它,而且,不应包含任何运行循环或未决指令。 如果不满足这些条件之一,则在处理线程时将面临严重的问题。
线程同步工具
多线程编程通常需要维护线程之间的冲突和问题,这些冲突和问题是由于并行性以及底层操作系统负责照顾线程将在何时以及确切地运行多长时间的原因而简单产生的。 一个提供多线程功能的强大框架(例如 Qt 框架)还必须提供处理此类问题的方法,所幸的是,正如我们在本章中将学到的那样,它确实可以做到。
在本节中,我们将学习由多线程编程引起的可能问题以及 Qt 中可用于解决这些问题的现有类。 这些类通常称为线程同步工具。 线程同步是指以这样的方式处理和编程线程:它们使用简单易用的方式了解其他线程的状态,同时,它们可以继续完成自己的特定任务。
互斥体
如果您对本主题以及即将到来的有关线程同步工具的部分感到熟悉,那么您将很轻松地掌握所涵盖的主题,并且您将很快了解 Qt 中实现的相同工具的易用性; 否则,最好彻底,仔细地遵循这些部分。 因此,让我们从第一个线程同步工具开始。 通常,如果两个线程尝试同时访问同一对象(例如变量或类实例等),并且如果每个线程对对象的处理顺序很重要,那么有时生成的对象可能会与我们期望的有所不同。 让我们用一个例子来分解它,因为即使您完全遵循刚才提到的内容,它仍然可能会令人困惑。 假设一个线程一直在使用以下代码行(在QThread的重新实现的run函数中,或者从另一个线程在使用moveToThread函数的类中)始终读取名为image的Mat类实例。):
forever
{
image = imread("image.jpg");
}
forever宏是一个 Qt 宏(与for(;;)相同),可用于创建无限循环。 使用此类 Qt 宏有助于提高代码的可读性。
第二个不同的线程一直在修改该图像。 让我们假设像这样一个非常简单的图像处理任务(将图像转换为灰度然后调整其大小):
forever
{
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
}
如果这两个线程同时运行,则在某个时候,可以在第二个线程的cvtColor之后和resize之前调用第一个线程的imread函数。 如果发生这种情况,我们将无法获得比输入图像大一半的灰度图像(如示例代码中所预期)。 我们无法用此代码来阻止它,因为在运行时在线程之间进行切换时,这完全取决于操作系统。 在多线程编程中,这是一种竞争条件问题,可以通过确保每个线程在访问和修改对象之前等待其轮换来解决。 该问题的解决方案称为访问序列化,在多线程编程中,通常使用互斥对象解决。
互斥锁只是一种保护和防止对象实例同时被多个线程访问的方法。 Qt 提供了一个名为QMutex的类(非常方便)来处理访问序列化,我们可以在前面的示例中非常轻松地使用它,如此处所示。 我们只需要确保Mat类存在QMutex实例即可。 由于我们的Mat类称为image,因此将其称为互斥锁imageMutex,那么我们将需要将该互斥锁锁定在访问图像的每个线程中,并在完成操作后将其解锁。 因此,对于第一个线程,我们将有以下内容:
forever
{
imageMutex.lock();
image = imread("image.jpg");
imageMutex.unlock();
}
对于第二个线程,我们将具有以下代码块:
forever
{
imageMutex.lock();
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
imageMutex.unlock();
}
这样,每当两个线程中的每个线程开始处理图像时,首先,它将使用lock函数锁定互斥锁。 如果简单地说,在过程的中间,操作系统决定切换到另一个线程,该线程也将尝试锁定互斥锁,但是由于互斥锁已被锁定,因此调用lock函数的新线程将被阻塞,直到第一个线程(称为锁)调用unlock为止。 从获取锁的钥匙的角度考虑它。 只有调用互斥量的lock函数的线程才能通过调用unlock函数将其解锁。 这样可以确保,只要一个线程正在访问一个对象,所有其他线程都应该简单地等待它完成!
从我们的简单示例中可能并不明显,但是在实践中,如果需要敏感对象的函数数量增加,则使用互斥可能会成为负担。 因此,在使用 Qt 时,最好使用QMutexLocker类来保护互斥锁。 如果我们回到前面的示例,则可以这样重写相同的代码:
forever
{
QMutexLocker locker(&imageMutex);
image = imread("image.jpg");
}
And for the second thread:
forever
{
QMutexLocker locker(&imageMutex);
cvtColor(image, image, CV_BGR2GRAY);
resize(image, image, Size(), 0.5, 0.5);
}
通过将互斥量传递给它来构造QMutexLocker类时,该互斥量将被锁定,并且一旦QMutexLocker被销毁(对于超出范围的实例),该互斥量将被解锁。
读写锁
与互斥锁一样强大,它们缺乏某些功能,例如不同类型的锁。 因此,尽管它们对于访问序列化非常有用,但是它们不能有效地用于诸如读写序列化之类的情况,该情况基本上依赖于两种不同类型的锁:读写。 让我们再用一个例子来分解。 假设我们希望各种线程能够同时从一个对象(例如变量,类实例,文件等)读取,但是我们要确保只有一个线程可以修改(或写入) 该对象在任何给定时间。 对于这种情况,我们可以使用读写锁定机制,该机制基本上是增强的互斥体。 Qt 框架提供QReadWriteLock类,可以使用与QMutex类类似的方式,除了它提供用于读取的锁定函数(lockForRead)和用于写入的另一个锁定函数(lockForWrite)。 以下是每个lock函数的功能:
- 如果在线程中调用
lockForRead函数,其他线程仍可以调用lockForRead并出于读取目的访问敏感对象。 (通过敏感对象,我们指的是我们正在为其使用锁的对象。) - 另外,如果在线程中调用了
lockForRead函数,则任何调用lockForWrite的线程都将被阻塞,直到该线程调用了解锁函数。 - 如果在线程中调用了
lockForWrite函数,则所有其他线程(无论是用于读取还是写入)都将被阻塞,直到该线程调用解锁为止。 - 如果在一个线程中调用了
lockForWrite函数,而先前的线程已经在其中设置了读锁定,则所有调用lockForRead的新线程将必须等待需要写锁定的线程。 因此,需要lockForWrite的线程将具有更高的优先级。
为了简化我们刚才提到的读写锁定机制的功能,可以说QReadWriteLock可用于确保多个读取器可以同时访问一个对象,而写入器将不得不等待读取器先完成。 另一方面,将只允许一个写者对该对象进行写操作。 并且,如果读者过多,为了保证作家不会永远等待,他们将获得更高的优先级。
现在,让我们看一下如何使用QReadWriteLock类的示例代码。 请注意,此处的lock变量具有QReadWriteLock类型,read_image函数是从对象读取的任意函数:
forever
{
lock.lockForRead();
read_image();
lock.unlock();
}
类似地,在需要写入对象的线程中,我们将具有以下内容(write_image是写入对象的任意函数):
forever
{
lock.lockForWrite();
write_image();
lock.unlock();
}
与QMutex相似,在其中我们使用QMutexLocker来更轻松地处理lock和unlock函数,我们可以使用QReadLocker和QWriteLocker类相应地锁定和解锁QReadWriteLock。 因此,对于前面示例中的第一个线程,我们将具有以下代码行:
forever
{
QReadLocker locker(&lock);
Read_image();
}
对于第二个,我们将需要以下代码行:
forever
{
QWriteLocker locker(&lock);
write_image();
}
信号量
有时,在多线程编程中,我们需要确保多个线程可以相应地访问有限数量的相同资源。 例如,将用于运行程序的设备上的内存可能非常有限,因此我们希望需要大量内存的线程考虑到这一事实并根据可用的内存数量采取行动。 多线程编程中的此问题和类似问题通常通过使用信号量来解决。 信号量类似于增强的互斥锁,它不仅能够锁定和解锁,而且还能跟踪可用资源的数量。
Qt 框架提供了一个名为QSemaphore的类(足够方便)以在多线程编程中使用信号量。 由于信号量是根据可用资源的数量用于线程同步的,因此函数名称也比lock和unlock函数更适合于此目的。 以下是QSemaphore类中的可用函数:
acquire:可用于获取特定数量的所需资源。 如果没有足够的资源,则线程将被阻塞,必须等待直到有足够的资源。release:可用于释放特定数量的已使用且不再需要的资源。available:可用于获取可用资源的数量。 如果我们希望我们的线程执行其他任务而不是等待资源,则可以使用此函数。
除了一个适当的例子,没有什么可以比这个更加清楚的了。 假设我们有100MB的可用内存空间供所有线程使用,并且每个线程需要X兆字节数来执行其任务,具体取决于线程,因此X在所有线程中都不相同,可以说它是使用将在线程中处理的图像大小或与此相关的任何其他方法来计算的。 对于当前的当前问题,我们可以使用QSemaphore类来确保我们的线程仅访问可用的内存空间,而不会访问更多。 因此,我们将在程序中创建一个信号量,如下所示:
QSemaphore memSem(100);
并且,在每个线程内部,在占用大量内存的过程之前和之后,我们将获取并释放所需的内存空间,如下所示:
memSem.acquire(X);
process_image(); // memory intensive process
memSem.release(X);
请注意,在此示例中,如果某个线程中的X大于100,则它将无法继续通过acquire,直到release函数调用(释放的资源)等于或大于该值。 acquire函数调用(获取的资源)。 这意味着可以通过调用release函数(其值大于获取的资源)来增加(创建)可用资源的数量。
等待条件
多线程编程中的另一个常见问题可能会发生,因为某个线程必须等待操作系统正在执行的线程以外的其他条件。 在这种情况下,如果很自然地线程使用了互斥锁或读写锁,则它可能会阻塞所有其他线程,因为轮到该线程运行并且正在等待某些特定条件。 人们会希望需要等待条件的线程在释放互斥锁或读写锁后进入睡眠状态,以便其他线程继续运行,并在满足条件时被另一个线程唤醒。
在 Qt 框架中,有一个名为QWaitCondition的类,专用于处理我们刚刚提到的此类问题。 此类可能需要等待某些条件的任何线程使用。 让我们通过一个简单的例子来进行研究。 假设有多个线程与Mat类一起使用(准确地说是一个图像),并且一个线程负责读取此图像(仅当它存在时)。 现在,还要假设另一个进程,程序或用户负责创建此图像文件,因此它可能暂时无法使用。 由于图像由多个线程使用,因此我们可能需要使用互斥锁以确保线程一次访问一个图像。 但是,如果图像仍然不存在,则读取器线程可能仍需要等待。 因此,对于阅读器线程,我们将具有以下内容:
forever
{
mutex.lock();
imageExistsCond.wait(&mutex);
read_image();
mutex.unlock();
}
注意,在该示例中,mutex的类型为QMutex,imageExistsCond的类型为QWaitCondition。 前面的代码段只是意味着锁定互斥锁并开始工作(读取图像),但是如果您必须等到图像存在后再释放互斥锁,以便其他线程可以继续工作。 这需要另一个负责唤醒阅读器线程的线程。 因此,我们将得到以下内容:
forever
{
if(QFile::exists("image.jpg"))
imageExistsCond.wakeAll();
}
该线程只是一直在检查图像文件的存在,如果存在,它将尝试唤醒所有等待此等待条件的线程。 我们也可以使用wakeOne函数代替wakeAll函数,该函数只是试图唤醒一个正在等待等待条件的随机线程。 如果满足条件,我们只希望一个线程开始工作,这将很有用。
这样就结束了我们对线程同步工具(或原语)的讨论。 本节中介绍的类是 Qt 框架中最重要的类,它们与线程结合使用以处理线程同步。 确保检查 Qt 文档,以了解那些类中存在的其他功能,这些功能可用于进一步改善多线程应用的行为。 当编写这样的多线程应用时,或者换句话说,使用低级方法时,我们必须确保线程使用本节刚刚介绍的类以一种方式或另一种方式彼此了解。 另外,请务必注意,这些技术并不是解决线程同步的唯一可能方法,有时(随着程序的发展变得越来越复杂),您肯定需要混合使用这些技术,进行调整, 弯曲它们,甚至自己发明一些。
使用QtConcurrent的高级多线程
除了在上一节中学到的知识之外,Qt 框架还提供了用于创建多线程程序的高级 API,而无需使用线程同步工具(例如互斥锁,锁等)。 QtConcurrent名称空间或 Qt 框架中的 Qt 并发模块,提供了易于使用的功能,这些功能可用于创建多线程应用,换句话说,并发性,方法是使用最佳数量的数据处理数据列表。 适用于任何平台的线程。 在经历了QtConcurrent中的功能以及与其结合使用的类之后,这将变得非常清晰。 之后,我们还将处理实际示例,以了解 Qt Concurrent模块的功能以及如何利用它。
总体上,以下函数(及其稍有不同的变体)可用于使用高级QtConcurrent API 处理多线程:
filter:可以用来过滤列表。 该函数需要提供一个包含要过滤的数据的列表和一个过滤函数。 我们提供的过滤函数将应用于列表中的每个项目(使用最佳或自定义线程数),并且根据过滤器函数返回的值,该项目将被删除或保留在列表中。filtered:它与filter的工作方式相同,除了它返回过滤的列表而不是原地更新输入列表。filteredReduced:其工作方式类似于filtered函数,但它还将第二个函数应用于通过过滤器的每个项目。map:可用于将特定函数应用于列表中的所有项目(使用最佳或自定义线程数)。 很明显,类似于filter函数,map函数也需要提供一个列表和一个函数。mapped:与map的工作方式相同,除了它返回结果列表而不是原地更新输入列表。mappedReduced:此函数的作用类似于mapped函数,但它还将第二个函数应用于除第一个映射函数之后的每个项目。run:此函数可用于在单独的线程中轻松执行函数。
每当我们谈论 Qt Concurrent 模块中的返回值时,我们实际上的意思是异步计算的结果。 原因很简单,因为 Qt Concurrent 在单独的线程中启动所有计算,并且无论您使用QtConcurrent名称空间中的哪个函数,它们都会立即返回给调用者,并且结果只有在计算完成之后才可用。 这是通过使用所谓的 Future 变量来完成的,或者使用 Qt 框架中的QFuture及其附属类来实现。
QFuture类可用于检索由QtConcurrent命名空间中提到的功能之一启动的计算结果; 通过暂停,恢复和类似方法控制其工作; 并监视该计算的进度。 为了能够使用 Qt 信号和插槽对QFuture类进行更灵活的控制,我们可以使用一个名为QFutureWatcher的便捷类,该类包含可以通过使用小部件更轻松地监视计算的信号和插槽。 例如进度条(QProgressBar或QProgressDialog)。
让我们总结并阐明在实际示例应用中提到的所有内容。 在不描述QtConcurrent命名空间功能的情况下,不可能描述QFuture及其关联类的使用方式,这只能通过一个示例来实现:
-
让我们开始使用 Qt Creator 创建一个 Qt Widgets 应用项目,并将其命名为
ConcurrentCV。 我们将创建一个使用 Qt Concurrent 模块处理多个图像的程序。 为了更加专注于程序的多线程部分,该过程将非常简单。 我们将读取每个图像的日期和时间,并将其写在图像的左上角。 -
创建项目后,通过在
ConcurrentCV.pro文件中添加以下行,将 OpenCV 框架添加到项目中:
win32: {
include("c:/dev/opencv/opencv.pri")
}
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
unix: macx{
INCLUDEPATH += /usr/local/include
LIBS += -L"/usr/local/lib" \
-lopencv_world
}
- 为了能够在 Qt 项目中使用 Qt 并发模块和
QtConcurrent命名空间,必须通过添加以下行来确保在.pro文件中指定了它:
QT += concurrent
- 现在,我们需要为应用中需要的几个函数编写代码。 第一个是在用户选择的文件夹中获取图像列表(
*.jpg和*.png文件已足够)。 为此,请将以下行添加到mainwindow.h私有成员中:
QFileInfoList getImagesInFolder();
- 不用说,
QFileInfoList必须在mainwindow.h文件的包含列表中。 实际上,QFileInfoList是包含QFileInfo元素的QList,可以使用QDir类的entryInfoList函数对其进行检索。 因此,将其实现添加到mainwindow.cpp,如此处所示。 请注意,仅出于简单起见,我们仅使用文件创建日期,而不处理图像EXIF数据以及使用相机拍摄照片的原始日期或时间:
QFileInfoList MainWindow::getImagesInFolder()
{
QDir dir(QFileDialog::getExistingDirectory(this,
tr("Open Images Folder")));
return dir.entryInfoList(QStringList()
<< "*.jpg"
<< "*.png",
QDir::NoDotAndDotDot | QDir::Files,
QDir::Name);
}
- 我们需要的下一个函数称为
addDateTime。 我们可以在类之外定义和实现它,这是稍后在调用QtConcurrent.map函数时将使用的函数。 在mainwindow.h文件中定义如下:
void addDateTime(QFileInfo &info);
- 将其实现添加到
mainwindow.cpp文件中,如下所示:
void addDateTime(QFileInfo &info)
{
using namespace cv;
Mat image = imread(info.absoluteFilePath().toStdString());
if(!image.empty())
{
QString dateTime = info.created().toString();
putText(image,
dateTime.toStdString(),
Point(30,30) , // 25 pixels offset from the corner
FONT_HERSHEY_PLAIN,
1.0,
Scalar(0,0,255)); // red
imwrite(info.absoluteFilePath().toStdString(),
image);
}
}
- 现在打开
mainwindow.ui文件,并在“设计”模式下创建类似于以下内容的 UI。 如下所示,loopBtn小部件是带有循环文本处理的QPushButton,而concurrentBtn小部件是同时带有文本处理的QPushButton。 为了能够比较使用多个线程或使用简单循环在单个线程中完成此任务的结果,我们将实现这两种情况,并测量每种情况下完成该任务所花费的时间。 另外,在继续执行下一步之前,请确保将progressBar小部件的value属性设置为零。

- 剩下要做的唯一事情就是使用
QtConcurrent(多线程)在一个循环中(单线程)执行该过程。 因此,为loopBtn的pressed插槽编写以下代码:
void MainWindow::on_loopBtn_pressed()
{
QFileInfoList list = getImagesInFolder();
QElapsedTimer elapsedTimer;
elapsedTimer.start();
ui->progressBar->setRange(0, list.count()-1);
for(int i=0; i<list.count(); i++)
{
addDateTime(list[i]);
ui->progressBar->setValue(i);
qApp->processEvents();
}
qint64 e = elapsedTimer.elapsed();
QMessageBox::information(this,
tr("Done!"),
QString(tr("Processed %1 images in %2 milliseconds"))
.arg(list.count())
.arg(e));
}
这很简单,而且绝对没有效率,我们稍后会学习。 此代码仅循环遍历文件列表,并将它们传递给addDateTime函数,该函数仅读取图像并添加日期时间戳并覆盖图像。
- 最后,为
concurrentBtn小部件的pressed插槽添加以下代码:
void MainWindow::on_concurrentBtn_pressed()
{
QFileInfoList list = getImagesInFolder();
QElapsedTimer elapsedTimer;
elapsedTimer.start();
QFuture<void> future = QtConcurrent::map(list, addDateTime);
QFutureWatcher<void> *watcher =
new QFutureWatcher<void>(this);
connect(watcher,
SIGNAL(progressRangeChanged(int,int)),
ui->progressBar,
SLOT(setRange(int,int)));
connect(watcher,
SIGNAL(progressValueChanged(int)),
ui->progressBar,
SLOT(setValue(int)));
connect(watcher,
&QFutureWatcher<void>::finished,
[=]()
{
qint64 e = elapsedTimer.elapsed();
QMessageBox::information(this,
tr("Done!"),
QString(tr("Processed %1 images in %2 milliseconds"))
.arg(list.count())
.arg(e));
});
connect(watcher,
SIGNAL(finished()),
watcher,
SLOT(deleteLater()));
watcher->setFuture(future);
}
在查看前面的代码并查看其工作方式之前,请尝试运行该应用并将两个按钮与测试图像文件夹一起使用。 尤其是在多核处理器上,性能差异如此之大,以至于不需要进行任何精确的测量。 在我使用的大约 50 张随机图像的测试机上(如今是中等级别的系统),并发(多线程)版本完成这项工作的速度至少快了三倍。 有多种方法可以使它更加高效,例如设置 Qt Concurrent 模块创建和使用的线程数,但是在此之前,让我们看看代码的作用。
起始行与之前相同,但是这次,我们而不是循环遍历文件列表,而是将列表传递给QtConcurrent::map函数。 然后,此函数自动启动多个线程(使用默认线程数和理想线程数,这也是可调的),并将addDateTime函数应用于列表中的每个条目。 项的处理顺序是完全不确定的,但是结果将是相同的。 然后将结果传递给QFuture<void>,该实例由QFutureWatcher<void>实例监视。 如前所述,QFutureWatcher类是监视来自QtConcurrent的计算的便捷方式,该计算已分配给QFuture类。 注意,在这种情况下,QFutureWatcher被定义为指针,并在处理完成时稍后删除。 原因是QFutureWatcher在整个过程继续进行期间必须保持活动状态,并且只有在计算完成后才能删除。 因此,首先完成QFutureWatcher的所有必需连接,然后相应地设置其将来变量。 重要的是要确保在建立所有连接后设置未来。 使用QtConcurrent进行多线程计算所需的全部内容也以正确的方式向 GUI 发送信号。
请注意,您还可以在全范围或全局范围内定义QFuture,然后使用其线程控制功能轻松控制QtConcurrent运行的计算。 QFuture包含以下(不言自明的)函数,可用于控制计算:
pauseresumecancel
您还可以使用以下函数(同样,由于命名而非常不言自明)来检索计算状态:
isStartedisPausedisRunningisFinishedisCanceled
至此,我们对前面的代码进行了回顾。 如您所见,只要您了解结构以及需要传递和连接的内容,使用QtConcurrent就非常容易,这就是应该的方式。
使用以下函数设置QtConcurrent函数的最大线程数:
QThreadPool::globalInstance()->setMaxThreadCount(n)
在我们的示例案例中尝试一下,看看改变线程数如何影响处理时间。 如果您使用不同数量的线程,则会注意到更多的线程并不一定意味着更高的性能或更快的代码,这就是为什么总有理想的线程数取决于处理器和处理器的原因。 其他与系统相关的规格。
我们可以类似的方式使用QtConcurrent过滤器和其他功能。 例如,对于过滤器函数,我们需要定义一个为每个项目返回布尔值的函数。 假设我们希望前面的示例应用跳过早于某个日期(2015 年之前)的图像,并将其从文件列表中删除,然后我们可以像这样定义过滤器函数:
bool filterImage(QFileInfo &info)
{
if(info.created().date().year() < 2015)
true;
else
false;
}
然后调用QtConcurrent来过滤我们的列表,如下所示:
QtConcurrent::filter(list, filterImage);
在这种情况下,我们需要将过滤后的结果传递给map函数,但是有一个更好的方法,那就是调用filteredReduced函数,如下所示:
QtConcurrent::filteredReduced(list, filterImage, addDateTime);
请注意,filteredReduced函数返回QFuture<T>结果,其中T与输入列表的类型相同。 与以前不同的是,我们仅收到适合监视计算进度的QFuture<void>,而QFuture<T>也包含结果列表。 请注意,由于我们并未真正修改列表中的单个元素(相反,我们正在更新文件),因此我们只能观察列表中元素数量的变化,但是如果我们尝试通过更新Mat类或QImage类的列表(或与此相关的任何其他变量),然后我们将观察到各个项也根据reduce函数中的代码进行了更改。
总结
不能说这就是谈论多线程和并行编程的全部内容,但是可以公平地说,我们涵盖了一些最重要的主题,可以帮助您编写多线程和高效的计算机视觉。 应用(或任何其他应用)。 您学习了如何对QThread进行子类化以创建执行特定任务的新线程类,或者如何使用moveToThread函数将负责复杂且耗时的计算的对象移动到另一个线程中。 您还了解了一些最重要的低级多线程原语,例如互斥体,信号量等。 到目前为止,您应该完全意识到由于在我们的应用中实现和使用多个线程而可能引起的问题,以及这些问题的解决方案。 如果您认为仍然需要练习以确保您熟悉所有提出的概念,那么您肯定对所有主题都给予了充分的关注。 多线程可能是一种困难且复杂的方法,但是如果您花大量时间练习不同的可能的多线程方案,那么最终还是值得的。 例如,您可以尝试将任务划分为之前编写的程序(或在网上,书中或其他地方看到的程序),然后将其转换为多线程应用。
在第 9 章,“视频分析”中,我们会将您在本章中学到的内容与之前的各章结合起来,并以此为基础,深入研究视频处理主题。 您将了解如何从摄像机或文件中跟踪视频中的运动对象,检测视频中的运动以及更多主题,所有这些都需要处理连续的帧并保留从先前帧中计算出的内容。 换句话说,计算不仅取决于图像,而且还取决于该图像(及时)。 因此,我们将使用线程,并使用您在本章中学习的任何方法来实现您将在下一章中学习的计算机视觉算法。
九、视频分析
除了本书到目前为止所看到的所有内容之外,计算机视觉的故事还有另一面,它涉及视频,摄像机以及输入帧的实时处理。 它是最受欢迎的计算机视觉主题之一,并且有充分的理由,因为它可以为有生命的机器或设备供电,这些机器或设备可以监视周围环境中是否存在感兴趣的对象,运动,图案,颜色等。 我们已经了解的所有算法和类,尤其是在第 6 章,“OpenCV 中的图像处理”和第 7 章,“特征和描述符”只能用于单个图像,因此,由于相同的原因,它们可以以完全相同的方式轻松地应用于单个视频帧。 我们只需要确保将单个帧正确地读取(例如,使用cv::VideoCapture类)到cv::Mat类实例中,然后作为单个图像传递到这些函数中即可。 但是,在处理视频以及视频时,我们指的是来自网络的视频,摄像机,视频文件等,有时我们需要通过处理特定时间段内的连续视频帧获得的结果。 这意味着结果不仅取决于当前从视频中获取的图像,还取决于之前获取的帧。
在本章中,我们将学习 OpenCV 中一些最重要的算法和类,这些算法和类可用于连续帧。 因此,视频。 我们将从学习这些算法使用的一些概念开始,例如直方图和反投影图像,然后通过使用示例并获得动手经验来更深入地研究每种算法。 我们将学习如何使用臭名昭著的 MeanShift 和 CamShift 算法进行实时对象跟踪,并且将继续进行视频中的运动分析。 我们将在本章中学到的大多数内容都与 OpenCV 框架中的视频分析模块(简称为video)有关,但我们还将确保遍历该模块所需的其他模块中的任何相关主题。 为了有效地遵循本章中的主题,尤其是直方图和反投影图像,这对于理解本章中涉及的视频分析主题至关重要。 背景/前景检测也是我们将在本章中学习的最重要主题之一。 通过结合使用这些方法,您将能够有效地处理视频以检测和分析运动,基于视频的颜色隔离视频帧中的零件或片段,或者使用现有的 OpenCV 算法以一种或另一种方式处理它们以进行图像处理。
同样,基于我们从第 8 章,“多线程”中学到的知识,我们将使用线程来实现在本章中学习的算法。 这些线程将独立于任何项目类型。 无论它是独立的应用,库,插件等,您都可以简单地包含和使用它们。
本章将涵盖以下主题:
- 直方图以及如何提取,使用或可视化它们
- 图像反投影
- MeanShift 和 CamShift 算法
- 背景/前景检测和运动分析
了解直方图
如本章介绍部分所述,计算机视觉中的一些概念在处理视频处理以及我们将在本章稍后讨论的算法时特别重要。 这些概念之一是直方图。 由于了解直方图对于理解大多数视频分析主题至关重要,因此在继续下一个主题之前,我们将在本节中详细了解它们。 直方图通常被称为表示数据分布的一种方式。 这是一个非常简单和完整的描述,但让我们也描述它在计算机视觉方面的含义。 在计算机视觉中,直方图是图像中像素值分布的图形表示。 例如,在灰度图像中,直方图将是表示包含灰度中每个可能强度(0 到 255 之间的值)的像素数的图表。 在 RGB 彩色图像中,它将是三个图形,每个图形代表包含所有可能的红色,绿色或蓝色强度的像素数。 请注意,像素值不一定表示颜色或强度值。 例如,在转换为 HSV 色彩空间的彩色图像中,其直方图将包含色相,饱和度和值数据。
OpenCV 中的直方图是使用calcHist函数计算的,并存储在Mat类中,因为它们可以存储为数字数组,可能具有多个通道。 calcHist函数需要以下参数来计算直方图:
images或输入图像是我们要为其计算直方图的图像。 它应该是cv::Mat类的数组。nimages是第一个参数中的图像数量。 请注意,您还可以为第一个参数传递cv::Mat类的std::vector,在这种情况下,您可以省略此参数。channels是一个数组,其中包含将用于计算直方图的通道的索引号。mask可用于遮盖图像,以便仅使用部分输入图像来计算直方图。 如果不需要遮罩,则可以传递一个空的Mat类,否则,我们需要提供一个单通道Mat类,对于应遮罩的所有像素,该类包含零,对于计算直方图时应考虑的所有像素,包含非零值。hist是输出直方图。 这应该是Mat类,并且在函数返回时将用计算出的直方图填充。dims是直方图的维数。 它可以包含一个介于 1 到 32 之间的值(在当前的 OpenCV 3 实现中)。 我们需要根据用于计算直方图的通道数进行设置。histSize是一个数组,其中包含每个维度中直方图的大小,即所谓的箱子大小。 直方图中的合并是指在计算直方图时将相似值视为相同值。 我们将在后面的示例中看到它的确切含义,但现在,我们只需提及直方图的大小与其箱数相同的事实就足够了。ranges是一个数组数组,其中包含每个通道的值范围。 简而言之,它应该是一个数组,其中包含一对值,用于通道的最小和最大可能值。uniform是一个布尔值标志,它决定直方图是否应该统一。accumulate是布尔值标志,它决定在计算直方图之前是否应清除该直方图。 如果我们要更新先前计算的直方图,这可能非常有用。
现在,让我们来看几个示例如何使用此函数。 首先,为了方便使用,我们将计算灰度图像的直方图:
int bins = 256;
int channels[] = {0}; // the first and the only channel
int histSize[] = { bins }; // number of bins
float rangeGray[] = {0,255}; // range of grayscale
const float* ranges[] = { rangeGray };
Mat histogram;
calcHist(&grayImg,
1, // number of images
channels,
Mat(), // no masks, an empty Mat
histogram,
1, // dimensionality
histSize,
ranges,
true, // uniform
false // not accumulate
);
在前面的代码中,grayImg是Mat类中的灰度图像。 图像数量仅为一个,并且channels索引数组参数仅包含一个值(对于第一个通道为零),因为我们的输入图像是单通道和灰度。 dimensionality也是一个,其余参数与它们的默认值相同(如果省略)。
执行完前面的代码后,我们将在histogram变量内获取生成的灰度图像直方图。 它是具有256行的单通道单列Mat类,每行代表像素值与行号相同的像素数。 我们可以使用以下代码将Mat类中存储的每个值绘制为图形,并且输出将以条形图的形式显示我们的直方图:
double maxVal = 0;
minMaxLoc(histogram,
Q_NULLPTR, // don't need min
&maxVal,
Q_NULLPTR, // don't need index min
Q_NULLPTR // don't need index max
);
outputImage.create(640, // any image width
360, // any image height
CV_8UC(3));
outputImage = Scalar::all(128); // empty grayish image
Point p1(0,0), p2(0,outputImage.rows-1);
for(int i=0; i<bins; i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
p1.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
rectangle(outputImage,
p1,
p2,
Scalar::all(0),
CV_FILLED);
p1.x = p2.x;
}
这段代码起初可能看起来有些棘手,但实际上它很简单,它基于以下事实:直方图中的每个值都需要绘制为矩形。 对于每个矩形,使用value变量和图像宽度除以箱数(即histSize)来计算左上角的点。 在示例代码中,我们简单地将最大可能值分配给了箱子(即 256),这导致了直方图的高分辨率可视化,因为条形图图中的每个条形图都会代表灰度级的一个像素强度 。
请注意,从这个意义上说,分辨率不是指图像的分辨率或质量,而是指构成条形图的最小块数的分辨率。
我们还假定输出可视化高度将与直方图的峰值(最高点)相同。 如果我们在下图左侧所示的灰度图像上运行这些代码,则所得的直方图将是右侧所示的直方图:

让我们解释输出直方图的可视化,并进一步说明我们在代码中使用的参数通常具有什么作用。 首先,每个条形从左到右是指具有特定灰度强度值的像素数。 最左边的条(非常低)指的是绝对黑色(强度值为零),最右边的条指的是绝对白色(255),中间的所有条指的是不同的灰色阴影。 注意最右边的小跳。 这实际上是由于输入图像的最亮部分(左上角)而形成的。 每个条形的高度除以最大条形值,然后缩放以适合图像高度。
我们还要看看bins变量的作用。 降低bins将导致强度分组在一起,从而导致较低分辨率的直方图被计算和可视化。 如果运行bins值为20的相同代码,则将得到以下直方图:

如果我们需要一个简单的图形而不是条形图视图,则可以在上一个代码末尾的绘图循环中使用以下代码:
Point p1(0,0), p2(0,0);
for(int i=0; i<bins; i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
line(outputImage,
p1,
Point(p1.x,value),
Scalar(0,0,0));
p1.y = p2.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
line(outputImage,
p1, p2,
Scalar(0,0,0));
p1.x = p2.x;
}
如果再次使用256的bins值,将导致以下输出:

同样,我们可以计算和可视化彩色(RGB)图像的直方图。 我们只需要为三个单独的通道修改相同的代码即可。 为了做到这一点,首先我们需要将输入图像划分为其基础通道,然后为每个图像计算直方图,就好像它是单通道图像一样。 这是如何拆分图像以获取三个Mat类,每个类代表一个通道:
vector<Mat> planes;
split(inputImage, planes);
现在,您可以在循环中使用planes[i]或类似的东西,并将每个通道视为图像,然后使用前面的代码示例来计算和可视化其直方图。 如果我们使用其自己的颜色可视化每个直方图,结果将是类似的结果(生成此直方图的图像是我们在整本书中使用的上一个示例的彩色图像):

同样,结果的内容几乎可以像以前一样解释。 前面的直方图图像显示了颜色如何分布在 RGB 图像的不同通道中。 但是,除了获取像素值分布的信息以外,我们如何真正使用直方图? 下一节将介绍直方图可用于修改图像的方式。
了解图像反投影
除了直方图中的视觉信息外,它还有更重要的用途。 这称为直方图的反投影,可用于使用其直方图来修改图像,或者正如我们将在本章稍后看到的那样,在图像中定位感兴趣的对象。 让我们进一步分解。 正如我们在上一节中了解到的,直方图是图像上像素数据的分布,因此如果我们以某种方式修改所得的直方图,然后将其重新应用于源图像(就好像它是像素值的查找表) ,则生成的图像将被视为反投影图像。 重要的是要注意,反投影图像始终是单通道图像,其中每个像素的值都是从直方图中的相应像素中提取的。
让我们将其视为另一个示例。 首先,这是在 OpenCV 中如何计算反投影:
calcBackProject(&image,
1,
channels,
histogram,
backprojection,
ranges);
calcBackProject函数的使用方式与calcHist函数非常相似。 您只需要确保传递一个附加的Mat类实例即可获得图像的反投影。 由于在背投图像中,像素值是从直方图中获取的,因此它们很容易超出标准灰度范围,该范围在0和255(含)之间。 这就是为什么我们需要在计算反投影之前相应地标准化直方图的结果。 方法如下:
normalize(histogram,
histogram,
0,
255,
NORM_MINMAX);
normalize函数将缩放直方图中的所有值以适合提供的最小值和最大值,分别为0和255。 只是重复一次,必须在calcBackProject之前调用此函数,否则,您将在反投影图像中产生溢出的数据,如果您尝试使用[[ imshow函数。
如果我们在查看反投影图像时未对生成它的直方图进行任何修改,那么在我们的示例情况下,我们将获得以下输出图像:

先前图像中每个像素的强度与包含该特定值的图像中像素的数量有关。 例如,请注意反投影图像的右上最暗部分。 与较亮的区域相比,该区域包含的像素值很少。 换句话说,明亮的区域包含的像素值在图像中以及图像的各个区域中都存在得多。 再说一遍,在处理图像和视频帧时如何使用呢?
本质上,反投影图像可用于为计算机视觉操作获取有用的遮罩图像。 到目前为止,我们还没有在 OpenCV 函数中真正使用掩码参数(并且它们存在于大多数函数中)。 让我们从使用前面的反投影图像的示例开始。 我们可以使用简单的阈值修改直方图,以获得用于过滤掉不需要的图像部分的遮罩。 假设我们想要一个可用于获取包含最暗值(例如,从0到39像素值)的像素的遮罩。 为此,首先我们可以通过将第一个40元素(只是最暗值的阈值,可以将其设置为任何其他值或范围)设置为灰度范围内的最大可能值来修改直方图(255),然后将其余的取到最小可能值(零),然后计算反投影图像。 这是一个例子:
calcHist(&grayImg,
1, // number of images
channels,
Mat(), // no masks, an empty Mat
histogram,
1, // dimensionality
histSize,
ranges);
for(int i=0; i<histogram.rows; i++)
{
if(i < 40) // threshold
histogram.at<float>(i,0) = 255;
else
histogram.at<float>(i,0) = 0;
}
Mat backprojection;
calcBackProject(&grayImg,
1,
channels,
histogram,
backprojection,
ranges);
通过运行前面的示例代码,我们将在backprojection变量内获得以下输出图像。 实际上,这是一种阈值技术,可为使用 OpenCV 的任何计算机视觉处理获得合适的遮罩,以隔离图像中最暗的区域。 我们使用此示例代码获得的遮罩可以传递到任何接受遮罩的 OpenCV 函数中,这些遮罩用于对与遮罩中白色位置对应的像素执行操作,而忽略与黑位置对应的像素:

类似于我们刚刚学习的阈值化方法的另一种技术可以用于遮盖图像中包含特定颜色的区域,因此可以将其仅用于处理(例如修改颜色)图像的某些部分,甚至跟踪图像的某些部分。 具有特定颜色的对象,我们将在本章稍后学习。 但是在此之前,让我们首先了解 HSV 颜色空间的直方图(使用色相通道)以及如何隔离具有特定颜色的图像部分。 让我们通过一个例子来进行研究。 假设您需要查找图像中包含特定颜色的部分,例如下图中的红玫瑰:

您不能根据阈值简单地滤除红色通道(在 RGB 图像中),因为它可能太亮或太暗,但仍然可以是红色的其他阴影。 另外,您可能需要考虑与红色过于相似的颜色,以确保您尽可能准确地获得玫瑰。 使用色调,饱和度,值(HSV)颜色空间,其中颜色保留在单个通道(色相或 H 通道)中,可以最好地处理这种情况以及需要处理颜色的类似情况。 这可以通过使用 OpenCV 进行示例实验来证明。 只需尝试在新应用中运行以下代码段即可。 它可以是控制台应用或小部件,没关系:
Mat image(25, 180, CV_8UC3);
for(int i=0; i<image.rows; i++)
{
for(int j=0; j<image.cols; j++)
{
image.at<Vec3b>(i,j)[0] = j;
image.at<Vec3b>(i,j)[1] = 255;
image.at<Vec3b>(i,j)[2] = 255;
}
}
cvtColor(image,image,CV_HSV2BGR);
imshow("Hue", image);
请注意,我们仅更改了三通道图像中的第一个通道,其值从0更改为179。 这将导致以下输出:

如前所述,其原因是这样的事实,即色调是造成每个像素颜色的原因。 另一方面,饱和度和值通道可用于获得相同颜色的较亮(使用饱和度通道)和较暗(使用值通道)变化。 请注意,在 HSV 颜色空间中,与 RGB 不同,色相是介于 0 到 360 之间的值。这是因为色相被建模为圆形,因此,每当其值溢出时,颜色就会回到起点。 如果查看上一张图像的开始和结尾,这两个都是红色,则很明显,因此 0 或 360 附近的色相值必须是带红色的颜色。
但是,在 OpenCV 中,色相通常会除以 2 以容纳 8 位(除非我们为像素数据使用 16 位或更多位),因此色相的值可以在0和180之间变化。 如果返回上一个代码示例,您会注意到在Mat类的列上,色相值从0设置为180,这将导致我们的色谱输出图像。
现在,让我们使用我们刚刚学到的东西创建一个颜色直方图,并使用它来获取背投图像以隔离我们的红玫瑰。 为了达到这个目的,我们甚至可以使用一段简单的代码将其变成蓝玫瑰,但是正如我们将在本章稍后学习的那样,该方法与 MeanShift 和 CamShift 算法结合使用来跟踪对象, 有特定的颜色。 我们的直方图将基于图像的 HSV 版本中的颜色分布或色相通道。 因此,我们需要首先使用以下代码将其转换为 HSV 颜色空间:
Mat hsvImg;
cvtColor(inputImage, hsvImg, CV_BGR2HSV);
然后,使用与上一个示例完全相同的方法来计算直方图。 这次(在可视化方面)的主要区别在于,由于直方图是颜色分布,因此直方图还需要显示每个垃圾箱的颜色,否则输出将难以解释。 为了获得正确的输出,这次我们将使用 HSV 到 BGR 的转换来创建一个包含所有箱子的颜色值的缓冲区,然后相应地填充输出条形图中的每个条形。 这是用于在计算出色相通道直方图(或换句话说就是颜色分布图)之后将其正确可视化的源代码:
Mat colors(1, bins, CV_8UC3);
for(int i=0; i<bins; i++)
{
colors.at<Vec3b>(i) =
Vec3b(saturate_cast<uchar>(
(i+1)*180.0/bins), 255, 255);
}
cvtColor(colors, colors, COLOR_HSV2BGR);
Point p1(0,0), p2(0,outputImage.rows-1);
for(int i=0; i<ui->binsSpin->value(); i++)
{
float value = histogram.at<float>(i,0);
value = maxVal - value; // invert
value = value / maxVal * outputImage.rows; // scale
p1.y = value;
p2.x = float(i+1) * float(outputImage.cols) / float(bins);
rectangle(outputImage,
p1,
p2,
Scalar(colors.at<Vec3b>(i)),
CV_FILLED);
p1.x = p2.x;
}
正如我们在前面的代码示例中看到的,maxVal是使用minMaxLoc函数从直方图数据中计算出来的。 bins只是箱子的数量(或直方图大小),在这种情况下不能高于180; 众所周知,色相只能在0和179之间变化。 其余部分几乎相同,除了设置图形中每个条形的填充颜色值。 如果我们在示例玫瑰图像中使用最大箱子大小(即180)执行上述代码,则将获得以下输出:

在此直方图中,基本上所有具有色相精度(八位)的可能颜色都在直方图中考虑,但是我们可以通过减小箱子大小来进一步简化此操作。 24的箱子大小足够小,可以简化并将相似的颜色分组在一起,同时提供足够的精度。 如果将箱子大小更改为24,则会得到以下输出:

通过查看直方图,可以明显看出直方图中24条的前两个(左起)和后两个条是最带红色的颜色。 就像以前一样,我们将简单地限制其他所有内容。 这是如何做:
for(int i=0; i<histogram.rows; i++)
{
if((i==0) || (i==22) || (i==23)) // filter
histogram.at<float>(i,0) = 255;
else
histogram.at<float>(i,0) = 0;
}
一个好的实践案例是创建一个用户界面,该界面允许选择直方图中的箱子并将其过滤掉。 您可以根据自己到目前为止所学的知识,通过使用QGraphicsScene和QGraphicsRectItem绘制条形图和直方图来进行此操作。 然后,您可以启用项目选择,并确保在按下Delete按钮时,条被删除并因此被滤除。
在简单阈值之后,我们可以使用以下代码来计算反投影。 请注意,由于我们的直方图是一维直方图,因此仅当输入图像也是单通道时,才可以使用反向投影重新应用它。 这就是为什么我们首先需要从图像中提取色相通道的原因。 mixChannels函数可用于将通道从一个Mat类复制到另一个。 因此,我们可以使用相同的函数将色相通道从 HSV 图像复制到单通道Mat类中。 仅需要为mixChannels函数提供源和目标Mat类(仅具有相同的深度,不一定是通道),源和目标图像的数量以及一对整数(在以下代码的fromto数组中),用于确定源通道索引和目标通道索引:
Mat hue;
int fromto[] = {0, 0};
hue.create(hsvImg.size(), hsvImg.depth());
mixChannels(&hsvImg, 1, &hue, 1, fromto, 1);
Mat backprojection;
calcBackProject(&hue,
1,
channels,
histogram,
backprojection,
ranges);
在将其转换为 RGB 颜色空间后,使用imshow或 Qt Widget 在输出中直接显示背投图像,您将在玫瑰图像示例中看到我们的红色微调完美遮罩:

现在,如果我们将色相通道中的值偏移正确的数量,则可以从红色玫瑰中得到蓝色玫瑰; 不仅是相同的静态蓝色,而且在所有相应像素中具有正确的阴影和亮度值。 如果返回本章前面创建的色谱图像输出,您会注意到红色,绿色,蓝色和红色再次与色相值0,120,240和360完全一致。 当然,再次,如果我们考虑除以二(因为360不能适合一个字节,但是180可以适合),它们实际上是0,60,120和180。 这意味着,如果我们要在色调通道中移动红色以获得蓝色,则必须将其偏移120,并且类似地要转换以获得其他颜色。 因此,我们可以使用类似的方法正确地改变颜色,并且只能在之前的背投图像突出显示的像素中进行。 请注意,我们还需要注意溢出问题,因为最高的色相值应为179,且不能大于:
for(int i=0; i<hsvImg.rows; i++)
{
for(int j=0; j<hsvImg.cols; j++)
{
if(backprojection.at<uchar>(i, j))
{
if(hsvImg.at<Vec3b>(i,j)[0] < 60)
hsvImg.at<Vec3b>(i,j)[0] += 120;
else if(hsvImg.at<Vec3b>(i,j)[0] > 120)
hsvImg.at<Vec3b>(i,j)[0] -= 60;
}
}
}
Mat imgHueShift;
cvtColor(hsvImg, imgHueShift, CV_HSV2BGR);
通过执行前面的代码,我们将获得下面的结果图像,它是从红色像素变为蓝色的图像转换回的 RGB 图像:

对于不同的柱状图大小,请尝试相同的操作。 另外,作为练习,您可以尝试构建适当的 GUI 以进行色移。 您甚至可以尝试编写一个程序,该程序可以将图像中具有特定颜色(精确的颜色直方图)的对象更改为其他颜色。 电影和照片编辑程序中广泛使用了一种非常相似的技术来改变图像或连续视频帧中特定区域的颜色(色相)。
直方图比较
使用calcHist函数计算出的两个直方图,或者从磁盘加载并填充到Mat类中的直方图,或者使用任何方法按字面意义创建的两个直方图,都可以相互比较以找出它们之间的距离或差异(或差异), 通过使用compareHist方法。 请注意,只要直方图的Mat结构与我们之前看到的一致(即列数,深度和通道),就可以实现。
compareHist函数采用存储在Mat类中的两个直方图和comparison方法,它们可以是以下常量之一:
HISTCMP_CORRELHISTCMP_CHISQRHISTCMP_INTERSECTHISTCMP_BHATTACHARYYAHISTCMP_HELLINGERHISTCMP_CHISQR_ALTHISTCMP_KL_DIV
请注意,compareHist函数的返回值以及应如何解释完全取决于comparison方法,它们的变化很大,因此请务必查看 OpenCV 文档页面以获取详细的列表。 每种方法中使用的基础比较方程。 这是示例代码,可使用所有现有方法来计算两个图像(或两个视频帧)之间的差异:
Mat img1 = imread("d:/dev/Packt/testbw1.jpg", IMREAD_GRAYSCALE);
Mat img2 = imread("d:/dev/Packt/testbw2.jpg", IMREAD_GRAYSCALE);
float range[] = {0, 255};
const float* ranges[] = {range};
int bins[] = {100};
Mat hist1, hist2;
calcHist(&img1, 1, 0, Mat(), hist1, 1, bins, ranges);
calcHist(&img2, 1, 0, Mat(), hist2, 1, bins, ranges);
qDebug() << compareHist(hist1, hist2, HISTCMP_CORREL);
qDebug() << compareHist(hist1, hist2, HISTCMP_CHISQR);
qDebug() << compareHist(hist1, hist2, HISTCMP_INTERSECT);
// Same as HISTCMP_HELLINGER
qDebug() << compareHist(hist1, hist2, HISTCMP_BHATTACHARYYA);
qDebug() << compareHist(hist1, hist2, HISTCMP_CHISQR_ALT);
qDebug() << compareHist(hist1, hist2, HISTCMP_KL_DIV);
我们可以在以下两个图像上尝试前面的代码:

比较的结果可以在 Qt Creator 输出中查看,如下所示:
-0.296291
1.07533e+08
19811
0.846377
878302
834340
通常,通常使用直方图差异来比较图像。 还可以在视频帧中使用类似的技术来检测与场景或场景中存在的对象的差异。 因此,应该存在一个预先准备好的直方图,然后将其与每个传入视频帧的直方图进行比较。
直方图均衡
图像的直方图可用于调整图像的亮度和对比度。 OpenCV 提供了一个称为equalizeHist的函数,该函数在内部计算给定图像的直方图,对直方图进行归一化,计算直方图的积分(所有仓位的总和),然后使用更新后的直方图作为查找表来更新输入图像的像素,导致输入图像中的亮度和对比度标准化。 使用此函数的方法如下:
equalizeHist(image, equalizedImg);
如果您在亮度不适当或收缩的图像上尝试使用此函数,则将在亮度和对比度方面将它们自动调整到视觉上更好的水平。 此过程称为直方图均衡。 以下示例显示两个亮度级别太低或太高的图像及其直方图,它们显示相应的像素值分布。 左侧的图像是使用equalizeHist函数生成的,对于左侧的两个图像,它看起来或多或少都是相同的。 注意输出图像的直方图中的变化,这反过来会导致图像更具视觉吸引力:

大多数数码相机使用类似的技术来根据像素在整个图像中的分布量来调整像素的暗度和亮度。 您也可以在任何常见的智能手机上尝试此操作。 只需将相机对准明亮的区域,智能手机上的软件就会开始降低亮度,反之亦然。
MeanShift 和 CamShift
到目前为止,我们在本章中学到的知识除了已经看到的用例之外,还旨在为我们正确使用 MeanShift 和 CamShift 算法做准备,因为它们从直方图和反投影图像中受益匪浅。 但是,MeanShift 和 CAMShift 算法是什么?
让我们从 MeanShift 开始,然后继续进行 CamShift,它基本上是同一算法的增强版本。 因此,MeanShift 的一个非常实用的定义(如当前 OpenCV 文档中所述)如下:
在反投影图像上找到对象
这是对 MeanShift 算法的一个非常简单但实用的定义,并且在使用它时我们将或多或少地坚持下去。 但是,值得注意的是底层算法,因为它有助于轻松,高效地使用它。 为了开始描述 MeanShift 的工作原理,首先,我们需要将反投影图像(或通常为二进制图像)中的白色像素视为二维平面上的分散点。 那应该很容易。 以此为借口,我们可以说,MeanShift 实际上是一种迭代方法,用于查找点在分布点的平面上最密集的位置。 该算法具有一个初始窗口(指定整个图像一部分的矩形),该窗口用于搜索质心,然后将窗口中心移动到新找到的质心。 重复查找质量中心并使窗口中心偏移的过程,直到所需的偏移小于提供的阈值(ε)或达到最大迭代次数为止。 下图显示了在 MeanShift 算法中每次迭代之后窗口移动到最密集的位置(或者甚至在达到迭代计数之前,甚至之前)移动窗口的方式:

基于此,通过确保在每个帧的反投影中区分对象,MeanShift 算法可用于跟踪视频中的对象。 当然,为此,我们需要使用与之前相似的阈值方法。 最常见的方法是应用已经准备好的直方图,并使用它来计算反投影(在我们之前的示例中,我们只是修改了输入直方图)。 让我们通过一个示例逐步进行此操作。 因此,我们将创建一个QThread子类,可以在任何独立的 Qt 应用中创建该子类,也可以在 DLL 或插件中使用该子类,这将用于computer_vision项目。 无论如何,对于所有项目类型,此线程将保持完全相同。
如第 8 章,“多线程处理”中所讨论的,处理视频应在单独的线程中完成(如果我们不希望找到任何丑陋的解决方法),以使它不会阻塞 GUI 线程,并且可以自由地响应用户的操作。 请注意,该相同线程也可以用作创建任何其他(相似)视频处理线程的模板。 因此,让我们开始:
- 我们将创建一个 Qt 窗口小部件应用,该应用可以跟踪一个对象(具有任何颜色,但在这种情况下不是完全白色或黑色),该对象最初将使用鼠标,相机的实时供稿并使用 MeanShift 算法进行选择。 在初始选择之后的任何时候,我们都可以再次从摄像机的实时供稿中更改到场景中的另一个对象。 第一次选择对象时,然后每次更改选择时,将提取视频帧的色相通道,并使用直方图和反投影图像计算并提供给 MeanShift 算法,并且该对象将被跟踪。 因此,我们需要首先创建一个 Qt Widgets 应用并为其命名,例如
MeanShiftTracker,然后继续实际的跟踪器实现。 - 正如我们在第 8 章,“多线程”中了解的那样,创建一个
QThread子类。 将其命名为QCvMeanShiftThread,并确保相应地在私有和公共成员区域中包括以下内容。 我们将使用setTrackRect函数通过此函数设置初始MeanShift跟踪窗口,但还将使用此函数提供将跟踪更改为另一个对象的方法。newFrame非常明显,它将在处理完每帧后发出,以便 GUI 可以显示它。 使用私有区域和 GUI 的成员将在后面的步骤中进行描述,但是它们包含了到目前为止我们已经了解的一些最重要的主题:
public slots:
void setTrackRect(QRect rect);
signals:
void newFrame(QPixmap pix);
private:
void run() override;
cv::Rect trackRect;
QMutex rectMutex;
bool updateHistogram;
setTrackRect函数只是用于更新我们希望 MeanShift 算法跟踪的矩形(初始窗口)的setter函数。 这是应如何实现:
void QCvMeanShiftThread::setTrackRect(QRect rect)
{
QMutexLocker locker(&rectMutex);
if((rect.width()>2) && (rect.height()>2))
{
trackRect.x = rect.left();
trackRect.y = rect.top();
trackRect.width = rect.width();
trackRect.height = rect.height();
updateHistogram = true;
}
}
QMutexLocker与rectMutex一起用于为我们的trackRect提供访问序列化。 由于我们还将以一种实时工作的方式实现跟踪方法,因此我们需要确保在处理trackRect时不会对其进行更新。 我们还确保其大小合理,否则将被忽略。
- 至于我们的跟踪器线程的
run函数,我们需要使用VideoCapture打开计算机上的默认相机并向我们发送帧。 请注意,如果框架为空(损坏),相机关闭或从线程外部请求线程中断,则循环将退出:
VideoCapture video;
video.open(0);
while(video.isOpened() && !this->isInterruptionRequested())
{
Mat frame;
video >> frame;
if(frame.empty())
break;
// rest of the process ...
....
}
在循环内,将其标记为rest of the process ...,首先,我们将使用cv::Rect类的area函数来查看trackRect是否已设置。 如果是,那么我们将锁定访问权限并继续进行跟踪操作:
if(trackRect.size().area() > 0)
{
QMutexLocker locker(&rectMutex);
// tracking code
}
至于 MeanShift 算法和真实跟踪,我们可以使用以下源代码:
Mat hsv, hue, hist;
cvtColor(frame, hsv, CV_BGR2HSV);
hue.create(hsv.size(), hsv.depth());
float hrange[] = {0, 179};
const float* ranges[] = {hrange};
int bins[] = {24};
int fromto[] = {0, 0};
mixChannels(&hsv, 1, &hue, 1, fromto, 1);
if(updateHistogram)
{
Mat roi(hue, trackRect);
calcHist(&roi, 1, 0, Mat(), hist, 1, bins, ranges);
normalize(hist,
hist,
0,
255,
NORM_MINMAX);
updateHistogram = false;
}
Mat backProj;
calcBackProject(&hue,
1,
0,
hist,
backProj,
ranges);
TermCriteria criteria;
criteria.maxCount = 5;
criteria.epsilon = 3;
criteria.type = TermCriteria::EPS;
meanShift(backProj, trackRect, criteria);
rectangle(frame, trackRect, Scalar(0,0,255), 2);
上面的代码按照以下顺序执行以下操作:
- 使用
cvtColor函数将输入帧从 BGR 转换为 HSV 色彩空间。 - 使用
mixChannels函数仅提取色调通道。 - 如果需要,可以使用
calcHist和normalize函数计算并归一化直方图。 - 使用
calcBackproject函数计算反投影图像。 - 通过提供迭代标准,在背投图像上运行 MeanShift 算法。 这是通过
TermCriteria类和meanShift函数完成的。meanShift会简单地更新提供的矩形(trackRect每帧有一个新矩形)。 - 在原始图像上绘制检索到的矩形。
除了TermCriteria类和meanShift函数本身之外,您刚刚看到的任何代码中都没有新内容。 如前所述,MeanShift 算法是一种迭代方法,需要根据移位量(ε)和迭代次数来确定一些停止条件。 简而言之,增加迭代次数可以减慢算法的速度,但也可以使其更加准确。 另一方面,提供较小的ε值将意味着更加敏感的行为。
在处理完每个帧之后,线程仍需要使用专用信号将其发送到另一个类。 方法如下:
emit newFrame(
QPixmap::fromImage(
QImage(
frame.data,
frame.cols,
frame.rows,
frame.step,
QImage::Format_RGB888)
.rgbSwapped()));
请注意,除了发送QPixmap或QImage等,我们还可以发送不是QObject子类的类。 为了能够通过 Qt 信号发送非 Qt 类,它必须具有公共默认构造器,公共副本构造器和公共析构器。 还需要先注册。 例如,Mat类包含必需的方法,但不是已注册的类型,因此可以按如下所示进行注册:qRegisterMetaType<Mat>("Mat");。 之后,您可以在 Qt 信号和插槽中使用Mat类。
- 除非我们完成此线程所需的用户界面,否则仍然看不到任何结果。 让我们用
QGraphicsView来做。 只需使用设计器将一个拖放到mainwindow.ui上,然后将以下内容添加到mainwindow.h中。 我们将使用QGraphicsView类的橡皮筋功能轻松实现对象选择:
private:
QCvMeanShiftThread *meanshift;
QGraphicsPixmapItem pixmap;
private slots:
void onRubberBandChanged(QRect rect,
QPointF frScn, QPointF toScn);
void onNewFrame(QPixmap newFrm);
- 在
mainwindow.cpp文件和MainWindow类的构造器中,确保添加以下内容:
ui->graphicsView->setScene(new QGraphicsScene(this));
ui->graphicsView->setDragMode(QGraphicsView::RubberBandDrag);
connect(ui->graphicsView,
SIGNAL(rubberBandChanged(QRect,QPointF,QPointF)),
this,
SLOT(onRubberBandChanged(QRect,QPointF,QPointF)));
meanshift = new QCvMeanShiftThread();
connect(meanshift,
SIGNAL(newFrame(QPixmap)),
this,
SLOT(onNewFrame(QPixmap)));
meanshift->start();
ui->graphicsView->scene()->addItem(&pixmap);
在第 5 章,“图形视图框架”中详细讨论了如何使用 Qt 图形视图框架。
- 还应确保在关闭应用时注意线程,如下所示:
meanshift->requestInterruption();
meanshift->wait();
delete meanshift;
- 剩下的唯一事情就是在 GUI 本身上设置传入的
QPixmap,并且还传递更新被跟踪对象所需的矩形:
void MainWindow::onRubberBandChanged(QRect rect,
QPointF frScn,
QPointF toScn)
{
meanshift->setTrackRect(rect);
}
void MainWindow::onNewFrame(QPixmap newFrm)
{
pixmap.setPixmap(newFrm);
}
尝试运行该应用并选择一个在相机上可见的对象。 使用鼠标在图形视图上绘制的矩形将跟随您选择的对象,无论它在屏幕上的任何位置。 这是从视图中选择 Qt 徽标后对其进行跟踪的一些屏幕截图:

可视化反投影图像并查看幕后发生的魔术也是一个好主意。 请记住,如前所述,MeanShift 算法正在搜索质心,当在反投影图像中观察时,这很容易感知。 只需用以下代码替换我们用于可视化线程内图像的最后几行:
cvtColor(backProj, backProj, CV_GRAY2BGR);
frame = backProj;
rectangle(frame, trackRect, Scalar(0,0,255), 2);
现在再试一次。 您应该在图形视图中具有反投影图像:

从结果可以看出,MeanShift 算法或精确的meanShift函数非常易于使用,只要为其提供灰度图像即可,该图像可以使用任何阈值方法隔离感兴趣的对象。 是的,反投影也类似于阈值设置,在该阈值设置中,您可以基于颜色,强度或其他条件让某些像素通过或某些其他像素不通过。 现在,如果我们回到 MeanShift 算法的初始描述,完全可以说它可以基于反投影图像找到并跟踪对象。
尽管meanShift函数易于使用,但它仍然缺少几个非常重要的功能。 这些是对被跟踪对象的比例和方向更改的容限。 无论对象的大小或其方向如何,camShift函数都将提供一个大小和旋转度完全相同的窗口,而该窗口只是试图以目标对象为中心。 这些问题在 MeanShift 算法的增强版本中得以解决,该增强版本称为连续自适应 MeanShift 算法,或简称为 CamShift。
CamShift函数是 OpenCV 中 CamShift 算法的实现,与 MeanShift 算法有很多共同之处,并且出于同样的原因,它的使用方式几乎相同。 为了证明这一点,只需将前面代码中对meanShift算法的调用替换为CamShift即可,如下所示:
CamShift(backProj, trackRect, criteria);
如果再次运行该程序,您会发现什么都没有真正改变。 但是,此函数还提供RotatedRect类型的返回值,该返回值基本上是矩形,但具有中心,大小和角度属性。 您可以保存返回的RotatedRect并将其绘制在原始图像上,如下所示:
RotatedRect rotRec = CamShift(backProj, trackRect, criteria);
rectangle(frame, trackRect, Scalar(0,0,255), 2);
ellipse(frame, rotRec, Scalar(0,255,0), 2);
请注意,我们实际上在这段代码中绘制了一个适合RotatedRect类属性的椭圆。 我们还绘制了先前存在的矩形,以便与旋转的矩形进行比较。 如果您尝试再次运行该程序,则结果如下:

请注意,绿色椭圆相对于红色矩形的旋转是CamShift函数的结果。 尝试将要跟踪的彩色物体移离相机或靠近相机,然后查看CamShift如何尝试适应这些变化。 另外,尝试使用非正方形物体观察CamShift提供的旋转不变跟踪。
CamShift函数还可以用于根据物体的颜色检测物体。 当然,如果可以与周围环境区分开。 因此,您需要设置一个预先准备好的直方图,而不是像我们的示例那样在运行时设置它。 您还需要将初始窗口大小设置为很大的窗口大小,例如整个图像的大小,或图像中预期将出现对象的最大区域。 通过运行相同的代码,您会注意到,在每一帧之后,窗口将变得越来越小,直到仅覆盖我们为其提供直方图的目标对象为止。
背景/前景检测
背景/前景检测或分割(由于很好的原因通常也称为背景减法)是一种区分图像(前景)中移动区域或变化区域的方法,而不是或多或少的恒定或静态区域(背景)。 该方法在检测图像中的运动时也非常有效。 OpenCV 包括许多不同的背景扣除方法,默认情况下,当前的 OpenCV 安装中提供了两种方法,即BackgroundSubtractorKNN和BackgroundSubtractorMOG2。 与我们在第 7 章,“特征和描述符”中了解到的特征检测器类相似,这些类也源自cv::Algorithm类,并且它们都非常容易且相似地使用,因为它们的用法或结果不同,而在类的实现方面不同。
BackgroundSubtractorMOG2可以通过使用高斯混合模型来检测背景/前景。 另一方面,通过使用 KNN 或 K 最近邻方法,BackgroundSubtractorKNN也可以用于实现相同的目标。
如果您对这些算法的内部细节或如何实现感兴趣,可以参考以下文章以获取更多信息:
Zoran Zivkovic and Ferdinand van der Heijden. Efficient adaptive density estimation per image pixel for the task of background subtraction. Pattern recognition letters, 27(7):773-780, 2006.
Zoran Zivkovic. Improved adaptive gaussian mixture model for background subtraction. In Pattern Recognition, 2004. ICPR 2004. Proceedings of the 17th International Conference on, volume 2, pages 28-31. IEEE, 2004.
首先让我们看看它们是如何使用的,然后再介绍它们的一些重要功能。 与上一节中创建的QCvMeanShiftThread类相似,我们可以通过将QThread子类化来创建新线程。 将其命名为QCvBackSubThread或您认为合适的任何名称。 唯一有区别的部分是覆盖的run函数,它看起来如下所示:
void QCvBackgroundDetect::run()
{
using namespace cv;
Mat foreground;
VideoCapture video;
video.open(0);
Ptr<BackgroundSubtractorMOG2> subtractor =
createBackgroundSubtractorMOG2();
while(video.isOpened() && !this->isInterruptionRequested())
{
Mat frame;
video >> frame;
if(frame.empty())
break; // or continue if this should be tolerated
subtractor->apply(frame, foreground);
Mat foregroundBgr;
cvtColor(foreground, foregroundBgr, CV_GRAY2BGR);
emit newFrame(
QPixmap::fromImage(
QImage(
foregroundBgr.data,
foregroundBgr.cols,
foregroundBgr.rows,
foregroundBgr.step,
QImage::Format_RGB888)
.rgbSwapped()));
}
}
请注意,背景减法所需的唯一调用是BackgroundSubtractorMOG2类的构造并调用apply函数。 就使用它们而言,仅此而已,这使它们非常简单易用。 在每帧,根据图像所有区域的变化历史更新前景,即Mat类。 由于我们只是通过调用createBackgroundSubtractorMOG2函数使用了默认参数,因此我们没有更改任何参数,而是继续使用默认值,但是如果要更改算法的行为,我们需要为此提供以下参数:
history(默认设置为 500)是影响背景减法算法的最后一帧的数量。 在我们的示例中,我们还在 30 FPS 摄像机或视频上使用了大约 15 秒的默认值。 这意味着,如果一个区域在过去 15 秒钟内完全未变,则它将完全变黑。varThreshold(默认设置为 16)是算法的差异阈值。detectShadows(默认设置为true)可用于忽略或计数检测阴影变化。
尝试运行前面的示例程序,该程序使用默认参数并观察结果。 如果镜头前没有任何动作,您应该会看到一个全黑的屏幕,但是即使很小的移动也可以被视为输出上的白色区域。 您应该会看到以下内容:

切换到BackgroundSubtractorKNN类非常容易,您只需要用以下内容替换构造线:
Ptr<BackgroundSubtractorKNN> subtractor =
createBackgroundSubtractorKNN();
没什么需要改变的。 但是,要修改此算法的行为,可以使用以下参数,其中一些参数也与BackgroundSubtractorMOG2类共享:
history与之前的算法完全相同。detectShadows,也与先前的算法相同。dist2Threshold默认情况下设置为400.0,并且是像素与样本之间平方距离的阈值。 为了更好地理解这一点,最好在线查看 K 最近邻算法。 当然,您可以简单地使用默认值并使用算法,而无需提供任何参数。
试用各种参数并观察结果,没有什么可以帮助您提高使用这些算法的效率。 例如,您会注意到增加历史值将有助于检测甚至更小的运动。 尝试更改其余参数,以自己观察和比较结果。
在前面的示例中,我们尝试输出通过使用背景减法类提取的前景遮罩图像。 您还可以在copyTo函数中使用相同的前景遮罩,以输出前景的实际像素。 这是如何做:
frame.copyTo(outputImage, foreground);
其中frame是相机输入的帧,foreground是通过背景减法算法获得的,与前面的示例相同。 如果尝试显示输出图像,则将具有以下类似内容:

请注意,此处看到的输出是移动摄像机的结果,与移动视频中的对象基本相同。 但是,如果您在视频中尝试在静态背景上四处移动其他任何彩色对象的视频中使用同一示例,则可以使用 CamShift 算法在移动对象周围获取一个边界框以提取该对象,或由于任何原因对其进行进一步处理。
使用 OpenCV 中的现有视频分析类编写应用的机会是巨大的,这仅取决于您对使用它们的熟悉程度。 例如,通过使用背景减除算法,您可以尝试编写运行警报的应用,或者在检测到运动时执行另一个过程。 可以通过测量在前面的示例中看到的提取的前景图像中像素的总和或平均值,然后检测超过某些阈值的突然增加,来轻松地完成类似的操作。 我们甚至无法开始列举所有可能性,但可以肯定的是,您是混合使用这些算法来解决特定任务的大师,并且包括该书在内的任何指南都只是您如何操作的路标集合。 使用现有算法。
总结
编写执行实时图像处理的计算机视觉应用是当今的热门话题,并且 OpenCV 包含许多类和函数来帮助简化此类应用的开发。 在本章中,我们试图介绍 OpenCV 提供的一些最重要的类和函数,这些类和函数用于实时处理视频和图像。 我们了解了 OpenCV 中的 MeanShift,CamShift 和背景减法算法,这些算法打包在快速高效的类中,同时,它们非常易于使用,前提是您熟悉大多数语言中使用的基本概念 ,例如直方图和反投影图像。 这就是为什么我们首先要学习所有有关直方图的知识,以及如何进行计算,可视化和相互比较。 我们还学习了如何计算反投影图像并将其用作查找表以更新图像。 我们在 MeanShift/CamShift 算法中也使用了相同的算法来跟踪特定颜色的对象。 到现在为止,我们应该能够高效地编写基于其中的零件和零件运动来处理视频和图像的应用。
本章是最后一章,我们将介绍 OpenCV 和 Qt 框架的详细信息。 一本书,甚至一本书,永远都不足以覆盖 OpenCV 和 Qt 框架中的所有现有材料,但是我们试图以一种可以跟进其余部分的方式来呈现整个情况的概述。 现有的类和函数可以自己开发有趣的计算机视觉应用。 确保与 OpenCV 和 Qt 框架的新开发保持同步,因为它们正在开展并吸引着正在进行中的项目,并且进展似乎不会很快停止。
本书的下一章将专门介绍如何调试,测试和部署 Qt 和 OpenCV 应用并将其部署给用户。 我们将首先了解 Qt Creator 的调试功能,然后继续使用 Qt Test 命名空间及其基础功能,这些功能可用于轻松进行 Qt 应用的单元测试。 在下一章中,我们还将介绍 Qt 安装程序框架,甚至为应用创建一个简单的安装程序。
十、调试与测试
自从使用 OpenCV 3 和 Qt5 框架进行计算机视觉之旅以来,我们已经走了很长一段路。 现在,我们可以非常轻松地安装这些强大的框架,并配置运行 Windows,MacOS 或 Linux 操作系统的计算机,以便我们可以设计和构建计算机视觉应用。 在前几章中,我们学习了如何使用 Qt 插件系统来构建模块化和基于插件的应用。 我们学习了如何使用 Qt 样式表对应用进行样式设置,以及如何使用 Qt 中的国际化技术使其支持多种语言。 我们使用 Qt 图形视图框架构建了功能强大的图形查看器应用。 该框架中的类帮助我们更加有效地显示图形项目,并具有更大的灵活性。 我们能够构建可以放大和缩小图像的图形查看器,而不必处理源图像本身(这要归功于场景-视图-项目架构)。 后来,我们开始更深入地研究 OpenCV 框架,并且了解了许多类和函数,这些类和函数使我们能够以多种方式转换图像并对其进行处理,以实现特定的计算机视觉目标。 我们学习了用于检测场景中对象的特征检测和描述符提取。 我们浏览了 OpenCV 中的许多现有算法,这些算法旨在以更加智能的方式处理图像内容,而不仅仅是原始像素值。 在最近的章节中,我们了解了 Qt 提供的多线程和线程同步工具。 我们了解了 Qt 框架提供的用于处理应用中多线程的低级(QThread)和高级(QtConcurrent)技术,而与平台无关。 最后,在上一章中,我们学习了视频的实时图像处理以及可以跟踪具有特定颜色的对象的 OpenCV 算法。 到现在为止,我们应该以这样一种方式熟悉 Qt 和 OpenCV 框架的许多方面:我们自己可以跟进更高级的主题,并且仅依赖于文档。
除了前面提到的所有内容以及在前几章中我们取得的成就的一长串清单之外,我们仍然没有谈论软件开发的一个非常重要的方面以及在与 Qt 和 OpenCV 一起工作时如何处理软件,即测试过程。 在将计算机程序部署到该应用的用户之前,无论该程序是简单的小型二进制文件,大型计算机视觉应用还是任何其他应用,都必须经过测试。 测试是开发过程中一个永无止境的阶段,它是在开发应用后立即进行的,并且时不时地解决问题或添加新功能。 在本章中,我们将学习现有技术来测试使用 Qt 和 OpenCV 构建的应用。 我们将学习开发时间测试和调试。 我们还将学习如何使用 Qt 测试框架对应用进行单元测试。 在将应用交付给最终用户之前,这是最重要的过程。
我们将在本章中介绍的主题如下:
- Qt Creator 的调试功能
- 如何使用 Qt 测试命名空间进行单元测试
- 数据驱动的测试
- GUI 测试和重放 GUI 事件
- 创建测试用例项目
将 Qt Creator 用于调试
调试器是一种程序,在程序执行过程中突然崩溃或程序逻辑中发生意外行为时,可用于测试和调试其他程序。 在大多数情况下(如果不是总是),调试器用于开发环境中,并与 IDE 结合使用。 在我们的案例中,我们将学习如何在 Qt Creator 中使用调试器。 重要的是要注意,调试器不是 Qt 框架的一部分,并且像编译器一样,它们通常由操作系统 SDK 提供。 如果系统中存在调试器,则 Qt Creator 会自动检测并使用调试器。 可以通过依次通过主菜单“工具”和“选项”进入“Qt Creator 选项”页面进行检查。 确保从左侧列表中选择Build&Run,然后从顶部切换到Debuggers选项卡。 您应该能够在列表上看到一个或多个自动检测到的调试器。
Windows 用户:此信息框后,您应该会看到类似于屏幕截图的内容。 如果没有,则意味着您尚未安装任何调试器。 您可以按照此处提供的说明轻松下载并安装它。
或者,您可以独立地在线搜索以下主题:
Windows 调试工具(WinDbg,KD,CDB,NTSD)。
但是,在安装调试器之后(假定是 Microsoft Visual C++ 编译器的 CDB 或 Microsoft 控制台调试器,以及 GCC 编译器的 GDB),您可以重新启动 Qt Creator 并返回此页面。 您应该可以具有一个或多个类似于以下内容的条目。 由于我们已经安装了 32 位版本的 Qt 和 OpenCV 框架,因此选择名称中带有 x86 的条目以查看其路径,类型和其他属性。
MacOS 和 Linux 用户:
不需要执行任何操作,根据操作系统,您会看到 GDB,LLDB 或其他调试器中的条目。
这是“选项”页面上“构建和运行”选项卡的屏幕截图:

根据操作系统和已安装的调试器的不同,前面的屏幕快照可能会略有不同。 但是,您将需要一个调试器,以确保已正确设置为所用 Qt Kit 的调试器。 因此,记下调试器的路径和名称,并切换到 Kits 选项卡,然后在选择了所用的 Qt Kit 后,请确保正确设置了调试器,如以下屏幕快照所示:

不必担心选择错误的调试器或任何其他选项,因为在顶部选择的 Qt Kit 图标旁边会警告您相关的图标。 当工具包一切正常时,通常会显示下图所示的图标,左侧的第二个图标表示有问题的不正确,右侧的图标表示严重错误。 将鼠标移到该图标上时,可以查看有关解决该问题所需的详细操作的更多信息:

Qt 套件的关键问题可能是由许多不同的因素引起的,例如缺少编译器,这将使套件在解决问题之前完全无用。 Qt 套件中的警告消息示例可能是缺少调试器,这不会使套件无用,但您将无法将其与调试器一起使用,因此,与完全配置的 Qt 套件相比,它意味着功能更少。
正确设置调试器后,您可以采用以下几种方法之一开始调试应用,这些方法基本上具有相同的结果:最终进入 Qt Creator 的调试器视图:
- 在调试模式下启动应用
- 附加到正在运行的应用(或进程)
请注意,可以通过多种方式来启动调试过程,例如通过将其附加到在另一台计算机上运行的过程中来远程启动。 但是,上述方法在大多数情况下就足够了,尤其是与 Qt + OpenCV 应用开发以及我们在本书中学到的内容有关的情况。
调试模式入门
要在调试模式下启动应用,请在打开 Qt 项目后使用以下方法之一:
- 按下
F5按钮 - 使用“开始调试”按钮,在通常的“运行”按钮下,带有类似图标,但上面有一个小错误
- 按以下顺序使用主菜单项:调试/开始调试/开始调试
要将调试器附加到正在运行的应用,可以按以下顺序使用主菜单项:调试/启动调试/附加到正在运行的应用。 这将打开“进程列表”窗口,从中可以使用其进程 ID 或可执行文件名选择应用或要调试的任何其他进程。 您还可以使用“过滤器”字段(如下图所示)来找到您的应用,因为很有可能进程列表很长。 选择正确的过程后,请确保按下“附加到过程”按钮。

无论使用哪种方法,都将最终进入 Qt Creator 调试模式,该模式与“编辑”模式非常相似,但是它还可以执行以下操作:
- 在代码中添加,启用,禁用和查看断点(断点只是我们希望调试器在过程中暂停的代码中的点或线,并允许我们对程序状态进行更详细的分析 )
- 中断正在运行的程序和进程以查看和检查代码
- 查看和检查函数调用栈(调用栈是一个包含导致断点或中断状态的函数的层次结构列表的栈)
- 查看和检查变量
- 反汇编源代码(从这种意义上来说,反汇编意味着提取与我们程序中的函数调用和其他 C++ 代码相对应的确切指令)
在调试模式下启动应用时,您会注意到性能下降,这显然是因为调试器正在监视和跟踪代码。 这是 Qt Creator 调试模式的屏幕截图,其中前面提到的所有功能都可以在单个窗口中以及在 Qt Creator 的调试模式下看到:

您在本书中已经使用并且非常熟悉的代码编辑器中的上一个屏幕快照中用数字1指定的区域。 每行代码都有一个行号; 您可以单击其左侧以在代码中所需的任何位置切换断点。 您还可以右键单击行号以设置,删除,禁用或启用断点,方法是选择“在行 X 处设置断点”,“删除断点 X”,“禁用断点 X”或“启用断点 X”,其中所有提到的命令中的 X 这里需要用行号代替。 除了代码编辑器,您还可以使用前面的屏幕快照中编号为4的区域来添加,删除,编辑和进一步修改代码中的断点。
在代码中设置断点后,只要程序到达代码中的该行,它将被中断,并且您将被允许使用代码编辑器正下方的控件来执行以下任务:
- 继续:这意味着继续执行程序的其余流程(或再次按
F5)。 - 步过:用于执行下一步(代码行),而无需进入函数调用或类似的代码,这些代码可能会更改调试光标的当前位置。 请注意,调试游标只是正在执行的当前代码行的指示器。 (这也可以通过按
F10来完成。) - 单步执行:与单步执行相反,它可以用于进一步深入函数调用,以更详细地分析代码和调试。 (与按
F11相同。) - 退出:可用于退出函数调用并在调试时返回到调用点。 (与按
Shift + F11相同。)
您也可以右键单击包含调试器控件的代码编辑器下方的同一工具栏,以打开以下菜单,并添加或删除更多窗格以显示其他调试和分析信息。 我们将介绍默认的调试器视图,但请确保自行检查以下每个选项,以进一步熟悉调试器:

在前面的代码中用数字2指定的区域可用于查看调用栈。 无论您是通过按“中断”按钮还是在运行时从菜单中选择“调试/中断”来中断程序,设置断点并在特定代码行中停止程序,还是发生故障的代码都会导致程序陷入陷阱,并暂停该过程(因为调试器将捕获崩溃和异常),您始终可以查看导致中断状态的函数调用的层次结构,或者通过检查前面 Qt Creator 屏幕截图中的区域 2 来进一步分析它们。
最后,您可以使用上一个屏幕快照中的第三个区域在代码中被中断的位置查看程序的局部变量和全局变量。 您可以查看变量的内容,无论它们是标准数据类型(例如整数和浮点数还是结构和类),还可以进一步扩展和分析其内容以测试和分析代码中的任何可能问题。
有效地使用调试器可能意味着数小时的测试和解决您的代码中的问题。 就调试器的实际使用而言,实际上没有别的方法,只有尽可能多地使用它,并养成使用调试器的习惯,而且还要记下您在使用过程中发现的良好做法和技巧, 我们刚刚经历的那些。 如果您有兴趣,还可以在线阅读有关其他可能的调试方法的信息,例如远程调试,使用故障转储文件的调试(在 Windows 上)等。
Qt 测试框架
在开发应用时进行调试和测试是完全不可避免的,但是许多开发人员往往会错过的一件事就是进行单元测试,这一点尤为重要,尤其是在大型项目和难以手动进行全面测试的应用中。 在构建它们的时间或在其代码中的某个位置修复了错误。 单元测试是一种测试应用中的零件(单元)以确保它们按预期工作的方法。 还值得注意的是,测试自动化是当今软件开发的热门话题之一,它是使用第三方软件或编程来自动化单元测试的过程。
在本节中,我们将学习精确使用 Qt 测试框架(即 Qt 测试命名空间)(以及一些其他与测试相关的类)的知识,这些知识可用于为使用 Qt 构建的应用开发单元测试。 与第三方测试框架相反,Qt 测试框架是内部(基于 Qt 框架本身)和轻量级测试框架,并且在其众多功能中,它提供基准测试,数据驱动的测试和 GUI。 测试:基准测试可用于衡量函数或特定代码段的性能,而数据驱动的测试可帮助运行使用不同数据集作为输入的单元测试。 另一方面,可以通过模拟鼠标和键盘的交互来进行 GUI 测试,这又是 Qt 测试框架涵盖的另一个方面。
创建单元测试
可以通过子类化QObject类并添加 Qt 测试框架所需的插槽以及一个或多个用于执行各种测试的插槽(测试函数)来创建单元测试。 下列插槽(专用插槽)可以存在于每个测试类中,并且除了测试函数外,还可以由 Qt Test 调用:
initTestCase:在调用第一个测试函数之前调用它。 如果此函数失败,则整个测试将失败,并且不会调用任何测试函数。cleanupTestCase:在调用最后一个测试函数后调用。init:在调用每个测试函数之前调用它。 如果此函数失败,将不会执行前面的测试函数。cleanup:在调用每个测试函数后调用。
让我们用一个真实的例子创建我们的第一个单元测试,看看如何将刚才提到的函数添加到测试类中,以及如何编写测试函数。 为了确保我们的示例是现实的并且易于同时进行,我们将避免过多地担心要测试的类的实现细节,而主要关注于如何测试它们。 基本上,相同的方法可用于测试具有任何级别复杂性的任何类。
因此,作为第一个示例,我们假设我们有一个返回图像像素数量(宽度乘以图像高度)的类,并且我们想使用单元测试进行测试:
- 可以使用 Qt Creator 创建单元测试,类似于创建 Qt 应用或库,也可以在“欢迎”模式下使用“新建项目”按钮,或者从“文件”菜单中选择“新建文件”或“项目”来创建单元测试。 确保选择以下内容作为项目模板:

-
单击选择,然后输入
HelloTest作为单元测试项目的名称,然后单击下一步。 -
选择与 Qt 项目完全相同的工具包,然后再次单击“下一步”。
-
在下一个屏幕截图中看到的“模块”页面中,您会注意到 QtCore 和 QtTest 模块是默认选择的,不能取消选择它们。 该页面只是一个帮助程序,或者是一个帮助您以交互方式选择所需模块的所谓向导。 如果忘记了添加类正常工作所需的模块,则以后也可以使用项目
*.pro文件添加或删除模块。 这使得有必要再次重复一个重要的观点。 单元测试就像使用您的类和函数的应用一样。 唯一的区别是,您仅将其用于测试目的,并且仅用于确保事情按预期运行,并且没有回归:

- 选择模块并单击下一步后,将显示“详细信息”页面或“测试类别信息”页面。 在以下屏幕截图中看到的“测试插槽”字段中输入
testPixelCount,然后单击“下一步”。 其余选项(如前一个窗口)只是简单的帮助程序,可轻松地以交互方式添加所需的函数,并包括对测试单元的指令,如果缺少任何内容,也可以稍后在源文件中添加这些指令。 不过,本章稍后将了解它们的含义以及如何使用它们。

- 确认所有对话框后,我们将进入 Qt Creator 编辑模式下的代码编辑器。 检查
HelloTest.pro文件,您会注意到它与标准 Qt 项目(小部件或控制台应用)的*.pro文件非常相似,具有以下模块定义,可将 Qt 测试模块导入该项目。 这就是您可以在任何单元测试项目中使用 Qt Test 的方式。 但是,如果您不使用“新建文件”或“项目”向导,则该向导会自动添加:
QT += testlib
在继续下一步之前,请确保像在 Qt Widgets 应用中一样将 OpenCV 库添加到 pro 文件。 (有关这方面的更多信息,请参阅本书的初始章节。)
- 现在,添加您创建的类以将图像的像素计数到该项目中。 请注意,在这种情况下,添加和复制不是同一回事。 您可以在单独的文件夹中将属于另一个项目的类头文件和源文件添加到项目中,而无需将其复制到项目文件夹中。 您只需要确保它们包含在
*.pro文件的HEADERS和SOURCES列表中,并且可以选择将类所在的文件夹添加到INCLUDEPATH变量中。
实际上,永远不要将要测试的类的源文件复制到测试项目中,正如我们将在本节中进一步讨论的那样,即使包含subdirs模板,也应始终使用subdirs模板制作单个项目,以便至少将一个单元测试添加到项目中,并在每次构建主项目时自动执行测试。 但是,严格来讲,无论您将类文件复制到其中还是将其简单地添加到它们中,单元测试都将以相同的方式工作。
- 现在该编写我们的测试类了,因此在 Qt Creator 代码编辑器中打开
tst_hellotesttest.cpp。 除了明显的#include指令外,这里还需要注意几件事:一个是HelloTestTest类,这是在“新建文件”或“项目”向导期间提供的类名称。 它不过是QObject子类,因此不要在此处查找任何隐藏内容。 它有一个称为testPixelCount的专用插槽,该插槽也是在向导期间设置的。 它的实现包括带有QVERIFY2宏的一行,我们将在后面的步骤中进行介绍。 但是,最后两行是新的:
QTEST_APPLESS_MAIN(HelloTestTest)
#include "tst_hellotesttest.moc"
QTEST_APPLESS_MAIN是由 C++ 编译器和moc扩展的宏(有关moc的更多信息,请参见第 3 章,“创建一个全面的 Qt + OpenCV 项目”),以创建适当的 C++ main函数来执行我们在HelloTestTest类中编写的测试函数。 它仅创建测试类的实例并调用QTest::qExec函数以启动测试过程。 测试过程将自动调用测试类中的所有专用插槽,并输出测试结果。 最后,如果我们在单个cpp源文件中创建测试类,而不是在单独的标头和源文件中创建 Qt 框架,则最后一行是必需的。 确保使用include指令将要测试的类添加到tst_hellotesttest.cpp文件中。 (为便于参考,我们假设其名为PixelCounter。)
- 现在,您可以使用适当的测试宏之一来测试此类中负责计算图像像素的函数。 假设该函数采用文件名和路径(
QString类型)并返回整数。 让我们使用testPixelCount插槽内已经存在的VERIFY2宏,如下所示:
void HelloTestTest::testPixelCount()
{
int width = 640, height = 427;
QString fname = "c:/dev/test.jpg";
PixelCounter c;
QVERIFY2(c.countPixels(fname) == width*height, "Failure");
}
在此测试中,我们仅提供了一个图像文件,该图像文件的像素数已知(宽度乘以高度),以测试我们的函数是否正常工作。 然后,我们将创建PixelCounter类的实例,并最终执行QVERIFY2宏,该宏将执行countPixels函数(假设这是我们要测试的公共函数的名称),并根据比较失败或通过来进行测试。 如果测试失败,它也会输出Failure字符串。
我们刚刚建立了第一个单元测试项目。 单击运行按钮以运行此测试,并在 Qt Creator 输出窗格中查看结果。 如果测试通过,那么您将看到类似以下内容:
********* Start testing of HelloTestTest *********
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by MSVC 2015)
PASS : HelloTestTest::initTestCase()
PASS : HelloTestTest::testPixelCount()
PASS : HelloTestTest::cleanupTestCase()
Totals: 3 passed, 0 failed, 0 skipped, 0 blacklisted, 26ms
********* Finished testing of HelloTestTest *********
如果发生故障,您将在输出中看到以下内容:
********* Start testing of HelloTestTest *********
Config: Using QtTest library 5.9.1, Qt 5.9.1 (i386-little_endian-ilp32 shared (dynamic) debug build; by MSVC 2015)
PASS : HelloTestTest::initTestCase()
FAIL! : HelloTestTest::testPixelCount() 'c.countPixels(fname) == width*height' returned FALSE. (Failure)
..HelloTesttst_hellotesttest.cpp(26) : failure location
PASS : HelloTestTest::cleanupTestCase()
Totals: 2 passed, 1 failed, 0 skipped, 0 blacklisted, 26ms
********* Finished testing of HelloTestTest *********
结果几乎是不言而喻的,但是我们可能需要注意一件事,那就是在所有测试函数之前调用initTestCase,在所有测试函数之后调用cleanupTestCase的事实, 正如我们前面提到的。 但是,由于这些函数实际上并不存在,因此它们被标记为PASS。 如果您实现这些函数并执行实际的初始化和完成任务,则可能会有所改变。
在前面的示例中,我们看到了单元测试的最简单形式,但现实情况是,编写一个高效且可靠的单元测试来解决所有可能的问题,这是一项艰巨的任务,并且与我们面对的情况相比要复杂得多。 为了能够编写适当的单元测试,您可以在每个测试函数中使用以下宏。 这些宏在QTest中定义如下:
QVERIFY:可用于检查是否满足条件。 条件只是一个布尔值或任何计算结果为布尔值的表达式。 如果不满足条件,则测试将停止,失败并记录在输出中;否则,测试将失败。 否则,它将继续。QTRY_VERIFY_WITH_TIMEOUT:类似于QVERIFY,但是此功能尝试检查提供的条件,直到达到给定的超时时间(以毫秒为单位)或满足条件。QTRY_VERIFY:类似于QTRY_VERIFY_WITH_TIMEOUT,但是超时设置为默认值 5 秒。QVERIFY2,QTRY_VERIFY2_WITH_TIMEOUT和QTRY_VERIFY2:这些宏与名称非常相似的以前的宏非常相似,除了在测试失败的情况下函数还会输出给定消息之外,这些宏也是如此。QCOMPARE:可用于将实际值与预期的值进行比较。 它非常类似于QVERIFY,不同之处在于此宏还输出实际值和期望值以供以后参考。QTRY_COMPARE_WITH_TIMEOUT:类似于QCOMPARE,但是此函数尝试比较实际值和期望值,直到达到给定的超时时间(以毫秒为单位)或相等为止。QTRY_COMPARE:类似于QTRY_COMPARE_WITH_TIMEOUT,但是超时设置为默认值 5 秒。
数据驱动的测试
除了与每个测试函数内部提供的输入数据进行简单比较外,QTest还提供了使用一组更有条理和结构化的输入数据执行单元测试的方法,以执行数据驱动的测试,或者换句话说,通过不同的输入数据集。 这是通过QFETCH宏以及QTest::addColumn和QTest::newRow函数来完成的。 QFETCH函数可在测试函数内使用,以获取所需的测试数据。 这需要为我们的测试函数创建一个数据函数。 数据函数还是另一个专用插槽,其名称与测试函数的名称完全相同,但名称后面附加了_data。 因此,如果我们回到前面的示例,要进行数据驱动的测试,我们需要在测试类中添加一个新的专用插槽,类似于以下内容:
void HelloTestTest::testPixelCount_data()
{
QTest::addColumn<QString>("filename");
QTest::addColumn<int>("pixelcount");
QTest::newRow("huge image") <<
"c:/dev/imagehd.jpg" << 2280000;
QTest::newRow("small image") <<
"c:/dev/tiny.jpg" << 51200;
}
请注意,数据函数名称在其名称末尾附加了_data。 QTest中的测试数据被视为表格; 这就是为什么在数据函数中,addColumn函数用于创建新的列(或字段),而addRow函数用于向其中添加新的行(或记录)的原因。 前面的代码将产生类似于以下内容的测试数据表:
| 索引 | 名称(或标签) | 文件名 | 像素计数 |
|---|---|---|---|
| 0 | 大图像 | c:/dev/imagehd.jpg |
2280000 |
| 1 | 小图像 | c:/dev/tiny.jpg |
51200 |
现在,我们可以修改测试函数testPixelCount以使用此测试数据,而不是在同一函数中使用提供的单个文件名。 我们新的testPixelCount看起来与此类似(同时,为了更好的测试日志输出,我们也将QVERIFY替换为QCOMPARE):
void HelloTestTest::testPixelCount()
{
PixelCounter c;
QFETCH(QString, filename);
QFETCH(int, pixelcount);
QCOMPARE(c.countPixels(filename), pixelcount);
}
重要的是要注意,必须为QFETCH提供在数据函数内部创建的测试数据中每一列的确切数据类型和元素名称。 如果我们再次执行测试,则测试框架将调用testPixelCount,与测试数据中的行一样多,每次它将通过获取并使用新行并记录输出来运行测试函数。 使用数据驱动的测试函数有助于保持实际的测试函数完整,并且不是从测试函数内部创建测试数据,而是从简单且结构化的数据函数中获取它们。 不用说,您可以扩展它以从磁盘上的文件或其他输入方法(例如网络位置)中获取测试数据。 无论数据来自何处,当数据函数存在时,数据都应完整存在并正确构造。
基准管理
QTest提供QBENCHMARK和QBENCHMARK_ONCE宏来测量函数调用或任何其他代码的性能(基准)。 这两个宏的区别仅在于它们重复一段代码以衡量其性能的次数,而后者显然只运行一次代码。 您可以通过以下方式使用这些宏:
QBENCHMARK
{
// Piece of code to be benchmarked
}
同样,我们可以在前面的示例中使用它来衡量PixelCounter类的性能。 您可以简单地将以下行添加到testPixelCount函数的末尾:
QBENCHMARK
{
c.countPixels(filename);
}
如果再次运行测试,您将在测试日志输出中看到类似于以下内容的输出。 请注意,这些数字仅是在随机测试 PC 上运行的示例,在各种系统上它们可能会有很大不同:
23 msecs per iteration (total: 95, iterations: 4)
前面的测试输出意味着每次使用特定的测试图像对函数进行测试都花费了 23 毫秒。 另一方面,迭代次数为4,用于基准测试的总时间约为 95 毫秒。
GUI 测试
与执行特定任务的测试类相似,也可以创建用于测试 GUI 功能或小部件行为的单元测试。 在这种情况下,唯一的区别是需要为 GUI 提供鼠标单击,按键和类似的用户交互。 QTest支持通过模拟鼠标单击和其他用户交互来测试使用 Qt 创建的 GUI。 QTest命名空间中提供以下函数,以编写能够执行 GUI 测试的单元测试。 注意,几乎所有它们都依赖于以下事实:Qt 中的所有小部件和 GUI 组件都是QWidget的子类:
keyClick:可以用来模拟单击键盘上的按键。 为了方便起见,此函数有许多重载版本。 您可以选择提供修改键(ALT,CTRL等)和/或单击该键之前的延迟时间。keyClick不应与mouseClick混淆,稍后我们将对其进行介绍,它指的是一次按键并释放,从而导致单击。keyClicks:这与keyClick十分相似,但是它可以用于模拟序列中的按键单击,同样具有可选的修饰符或两者之间的延迟。keyPress:这再次类似于keyClick,但是它仅模拟按键的按下,而不释放它们。 如果我们需要模拟按下一个键,这将非常有用。keyRelease:这与keyPress相反,这意味着它仅模拟键的释放而没有按下键。 如果我们想使用keyPress模拟释放先前按下的键,这将很有用。keyEvent:这是键盘模拟函数的更高级版本,带有一个附加的动作参数,该参数定义是否按下,释放,单击(按下并释放)键,或者它是快捷键。mouseClick:类似于keyClick,但是它可以通过单击鼠标进行操作。 这就是为此函数提供的键是鼠标按钮(例如,左,右,中等)的原因。 键的值应该是Qt::MouseButton枚举的条目。 它还支持键盘修饰符和模拟点击之前的延迟时间。 此外,此函数和所有其他鼠标模拟函数还带有一个可选点(QPoint),其中包含要单击的小部件(或窗口)内的位置。 如果提供了一个空白点,或者如果省略了此参数,则模拟的点击将发生在小部件的中间。mouseDClick:这是mouseClick函数的双击版本。mousePress:这与mouseClick十分相似,但是仅模拟鼠标的按下,而不释放它。 如果要模拟按住鼠标按钮,这将很有用。mouseRelease:与mousePress相反,这意味着它仅模拟鼠标按钮的释放而没有按下。 这可以用来模拟一段时间后释放鼠标按钮。mouseMove:可以用来模拟在小部件上移动鼠标光标。 此函数必须提供点和延迟。 与其他鼠标交互函数类似,如果未设置任何点,则将鼠标移动到小部件的中间点。 与mousePress和mouseRelease结合使用时,此函数可用于模拟和测试拖放。
让我们创建一个简单的 GUI 测试以熟悉在实践中如何使用上述函数。 假设要测试已经创建的窗口或窗口小部件,则必须首先将其包含在 Qt 单元测试项目中。 因此,从创建单元测试项目开始,与在上一个示例以及我们的第一个测试项目中类似。 在项目创建期间,请确保还选择QtWidgets作为必需的模块之一。 然后,将窗口小部件类文件(可能是标头,源文件和 UI 文件)添加到测试项目。 在我们的示例中,我们假设我们有一个带有按钮和标签的简单 GUI。 每次按下该按钮,标签上的数字将乘以 2。 为了能够测试此功能或任何其他 GUI 功能,我们必须首先通过将其公开,确保表单,容器小部件或窗口上的小部件对测试类公开。 在实现此目的的许多方法中,最快,最简单的方法是在类声明中也以公共成员的身份定义相同的小部件。 然后,只需将ui变量(在使用“新建文件”或“项目”向导创建的所有 Qt 窗口小部件中找到的)变量中的类分配给整个类的成员。 假设我们窗口上的按钮和标签分别命名为nextBtn和infoLabel(使用设计器设计时),然后我们必须在类声明public成员中定义以下内容:
QPushButton *nextBtn;
QLabel *infoLabel;
并且,我们必须在构造器中分配它们,如下所示:
ui->setupUi(this);
this->nextBtn = ui->nextBtn;
this->infoLabel = ui->infoLabel;
确保在调用setupUi之后始终分配使用设计器和 UI 文件创建的窗口小部件; 否则,您的应用肯定会崩溃,因为直到调用setupUi才真正创建任何小部件。 现在,假设我们的小部件类称为TestableForm,我们可以在测试类中拥有一个专用的testGui插槽。 请记住,每次按下nextBtn时,infoLabel上的数字都将乘以 2,因此testGui函数中可以有类似以下内容:
void GuiTestTest::testGui()
{
TestableForm t;
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(1));
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(2));
QTest::mouseClick(t.nextBtn, Qt::LeftButton);
QCOMPARE(t.infoLabel->text(), QString::number(4));
// repeated until necessary
}
替换以下行也非常重要:
QTEST_APPLESS_MAIN(GuiTestTest)
添加以下行:
QTEST_MAIN(GuiTestTest)
否则,不会在幕后创建QApplication,并且测试将完全失败。 使用 Qt 测试框架测试 GUI 时要记住这一点很重要。 现在,如果您尝试运行单元测试,则将单击nextBtn小部件 3 次,然后每次检查infoLabel显示的值是否正确。 如果发生故障,它将记录在输出中。 这很容易,但是问题是,如果所需交互的数量增加了怎么办? 如果必须执行大量的 GUI 交互该怎么办? 为了克服这个问题,您可以结合使用数据驱动的测试和 GUI 测试来轻松重放 GUI 交互(或事件,在 Qt 框架中称为事件)。 请记住,要在测试类中具有测试函数的数据函数,必须创建一个新函数,该函数的名称应与_data完全相同。 因此,我们可以创建一个名为testGui_data的新函数,该函数准备交互集和结果集,并使用QFETCH将其传递给测试函数,就像我们在前面的示例中使用的那样:
void GuiTestTest::testGui_data()
{
QTest::addColumn<QTestEventList>("events");
QTest::addColumn<QString>("result");
QTestEventList mouseEvents; // three times
mouseEvents.addMouseClick(Qt::LeftButton);
mouseEvents.addMouseClick(Qt::LeftButton);
mouseEvents.addMouseClick(Qt::LeftButton);
QTest::newRow("mouse") << mouseEvents << "4";
QTestEventList keybEvents; // four times
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
keybEvents.addDelay(250);
keybEvents.addKeyClick(Qt::Key_Space);
QTest::newRow("keyboard") << keybEvents << "8";
}
QTestEventList类是 Qt 测试框架中的便捷类,可用于轻松创建 GUI 交互列表并对其进行仿真。 它包含添加所有我们之前提到的所有可能交互的功能,这些交互是可以使用 Qt Test 执行的可能事件的一部分。
要使用此数据函数,我们需要覆盖testGui函数,如下所示:
void GuiTestTest::testGui()
{
TestableForm t;
QFETCH(QTestEventList, events);
QFETCH(QString, result);
events.simulate(t.nextBtn);
QCOMPARE(t.infoLabel->text(), result);
}
类似于任何数据驱动的测试,QFETCH获取由数据函数提供的数据。 但是,在这种情况下,存储的数据为QEventList,并填充了一系列必需的交互操作。 此测试方法在重放错误报告中的一系列事件以重现,修复和进一步测试特定问题方面非常有效。
测试用例项目
在前面的部分及其相应的示例中,我们看到了一些简单的测试用例,并使用 Qt Test 函数对其进行了解决。 我们了解了数据驱动和 GUI 测试,以及如何结合两者以重放 GUI 事件并执行更复杂的 GUI 测试。 我们在每种情况下学到的相同方法都可以进一步扩展,以应用于更复杂的测试用例。 在本节中我们将学习确保在构建项目时自动执行测试。 当然,根据测试所需的时间和我们的喜好,我们可能希望轻松地暂时跳过自动测试,但是最终,在构建项目时,我们将需要轻松执行测试。 为了能够自动运行您的 Qt 项目的测试单元(我们将其称为主项目),首先,我们需要确保始终使用Subdirs模板创建它们,然后将单元测试项目配置为测试案例项目。 这也可以通过已经存在但不在Subdirs模板中的项目来完成。 只需按照本节提供的步骤将现有项目添加到Subdirs模板,并为其创建一个单元测试(配置为测试用例),该单元测试在您构建主项目时将自动运行:
-
首先使用 Qt Creator 中“欢迎”模式下的“新建项目”按钮创建一个新项目,或者从“文件”菜单中选择“新建文件”或“项目”项。
-
确保选择
Subdirs项目,如以下屏幕截图所示,然后单击“选择”:

- 为您的项目选择一个名称。 该名称可以与您的主项目名称相同。 假设它称为
computer_vision。 继续前进,然后在最后一个对话框中,单击“完成 & 添加子项目”按钮。 如果您是从头开始创建项目,则可以像整本书一样简单地创建项目。 否则,这意味着如果您想添加现有项目(假设在名为src的文件夹内),只需单击“取消”,然后将要为其构建测试的现有项目复制到此新创建的项目文件夹subdirs中。 然后,打开computer_vision.pro文件,并将其修改为类似于以下代码行:
TEMPLATE = subdirs
SUBDIRS += src
- 现在,您可以创建一个单元测试项目,该项目也是
computer_vision子目录项目的子项目,并对它进行编程以测试src文件夹中存在的类(您的主项目,它是实际的应用本身) )。 因此,再次从项目窗格中右键单击computer_vision,然后通过选择“新建子项目”,开始使用在上一节中学到的所有内容来创建单元测试。 - 创建测试后,无论使用哪个主项目查看测试结果,都应该能够单独运行它。 但是,要确保将其标记为测试用例项目,需要将以下代码行添加到单元测试项目的
*.pro文件中:
CONFIG += testcase
- 最后,您需要在 Qt Creator 中切换到项目模式,并将检查添加到
Make arguments字段中,如以下屏幕截图所示。 确保首先使用“详细信息”扩展器按钮扩展“制作”部分; 否则,它将不可见:

现在,无论您是否专门运行单元测试项目都没有关系,并且每次运行主项目或尝试构建它时,测试都会自动执行。 这是一种非常有用的技术,可确保对一个库的更改不会对另一个库造成负面影响。 关于此技术要注意的重要一点是,测试结果实际上会影响构建结果。 意思是,您会在构建测试时注意到测试是否自动失败,并且测试结果将在 Qt Creator 的编译器输出窗格中可见,可以使用底部的栏或按ALT + 4键。
总结
在本章中,您学习了如何使用 Qt Creator 进行调试以及它提供的功能,以便进一步分析代码,发现问题并尝试使用断点,调用栈查看器等对其进行修复。 这只是使用调试器可以完成的工作的一点点尝试,它的目的是让您准备自己继续使用调试器,并养成自己的编码和调试习惯,从而可以帮助您克服更多编程问题。 缓解。 除了调试和开发人员级别的测试外,我们还了解了 Qt 中的单元测试,这对于使用 Qt 框架编写的越来越多的应用和项目尤其重要。 测试自动化是当今应用开发行业中的热门话题之一,对 Qt 测试框架有清晰的想法将有助于您开发更好和可靠的测试。 习惯于为项目编写单元测试非常重要,是的,即使是非常小的项目也是如此。 对于初学者或业余爱好者而言,测试应用和避免回归的成本并不容易理解,因此,为在开发生涯的后期肯定会遇到的事情做好准备是一个好主意。
在接近本书最后几章的同时,我们也越来越关注使用 Qt 和 OpenCV 进行应用开发的最后阶段。 因此,在下一章中,您将学习有关向最终用户部署应用的知识。 您还将了解应用的动态和静态链接,以及创建可以轻松安装在具有不同操作系统的计算机上的应用包。 下一章将是我们在台式机平台上使用 OpenCV 和 Qt 进行计算机视觉之旅的最后一章。
十一、链接与部署
在前几章中了解了使用 Qt Creator 和 Qt Test 框架调试和测试应用之后,我们进入了应用开发的最后阶段之一,即将应用部署到最终用户。 该过程本身具有多种变体,并且可以根据目标平台采取很多不同的形式,但是它们都有一个共同点,就是以一种可以在目标平台中简单地执行它的方式打包应用。 困扰应用的依赖项。 请记住,并非所有目标平台(无论是 Windows,MacOS 还是 Linux)都具有 Qt 和 OpenCV 库。 因此,如果继续进行操作,仅向应用的用户提供应用的可执行文件,它很可能甚至不会开始执行,更不用说正常工作了。
在本章中,我们将通过学习创建应用包(通常是包含所有必需文件的文件夹)的正确方法来解决这些问题,该应用包可以在我们自己的计算机以及开发环境以外的其他计算机上简单执行,而无需用户照顾任何必需的库。 为了能够理解本章中描述的一些概念,我们首先需要了解创建应用可执行文件时幕后发生情况的一些基础知识。 我们将讨论构建过程的三个主要阶段,即预处理,编译和链接应用可执行文件(或库)。 然后,我们将学习可以用两种不同的方式完成链接,即动态链接和静态链接。 我们将讨论它们之间的差异以及它们如何影响部署,以及如何在 Windows,MacOS 和 Linux 操作系统上动态或静态地构建 Qt 和 OpenCV 库。 之后,我们将为所有提到的平台创建并部署一个简单的应用。 我们将借此机会还了解 Qt Installer 框架,以及如何创建网站下载链接,闪存驱动器或任何其他媒体上交付给最终用户的安装程序。 到本章结束时,我们将仅向最终用户提供他们执行我们的应用所需的内容,仅此而已。
本章将讨论的主题包括:
- Qt 和 OpenCV 框架的动态和静态链接
- 配置 Qt 项目来使用静态库
- 部署使用 Qt 和 OpenCV 编写的应用
- 使用 Qt Installer 框架创建跨平台安装程序
幕后制作过程
当我们通过编辑一些 C++ 头文件或源文件,在项目文件中添加一些模块并最后按下运行按钮来编写应用时,这似乎很自然。 但是,在幕后还有一些流程,这些流程通过按正确的顺序由 IDE(在我们的情况下为 Qt Creator)执行,从而使开发过程具有顺畅自然的感觉。 通常,当我们按 Qt Creator 或任何其他 IDE 的运行或构建按钮时,有三个主要过程可导致创建可执行文件(例如*.exe)。 这是这三个过程:
- 预处理
- 编译
- 链接
这是从源文件创建应用时所经过的过程和阶段的非常高级的分类。 这种分类允许对过程进行更简单的概述,并以更简单的方式大致了解其目的。 但是,这些过程包括许多子过程和阶段,不在本书的讨论范围之内,因为我们对以一种或另一种方式影响部署过程的过程最为感兴趣。 但是,您可以在线阅读它们,也可以阅读有关编译器和链接器的任何书籍。
预处理
此阶段是在将源代码传递到实际编译器之前将其转换为最终状态的过程。 为了进一步解释这一点,请考虑所有包含的文件,各种编译器指令,或更重要的是,对于 Qt 框架,请考虑不属于标准 C++ 语言的 Qt 特定的宏和代码。 在第 3 章,“创建全面的 Qt + OpenCV 项目”中,我们了解了uic和moc,它们可以转换使用 Qt 特定宏和准则编写的 UI 文件和 C++ 代码。 转换为标准 C++ 代码(确切地说,是在最新版本的 Qt 中,转换为 C++ 11 或更高版本)。 即使这些不是对 C++ 源代码执行的标准预处理的一部分,但是当我们使用 Qt 框架或基于自己的规则集生成代码的框架时,它们仍处于大致相同的阶段。
下图描述了预处理阶段,该阶段与使用uic,moc等进行 Qt 特定的代码生成相结合:

该过程的输出在上一个图像中被标记为用于编译器的单个输入文件,显然是一个单个文件,其中包含用于编译源代码的所有必需标记和信息。 然后将该文件传递给编译器和编译阶段。
编译
在构建过程的第二个主要阶段,编译器获取预处理器的输出,或者在我们的示例中为预处理阶段,该输出还包括uic和moc生成的代码,并将其编译为机器代码。 。 可以在构建过程中保存并重复使用该机器代码,因为只要不更改源文件,生成的机器代码也将保持不变。 通过确保重复使用各个单独编译的对象(例如*.obj或*.lib文件),而不是在每次构建项目时都生成该对象,此过程有助于节省大量时间。 所有这一切的好处是,IDE 会照顾它,我们通常不需要理会它。 然后,由编译器生成的输出文件将传递到链接器,然后我们进入链接阶段。
链接
链接器是在构建过程链中被调用的最后一个程序,其目标是链接由编译器生成的对象以生成可执行文件或库。 这个过程对我们而言至关重要,因为它会对部署应用的方式,可执行文件的大小等产生巨大影响。 为了更好地理解这一点,首先我们需要讨论两种可能的链接类型之间的区别:
- 动态链接
- 静态链接
动态链接是链接编译器生成的对象的过程,方法是将函数的名称放在生成的可执行文件或库中,以使该特定函数的实际代码位于共享库(例如*.dll文件)中 ),并且库的实际链接和加载是在运行时完成的。 动态链接的最明显的优缺点是:
- 您的应用将在运行时需要共享库,因此您必须将它们与应用的可执行文件一起部署,并确保可以访问它们。 例如,在 Windows 上,可以通过将其复制到与应用可执行文件相同的文件夹中来完成,或者在 Linux 上,可以将它们放在默认库路径(例如
/lib/)中来完成。 - 动态链接通过将应用的各个部分保留在单独的共享库文件中,提供了极大的灵活性。 这样,共享库可以单独更新,而无需重新编译应用的每个部分。
与动态链接相反,可以使用静态链接将所有必需的代码链接到生成的可执行文件中,从而创建静态库或可执行文件。 您可以猜测,使用静态库与使用共享库具有完全相反的优点和缺点,它们是:
- 您不需要部署用于构建应用的静态库,因为它们的所有代码实际上都已复制到生成的可执行文件中
- 应用可执行文件的大小将变大,这意味着更长的初始加载时间和更大的文件要部署
- 对库或应用任何部分的任何更改都需要对其所有组成部分进行完整的重建过程
在整本书中,特别是在为我们全面的计算机视觉应用开发插件时,我们使用了共享库和动态链接。 这是因为当我们使用所有默认的 CMake 设置构建 OpenCV,并使用第 1 章,“OpenCV 和 Qt 简介”中的官方安装程序安装 Qt 框架时, 动态链接和共享的库(Windows 上为*.dll,MacOS 上为*.dylib等)。 不过,在下一节中,我们将学习如何使用它们的源代码静态地构建 Qt 和 OpenCV 库。 通过使用静态链接库,我们可以创建不需要在目标系统上存在任何共享库的应用。 这可以极大地减少部署应用所需的工作量。 在 MacOS 和 Linux 操作系统中的 OpenCV 尤其如此,您的用户除了复制和运行您的应用外完全不需要执行任何操作,而他们将需要采取一些措施或必须执行一些脚本操作以确保执行您的应用时,所有必需的依赖项均已就绪。
构建 OpenCV 静态库
让我们从 OpenCV 开始,它遵循与构建动态库几乎相同的指令集来构建静态库。 您可以参考第 1 章,“OpenCV 和 Qt 简介”以获得更多信息。 只需下载源代码,解压缩并使用 CMake 来配置您的构建,如本章所述。 但是,这次,除了选中BUILD_opencv_world选项旁边的复选框外,还要取消选中每个选项旁边的复选框,以确保关闭了以下所有选项:
BUILD_DOCSBUILD_EXAMPLESBUILD_PERF_TESTSBUILD_TESTSBUILD_SHARED_LIBSBUILD_WITH_STATIC_CRT(仅在 Windows 上可用)
关闭前四个参数仅是为了加快构建过程,并且是完全可选的。 禁用BUILD_SHARED_LIBS仅启用 OpenCV 库的静态(非共享)构建模式,而最后一个参数(在 Windows 上)有助于避免库文件不兼容。 现在,如果您使用第 1 章,“OpenCV 和 Qt 简介”中提供的相同说明开始构建过程,这次,而不是共享库(例如,在 Windows 上, *.lib和*.dll文件),您将在安装文件夹中得到静态链接的 OpenCV 库(同样,在 Windows 中,仅*.lib文件,没有任何*.dll文件)。 接下来需要做的是将项目配置为使用 OpenCV 静态库。 通过使用*.pri文件,或直接将它们添加到 Qt 项目*.pro文件中,您需要以下几行,以便您的项目可以使用 OpenCV 静态库:
win32: {
INCLUDEPATH += "C:/path_to_opencv_install/include"
Debug: {
LIBS += -L"C:/path_to_opencv_install/x86/vc14/staticlib"
-lopencv_world330d
-llibjpegd
-llibjasperd
-littnotifyd
-lIlmImfd
-llibwebpd
-llibtiffd
-llibprotobufd
-llibpngd
-lzlibd
-lipp_iw
-lippicvmt
}
Release: {
LIBS += -L"C:/path_to_opencv_install/x86/vc14/staticlib"
-lopencv_world330
-llibjpeg
-llibjasper
-littnotify
-lIlmImf
-llibwebp
-llibtiff
-llibprotobuf
-llibpng
-lzlib
-lipp_iw
-lippicvmt
}
}
前面代码中库的顺序不是随机的。 这些库需要以其依赖关系的正确顺序包括在内。 您可以在 Visual Studio 2015 中自己检查一下,方法是从主菜单中选择Project,然后选择Project Build Order…。 对于 MacOS 用户,必须在前面的代码中将win32替换为unix: macx,并且库的路径必须与您的构建文件夹中的路径匹配。 对于 Linux,您可以使用与动态库相同的pkgconfig行,如下所示:
unix: !macx{
CONFIG += link_pkgconfig
PKGCONFIG += opencv
}
请注意,即使在 Windows OS 上以静态方式构建 OpenCV 时,输出文件夹中仍将有一个库作为动态库,即opencv_ffmpeg330.dll。 您无需将其包含在*.pro文件中; 但是,您仍然需要将其与应用可执行文件一起部署,因为 OpenCV 本身依赖于它才能支持某些众所周知的视频格式和编码。
构建 Qt 静态库
默认情况下,官方 Qt 安装程序仅提供动态 Qt 库。 在第 1 章,“OpenCV 和 Qt 简介”中也是如此,当我们使用以下链接提供的安装程序在开发环境中安装 Qt 时。
因此,简单来说,如果要使用静态 Qt 库,则必须使用其源代码自行构建它们。 您可以按照此处提供的步骤来配置,构建和使用静态 Qt 库:
- 为了能够构建一组静态 Qt 库,您需要首先从 Qt 下载网站下载源代码。 通常将它们作为包含所有必需源代码的单个压缩文件(
*.zip,*.tar.xz等)提供。 在我们的情况下(Qt 版本 5.9.1),您可以使用以下链接下载 Qt 源代码。
下载qt-everywhere-opensource-src-5.9.1.zip(或*.tar.xz),然后继续下一步。
- 将源代码提取到您选择的文件夹中。 我们假定提取的文件夹名为
Qt_Src,并且位于c:/dev文件夹中(在 Windows 操作系统上)。 因此,假设我们提取的 Qt 源代码的完整路径为c:/dev/Qt_Src。
对于 MacOS 和 Linux 用户,该路径可能类似于Users/amin/dev/Qt_Src,因此,如果您使用的是上述操作系统之一而不是 Windows,则需要在提供的所有引用它的说明中将其替换。 现在应该已经很明显了。
- 现在,您需要先处理一些依赖关系,然后再继续下一步。 MacOS 和 Linux 用户通常不需要执行任何操作,因为默认情况下,所有必需的依赖项都存在于这些操作系统上。 但是,这不适用于 Windows 用户。 通常,在从源代码构建 Qt 之前,计算机上必须存在以下依赖项:
- ActivePerl。
- Python,您需要版本 2.7.X,而 X 已被最新的现有版本替换,在撰写本书时为 14。
- 为了方便 Windows 用户,在 Qt 源代码 ZIP 文件的
gnuwin32子文件夹内提供了 Bison。 只需确保将c:/dev/Qt_Src/gnuwin32/bin添加到PATH环境变量即可。 - Flex 与 Bison 相同,位于
gnuwin32子文件夹内,需要添加到PATH中。 - 在
gnuwin32子文件夹内提供了与 Bison 和 Flex 相同的 GNUgperf,需要将其添加到PATH中。
为确保一切正常,请尝试运行相关命令以执行我们刚刚提到的每个依赖项。 可能是您忘记将其中一个依赖项添加到PATH的情况,或者对于 MacOS 和 Linux 用户,由于任何可能的原因,它们已被删除并且不存在。 仅在命令提示符(或终端)中执行以下每个命令并确保您不会遇到无法识别或找不到的错误类型就足够了:
perl
python bison
flex gperf
- 现在,在 Windows 上运行 VS2015 的开发人员命令提示符。 在 MacOS 或 Linux 上,运行终端。 您需要运行一组连续的命令,以根据源代码配置和构建 Qt。 该配置是此步骤中最关键的部分,是通过使用
configure命令完成的。configure命令位于 Qt 源文件夹的根目录中,接受以下参数(请注意,实际的参数集很长,因此我们可以满足使用最广泛的参数的要求):
此处提供的参数列表应足以构建具有更多或更少默认设置的静态版本的 Qt 框架:
- 现在是时候配置我们的 Qt 构建了。 首先,我们需要使用以下命令切换到 Qt 源代码文件夹:
cd c:/dev/Qt_Src"
- 然后通过键入以下命令开始配置:
configure -opensource -confirm-license -static -skip webengine
-prefix "c:devQtStatic" -platform win32-msvc
我们提供-skip webengine的原因是因为(编写本书时)目前尚不支持静态构建 Qt WebEngine 模块。 另请注意,我们提供了-prefix参数,这是我们要获取静态库的文件夹。您需要谨慎使用此参数,因为您不能稍后再复制它,并且由于您的构建配置, 静态库仅在它们保留在磁盘上的该位置时才起作用。 我们已经在参数列表中描述了其余参数。
您还可以将以下内容添加到configure命令中,以跳过可能不需要的部分并加快构建过程,因为这将花费很长时间:
-nomake tests -nomake examples
在 MacOS 和 Linux 上,必须从configure命令中省略以下部分。 这样做的原因仅仅是该平台将被自动检测的事实。 在 Windows 上当然也是如此,但是由于我们要强制 Qt 库的 32 位版本(以支持更大范围的 Windows 版本),因此我们将坚持使用此参数:
-platform win32-msvc
根据您的计算机规格,配置过程不会花费太长时间。 配置完成后,您应该会看到类似以下的输出,否则,您需要再次仔细地执行上述步骤:
Qt is now configured for building. Just run 'nmake'.
Once everything is built, you must run 'nmake install'.
Qt will be installed into 'c:devQtStatic'.
Prior to reconfiguration, make sure you remove any leftovers from
the previous build.
请注意,在 MacOS 和 Linux 上,上述输出中的nmake将替换为make。
- 正如配置输出中提到的那样,您需要输入
build和install命令。
在 Windows 上,使用以下命令:
nmake
nmake install
在 MacOS 和 Linux 上,使用以下命令:
make
make install
请注意,由于 Qt 框架包含许多需要构建的模块和库,因此第一个命令通常需要很长时间才能完成(取决于您的计算机规格),因此在此步骤中需要耐心等待。 无论如何,如果您到目前为止已经完全按照提供的所有步骤进行操作,则应该没有任何问题。
重要的是要注意,如果您使用计算机受限区域中的安装文件夹(-prefix参数),则必须确保使用管理员级别(如果使用 Windows)运行命令提示符实例(如果使用 Windows) 带有sudo前缀的build和install命令(如果您使用的是 MacOS 或 Linux)。
- 运行
install命令后,应该将静态 Qt 库放入配置过程中作为前缀参数提供的文件夹(即安装文件夹)中。 因此,在此步骤中,您需要在 Qt Creator 中将这组新建的 Qt 静态库添加为工具包。 为此,请打开 Qt Creator,然后从主菜单中选择“工具”,然后选择“选项”。 从左侧的列表中,选择Build & Run,然后选择Qt Versions选项卡。 现在,按“添加”按钮,然后浏览至Qt build安装文件夹,选择qmake.exe,在本例中,该文件应位于C:devQtStaticbin文件夹内。 以下屏幕截图显示了正确添加新的 Qt 构建后 Qt 版本标签中的状态:

- 现在,切换到“套件”选项卡。 您应该能够看到整本书中用来构建 Qt 应用的工具包。 例如,在 Windows 上,它应该是 Desktop Qt 5.9.1 MSVC2015 32bit。 选择它并按“克隆”按钮,然后选择在上一步的“Qt 版本”选项卡中设置的 Qt 版本(如果您在那里看不到自己的版本,则可能需要按一次“应用”按钮,然后按“将显示在组合框中)。 另外,请确保从其名称中删除
Clone of,并在其后附加Static一词,以便于区分。 以下屏幕快照表示“工具”选项卡的状态及其配置方式:

关于构建和配置静态 Qt 套件的问题。 现在,您可以使用与默认 Qt 套件(动态套件)完全相同的方式开始使用它创建 Qt 项目。 您唯一需要注意的就是在创建和配置 Qt 项目时将其选择为目标套件。 让我们用一个简单的例子来做到这一点。 首先创建一个 Qt Widgets 应用,并将其命名为StaticApp。 在“工具包选择”页面上,确保选择了新建的静态 Qt 工具包,然后继续按“下一步”,直到进入 Qt 代码编辑器。 以下屏幕快照描述了“工具包选择”页面及其外观(在 Window OS 上):

无需进行太多更改或添加任何代码,只需按“运行”按钮即可构建并执行该项目。 现在,如果浏览到该项目的build文件夹,您会注意到可执行文件的大小比我们使用默认动态工具包进行构建时的大小要大得多。 为了进行比较,在 Windows 操作系统和调试模式下,动态构建的版本应小于 1 兆字节,而静态构建的版本应约为 30 兆字节,甚至更多。 如前所述,这样做的原因是所有必需的 Qt 代码现在都链接到可执行文件中。 尽管严格说来,从技术上讲它并不正确,但是您可以将其视为将库(*.dll文件等)嵌入可执行文件本身中。
现在,让我们尝试在示例项目中也使用静态 OpenCV 库。 只需将所需的附加内容添加到StaticApp.pro文件中,然后尝试使用几个简单的 OpenCV 函数(例如imread,dilate和imshow)来测试一组静态 OpenCV 库。 如果现在检查静态链接的可执行文件的大小,您会发现文件大小现在更大。 这样做的明显原因是所有必需的 OpenCV 代码都链接到可执行文件本身。
部署 Qt + OpenCV 应用
向最终用户提供应用包是非常重要的,该包包含它能够在目标平台上运行所需的一切,并且在照顾所需的依赖方面几乎不需要用户付出任何努力。 为应用实现这种开箱即用的条件主要取决于用于创建应用的链接的类型(动态或静态),以及目标操作系统。
使用静态链接的部署
静态部署应用意味着您的应用将独立运行,并且消除了几乎所有需要的依赖项,因为它们已经在可执行文件内部。 只需确保在构建应用时选择发布模式即可,如以下屏幕截图所示:

在发布模式下构建应用时,您只需选择生成的可执行文件并将其发送给用户。
如果尝试将应用部署到 Windows 用户,则在执行应用时可能会遇到类似于以下错误:

发生此错误的原因是,在 Windows 上,即使以静态方式构建 Qt 应用,您仍然需要确保目标系统上存在 Visual C++ 可再发行组件。 这是使用 Microsoft Visual C++ 生成的 C++ 应用所必需的,并且所需的可再发行版本与计算机上安装的 Microsoft Visual Studio 相对应。 在我们的案例中,这些库的安装程序的正式名称是 Visual Studio 2015 的 Visual C++ 可再发行组件,可以从以下链接下载。
通常的做法是在我们的应用的安装程序中包含可再发行文件的安装程序,如果尚未安装,请对其进行静默安装。 大多数情况下,您在 Windows PC 上使用的大多数应用都会执行此过程,而您甚至没有注意到它。
我们已经简要地讨论了静态链接的优点(要部署的文件较少)和缺点(可执行文件的大小较大)。 但是,当在部署环境中使用它时,还需要考虑更多的复杂性。 因此,当使用静态链接部署应用时,这是另一个(更完整的)缺点列表:
- 构建花费更多的时间,并且可执行文件的大小越来越大。
- 您不能混合使用静态和共享(动态)Qt 库,这意味着您不能使用插件的功能和扩展应用而无需从头开始构建所有内容。
- 从某种意义上说,静态链接意味着隐藏用于构建应用的库。 不幸的是,并非所有库都提供此选项,并且不遵守该选项可能导致应用出现许可问题。 之所以会出现这种复杂性,部分原因是 Qt 框架使用了一些第三方库,这些库没有提供与 Qt 本身相同的许可选项。 谈论许可问题不是适合本书的讨论,因此,当您计划使用 Qt 库的静态链接创建商业应用时,您必须一定要小心。 有关 Qt 内第三方库使用的许可证的详细列表,您可以始终通过以下链接引用 Qt 网页中使用的许可证。
有关 Qt 模块中使用的各种 LGPL 许可证及其版本(以及可在网上找到的许多其他开源软件)的完整参考,请参考以下链接。
您还可以使用以下链接完整讨论有关选择 Qt 开源许可证之前需要了解的知识。
静态链接,即使有我们刚刚提到的所有缺点,仍然是一种选择,在某些情况下,如果您可以遵守 Qt 框架的许可选项,那么它还是一个很好的选择。 例如,在 Linux 操作系统中,为我们的应用创建安装程序需要额外的工作和精力,静态链接可以极大地减少部署应用所需的工作量(仅复制和粘贴)。 因此,是否使用静态链接的最终决定主要取决于您以及您打算如何部署应用。 当您对可能的链接和部署方法进行了概述时,到本章末尾,制定此重要决定将变得更加容易。
使用动态链接的部署
使用共享库(或动态链接)部署使用 Qt 和 OpenCV 构建的应用时,需要确保应用的可执行文件能够访问 Qt 和 OpenCV 的运行时库,以便加载和使用它们。 运行时库的这种可到达性或可见性取决于操作系统,可能具有不同的含义。 例如,在 Windows 上,您需要将运行时库复制到应用可执行文件所在的文件夹中,或将它们放在附加到PATH环境值的文件夹中。
Qt 框架提供了命令行工具,以简化 Windows 和 MacOS 上 Qt 应用的部署。 如前所述,您需要做的第一件事是确保您的应用是在“发布”模式而不是“调试”模式下构建的。 然后,如果您使用的是 Windows,请首先将可执行文件(假设我们将其称为app.exe)从构建文件夹复制到一个单独的文件夹(我们将其称为deploy_path),然后使用命令执行以下命令行实例:
cd deploy_path
QT_PATHbinwindeployqt app.exe
windeployqt工具是一个部署帮助工具,可简化将所需的 Qt 运行时库复制到与应用可执行文件相同的文件夹中的过程。 它只是将可执行文件作为参数,并在确定用于创建可执行文件的模块之后,复制所有必需的运行时库以及所有其他必需的依赖项,例如 Qt 插件,翻译等。 这将处理所有必需的 Qt 运行时库,但是我们仍然需要处理 OpenCV 运行时库。 如果您遵循第 1 章,“OpenCV 和 Qt 简介”中的所有步骤来动态构建 OpenCV 库,则只需手动复制opencv_world330.dll和opencv_ffmpeg330.dll 将文件从 OpenCV 安装文件夹(在x86vc14bin文件夹内)复制到应用可执行文件所在的文件夹中。
在本书的早期章节中构建 OpenCV 时,我们并没有真正受益于打开BUILD_opencv_world选项的好处。 但是,现在应该清楚的是,这通过以下方式简化了 OpenCV 库的部署和使用:在*.pro文件中只要求 LIBS 的单个条目,并且在以下情况下仅手动复制单个文件(不计算ffmpeg库): 部署 OpenCV 应用。 还应注意的是,即使您在项目中不需要或不使用所有 OpenCV 代码的所有模块,此方法也存在沿应用复制所有 OpenCV 代码的缺点。
还请注意,在 Windows 上,如在“使用静态链接进行部署”一节中所述,您仍然需要类似地向应用的最终用户提供 Microsoft Visual C++ 重分发版。
在 MacOS 操作系统上,还可以轻松部署使用 Qt 框架编写的应用。 因此,可以使用 Qt 提供的macdeployqt命令行工具。 与windeployqt相似,该文件接受 Windows 可执行文件并用所需的库填充同一文件夹,macdeployqt接受 MacOS 应用捆绑包,并通过将所有必需的 Qt 运行时复制为捆绑包内部的私有框架,使其可部署。 这是一个例子:
cd deploy_path
QT_PATH/bin/macdeployqt my_app_bundle
(可选)您还可以提供一个附加的-dmg参数,该参数导致创建 macOS *.dmg(磁盘图像)文件。 至于使用动态链接时 OpenCV 库的部署,您可以使用 Qt Installer 框架(我们将在下一节中学习),第三方供应商或确保所需运行时库的脚本来创建安装程序。 复制到其所需的文件夹。 这是因为以下事实:仅将运行时库(无论是 OpenCV 还是其他文件)复制到与应用可执行文件相同的文件夹中,并不能使它们对 MacOS 上的应用可见。 这同样适用于 Linux 操作系统,不幸的是,该操作系统甚至还没有用于部署 Qt 运行时库的工具(至少目前是这样),因此除了 OpenCV 库,我们还需要照顾 Qt 库,方法是受信任的第三方供应商(您可以在线搜索)或通过使用 Qt 本身提供的跨平台安装程序,再结合一些脚本来确保执行我们的应用时所有内容都就位。
Qt 安装程序框架
Qt 安装程序框架允许您为 Windows,MacOS 和 Linux 操作系统创建 Qt 应用的跨平台安装程序。 它允许创建标准的安装程序向导,在该向导中,用户会通过提供所有必要信息的连续对话框进入,并最终显示安装应用时的进度等,这与您可能遇到的大多数安装类似,尤其是安装 Qt 框架本身。 Qt 安装程序框架基于 Qt 框架本身,但以不同的包提供,并且不需要计算机上存在 Qt SDK(Qt 框架,Qt Creator 等)。 也可以使用 Qt Installer 框架来为任何应用(不仅仅是 Qt 应用)创建安装包。
在本节中,我们将学习如何使用 Qt Installer 框架创建基本的安装程序,该程序将在目标计算机上安装应用并复制所有必要的依赖项。 结果将是一个可执行的安装程序文件,您可以将其放在 Web 服务器上进行下载,或以 USB 记忆棒或 CD 或任何其他媒体类型提供。 该示例项目将帮助您自己着手解决 Qt Installer 框架的许多强大功能。
您可以使用以下链接下载并安装 Qt 安装程序框架。 使用此链接或其他任何下载源时,请确保仅下载最新版本。 目前,最新版本是 3.0.2。
下载并安装 Qt Installer 框架之后,可以开始创建 Qt Installer 框架创建安装程序所需的必需文件。 您可以通过简单地浏览到 Qt Installer 框架并从examples文件夹复制tutorial文件夹来完成此操作,如果要快速重命名和重新编辑所有文件并创建自己的文件夹,这也是一个快速安装模板。 我们将采用另一种方式手动创建它们; 首先,因为我们想了解 Qt Installer 框架所需文件和文件夹的结构,其次,因为它仍然非常容易和简单。 以下是创建安装程序的必需步骤:
- 假设您已经完成了 Qt 和 OpenCV 应用的开发,则可以从创建一个包含安装程序文件的新文件夹开始。 假设此文件夹名为
deploy。 - 在
deploy文件夹中创建一个 XML 文件,并将其命名为config.xml。 此 XML 文件必须包含以下内容:
<?xml version="1.0" encoding="UTF-8"?>
<Installer>
<Name>Your application</Name>
<Version>1.0.0</Version>
<Title>Your application Installer</Title>
<Publisher>Your vendor</Publisher>
<StartMenuDir>Super App</StartMenuDir>
<TargetDir>@HomeDir@/InstallationDirectory</TargetDir>
</Installer>
确保用与您的应用相关的信息替换前面代码中的必需 XML 字段,然后保存并关闭此文件:
-
现在,在
deploy文件夹内创建一个名为packages的文件夹。 该文件夹将包含您希望用户能够安装的单个包,或者使它们成为必需或可选的包,以便用户可以查看并决定要安装的包。 -
对于使用 Qt 和 OpenCV 编写的更简单的 Windows 应用,通常仅包含一个包就可以运行您的应用,甚至可以静默安装 Microsoft Visual C++ 重分发版。 但是对于更复杂的情况,尤其是当您想更好地控制应用的各个可安装元素时,您还可以使用两个或多个包,甚至子包。 通过为每个包使用类似域的文件夹名称来完成此操作。 每个包文件夹都可以具有类似
com.vendor.product的名称,其中,供应商和产品将被开发人员名称或公司及应用所代替。 可以通过在父包的名称后添加.subproduct来标识包的子包(或子组件)。 例如,您可以在packages文件夹中包含以下文件夹:
com.vendor.product
com.vendor.product.subproduct1
com.vendor.product.subproduct2
com.vendor.product.subproduct1.subsubproduct1
...
我们可以根据需要选择任意数量的产品(包装)和子产品(子包装)。 对于我们的示例案例,让我们创建一个包含可执行文件的文件夹,因为它描述了所有可执行文件,您可以通过将其他包简单地添加到packages文件夹中来创建其他包。 让我们将其命名为com.amin.qtcvapp。 现在,请执行以下必需步骤:
- 现在,在我们创建的新包文件夹
com.amin.qtcvapp文件夹中创建两个文件夹。 将它们重命名为data和meta。 这两个文件夹必须存在于所有包中。 - 将您的应用文件复制到
data文件夹中。 该文件夹将完全按原样提取到目标文件夹中(我们将在后面的步骤中讨论如何设置包的目标文件夹)。 如果您打算创建多个包,请确保以合理的方式正确分离其数据。 当然,如果不这样做,您将不会遇到任何错误,但是您的应用的用户可能会感到困惑,例如,通过跳过应始终安装的包并最终安装已安装的应用,这行不通。 - 现在,切换到
meta文件夹并在该文件夹中创建以下两个文件,并为每个文件提供的代码填充它们。
package.xml文件应包含以下内容。 无需提及,您必须使用与包相关的值填充 XML 内的字段:
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<DisplayName>The component</DisplayName>
<Description>Install this component.</Description>
<Version>1.0.0</Version>
<ReleaseDate>1984-09-16</ReleaseDate>
<Default>script</Default>
<Script>installscript.qs</Script>
</Package>
前一个 XML 文件中的脚本(可能是安装程序创建中最重要的部分)是指 Qt 安装程序脚本(*.qs文件),其名称为installerscript.qs,可用于进一步自定义包 ,其目标文件夹等。 因此,让我们在meta文件夹中创建一个具有相同名称(installscript.qs)的文件,并在其中使用以下代码:
function Component()
{
// initializations go here
}
Component.prototype.isDefault = function()
{
// select (true) or unselect (false) the component by default
return true;
}
Component.prototype.createOperations = function()
{
try {
// call the base create operations function
component.createOperations();
} catch (e) {
console.log(e);
}
}
这是最基本的组件脚本,可自定义我们的包(很好,它仅执行默认操作),并且可以选择扩展它以更改目标文件夹,在“开始”菜单或桌面(在 Windows 上)中创建快捷方式,等等。 。 密切注意 Qt Installer 框架文档并了解其脚本,以便能够创建功能更强大的安装程序,这些程序可以自动将应用的所有必需依赖项放置到位,是一个好主意。 您还可以浏览 Qt Installer 框架examples文件夹内的所有示例,并了解如何处理不同的部署案例。 例如,您可以尝试为 Qt 和 OpenCV 依赖关系创建单独的包,并允许用户取消选择它们,前提是他们的计算机上已经具有 Qt 运行时库。
- 最后一步是使用
binarycreator工具来创建我们的单个和独立安装程序。 只需使用命令提示符(或终端)实例运行以下命令:
binarycreator -p packages -c config.xml myinstaller
binarycreator位于 Qt Installer 框架bin文件夹内。 它需要我们已经准备好的两个参数。 -p之后必须是我们的packages文件夹,-c之后必须是配置文件(或config.xml)文件。 执行此命令后,您将获得myinstaller(在 Windows 上,可以在其后附加*.exe),可以执行该命令来安装应用。 该单个文件应包含运行您的应用所需的所有必需文件,其余部分将得到处理。 您只需要提供此文件的下载链接,或通过 CD 将其提供给您的用户。
以下是此默认和最基本的安装程序中将面对的对话框,其中包含安装应用时可能会遇到的大多数常见对话框:

如果转到安装文件夹,您会注意到其中包含的文件比放入包数据文件夹中的文件多。 安装程序需要这些文件来处理修改和卸载应用。 例如,您的应用用户可以通过执行maintenancetool可执行文件轻松卸载您的应用,这将产生另一个简单且用户友好的对话框来处理卸载过程:

总结
无论您是否可以在目标计算机上轻松安装并在目标计算机上轻松使用,这都意味着赢得或失去大量用户。 特别是对于非专业用户而言,必须确保创建和部署包含所有必需依赖项的安装程序,并且可以在目标平台上直接使用。 在本章中,我们对此进行了相当多的讨论。 我们了解了构建过程以及所选择的链接方法如何完全改变部署体验。 我们了解了现有的 Qt 工具,以简化 Windows 和 MacOS 上的部署过程。 请注意,这些工具包含的参数比我们在本章中看到的要多得多,因此值得您自己深入研究,并尝试各种参数以了解它们对自己的影响。 在本章的最后一部分,我们了解了 Qt Installer 框架,并通过使用它创建了一个简单的安装程序。 我们学习了如何创建使用安装程序在目标系统上提取的包。 可以使用此相同技能将所有依赖项放入其所需的文件夹中。 例如,可以将 OpenCV 库添加到包中,并在安装时将它们放在 Linux 操作系统的/usr/lib/或/usr/local/lib/中,以便您的应用可以毫无问题地访问它们。 有了这最后一组技能,我们现在已经熟悉了开发人员(尤其是计算机视觉开发人员)必须知道的开发周期的大多数现有阶段。
在本书的最后一章中,我们将向您介绍 Qt Quick 和 QML。 我们将学习如何使用 Qt 的功能和 QML 的简单性来创建漂亮的 UI。 我们还将学习如何组合 C++ 和 QML 代码,以编写使用第三方框架(例如 OpenCV)的类,这些类可从我们的 QML 代码中轻松使用。 本书的最后一章旨在帮助您结合使用 OpenCV 和极其易于使用且美观的 Qt Quick Controls,开始开发用于移动设备(Android 和 iOS)的计算机视觉应用。
十二、Qt Quick 应用
使用 Qt 窗口小部件应用项目允许通过使用 Qt Creator 设计模式创建灵活而强大的 GUI,或者在文本编辑器中手动修改 GUI 文件(*.ui)。 到目前为止,在本书的所有章节中,我们都基于 Qt Widgets 应用作为创建的 GUI 的基础,并且我们在第 3 章,“创建一个全面的 Qt + OpenCV 项目”中了解到,我们可以使用样式表来有效地更改 Qt 应用的外观。 但是,除了 Qt Widgets 应用并使用QtWidgets和QtGui模块之外,Qt 框架还提供了另一种创建 GUI 的方法。 这种方法基于QtQuick模块和 QML 语言,并且允许创建更加灵活的 GUI(在外观,感觉,动画,效果等方面),并且更加轻松。 使用这种方法创建的应用称为 Qt Quick 应用。 请注意,在较新的 Qt 版本(5.7 和更高版本)中,您还可以创建 Qt Quick Controls 2 应用,它为创建 Qt Quick 应用提供了更多改进的类型,我们还将重点关注这一点。
QtQuick模块和QtQml模块是包含所有必需类的模块,以便在 C++ 应用中使用 Qt Quick 和 QML 编程。 另一方面,QML 本身是一种高度可读的声明性语言,它使用类似于 JSON 的语法(与脚本结合)来描述用户界面的各种组件以及它们之间的交互方式。 在本章中,我们将向您介绍 QML 语言以及如何使用它简化创建 GUI 应用的过程。 通过创建示例基于 QML 的 GUI 应用(或更确切地说是 Qt Quick Controls 2 应用),我们将了解其简单易读的语法以及如何在实践中使用它。 尽管使用 QML 语言不一定需要对 C++ 语言有深入的了解,但了解 Qt Quick 项目的结构仍然非常有用,因此我们将简要介绍最基本的 Qt Quick 应用的结构。 通过研究一些最重要的 QML 库,我们将了解现有的可视和非可视 QML 类型,这些类型可用于创建用户界面,向其中添加动画,访问硬件等。 我们将学习如何使用集成到 Qt Creator 中的 Qt Quick Designer 通过图形设计器修改 QML 文件。 稍后,通过学习 C++ 和 QML 的集成,我们将填补它们之间的空白,并学习如何在 Qt Quick 应用中使用 OpenCV 框架。 在最后一章中,我们还将学习如何使用与 Qt 和 OpenCV 相同的桌面项目来创建移动计算机视觉应用,并将我们的跨平台范围扩展到桌面平台之外,并扩展到移动世界。
本章涵盖的主题包括:
- QML 简介
- Qt Quick 应用项目的结构
- 创建 Qt Quick Controls 2 应用
- 使用 Qt Quick Designer
- 集成 C++ 和 QML
- 在 Android 和 iOS 上运行 Qt 和 OpenCV 应用
QML 简介
如引言中所述,QML 具有类似于 JSON 的结构,可用于描述用户界面上的元素。 QML 代码导入一个或多个库,并且具有一个包含所有其他可视和非可视元素的根元素。 以下是 QML 代码的示例,该代码导致创建具有指定宽度,高度和标题的空窗口(ApplicationWindow类型):
import QtQuick 2.7
import QtQuick.Controls 2.2
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
}
每个import语句后都必须带有 QML 库名称和版本。 在前面的代码中,导入了包含大多数默认类型的两个主要 QML 库。 例如,在QtQuick.Controls 2.2库中定义了ApplicationWindow。 现有 QML 库及其正确版本的唯一真实来源是 Qt 文档,因此请确保始终引用它,以防需要使用其他任何类。 如果使用 Qt Creator 帮助模式搜索ApplicationWindow,您将发现所需的import语句就是我们刚刚使用的。 值得一提的另一件事是,先前代码中的ApplicationWindow是单个根元素,并且所有其他 UI 元素都必须在其中创建。 让我们通过添加显示一些文本的Label元素来进一步扩展代码:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
Label
{
x: 25
y: 25
text: "This is a label<br>that contains<br>multiple lines!"
}
}
由于它们与以前的代码相同,因此我们跳过了前面的代码中的import语句。 请注意,新添加的Label具有text属性,该属性是标签上显示的文本。 x和y只是指Label在ApplicationWindow内部的位置。 可以使用非常类似的方式添加诸如组框之类的容器项。 让我们添加一个,看看它是如何完成的:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
GroupBox
{
x: 50
y: 50
width: 150
height: 150
Label
{
x: 25
y: 25
text: "This is a label<br>that contains<br>multiple lines!"
}
}
}
此 QML 代码将导致一个类似于以下所示的窗口:

请注意,每个元素的位置都是与其父元素的偏移量。 例如,将GroupBox内提供给Label的x和y值添加到GroupBox本身的x和y属性中,这就是在根元素(在本例中为ApplicationWindow)中确定 UI 元素的最终位置。
与 Qt 窗口小部件类似,您也可以在 QML 代码中使用布局来控制和组织 UI 元素。 为此,可以使用GridLayout,ColumnLayout和RowLayout QML 类型,但首先,需要使用以下语句导入它们:
import QtQuick.Layouts 1.3
现在,您可以将 QML 用户界面元素作为子项添加到布局中,并由其自动管理。 让我们在ColumnLayout中添加一些按钮,看看如何完成此操作:
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
Button
{
text: "First Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
Button
{
text: "Second Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
Button
{
text: "Third Button"
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
}
}
}
这将导致类似于以下的窗口:

在前面的代码中,ColumnLayout的行为类似于我们在 Qt Widgets 应用中使用的垂直布局。 从上到下,作为子元素添加到ColumnLayout的每个元素都会显示在前一个元素之后,无论ColumnLayout的大小如何,始终调整其大小和位置以保持垂直布局视图。 关于上述内容,还有两点需要注意。 首先,使用以下代码将ColumnLayout本身的大小设置为父大小:
anchors.fill: parent
anchors是 QML 视觉元素的最重要属性之一,它照顾元素的大小和位置。 在这种情况下,通过将anchors的fill值设置为另一个对象(parent对象),我们将ColumnLayout的大小和位置描述为与ApplicationWindow相同。 通过正确使用锚点,我们可以以更大的功能和灵活性处理对象的大小和位置。 作为另一个示例,将代码中的anchors.fill行替换为以下内容,然后看看会发生什么:
width: 100
height: 100
anchors.centerIn: parent
显然,我们的ColumnLayout现在具有恒定的大小,并且当ApplicationWindow调整大小时它不会改变; 但是,布局始终保持在ApplicationWindow的中心。 关于上述代码的最后一点是:
Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter
添加到ColumnLayout的每个项目内的该行使该项目将自身垂直和水平定位在其单元格的中心。 请注意,这种意义上的单元格不包含任何可视边界,并且与布局本身一样,布局内的单元格也是在其中组织项目的非可视方式。
QML 代码的扩展遵循相同的模式,无论添加或需要多少项。 但是,随着 UI 元素的数量越来越大,最好将用户界面分成单独的文件。 可以将同一文件夹中的 QML 文件用作预定义的重要项目。 假设我们有一个名为MyRadios.qml的 QML 文件,其中包含以下代码:
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
Item
{
ColumnLayout
{
anchors.centerIn: parent
RadioButton
{
text: "Video"
}
RadioButton
{
text: "Image"
}
}
}
您可以在同一文件夹的另一个 QML 文件中使用此 QML 文件及其Item。 假设我们在MyRadios.qml所在的文件夹中有一个main.qml文件。 然后,您可以像这样使用它:
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
ApplicationWindow
{
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
MyRadios
{
width: 100
height: 200
}
}
}
请注意,只要 QML 文件都在同一文件夹中,就不需要导入语句。 如果要在代码中使用的 QML 文件位于单独的文件夹(同一文件夹中的子文件夹)中,则必须使用以下语句将其导入:
import "other_qml_path"
显然,在前面的代码中,other_qml_path是我们的 QML 文件的相对路径。
QML 中的用户交互和脚本编写
对 QML 代码中的用户操作和事件的响应是通过将脚本添加到项目的插槽中来完成的,这与 Qt 窗口小部件非常相似。 此处的主要区别在于,在 QML 类型内部定义的每个信号还具有为其自动生成的对应插槽,并且可以填充脚本以在发出相关信号时执行操作。 好吧,让我们看另一个例子。 QML Button类型具有按下信号。 这自动意味着有一个onPressed插槽,可用于编码特定按钮的所需操作。 这是一个示例代码:
Button
{
onPressed:
{
// code goes here
}
}
有关 QML 类型的可用插槽的列表,请参阅 Qt 文档。 如前所述,您可以通过大写信号名称的第一个字母并在其前面加上on来轻松猜测每个信号的插槽名称。 因此,对于pressed信号,您将有一个onPressed插槽,对于released信号,您将有一个onReleased插槽,依此类推。
为了能够从脚本或插槽中访问其他 QML 项目,首先,您必须为其分配唯一的标识符。 请注意,这仅是您要访问和修改或与之交互的项目所必需的。 在本章的所有先前示例中,我们仅创建了项目,而没有为其分配任何标识符。 通过为项目的id属性分配唯一标识符,可以轻松完成此操作。 id属性的值遵循变量命名约定,这意味着它区分大小写,不能以数字开头,依此类推。 这是一个示例代码,演示如何在 QML 代码中分配和使用id:
ApplicationWindow
{
id: mainWindow
visible: true
width: 300
height: 500
title: "Hello QML"
ColumnLayout
{
anchors.fill: parent
Button
{
text: "Close"
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
onPressed:
{
mainWindow.close()
}
}
}
}
在前面的代码中,ApplicationWindow分配有一个 ID; 也就是mainWindow,它在Button的onPressed插槽内用于访问它。 您可以猜测,按前面代码中的“关闭”按钮将导致mainWindow被关闭。 无论在 QML 文件中的哪个位置定义 ID,都可以在该特定 QML 文件中的任何位置访问它。 这意味着 ID 的范围不限于相同的项目组或项目的子级,依此类推。 简而言之,任何 ID 对 QML 文件中的所有项目都是可见的。 但是,单独的 QML 文件中某项的id呢? 为了能够访问单独的 QML 文件中的项目,我们需要通过将其分配给property alias来导出它,如以下示例所示:
Item
{
property alias videoRadio: videoRadio
property alias imageRadio: imageRadio
ColumnLayout
{
anchors.centerIn: parent
RadioButton
{
id: videoRadio
text: "Video"
}
RadioButton
{
id: imageRadio
text: "Image"
}
}
}
前面的代码是相同的MyRadios.qml文件,但是这次,我们使用根项的别名属性导出了其中的两个RadioButton项。 这样,我们可以在使用MyRadios的单独 QML 文件中访问这些项目。 除了导出项目中的项目外,属性还可用于包含特定项目所需的任何其他值。 因此,这是在 QML 项中定义附加属性的一般语法:
property TYPE NAME: VALUE
在TYPE可以包含任何 QML 类型的情况下,NAME是属性的给定名称,VALUE是属性的值,必须与提供的类型兼容。
使用 Qt Quick Designer
由于 QML 文件的语法简单易读,因此可以使用任何代码编辑器轻松对其进行修改和扩展。 但是,您也可以使用 Qt Creator 中集成的快速设计器来简化 QML 文件的设计和修改。 如果您尝试在 Qt Creator 中打开 QML 文件并切换到“设计”模式,则会看到以下“设计”模式,它与标准 Qt Widgets 设计器(用于*.ui文件)有很大不同, 包含使用 QML 文件快速设计用户界面所需的大部分内容:

在“Qt Quick 设计器”屏幕的左侧,您可以在“库”窗格中看到可以添加到用户界面的 QML 类型的库。 它与 Qt Widgets 工具箱类似,但肯定有更多组件可用于设计应用的用户界面。 您只需在用户界面上拖放它们中的每一个,它们就会自动添加到您的 QML 文件中:

“库”窗格的正下方是“导航器”窗格,它在用户界面上显示组件的层次结构视图。 您可以使用“导航器”窗格,只需双击它们即可快速设置 QML 文件中的项目 ID。 此外,您可以将项目导出为别名,以便可以在其他 QML 文件中使用它,也可以在设计时将其隐藏(以便查看重叠的 QML 项目)。 在“导航器”窗格上的以下屏幕快照中,请注意在将button2导出为别名并将button3在设计期间隐藏之后,组件旁边的小图标是如何变化的:

在 Qt Quick 设计器的右侧,您可以找到“属性”窗格。 与标准 Qt 设计模式下的“属性”窗格相似,此窗格可用于详细操作和修改 QML 项的属性。 该窗格的内容根据用户界面上的选定项目而变化。 除了 QML 项目的标准属性外,此窗格还允许修改与单个项目的布局有关的属性。 以下屏幕快照描绘了在用户界面上选择“按钮”项时“属性”窗格的不同视图:

除了用于设计 QML 用户界面的辅助工具外,Qt Quick Designer 可以帮助您了解 QML 语言本身,因为在设计器中完成的所有修改都将转换为 QML 代码并存储在同一 QML 文件中。 通过使用它来设计用户界面,以确保熟悉它的用法。 例如,您可以尝试设计一些与创建 Qt Widgets 应用时相同的用户界面,但是这次使用 Qt Quick Designer 和 QML 文件。
Qt Quick 应用的结构
在本节中,我们将学习 Qt Quick 应用项目的结构。 与 Qt Widgets 应用项目类似,使用 Qt Creator 创建新项目时,会自动创建 Qt Quick 应用项目所需的大多数文件,因此您实际上并不需要记住所有的最低要求,但是仍然重要的是要理解如何处理 Qt Quick 应用的一些基本概念,以便能够进一步扩展它,或者,如我们将在本章后面的部分中了解的那样,在 QML 文件中集成和使用 C++ 代码。
让我们通过创建一个示例应用来解决这个问题。 首先打开 Qt Creator,然后在欢迎屏幕上按“新建项目”按钮,或者从“文件”菜单中选择“新建文件”或“项目”。 选择“Qt Quick Controls 2 应用”作为模板类型,然后按“选择”,如以下屏幕截图所示:

将项目名称设置为CvQml,然后按Next。 在“定义构建系统”页面中,将“构建系统”保留为qmake,默认情况下应将其选中。 在“定义项目详细信息”页面中,可以为 Qt Quick Controls 2 样式选择以下选项之一:
- 默认
- 材料
- 通用
您在此屏幕中选择的选项会影响应用的整体样式。 “默认”选项会导致使用默认样式,从而使 Qt Quick Controls 2 以及我们的 Qt Quick 应用具有最高性能。 Material 样式可用于根据 Google Material Design 准则创建应用。 它提供了更具吸引力的组件,但也需要更多资源。 最后,通用样式可用于基于 Microsoft 通用设计准则创建应用。 与 Material 风格相似,这也需要更多资源,但提供了另一套引人注目的用户界面组件。
您可以参考以下链接,以获得有关用于创建“材质”和“通用”样式的准则的更多信息:
https://dev.windows.com/design
下面的截图描述了一些常见的组件之间的差异,在选择的三种可能的风格每一个选项如何看您的应用:

无论您选择什么,以后都可以在名为qtquickcontrols2.conf的专用设置文件中轻松更改此设置,该文件会自动包含在新项目中。 甚至可以在以后更改颜色以匹配深色或浅色主题或任何其他颜色。 无论如何,请选择所需的一个(或将其保留为默认),然后继续按Next,直到最终进入 Qt 代码编辑器。 现在,您的项目几乎包含 Qt Quick 应用所需的最少文件。
请注意,每当我们在本章中提到 Qt Quick 应用时,我们实际上是指 Qt Quick Controls 2 应用,它是我们刚刚创建并将扩展到的新的增强型 Qt Quick 应用(在 Qt 5.7 和更高版本中可用)。 完整,美观的跨平台计算机视觉应用。
首先,让我们看一下项目(*.pro)文件中的区别。 在与 Qt Widgets 应用相对的 Qt Quick 应用中,默认情况下使用QtQml和QtQuick模块代替QtCore,QtGui和QtWidgets模块。 您可以通过打开CvQml.pro文件来进行检查,该文件的顶部具有以下行:
QT += qml quick
您可以在 Qt 项目中期望的两个文件,无论是 Qt Widgets 应用还是 Qt Quick 应用,都是一个项目和一个包含main函数的 C++ 源文件。 因此,除了CvQml.pro文件之外,还有一个main.cpp文件,其中包含以下内容:
#include <QGuiApplication>
#include <QQmlApplicationEngine>
int main(int argc, char *argv[])
{
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
QGuiApplication app(argc, argv);
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
return app.exec();
}
该main.cpp与创建 Qt Widgets 应用时所看到的完全不同。 记住,在 Qt Widgets 应用的main.cpp内部和主函数中,创建了QApplication,然后显示主窗口,程序进入事件循环,以便该窗口保持活动状态,并且所有事件已处理,如下所示:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
类似地,在 Qt Quick 应用中,创建了QGuiApplication,但是这次没有加载任何窗口,而是使用QQmlApplicationEngine加载了 QML 文件,如下所示:
QQmlApplicationEngine engine;
engine.load(QUrl(QLatin1String("qrc:/main.qml")));
if (engine.rootObjects().isEmpty())
return -1;
这清楚地表明 QML 文件实际上是在运行时加载的,因此您可以从磁盘加载它们,或者在我们的示例中,可以从作为资源存储在qml.qrc文件中并嵌入到可执行文件中的main.qml文件加载它们。 实际上,这是开发 Qt Quick 应用的常用方法,如果您检查新创建的CvQml项目,则会注意到它包含一个名为qml.qrc的 Qt 资源文件,其中包含该项目的所有 QML 文件 。 qml.qrc文件包含以下文件:
main.qml,它是main.cpp文件中加载的 QML 文件,它是我们 QML 代码的入口点。Page1.qml包含Page1FormQML 类型的交互和脚本。Page1Form.ui.qml包含Page1Form类型内的用户界面和 QML 项目。 请注意,成对的Page1.qml和Page1Form.ui.qml是分离用户界面及其底层代码的常用方法,类似于在开发 Qt Widgets 应用时使用mainwindow.ui,mainwindow.h和mainwindow.cpp文件的方法。 。qtquickcontrols2.conf文件是可用于更改 Qt Quick 应用样式的配置文件。 它包含以下内容:
; This file can be edited to change the style of the application
; See Styling Qt Quick Controls 2 in the documentation ...
; http://doc.qt.io/qt-5/qtquickcontrols2-styles.html
[Controls]
Style=Default
[Universal]
Theme=Light
;Accent=Steel
[Material]
Theme=Light
;Accent=BlueGrey
;Primary=BlueGray
行首的分号;表示仅是注释。 您可以将前面代码中的Style变量的值更改为Material和Universal,以更改应用的整体样式。 根据所设置的样式,可以在前面的代码中使用Theme,Accent或Primary值来更改应用中使用的主题。
有关主题和颜色的完整列表,以及有关如何在每个主题中使用各种可用的自定义设置的其他信息,您可以参考以下链接:
https://goo.gl/jDZGPm(用于默认样式)
https://goo.gl/Um9qJ4(用于材料样式)
https://goo.gl/U6uxrh(用于通用样式)
关于 Qt Quick 应用的一般结构。 这种结构可立即用于任何平台的任何类型的应用。 请注意,您没有义务使用自动创建的文件,并且可以简单地从一个空项目开始或删除不必要的默认文件并从头开始。 例如,在我们的示例 Qt Quick 应用(标题为CvQml)中,我们不需要Page1.qml和Page1Form.ui.qml文件,因此只需从qml.qrc文件中选择它们并通过右键单击将其删除。 然后选择删除文件。 当然,这将导致main.qml文件中缺少代码。 因此,在继续下一部分之前,请确保将其更新为以下内容:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
ApplicationWindow
{
visible: true
width: 300
height: 500
title: qsTr("CvQml")
}
集成 C++ 和 QML 代码
即使 QML 库已经成长为可以处理视觉,网络,摄像机等的完整类型集合,但仍然可以使用 C++ 类的功能对其进行扩展仍然很重要。 幸运的是,QML 和 Qt 框架提供了足够的规定以能够轻松地处理此问题。 在本节中,我们将学习如何创建一个非可视的 C++ 类,该类可以在 QML 代码内使用 OpenCV 处理图像。 然后,我们将创建一个 C++ 类,该类可用作 QML 代码中的可视项以显示图像。
请注意,默认情况下,QML 中有一个图像类型,可通过将其 URL 提供给“图像”项来显示保存在磁盘上的图像。 但是,我们将创建一个可用于显示QImage对象的图像查看器 QML 类型,并利用此机会来学习 CML 类(可视化)在 QML 代码中的集成。
首先将 OpenCV 框架添加到上一节中创建的项目中。 这与创建 Qt Widgets 应用时完全相同,并且在*.pro文件中包含必需的行。 然后,通过在项目窗格中右键单击新的 C++ 类并将其添加到项目中,然后选择“添加新的”。 确保类名称为QImageProcessor且其基类为QObject,如以下屏幕截图所示:

将以下#include指令添加到qimageprocessor.h文件中:
#include <QImage>
#include "opencv2/opencv.hpp"
然后将以下函数添加到QImageProcessor类的公共成员区域:
Q_INVOKABLE void processImage(const QString &path);
Q_INVOKABLE是 Qt 宏,它允许使用 Qt 元对象系统调用(调用)函数。 由于 QML 使用相同的 Qt 元对象作为对象之间的基础通信机制,因此用Q_INVOKABLE宏标记函数就足够了,以便可以从 QML 代码中调用它。 另外,将以下信号添加到QImageProcessor类:
signals:
void imageProcessed(const QImage &image);
我们将使用此信号将经过处理的图像传递给稍后将创建的图像查看器类。 最后,为了实现processImage函数,请将以下内容添加到qimageprocessor.cpp文件中:
void QImageProcessor::processImage(const QString &path)
{
using namespace cv;
Mat imageM = imread(path.toStdString());
if(!imageM.empty())
{
bitwise_not(imageM, imageM); // or any OpenCV code
QImage imageQ(imageM.data,
imageM.cols,
imageM.rows,
imageM.step,
QImage::Format_RGB888);
emit imageProcessed(imageQ.rgbSwapped());
}
else
{
qDebug() << path << "does not exist!";
}
}
这里没有我们没有看到或使用过的新东西。 此函数仅获取图像的路径,从磁盘读取图像,执行图像处理,但为了简单起见,我们可以使用bitwise_not函数将所有通道中的像素值取反,最后使用我们定义的信号的图像产生结果。
我们的图像处理器现已完成。 现在,我们需要创建一个 Visual C++ 类型,该类型可在 QML 中用于显示QImage对象。 因此,创建另一个类并将其命名为QImageViewer,但这一次请确保它是QQuickItem子类,如以下新类向导屏幕截图所示:

修改qimageviewer.h文件,如下所示:
#include <QQuickItem>
#include <QQuickPaintedItem>
#include <QImage>
#include <QPainter>
class QImageViewer : public QQuickPaintedItem
{
Q_OBJECT
public:
QImageViewer(QQuickItem *parent = Q_NULLPTR);
Q_INVOKABLE void setImage(const QImage &img);
private:
QImage currentImage;
void paint(QPainter *painter);
};
我们已经将QImageViewer类设为QQuickPaintedItem的子类。 同样,构造器也会进行更新以匹配此修改。 我们在此类中使用Q_INVOKABLE宏定义了另一个函数,该函数将用于设置要在此类实例上显示的QImage,或者确切地说,将设置使用该类型创建的 QML 项。 QQuickPaintedItem提供了一种创建新的可视 QML 类型的简单方法; 也就是说,通过对其进行子类化并重新实现paint函数,如前面的代码所示。 传递给此类中的paint函数的painter指针可用于绘制我们需要的任何内容。 在这种情况下,我们只想在其上绘制图像; 也就是说,我们已经定义了currentImage,它是QImage,它将保存要在QImageViewer类上绘制的图像。
现在,我们需要添加setImage的实现并绘制函数,并根据在头文件中所做的更改来更新构造器。 因此,请确保qimageviewer.cpp文件如下所示:
#include "qimageviewer.h"
QImageViewer::QImageViewer(QQuickItem *parent)
: QQuickPaintedItem(parent)
{
}
void QImageViewer::setImage(const QImage &img)
{
currentImage = img.copy(); // perform a copy
update();
}
void QImageViewer::paint(QPainter *painter)
{
QSizeF scaled = QSizeF(currentImage.width(),
currentImage.height())
.scaled(boundingRect().size(), Qt::KeepAspectRatio);
QRect centerRect(qAbs(scaled.width() - width()) / 2.0f,
qAbs(scaled.height() - height()) / 2.0f,
scaled.width(),
scaled.height());
painter->drawImage(centerRect, currentImage);
}
在前面的代码中,setImage函数非常简单; 它会复制图像并将其保存,然后调用QImageViwer类的更新函数。 在QQuickPaintedItem(类似于QWidget)内部调用update时,将导致重新绘制,因此将调用我们的绘制函数。 如果我们想在QImageViewer的整个可显示区域上拉伸图像,则此函数仅需要最后一行(centerRect替换为boundingRect); 但是,我们希望结果图像适合屏幕并保留宽高比。 因此,我们进行了比例转换,然后确保图像始终位于可显示区域的中心。
我们快到了,我们的两个新 C++ 类(QImageProcessor和QImageViewer)都可以在 QML 代码中使用。 剩下要做的唯一事情就是确保它们对我们的 QML 代码可见。 因此,我们需要确保使用qmlRegisterType函数注册了它们。 必须在我们的main.cpp文件中调用此函数,如下所示:
qmlRegisterType<QImageProcessor>("com.amin.classes",
1, 0, "ImageProcessor");
qmlRegisterType<QImageViewer>("com.amin.classes",
1, 0, "ImageViewer");
然后将其放在main.cpp文件中定义QQmlApplicationEngine的位置之前。 不用说,您必须通过使用以下#include指令在main.cpp文件中包括我们两个新类:
#include "qimageprocessor.h"
#include "qimageviewer.h"
请注意,qmlRegisterType函数调用中的com.amin.classes可以用您自己的类似域的标识符替换,这是我们为包含QImageProcessor和QImageViewer类的库提供的名称。 以下1和0引用该库的版本 1.0,最后一个文字字符串是可在我们的 QML 类型内部使用的类型标识符,以访问和使用这些新类。
最后,我们可以开始使用main.qml文件中的 C++ 类。 首先,确保您的import语句符合以下条件:
import QtQuick 2.7
import QtQuick.Controls 2.0
import QtQuick.Layouts 1.3
import QtMultimedia 5.8
import com.amin.classes 1.0
最后一行包括我们刚刚创建的ImageProcessor和ImageViewer QML 类型。 我们将使用 QML 摄像机类型访问摄像机并使用它捕获图像。 因此,将以下内容添加为main.qml文件中ApplicationWindow项目的直接子代:
Camera
{
id: camera
imageCapture
{
onImageSaved:
{
imgProcessor.processImage(path)
}
}
}
在前面的代码中,imgProcessor是我们的ImageProcessor类型的id,还需要将其定义为ApplicationWindow的子项,如下所示:
ImageProcessor
{
id: imgProcessor
onImageProcessed:
{
imgViewer.setImage(image);
imageDrawer.open()
}
}
请注意,由于我们在QImageProcessor类内部创建了imageProcessed信号,因此自动生成了前面代码中的onImageProcessed插槽。 您可以猜测imgViewer是我们之前创建的QImageViewer类,并且将其图像设置在onImageProcessed插槽内。 在此示例中,我们还使用了 QML Drawer,该 QML Drawer在调用其打开函数时在另一个窗口上滑动,并且我们已嵌入imgViewer作为此Drawer的子项。 Drawer和ImageViewer的定义如下:
Drawer
{
id: imageDrawer
width: parent.width
height: parent.height
ImageViewer
{
id: imgViewer
anchors.fill: parent
Label
{
text: "Swipe from right to left<br>to return to capture mode!"
color: "red"
}
}
}
就是这样,剩下要做的唯一一件事情就是添加一个 QML VideoOutput,它可以预览摄像机。 我们将使用此VideoOutput捕获图像,从而调用 QML Camera类型的imageCapture.onImageSaved插槽,如下所示:
VideoOutput
{
source: camera
anchors.fill: parent
MouseArea
{
anchors.fill: parent
onClicked:
{
camera.imageCapture.capture()
}
}
Label
{
text: "Touch the screen to take a photo<br>and process it using OpenCV!"
color: "red"
}
}
如果立即启动该应用,您将立即面对计算机上默认照相机的输出。 如果单击视频输出内部,将捕获并处理图像,然后将其显示在Drawer上,该Drawer在当前页面上从左到右滑动。 以下是该应用执行时的屏幕截图:

Android 和 iOS 上的 Qt 和 OpenCV 应用
理想情况下,您可以在台式机和移动平台上构建并运行通过使用 Qt 和 OpenCV 框架创建的应用,而无需编写任何特定于平台的代码。 但是,实际上,这并不像看起来那样容易,因为 Qt 和 OpenCV 之类的框架充当操作系统本身功能的包装器(在某些情况下),并且由于它们仍在进行广泛的开发,因此可能会有一些尚未在特定操作系统(例如 Android 或 iOS)中完全实现的案例。 好消息是,随着新版本的 Qt 和 OpenCV 框架的发布,这些情况变得越来越罕见,即使现在(Qt 5.9 和 OpenCV 3.3),这两个框架中的大多数类和函数都可以在 Windows 中轻松使用。 ,Linux,macOS,Android 和 iOS 操作系统。
因此,首先,请牢记我们刚才提到的内容,可以说(实际上是相对于理想情况而言),要能够在 Android 和 iOS 上构建和运行使用 Qt 和 OpenCV 编写的应用,我们需要确保以下东西:
- 必须安装适用于 Android 和 iOS 的相应 Qt 套件。 这可以在 Qt 框架的初始安装过程中完成(有关此信息,请参阅第 1 章,“OpenCV 和 Qt 简介”)。
请注意,Android 套件可在 Windows,Linux 和 MacOS 上使用,而 iOS 套件仅适用于 macOS,因为使用 Qt 的 iOS 应用开发仅限于 macOS(目前)。
- 必须从 OpenCV 网站上下载适用于 Android 和 iOS 的预构建 OpenCV 库(目前,它们是从 opencv.org 提供)并提取到您的计算机中。 必须按照在 Windows 或任何其他桌面平台中添加的方式将它们添加到 Qt 项目文件中。
- 对于 iOS,在您的 MacOS 操作系统上拥有最新版本的 Xcode 就足够了。
- 对于 Android,您必须确保在计算机上安装 JDK,Android SDK,Android NDK 和 Apache Ant。 如果使用 Qt Creator 选项内“设备”页面中的 Android 选项卡,将所需的程序下载并安装到计算机上,则 Qt Creator 可以简化 Android 开发环境的配置(请参见以下屏幕截图):

请注意上图中“浏览”按钮旁边的按钮。 它们提供了下载页面的链接以及在线链接,您可以从中获得所有必需依赖项的副本。
如果要为 Android 和 iOS 操作系统构建应用,这就是您需要照顾的所有事情。 使用 Qt 和 OpenCV 构建的应用也可以在 Windows,macOS,Android 和 iOS 的应用商店中发布。 此过程通常涉及与这些操作系统的提供者注册为开发人员。 您可以在上述应用商店中找到在线和在全球范围内发布应用的准则和要求。
总结
在本章中,我们了解了 Qt Quick 应用开发和 QML 语言。 我们从这种高度可读且易于使用的语言的裸露语法开始,然后转向开发包含可以相互交互以实现一个共同目标的组件的应用。 我们学习了如何填补 QML 和 C++ 代码之间的空白,然后建立了可视类和非可视类来处理和显示使用 OpenCV 处理的图像。 我们还简要介绍了在 Android 和 iOS 平台上构建和运行相同应用所需的工具。 本书的最后一章旨在通过开始使用新的 Qt Quick Controls 2 模块开发快速,美观的应用,并将 C++ 代码和 OpenCV 等第三方框架的功能结合起来,来帮助您站起来。 在开发移动和桌面应用时获得最大的功能和灵活性。
构建跨平台和吸引人的应用从未如此简单。 通过使用 Qt 和 OpenCV 框架,尤其是 QML 的功能,可以快速轻松地构建应用,您可以立即开始实现所有计算机视觉创意。 我们在本章中学到的只是对 Qt Quick 和 QML 语言必须提供的所有可能性的介绍。 但是,您是需要将这些部分放在一起以构建可解决该领域中现有问题的应用的人。


浙公网安备 33010602011771号