一、构建图像查看器
计算机视觉是使计算机能够对数字图像和视频有较高了解的技术,而不仅仅是将它们视为字节或像素。 它广泛用于场景重建,事件检测,视频跟踪,对象识别,3D 姿态估计,运动估计和图像恢复。
OpenCV(开源计算机视觉)是一个实现几乎所有计算机视觉方法和算法的库。 Qt 是一个跨平台的应用框架和窗口小部件工具箱,用于创建具有图形用户界面的应用,这些用户界面可以在所有主要的台式机平台,大多数嵌入式平台甚至移动平台上运行。
在许多受益于计算机视觉技术的行业中,这两个功能强大的库被许多开发人员一起使用,以创建具有可靠 GUI 的专业软件。 在本书中,我们将演示如何使用 Qt 5 和 OpenCV 4 构建这些类型的功能应用,它们具有友好的图形用户界面以及与计算机视觉技术相关的多种功能。
在第一章中,我们将从构建一个简单的 GUI 应用开始,以使用 Qt 5 进行图像查看。
本章将涵盖以下主题:
- 设计用户界面
- 使用 Qt 读取和显示图像
- 放大和缩小图像
- 以任何受支持的格式保存图像副本
- 响应 Qt 应用中的热键
技术要求
确保至少安装了 Qt 版本 5 并具有 C++ 和 Qt 编程的一些基本知识。 还需要兼容的 C++ 编译器,即 Linux 上的 GCC 5 或更高版本,MacOS 上的 Clang 7.0 或更高版本,以及 Microsoft Windows 的 MSVC 2015 或更高版本。
由于必须具备一些相关的基础知识,因此本书不包括 Qt 安装和编译器环境设置。 有很多书籍,在线文档或教程(例如,《使用 C++ 和 Qt5 的 GUI 编程》,作者 Lee Zhi Eng 以及官方的 Qt 库文档)可以帮助您逐步讲解这些基本配置过程; 用户可以根据需要自行参考。
具备所有这些先决条件后,让我们开始开发第一个应用-简单的图像查看器。
设计用户界面
构建应用的第一部分是定义应用将要执行的操作。 在本章中,我们将开发一个图像查看器应用。 它应具有的功能如下:
- 从硬盘打开图像
- 放大/缩小
- 查看同一文件夹中的上一张或下一张图像
- 将当前图像的副本以其他格式另存为另一个文件(具有不同的路径或文件名)
我们可以遵循许多图像查看器应用,例如 Linux 上的 gThumb 和 MacOS 上的 Preview 应用。 但是,我们的应用比进行一些预先计划的应用要简单。 这涉及使用铅笔绘制应用原型的线框。
Pencil 是功能性的原型制作工具。 有了它,您可以轻松创建模型。 它是开源且独立于平台的软件。 铅笔的最新版本现在是基于电子的应用。 它可以在 Windows,Linux 和 MacOS 上良好运行。 您可以从这里免费下载。
以下是显示我们的应用原型的线框:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PaTCrNVP-1681871114312)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/251333cc-d29d-4708-aaba-717b82b2af4d.png)]
如上图所示,我们在主窗口中有四个区域:菜单栏,工具栏,主区域和状态栏。
菜单栏上有两个菜单选项-文件和视图菜单。 每个菜单将具有其自己的一组操作。 文件菜单包含以下三个操作,如下所示:
- 打开:此选项从硬盘打开图像。
- 另存为:此选项以任何受支持的格式将当前图像的副本另存为另一个文件(具有不同的路径或文件名)。
- 退出:此选项退出应用。
视图菜单包含四个操作,如下所示:
- 放大:此选项放大图像。
- 缩小:此选项缩小图像。
- 上一个:此选项可打开当前文件夹中的上一个图像。
- 下一个:此选项可打开当前文件夹中的下一张图像。
工具栏由几个按钮组成,也可以在菜单选项中找到。 我们将它们放在工具栏上,为用户提供触发这些操作的快捷方式。 因此,有必要包括所有经常使用的操作,包括以下内容:
- 打开
- 放大
- 缩小
- 上一张图片
- 下一张图片
主区域用于显示由应用打开的图像。
状态栏用于显示与我们正在查看的图像有关的一些信息,例如其路径,尺寸及其大小(以字节为单位)。
您可以在 GitHub 上的代码存储库中找到此设计的源文件。 该文件仅位于存储库的根目录中,名为WireFrames.epgz
。 不要忘记应该使用 Pencil 应用将其打开。
从头开始项目
在本节中,我们将从头开始构建图像查看器应用。 您所使用的集成开发环境(IDE)或编辑器均不做任何假设。 我们将只关注代码本身以及如何在终端中使用qmake
来构建应用。
首先,让我们为我们的项目创建一个名为ImageViewer
的新目录。 我使用 Linux 并在终端中执行此操作,如下所示:
$ pwd /home/kdr2/Work/Books/Qt5-And-OpenCV4-Computer-Vision-Projects/Chapter-01 $ mkdir ImageViewer $
然后,我们在该目录中创建一个名为main.cpp
的 C++ 源文件,其内容如下:
#include <QApplication> #include <QMainWindow> int main(int argc, char *argv[]) { QApplication app(argc, argv); QMainWindow window; window.setWindowTitle("ImageViewer"); window.show(); return app.exec(); }
该文件将成为我们应用的网关。 在此文件中,我们首先包括 Qt 库提供的基于 GUI 的 Qt 应用的专用头文件。 然后,我们定义main
函数,就像大多数 C++ 应用一样。 在main
函数中,我们定义了QApplication
类的实例,该实例表示我们的图像查看器应用正在运行,并且定义了QMainWindow
的实例,它将作为主 UI 窗口,并且我们在上一节中进行了设计。 创建QMainWindow
实例后,我们调用它的一些方法:setWindowTitle
设置窗口的标题,show
允许窗口出现。 最后,我们调用应用实例的exec
方法以进入 Qt 应用的主事件循环。 这将使应用等待,直到调用exit()
,然后返回设置为exit()
的值。
一旦main.cpp
文件保存在我们的项目目录中,我们在终端中进入该目录并运行qmake -project
来生成 Qt 项目文件,如下所示:
$ cd ImageViewer/ $ ls main.cpp $ qmake -project $ ls ImageViewer.pro main.cpp $
如您所见,将生成一个名为ImageViewer.pro
的文件。 该文件包含
Qt 项目的许多指令和配置,qmake
稍后将使用此
ImageViewer.pro
文件生成生成文件。 让我们检查该项目文件。 在我们省略以#
开头的所有注释行之后,以下片段中列出了其内容,如下所示:
TEMPLATE = app TARGET = ImageViewer INCLUDEPATH += . DEFINES += QT_DEPRECATED_WARNINGS SOURCES += main.cpp
让我们逐行处理。
第一行TEMPLATE = app
指定app
作为生成项目时要使用的模板。 此处允许使用许多其他值,例如lib
和subdirs
。 我们正在构建一个可以直接运行的应用,因此app
值对我们来说是合适的。 使用其他值超出了本章的范围。 您可以自己参考上的qmake
手册,以进行探索。
第二行TARGET = ImageViewer
指定应用可执行文件的名称。 因此,一旦构建项目,我们将获得一个名为ImageViewer
的可执行文件。
其余各行为编译器定义了几个选项,例如include
路径,宏定义和输入源文件。 您可以根据这些行中的变量名称轻松确定哪个行在做什么。
现在,让我们构建项目,运行qmake -makefile
生成生成文件,然后运行make
生成项目,即,将源代码编译为目标可执行文件:
$ qmake -makefile $ ls ImageViewer.pro main.cpp Makefile $ make g++ -c -pipe -O2 -Wall -W -D_REENTRANT -fPIC -DQT_DEPRECATED_WARNINGS -DQT_NO_DEBUG -DQT_GUI_LIB -DQT_CORE_LIB -I. -I. -isystem /usr/include/x86_64-linux-gnu/qt5 -isystem /usr/include/x86_64-linux-gnu/qt5/QtGui -isystem /usr/include/x86_64-linux-gnu/qt5/QtCore -I. -isystem /usr/include/libdrm -I/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++ -o main.o main.cpp main.cpp:1:10: fatal error: QApplication: No such file or directory #include <QApplication> ^~~~~~~~~~~~~~ compilation terminated. make: *** [Makefile:395: main.o] Error 1 $
糟糕! 我们遇到了一个大错误。 这是因为从 Qt 版本 5 开始,所有本机 GUI 功能都已从核心模块移至单独的模块,即小部件模块。 通过将行greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
添加到项目文件中,我们应该告诉qmake
我们的应用依赖于该模块。 进行此修改后,ImageViewer.pro
的内容如下所示:
TEMPLATE = app TARGET = ImageViewer greaterThan(QT_MAJOR_VERSION, 4): QT += widgets INCLUDEPATH += . DEFINES += QT_DEPRECATED_WARNINGS SOURCES += main.cpp
现在,让我们通过在终端中发出qmake -makefile
和make
命令来再次构建应用,如下所示:
$ qmake -makefile $ make g++ -c -pipe -O2 -Wall -W -D_REENTRANT -fPIC -DQT_DEPRECATED_WARNINGS -DQT_NO_DEBUG -DQT_WIDGETS_LIB -DQT_GUI_LIB -DQT_CORE_LIB -I. -I. -isystem /usr/include/x86_64-linux-gnu/qt5 -isystem /usr/include/x86_64-linux-gnu/qt5/QtWidgets -isystem /usr/include/x86_64-linux-gnu/qt5/QtGui -isystem /usr/include/x86_64-linux-gnu/qt5/QtCore -I. -isystem /usr/include/libdrm -I/usr/lib/x86_64-linux-gnu/qt5/mkspecs/linux-g++ -o main.o main.cpp g++ -Wl,-O1 -o ImageViewer main.o -lQt5Widgets -lQt5Gui -lQt5Core -lGL -lpthread $ ls ImageViewer ImageViewer.pro main.cpp main.o Makefile $
万岁! 最后,我们在项目目录中获得了可执行文件ImageViewer
。 现在,让我们执行它,看看窗口是什么样的:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-fBaoJGL0-1681871114317)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/bc8f3961-2404-413c-a509-21248663fe49.png)]
如我们所见,这只是一个空白窗口。 我们将在下一部分中根据我们设计的线框实现完整的用户界面。
尽管我们没有提到任何 IDE 或编辑器,而是使用qmake
在终端中构建了该应用,但是您可以使用任何您熟悉的 IDE,例如 Qt Creator。 特别是在 Windows 上,终端(CMD 或 MinGW)的性能不如 Linux 和 MacOS 上的终端,因此请随时使用 IDE。
设置完整的用户界面
让我们继续开发。 在上一节中,我们建立了一个空白窗口,现在我们将菜单栏,工具栏,图像显示组件和状态栏添加到窗口中。
首先,我们将自己定义一个名为MainWindow
的类,而不是使用QMainWindow
类,该类扩展了QMainWindow
类。 让我们在mainwindow.h
中查看其声明:
class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent = nullptr); ~MainWindow(); private: void initUI(); private: QMenu *fileMenu; QMenu *viewMenu; QToolBar *fileToolBar; QToolBar *viewToolBar; QGraphicsScene *imageScene; QGraphicsView *imageView; QStatusBar *mainStatusBar; QLabel *mainStatusLabel; };
一切都很简单。 Q_OBJECT
是 Qt 库提供的关键宏。 如果我们要声明一个具有自定义信号和插槽的类,或者使用 Qt 元对象系统中的任何其他功能,则必须在该类声明中或更确切地说在私有声明中并入这个关键宏。 就像我们刚才所做的那样。 initUI
方法初始化在私有部分中声明的所有窗口小部件。 imageScene
和imageView
小部件将放置在窗口的主要区域中以显示图像。 其他小部件的类型和名称是不言自明的,因此为了使本章简洁,我将不对它们进行过多说明。
为了使本章简洁明了,在介绍该文件时,我没有将每个源文件完整地包含在文本中。 例如,在大多数情况下,文件开头的#include ...
方向被忽略。 您可以在 GitHub 上的代码存储库中引用源文件以检查详细信息(如果需要)。
另一个关键方面是mainwindow.cpp
中initUI
方法的实现,如下所示:
void MainWindow::initUI() { this->resize(800, 600); // setup menubar fileMenu = menuBar()->addMenu("&File"); viewMenu = menuBar()->addMenu("&View"); // setup toolbar fileToolBar = addToolBar("File"); viewToolBar = addToolBar("View"); // main area for image display imageScene = new QGraphicsScene(this); imageView = new QGraphicsView(imageScene); setCentralWidget(imageView); // setup status bar mainStatusBar = statusBar(); mainStatusLabel = new QLabel(mainStatusBar); mainStatusBar->addPermanentWidget(mainStatusLabel); mainStatusLabel->setText("Image Information will be here!"); }
如您所见,在此阶段,我们并未为菜单和工具栏创建所有项目和按钮; 我们只是设置了主要骨架。 在前面的代码中,imageScene
变量是QGraphicsSence
实例。 这样的实例是 2D 图形项目的容器。 根据其设计,它仅管理图形项目,而没有视觉外观。 为了可视化它,我们应该使用它创建QGraphicsView
类的实例,这就是imageView
变量在那里的原因。 在我们的应用中,我们使用这两个类来显示图像。
在实现MainWindow
类的所有方法之后,该编译源代码了。 在执行此操作之前,需要对ImageViewer.pro
项目文件进行许多更改,如下所示:
- 我们只是编写了一个新的源文件,它应该被
qmake
所知道:
# in ImageViewer.pro SOURCES += main.cpp mainwindow.cpp
- 头文件
mainwindow.h
具有一个特殊的宏Q_OBJECT
,它指示它具有标准 C++ 预处理器无法处理的内容。 该头文件应由 Qt 提供的名为moc
,元对象编译器的预处理器正确处理,以生成包含某些与 Qt 元对象系统相关的代码的 C++ 源文件。 因此,我们应该通过将以下行添加到ImageViewer.pro
来告诉qmake
检查该头文件:
HEADERS += mainwindow.h
好。 现在,所有步骤都已完成,让我们再次运行qmake -makefile
和make
,然后运行新的可执行文件。 您应该看到以下窗口:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PFoAlpEW-1681871114318)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/d1a1fe3b-aa9d-4e28-a6f0-0eb459e56e50.png)]
好吧,到目前为止一切都很好。 现在,让我们继续添加应该在菜单中显示的项目。 在 Qt 中,菜单中的每个项目都由QAction
的实例表示。 在这里,我们以打开一个新图像为例进行操作。 首先,我们声明一个指向QAction
实例的指针作为MainWindow
类的私有成员:
QAction *openAction;
然后,在initUI
方法的主体中,通过调用new
运算符将操作创建为主窗口的子窗口小部件,并将其添加到“文件”菜单中,如下所示:
openAction = new QAction("&Open", this); fileMenu->addAction(openAction);
您可能会注意到,我们通过调用new
运算符创建了许多 Qt 对象,但从未删除它们。 很好,因为所有这些对象都是QObject
的实例或其子类。 QObject
的实例被组织在 Qt 库中的一个或多个对象树中。 当将QObject
创建为另一个对象的子对象时,该对象将自动添加到其父对象的children()
列表中。 父对象将获得子对象的所有权。 并且,当处置父对象时,其子对象将自动在其析构器中删除。 在我们的应用中,我们将QObject
的大多数实例创建为主窗口对象的子代,因此不需要删除它们。
幸运的是,工具栏上的按钮也可以用QAction
表示,因此我们可以将openAction
直接添加到文件工具栏:
fileToolBar->addAction(openAction);
如前所述,我们要创建七个动作:打开,另存为,退出,放大,缩小,上一张图像和下一张图像。 可以按照添加打开操作的相同方式添加所有内容。 另外,鉴于添加这些动作需要很多代码行,因此我们可以对代码进行一些重构—创建一个名为createActions
的新私有方法,将该动作的所有代码插入该方法,然后在initUI
中调用它。
现在,重构后,所有操作都在单独的方法createActions
中创建。 让我们编译源代码,看看窗口现在是什么样子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fp3GGMJ7-1681871114318)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/51dc64d7-43b2-4ddf-9bd1-5118bd62bdff.png)]
大! 该窗口看起来就像我们设计的线框一样,现在我们可以通过单击菜单栏上的项目来展开菜单!
实现动作函数
在上一节中,我们向菜单和工具栏添加了一些操作。 但是,如果单击这些操作,则什么也不会发生。 那是因为我们还没有为他们编写任何处理器。 Qt 使用信号和插槽连接机制来建立事件及其处理器之间的关系。 当用户对窗口小部件执行操作时,将发出该窗口小部件的信号。 然后,Qt 将确定是否有与该信号相连的插槽。 如果找到该插槽,则将调用该插槽。 在本节中,我们将为在上一节中创建的动作创建插槽,并将动作信号分别连接到这些插槽。 另外,我们将为常用操作设置一些热键。
退出动作
以退出动作为例。 如果用户从“文件”菜单中单击它,则将发出名为triggered
的信号。 因此,让我们将该信号连接到MainWindow
类的成员函数createActions
中的应用实例的插槽中:
connect(exitAction, SIGNAL(triggered(bool)), QApplication::instance(), SLOT(quit()));
connect
方法采用四个参数:信号发送器,信号,接收器和插槽。 一旦建立连接,发送方的信号一发出,接收方的插槽就会被调用。 在这里,我们将退出操作的triggered
信号与应用实例的quit
插槽连接,以使我们能够在单击退出操作时退出。
现在,要编译并运行,请从“文件”菜单中单击“退出”项。 如果一切顺利,该应用将按我们期望的那样退出。
打开图像
Qt 提供了QApplication
的quit
插槽,但是如果要在单击打开操作时打开图像,我们应该使用哪个插槽? 在这种情况下,这种自定义任务没有内置的插槽。 我们应该自己写一个插槽。
要编写插槽,首先我们应该在类MainWindow
的主体中声明一个函数,并将其放在插槽部分中。 由于其他类未使用此函数,因此将其放在专用插槽部分中,如下所示:
private slots: void openImage();
然后,为该插槽(也是成员函数)提供一个简单的测试定义:
void MainWindow::openImage() { qDebug() << "slot openImage is called."; }
现在,我们将打开动作的triggered
信号连接到createActions
方法主体中主窗口的openImage
插槽:
connect(openAction, SIGNAL(triggered(bool)), this, SLOT(openImage()));
现在,让我们再次编译并运行它。 单击“文件”菜单中的“打开”项,或单击工具栏上的“打开”按钮,slot openImage is called.
消息将打印在终端中。
我们现在有一个测试位置,可以很好地与打开动作配合使用。 让我们更改其主体,如下面的代码所示,以实现从磁盘打开图像的功能:
QFileDialog dialog(this); dialog.setWindowTitle("Open Image"); dialog.setFileMode(QFileDialog::ExistingFile); dialog.setNameFilter(tr("Images (*.png *.bmp *.jpg)")); QStringList filePaths; if (dialog.exec()) { filePaths = dialog.selectedFiles(); showImage(filePaths.at(0)); }
让我们逐行浏览此代码块。 在第一行中,我们创建QFileDialog
的实例,其名称为dialog
。 然后,我们设置对话框的许多属性。 此对话框用于从磁盘本地选择一个图像文件,因此我们将其标题设置为“打开图像”,并将其文件模式设置为QFileDialog::ExistingFile
,以确保它只能选择一个现有文件,而不能选择许多文件或文件。 不存在的文件。 名称过滤器图像(* .png * .bmp * .jpg
)确保只能选择具有提到的扩展名(即.png
,.bmp
和.jpg
)的文件。 完成这些设置后,我们调用dialog
的exec
方法将其打开。 如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gULAbpzl-1681871114318)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/55ee82be-4f51-44c3-82bd-66547dfff600.png)]
如果用户选择一个文件并单击“打开”按钮,则dialog.exec
将返回一个非零值。 然后,我们调用dialog.selectedFiles
来获取被选为QStringList
实例的文件的路径。 在这里,只允许一个选择。 因此,结果列表中只有一个元素:我们要打开的图像的路径。 因此,我们用唯一的元素调用MainWindow
类的showImage
方法来显示图像。 如果用户单击“取消”按钮,则exec
方法将返回零值,我们可以忽略该分支,因为这意味着用户已放弃打开图像。
showImage
方法是我们刚刚添加到MainWindow
类的另一个私有成员函数。 它的实现如下:
void MainWindow::showImage(QString path) { imageScene->clear(); imageView->resetMatrix(); QPixmap image(path); imageScene->addPixmap(image); imageScene->update(); imageView->setSceneRect(image.rect()); QString status = QString("%1, %2x%3, %4 Bytes").arg(path).arg(image.width()) .arg(image.height()).arg(QFile(path).size()); mainStatusLabel->setText(status); }
在显示图像的过程中,我们将图像添加到imageScene
,然后更新场景。 之后,场景通过imageView
可视化。 鉴于在打开并显示另一幅图像时应用可能已经打开了一幅图像,我们应该删除旧图像,并在显示新图像之前重置视图的任何变换(例如,缩放或旋转)。 这项工作在前两行中完成。 此后,我们使用选定的文件路径构造QPixmap
的新实例,然后将其添加到场景中并更新场景。 接下来,我们在imageView
上调用setSceneRect
来告诉它场景的新范围-它与图像的大小相同。
至此,我们已经在主要区域的中心以原始尺寸显示了目标图像。 最后要做的是在状态栏上显示与图像有关的信息。 我们构造一个包含其路径,尺寸和大小(以字节为单位)的字符串,然后将其设置为mainStatusLabel
的文本,该文本已添加到状态栏中。
让我们看看该图像在打开时如何显示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0v4Hj3vj-1681871114319)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/be5379ea-a17b-459e-bb6e-2a68f5b1b65f.png)]
不错! 该应用现在看起来像一个真正的图像查看器,因此让我们继续实现其所有预期功能。
放大和缩小
好。 我们已经成功显示了图像。 现在,让我们扩展一下。 在这里,我们以放大为例。 根据上述操作的经验,我们应该对如何执行操作有一个清晰的认识。 首先,我们声明一个专用插槽zoomIn
,并提供其实现,如以下代码所示:
void MainWindow::zoomIn() { imageView->scale(1.2, 1.2); }
容易吧? 只需使用宽度的缩放比例和高度的缩放比例调用imageView
的scale
方法。 然后,在MainWindow
类的createActions
方法中,将zoomInAction
的triggered
信号连接到此插槽:
connect(zoomInAction, SIGNAL(triggered(bool)), this, SLOT(zoomIn()));
编译并运行该应用,使用它打开一个图像,然后单击工具栏上的“放大”按钮。 您会发现,每次单击时图像会放大到其当前大小的 120%。
缩小仅需要以小于1.0
的速率缩放imageView
。 请尝试自己实现。 如果发现困难,可以参考我们在 GitHub 上的代码存储库。
通过我们的应用,我们现在可以打开图像并将其缩放以进行查看。 接下来,我们将实现saveAsAction
操作的功能。
保存副本
让我们回顾一下MainWindow
的showImage
方法。 在该方法中,我们从图像创建了QPixmap
的实例,然后通过调用imageScene->addPixmap
将其添加到imageScene
中。 我们没有从该函数中保留任何图像处理器; 因此,现在我们没有方便的方法来在新插槽中获取QPixmap
实例,我们将为saveAsAction
实现该实例。
为了解决这个问题,我们在MainWindow
中添加了一个新的私有成员字段QGraphicsPixmapItem *currentImage
来保存imageScene->addPixmap
的返回值,并在MainWindow
的构造器中使用nullptr
对其进行初始化。 然后,我们在MainWindow::showImage
主体中找到代码行:
imageScene->addPixmap(image);
为了保存返回的值,我们将这一行替换为以下一行:
currentImage = imageScene->addPixmap(image);
现在,我们准备为saveAsAction
创建一个新插槽。 专用插槽部分中的声明很简单,如下所示:
void saveAs();
定义也很简单:
void MainWindow::saveAs() { if (currentImage == nullptr) { QMessageBox::information(this, "Information", "Nothing to save."); return; } QFileDialog dialog(this); dialog.setWindowTitle("Save Image As ..."); dialog.setFileMode(QFileDialog::AnyFile); dialog.setAcceptMode(QFileDialog::AcceptSave); dialog.setNameFilter(tr("Images (*.png *.bmp *.jpg)")); QStringList fileNames; if (dialog.exec()) { fileNames = dialog.selectedFiles(); if(QRegExp(".+\\.(png|bmp|jpg)").exactMatch(fileNames.at(0))) { currentImage->pixmap().save(fileNames.at(0)); } else { QMessageBox::information(this, "Information", "Save error: bad format or filename."); } } }
首先,我们检查currentImage
是否为nullptr
。 如果为true
,则表示我们尚未打开任何图像。 因此,我们打开QMessageBox
告诉用户没有任何可保存的内容。 否则,我们将创建一个QFileDialog
,为其设置相关属性,然后通过调用其exec
方法将其打开。 如果用户为对话框提供文件名,然后单击对话框上的打开按钮,我们将获得其中仅包含一个元素的文件路径列表,作为我们的QFileDialog
的最后用法。 然后,我们使用正则表达式匹配检查文件路径是否以我们支持的扩展名结尾。 如果一切顺利,我们将从currentImage->pixmap()
获取当前图像的QPixmap
实例,并将其保存到指定的路径。 插槽准备就绪后,我们将其连接到createActions
中的信号:
connect(saveAsAction, SIGNAL(triggered(bool)), this, SLOT(saveAs()));
要测试此功能,我们可以在“另存图像为…”文件对话框中提供一个以.jpg
结尾的文件名,以打开 PNG 图像并将其另存为 JPG 图像。 然后,使用另一个图像查看应用打开刚刚保存的新 JPG 图像,以检查图像是否已正确保存。
浏览文件夹
现在,我们已经完成了与单个图像有关的所有操作,让我们进一步浏览一下当前图像所在目录(即prevAction
和nextAction
)中的所有图像。
要知道上一个或下一个图像是什么构成的,我们应该注意以下两点:
- 当前是哪个
- 我们计算它们的顺序
因此,首先我们向MainWindow
类添加一个新的成员字段QString currentImagePath
,以保存当前图像的路径。 然后,在showImage
中显示图像时,通过向该方法添加以下行来保存图像的路径:
currentImagePath = path;
然后,我们决定根据图像的名称按字母顺序对图像进行计数。 有了这两条信息,我们现在可以确定哪个是上一个图像或下一个图像。 让我们看看如何为prevAction
定义广告位:
void MainWindow::prevImage() { QFileInfo current(currentImagePath); QDir dir = current.absoluteDir(); QStringList nameFilters; nameFilters << "*.png" << "*.bmp" << "*.jpg"; QStringList fileNames = dir.entryList(nameFilters, QDir::Files, QDir::Name); int idx = fileNames.indexOf(QRegExp(QRegExp::escape(current.fileName()))); if(idx > 0) { showImage(dir.absoluteFilePath(fileNames.at(idx - 1))); } else { QMessageBox::information(this, "Information", "Current image is the first one."); } }
首先,我们获得当前图像所在的目录作为QDir
的实例,然后列出带有名称过滤器的目录,以确保仅返回 PNG,BMP 和 JPG 文件。 在列出目录时,我们使用QDir::Name
作为第三个参数,以确保返回的列表按文件名按字母顺序排序。 由于我们正在查看的当前图像也在此目录中,因此其文件名必须在文件名列表中。 我们通过使用由QRegExp::escape
生成的正则表达式调用列表中的indexOf
来找到其索引,以便它可以完全匹配其文件名。 如果索引为零,则表示当前图像是该目录中的第一张。 弹出一个消息框,向用户提供此信息。 否则,我们将显示文件名位于index - 1
位置的图像以完成操作。
在测试prevAction
是否有效之前,请不要忘记在createActions
方法的主体中添加以下行来连接信号和插槽:
connect(prevAction, SIGNAL(triggered(bool)), this, SLOT(prevImage()));
好吧,这并不太难,所以您可以自己尝试nextAction
的工作,或者只是在 GitHub 上的代码存储库中阅读其代码。
响应热键
至此,几乎所有功能都按照我们的预期实现了。 现在,让我们为常用操作添加一些热键,以使我们的应用更易于使用。
您可能已经注意到,在创建动作时,有时会在其文本中添加一个奇怪的&
,例如&File
和E&xit
。 实际上,这是在 Qt 中设置快捷方式的一种方式。 在某些 Qt 小部件中,在字符前面使用&
将自动为该字符创建助记符(快捷方式)。 因此,在我们的应用中,如果按Alt + F
,将触发“文件”菜单,并且在“文件”菜单展开时,我们可以看到对其的“退出”操作。 此时,您按Alt + X
,将触发退出操作,以使应用退出。
现在,让我们为最常用的操作提供一些单键快捷方式,以使其更方便快捷地使用它们,如下所示:
- 加号(
+
)或等于(=
)用于放大 - 减号(
-
)或下划线(_
)用于缩小 - 向上或向左查看上一张图像
- 向下或向右查看下一张图像
为实现此目的,我们在MainWindow
类中添加了一个名为setupShortcuts
的新私有方法,并按如下方式实现它:
void MainWindow::setupShortcuts() { QList<QKeySequence> shortcuts; shortcuts << Qt::Key_Plus << Qt::Key_Equal; zoomInAction->setShortcuts(shortcuts); shortcuts.clear(); shortcuts << Qt::Key_Minus << Qt::Key_Underscore; zoomOutAction->setShortcuts(shortcuts); shortcuts.clear(); shortcuts << Qt::Key_Up << Qt::Key_Left; prevAction->setShortcuts(shortcuts); shortcuts.clear(); shortcuts << Qt::Key_Down << Qt::Key_Right; nextAction->setShortcuts(shortcuts); }
为了支持一个动作的多个快捷键,例如用于放大的+
和=
,对于每个动作,我们将QKeySequence
的空白QList
设为空,然后将每个快捷键序列添加到列表中。 在 Qt 中,QKeySequence
封装了快捷方式使用的键序列。 因为QKeySequence
具有带有int
参数的非显式构造器,所以我们可以将Qt::Key
值直接添加到列表中,并将它们隐式转换为QKeySequence
的实例。 填充列表后,我们对每个带有填充列表的操作调用setShortcuts
方法,这样设置快捷方式将更加容易。
在createActions
方法主体的末尾添加setupShortcuts()
方法调用,然后编译并运行; 现在您可以在应用中测试快捷方式,它们应该可以正常工作。
总结
在本章中,我们使用 Qt 从头构建了一个用于查看图像的桌面应用。 我们学习了如何设计用户界面,从头开始创建 Qt 项目,构建用户界面,打开和显示图像,响应热键以及保存图像副本。
在下一章中,我们将向应用添加更多操作,以允许用户使用 OpenCV 提供的功能来编辑图像。 另外,我们将使用 Qt 插件机制以更灵活的方式添加这些编辑操作。
问题
尝试以下问题,以测试您对本章的了解:
- 我们使用一个消息框来告诉用户,当他们试图查看第一个图像之前的上一个图像或最后一个图像之后的下一个图像时,他们已经在查看第一个或最后一个图像。 但是还有另一种处理这种情况的方法-当用户查看第一张图像时禁用
prevAction
,而当用户查看最后一张图像时禁用nextAction
。 如何实现? - 我们的菜单项或工具按钮仅包含文本。 我们如何向他们添加图标图像?
- 我们使用
QGraphicsView.scale
放大或缩小图像视图,但是如何旋转图像视图? moc
有什么作用?SIGNAL
和SLOT
宏执行什么动作?
二、像专业人士一样编辑图像
在第 1 章,“构建图像查看器”中,我们构建了一个简单的应用,用于从头开始使用 Qt 进行图像查看。 使用该应用,我们可以从本地磁盘查看图像,放大或缩小视图,以及在打开目录中导航。 在本章中,我们将继续该应用并添加一些功能,以允许用户编辑打开的图像。 为了实现这个目标,我们将使用本书开头提到的 OpenCV 库。 为了使应用可扩展,我们将使用 Qt 的插件机制将这些编辑功能中的大多数开发为插件。
本章将涵盖以下主题:
- 在 Qt 和 OpenCV 之间转换图像
- 通过 Qt 的插件机制扩展应用
- 使用 OpenCV 提供的图像处理算法修改图像
技术要求
要求用户正确运行我们在上一章中构建的ImageViewer
应用。 本章中的开发将基于该应用。
此外,还必须具备一些 OpenCV 的基本知识。 我们将使用最新版本的 OpenCV,即 4.0 版,该版本于 2018 年 12 月编写本书时发布。 由于新版本尚未包含在许多操作系统(例如 Debian,Ubuntu 或 Fedora)的软件存储库中,因此我们将从源头构建它。 请不要担心,我们将在本章稍后简要介绍安装说明。
ImageEditor
应用
在本章中,我们将构建一个可用于编辑图像的应用,因此将其命名为ImageEditor
。 要使用 GUI 应用编辑图像,第一步是使用该应用打开和查看图像,这是我们在上一章中所做的。 因此,在添加图像编辑功能之前,我决定制作一个ImageViewer
应用的副本并将其重命名为ImageEditor
。
让我们从复制源开始:
$ mkdir Chapter-02 $ cp -r Chapter-01/ImageViewer/ Chapter-02/ImageEditor $ ls Chapter-02 ImageEditor $ cd Chapter-02/ImageEditor $ make clean $ rm -f ImageViewer
使用这些命令,我们将Chapter-01
目录下的ImageViewer
目录复制到Chapter-02/ImageEditor
。 然后,我们可以进入该目录,运行make clean
来清理在编译过程中生成的所有中间文件,并使用rm -f ImageViewer
删除旧的目标可执行文件。
现在我们有一个清理的项目,让我们重命名一些项目:
- 在复制过程中,项目目录使用新的项目名称
ImageEditor
命名,因此我们无需在此处做任何事情。 - Qt 项目文件
ImageViewer.pro
应该重命名为ImageEditor.pro
。 您可以在文件管理器或终端中执行此操作。 - 在
ImageEditor.pro
文件中,我们应该通过将TARGET = ImageViewer
行更改为TARGET = ImageEditor
将TARGET
重命名为ImageEditor
。 - 在源文件
main.cpp
中,我们应该通过将window.setWindowTitle("ImageViewer");
行更改为window.setWindowTitle("ImageEditor");
来更改窗口标题。
现在,所有内容都已重命名,让我们编译并运行新的ImageEditor
应用,该应用已从ImageViewer
复制:
$ qmake -makefile $ make g++ -c -pipe ... # output truncated # ... $ ls ImageEditor ImageEditor.pro main.cpp main.o mainwindow.cpp mainwindow.h mainwindow.o Makefile moc_mainwindow.cpp moc_mainwindow.o moc_predefs.h $ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib/ $ ./ImageEditor
您将看到该窗口与ImageViewer
的窗口完全相同,但是它具有不同的窗口标题ImageEditor
。 无论如何,我们已经设置了编辑器应用,即使它现在没有图像编辑功能。 我们将在下一章中添加一个简单的编辑功能。
使用 OpenCV 模糊图像
在上一节中,我们设置了编辑器应用。 在本节中,我们将添加一个简单的图像编辑功能-一个操作(在菜单和工具栏上)以使图像模糊。
我们将分两步执行此操作:
- 首先,我们将设置 UI 并添加操作,然后将操作连接到虚拟插槽。
- 然后,我们将覆盖虚拟插槽以使图像模糊,这将涉及到 OpenCV 库。
添加模糊动作
我们将在本章中添加的大多数操作将用于编辑图像,因此我们应将其归类到新的菜单和工具栏中。 首先,我们将在mainwindow.h
头文件的私有部分中声明三个成员,即编辑菜单,编辑工具栏和模糊动作:
QMenu *editMenu; QToolBar *editToolBar; QAction *blurAction;
然后,我们将分别在MainWindow::initUI
和MainWindow::createActions
方法中创建它们,如下所示:
在MainWindow::initUI
中,执行如下:
editMenu = menuBar()->addMenu("&Edit"); editToolBar = addToolBar("Edit");
在MainWindow::createActions
中,执行如下:
blurAction = new QAction("Blur", this); editMenu->addAction(blurAction); editToolBar->addAction(blurAction);
到现在为止,我们都有一个编辑菜单和一个编辑工具栏,它们两个都带有模糊操作。 但是,如果用户单击工具栏上的模糊按钮或编辑菜单下的模糊项目,则不会发生任何事情。 这是因为我们尚未将插槽连接到该操作。 让我们现在为该动作添加一个插槽。 首先,我们将在mainwindow.h
的private slot
部分中声明一个插槽,如下所示:
// for editting void blurImage();
然后,我们将在mainwindow.cpp
中为其提供一个虚拟实现:
void MainWindow::blurImage() { qDebug() << "Blurring the image!"; }
现在插槽已经准备好了,是时候在mainwindow::createActions
方法的末尾将模糊操作的triggered
信号与此插槽连接了:
connect(blurAction, SIGNAL(triggered(bool)), this, SLOT(blurImage()));
编译并运行应用时,您将看到菜单,工具栏和操作。 如果通过单击触发操作,您将看到消息Blurring the image!
正在打印。
窗口和打印的消息如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-juBo2zNh-1681871114319)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/558c9347-79be-4627-b4ec-cc8a69882b5e.png)]
UI 部分现已准备就绪,这意味着我们可以集中精力在以下部分的插槽中,通过使用 OpenCV 来模糊图像。
从源代码构建和安装 OpenCV
在上一节中,我们为模糊操作安装了一个虚拟插槽,该插槽什么都不做,只显示一条简单消息。 现在,我们将覆盖该插槽的实现以进行真正的模糊处理。
如前几节所述,我们将使用 OpenCV 库(更确切地说是它的最新版本(4.0))来编辑图像。 因此,在开始编写代码之前,我们将安装最新版本的 OpenCV 库并将其包含在我们的项目中。
OpenCV 是一组库,工具和模块,包含构建计算机视觉应用所需的类和函数。 可以在其官方网站的发布页面上找到其发布文件。 我们需要知道的另一件事是,OpenCV 使用了一种称为 CMake 的现代构建工具来构建其构建系统。 这意味着我们必须在操作系统上安装 CMake 才能从源代码构建 OpenCV,并且至少需要 CMake 3.12 版本,因此请确保正确设置了 CMake 版本。
在软件工程界,如何构建项目(尤其是大型项目)是一个复杂的话题。 在软件工程的开发过程中,发明了许多工具来应对与该主题有关的各种情况。 从make
到 Autotools,从 SCons 到 CMake,从 Ninja 到 bazel,这里有太多要讨论的话题。 但是,到目前为止,在我们的书中只介绍了其中的两个:qmake
是 Qt 团队开发的,专门用于构建 Qt 项目。 CMake 是当今许多项目(包括 OpenCV)广泛采用的另一种方法。
在我们的书中,我们将尽力使这些工具的使用简单明了。
OpenCV 发行页面如下所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IG0Iuutr-1681871114319)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/251cce41-fd3d-44b3-9131-46f6e34054b0.png)]
我们可以单击Sources
链接将其源的 ZIP 包下载到本地磁盘,然后将其解压缩。 我们将在终端中使用 CMake 来构建 OpenCV,因此,我们将打开一个终端并将其工作目录更改为未压缩源的目录。 另外,OpenCV 不允许您直接在其源代码树的根目录中进行构建,因此我们应该创建一个单独的目录来进行构建。
以下是我们在终端中用于构建 OpenCV 的说明:
$ cd ~/opencv-4.0.0 # path to the unzipped source $ mkdir release # create the separate dir $ cd release $ cmake -D CMAKE_BUILD_TYPE=RELEASE -D CMAKE_INSTALL_PREFIX=$HOME/programs/opencv .. # ... output of cmake ... # rm ../CMakeCache.txt if it tells you are not in a separate dir $ make # ... output of make ... $ make install
cmake ...
行读取已解压缩源的根目录中的CMakeLists.txt
文件,并生成一个 makefile。 使用-D
传递给cmake
命令的CMAKE_BUILD_TYPE
变量指定我们以RELEASE
模式构建 OpenCV。 同样,CMAKE_INSTALL_PREFIX
变量指定将 OpenCV 库安装到的路径。 在这里,我将 OpenCV 安装到$HOME/programs/opencv
,即/home/kdr2/programs/opencv
-如果需要,可以更改CMAKE_INSTALL_PREFIX
的值以更改目标目录。 cmake
命令成功结束后,将生成一个名为Makefile
的文件。 使用 makefile,现在我们可以运行make
和make install
来编译和安装该库。
如果上述所有说明均操作正确,则将正确安装您的 OpenCV 版本。 您可以通过浏览安装目录进行检查:
$ ls ~/programs/opencv/ bin include lib share $ ls ~/programs/opencv/bin/ opencv_annotation opencv_interactive-calibration opencv_version opencv_visualisation setup_vars_opencv4.sh $ ls -l ~/programs/opencv/lib/ # ... lrwxrwxrwx 1 kdr2 kdr2 21 Nov 20 13:28 libopencv_core.so -> libopencv_core.so.4.0 lrwxrwxrwx 1 kdr2 kdr2 23 Nov 20 13:28 libopencv_core.so.4.0 -> libopencv_core.so.4.0.0 -rw-r--r-- 1 kdr2 kdr2 4519888 Nov 20 12:34 libopencv_core.so.4.0.0 # ... lrwxrwxrwx 1 kdr2 kdr2 24 Nov 20 13:28 libopencv_imgproc.so -> libopencv_imgproc.so.4.0 lrwxrwxrwx 1 kdr2 kdr2 26 Nov 20 13:28 libopencv_imgproc.so.4.0 -> libopencv_imgproc.so.4.0.0 -rw-r--r-- 1 kdr2 kdr2 4714608 Nov 20 12:37 libopencv_imgproc.so.4.0.0 # ... output truncated
OpenCV 是一个模块化库。 它由两种类型的模块组成-主模块和附加模块。
从源代码构建时,默认情况下,主要模块包含在 OpenCV 中,它们包含所有 OpenCV 核心功能,以及用于图像处理任务,过滤,转换和更多功能的模块。
额外的模块包括默认情况下未包含在 OpenCV 库中的所有 OpenCV 功能,并且它们大多包含其他与计算机视觉相关的功能。
如果在检查 OpenCV 是否正确安装时回顾一下 OpenCV 安装路径下lib
目录的内容,则会发现许多以libopencv_*.so*
模式命名的文件。 通常,每个文件都对应一个 OpenCV 模块。 例如,libopencv_imgproc.so
文件是imgproc
模块,该模块用于图像处理任务。
现在我们已经安装了 OpenCV 库,是时候将其包含在我们的 Qt 项目中了。 让我们打开 Qt 项目文件ImageEditor.pro
,并在其中添加以下几行:
unix: !mac { INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4 LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -l opencv_imgproc }
unix: !mac
指令的意思是在除 MacOS 之外的任何类似 UNIX 的系统上使用其旁边方括号中的配置。 我使用此指令是因为我正在使用 Debian GNU/Linux。 括号内的指令是在以下行中导入 OpenCV 库的关键部分:
- 第一行通过更新
INCLUDEPATH
的值来告诉编译器我们将在代码中使用的 OpenCV 头文件在哪里。 - 第二行告诉链接器我们的应用应该链接到哪个 OpenCV 模块(共享对象),以及在哪里找到它们。 更具体地说,
-lopencv_core -l opencv_imgproc
表示我们应将应用与libopencv_core.so
和libopencv_imgproc.so
链接,而-L...
则意味着链接器应在/home/kdr2/programs/opencv/lib
目录下找到这些库文件(共享对象)。
在 MacOS 或 Windows 上,OpenCV 以另一种方式构建和链接,但不在模块的每个库文件中。 在这种情况下,所有模块都链接到一个名为opencv_world
的库。 我们可以将-DBUILD_opencv_world=on
传递给 CMake 在 Linux 上达到相同的效果:
# on mac $ ls -l -rwxr-xr-x 1 cheftin staff 25454204 Dec 3 13:47 libopencv_world.4.0.0.dylib lrwxr-xr-x 1 cheftin staff 27 Dec 3 13:36 libopencv_world.4.0.dylib -> libopencv_world.4.0.0.dylib lrwxr-xr-x 1 cheftin staff 25 Dec 3 13:36 libopencv_world.dylib -> libopencv_world.4.0.dylib # on Linux with -D BUILD_opencv_world=on $ ls -l lrwxrwxrwx 1 kdr2 kdr2 22 Nov 29 22:55 libopencv_world.so -> libopencv_world.so.4.0 lrwxrwxrwx 1 kdr2 kdr2 24 Nov 29 22:55 libopencv_world.so.4.0 -> libopencv_world.so.4.0.0 -rw-r--r-- 1 kdr2 kdr2 57295464 Nov 29 22:09 libopencv_world.so.4.0.0
以这种方式构建 OpenCV 可以简化我们在编译源代码时的链接器选项-我们不需要像-lopencv_core -lopencv_imgproc
那样为链接器提供模块列表。 告诉链接器链接到opencv_world
就足够了。 对于 MacOS 和 Windows,我们可以将以下代码放入ImageEditor.pro
:
unix: mac { INCLUDEPATH += /path/to/opencv/include/opencv4 LIBS += -L/path/to/opencv/lib -lopencv_world } win32 { INCLUDEPATH += c:/path/to/opencv/include/opencv4 LIBS += -lc:/path/to/opencv/lib/opencv_world }
尽管这种方法比较容易,但是本书仍然使用单独的模块,使您可以深入了解我们正在学习和使用的 OpenCV 模块。
qmake
为您提供了另一种配置第三方库的方法,即通过pkg-config
,它是用于维护库的元信息的工具。 不幸的是,根据这个页面 4的说法,OpenCV 从 4.0 版开始不再支持pkg-config
。 这意味着我们需要使用更直接,更灵活的方法在 Qt 项目中配置 OpenCV,而不是使用pkg-config
方法。
图像模糊
最后,我们已经安装并配置了 OpenCV 库。 现在,让我们尝试使用它来模糊连接到我们的模糊操作的插槽中的图像。
首先,我们将以下行添加到mainwindow.cpp
文件的开头,以便我们可以包含 OpenCV 头文件:
#include "opencv2/opencv.hpp"
现在准备工作已经完成,因此让我们集中讨论 slot 方法的实现。 像打算在单个打开的图像上运行的任何插槽一样,在执行任何操作之前,我们需要检查当前是否有打开的图像:
if (currentImage == nullptr) { QMessageBox::information(this, "Information", "No image to edit."); return; }
如您所见,如果没有打开的图像,我们将提示一个消息框并立即从函数返回。
在确定当前在应用中有打开的图像之后,我们知道可以将打开的图像作为QPixmap
的实例。 但是,如何使用 OpenCV 来使图像QPixmap
形式的图像模糊? 答案是,我们不能。 在使用 OpenCV 对图像进行任何操作之前,我们必须使图像具有 OpenCV 如何保存图像的形式,这通常是Mat
类的实例。 OpenCV 中的Mat
类表示矩阵-实际上,任何图像都是具有给定宽度,高度,通道数和深度的矩阵。 在 Qt 中,我们有一个类似的类QImage
,它用于保存图像的矩阵数据。 这意味着我们有了一个如何使用 OpenCV 模糊QPixmap
的想法-我们需要将QPixmap
转换为QImage
,使用QImage
构造Mat
,模糊Mat
,然后转换 Mat
分别返回到QImage
和QPixmap
。
在转换方面,我们必须做很多工作。 让我们通过以下几行代码来讨论:
QPixmap pixmap = currentImage->pixmap(); QImage image = pixmap.toImage();
此代码段非常简单。 我们获取当前图像的数据作为QPixmap
的实例,然后通过调用其toImage
方法将其转换为QImage
实例。
下一步是将QImage
转换为Mat
,但是这里有些复杂。 我们正在打开的图像可以是任何格式-它可以是单色图像,灰度图像或深度不同的彩色图像。 要模糊它,我们必须知道它的格式,因此尽管它是原始格式,我们仍将其转换为具有 8 位深度和三个通道的常规格式。 这由 Qt 中的QImage::Format_RGB888
和 OpenCV 中的CV_8UC3
表示。 现在让我们看看如何进行转换并构造Mat
对象:
image = image.convertToFormat(QImage::Format_RGB888); cv::Mat mat = cv::Mat( image.height(), image.width(), CV_8UC3, image.bits(), image.bytesPerLine());
最后,这是一段可编辑的代码。 现在我们有了Mat
对象,让我们对其进行模糊处理:
cv::Mat tmp; cv::blur(mat, tmp, cv::Size(8, 8)); mat = tmp;
OpenCV 在其imgproc
模块中提供blur
函数。 它使用带有核的归一化框过滤器来模糊图像。 第一个参数是我们要模糊的图像,而第二个参数是我们要放置模糊的图像的位置。 我们使用临时矩阵存储模糊的图像,并在模糊结束后将其分配回原始图像。 第三个参数是核的大小。 在这里,核用于告诉 OpenCV 如何通过将其与不同数量的相邻像素组合来更改任何给定像素的值。
现在,我们已经将模糊图像作为Mat
的实例,我们必须将其转换回QPixmap
的实例,并在场景和视图中进行显示:
QImage image_blurred( mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); pixmap = QPixmap::fromImage(image_blurred); imageScene->clear(); imageView->resetMatrix(); currentImage = imageScene->addPixmap(pixmap); imageScene->update(); imageView->setSceneRect(pixmap.rect());
对我们来说,前面代码的新部分是从mat
对象构造QImage
对象image_blurred
,然后使用QPixmap::fromImage
静态方法将QImage
对象转换为QPixmap
。 尽管这是新的,但很明显。 这段代码的其余部分对我们来说并不陌生,它与我们在MainWindow
类的showImage
方法中使用的代码相同。
现在我们已经显示了模糊的图像,我们可以更新状态栏上的消息以告诉用户他们正在查看的该图像是已编辑的图像,而不是他们打开的原始图像:
QString status = QString("(editted image), %1x%2") .arg(pixmap.width()).arg(pixmap.height()); mainStatusLabel->setText(status);
至此,我们已经完成了MainWindow::blurImage
方法。 让我们通过在终端中发出qmake -makefile
和make
命令来重建项目,然后运行新的可执行文件。
如果像我一样,在非/usr
或/usr/local
的路径中安装 OpenCV,则在运行可执行文件时可能会遇到问题:
$ ./ImageEditor ./ImageEditor: error while loading shared libraries: libopencv_core.so.4.0: cannot open shared object file: No such file or directory
这是因为我们的 OpenCV 库不在系统的库搜索路径中。 通过在 Linux 上设置LD_LIBRARY_PATH
环境变量,在 MacOS 上设置DYLD_LIBRARY_PATH
,我们可以将其路径添加到库搜索路径:
$ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib/ $ ./ImageEditor
使用我们的应用打开图像时,将获得以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xd2oh1AO-1681871114319)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/38507eec-6352-46a7-bc72-c1b42ccbb5b2.png)]
单击工具栏上的“模糊”按钮后,其显示如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5YtGroNy-1681871114320)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/47fa78e2-fba7-4de6-86c9-d41c2727f478.png)]
我们可以看到我们的图像已成功模糊。
QPixmap
,QImage
和Mat
在上一节中,我们添加了一项新功能来模糊在ImageEditor
应用中打开的图像。 在模糊图像的同时,我们将图像从QPixmap
转换为QImage
并转换为Mat
,然后在使用 OpenCV 对其进行模糊处理之后将其向后转换。 在那里,我们做了工作,但对这些类没有多说。 让我们现在谈论它们。
QPixmap
QPixmap
是 Qt 库提供的一个类,打算在需要在屏幕上显示图像时使用。 这正是我们在项目中使用它的方式—我们读取图像作为其实例,并将该实例添加到QGraphicsSence
中以显示它。
有很多方法可以创建QPixmap
的实例。 就像我们在第 1 章,“构建图像查看器”以及本章前面的部分中所做的一样,我们可以使用图像文件的路径实例化它:
QPixmap map("/path/to/image.png");
另外,我们可以实例化一个空的QPixmap
,然后将数据加载到其中:
QPixmap map; map.load("/path/to/image.png");
对于实例中包含图像的实例,我们可以通过调用其save
方法将其保存到文件中,就像我们在“另存为”操作的插槽中所做的那样:
map.save("/path/to/output.png");
最后,我们可以通过调用toImage
方法将QPixmap
方法转换为QImage
方法:
//... QImage image = map.toImage();
QImage
尽管QPixmap
主要用于以 Qt 显示图像,但QImage
是针对 I/O 以及直接像素访问和操纵而设计和优化的。 通过此类,我们可以获得有关图像的信息,例如图像的大小,是否具有 alpha 通道,是否为灰度图像以及其中任何像素的颜色。
QImage
设计用于直接像素访问和操纵,并且它提供进行图像处理的功能,例如像素操纵和变换。 毕竟,Qt 库不是专门用于图像处理的库,因此它在此域中提供的功能不能满足本章的要求。 因此,在将QImage
对象转换为Mat
对象后,我们将使用 OpenCV 进行图像处理。
然后,问题是,如何在QImage
,QPixmap
和Mat
这三种数据类型之间转换? 在上一节中,我们讨论了如何将QPixmap
转换为QImage
,但现在让我们看一下如何将其转换回:
QPixmap pixmap = QPixmap::fromImage(image);
如您所见,这是一个简单的过程-您只需使用QImage
对象作为唯一参数来调用QPixmap
类的fromImage
静态方法。
如果您对QImage
其他功能的详细信息感兴趣,可以在这个页面上参考其文档。 在下一节中,我们将讨论如何将QImage
转换为Mat
,反之亦然。
Qt5 和 OpenCV4 计算机视觉项目:1~5(2)https://developer.aliyun.com/article/1427053