Qt5 和 OpenCV4 计算机视觉项目:1~5(1)https://developer.aliyun.com/article/1427052
Mat
Mat
类是 OpenCV 库中最重要的类之一,其名称是矩阵的简称。 在计算机视觉领域,正如我们前面提到的,任何图像都是具有给定宽度,高度,通道数量和深度的矩阵。 因此,OpenCV 使用Mat
类表示图像。 实际上,Mat
类是一个 N 维数组,可用于存储具有任何给定数据类型的单个或多个数据通道,并且它包含许多以多种方式创建,修改或操纵它的成员和方法。 。
Mat
类具有许多构造器。 例如,我们可以创建一个实例,该实例的宽度(列)为800
,高度(行)为600
,其中三个通道包含 8 位无符号int
值,如下所示:
Mat mat(600, 800, CV_8UC3);
此构造器的第三个参数是该矩阵的type
; OpenCV 预定义了许多可用于它的值。 这些预定义的值在名称中都有一个模式,以便我们在看到名称时可以知道矩阵的类型,或者可以在确定矩阵的性质时猜测应该使用的名称。
此模式称为CV_C
:
可以用
8
,16
,32
或64
代替,它们表示用于在像素中存储每个元素的位数- 对于无符号整数,有符号整数和浮点值,需要分别用
U
,S
或F
替换 应该是通道数
因此,在我们的代码中,CV_8UC3
表示声明的图像的深度为8
,其像素的每个元素都存储在 8 位无符号int
中,并且具有三个通道。 换句话说,每个像素中具有 3 个元素,CV_8UC3
占据 24 位(depth * channels
)。
我们还可以在构建图像时为其填充一些数据。 例如,我们可以用恒定的颜色填充它,如下所示:
int R = 40, G = 50, B = 60; Mat mat(600, 800, CV_8UC3, Scalar(B, G, R));
在前面的代码中,我们创建了与上一个示例中相同的图像,但是使用第四个参数指定的恒定颜色RGB(40, 50, 60)
填充了该图像。
重要的是要注意,OpenCV 中默认的颜色顺序是 BGR,而不是 RGB,这意味着B
和R
值互换了。 因此,我们在代码中将恒定颜色表示为Scalar(B, G, R)
而不是Scalar(R, G, B)
。 如果我们使用 OpenCV 读取图像,但使用另一个对颜色使用不同顺序的库来处理图像,则反之亦然,尤其是当我们的处理方法分别处理图像的每个通道时,这一点很重要。
那就是在我们的应用中发生的事情-我们使用 Qt 加载图像并将其转换为 OpenCV Mat
数据结构,然后对其进行处理并将其转换回QImage
。 但是,如您所见,在使图像模糊时,我们没有交换红色和蓝色通道来求助于颜色顺序。 这是因为blur
函数在通道上对称运行; 通道之间没有干扰,因此在这种情况下颜色顺序并不重要。 如果执行以下操作,则可以省略通道交换:
- 我们将
QImage
转换为Mat
,然后处理Mat
并将其转换回QImage
- 我们在
Mat
上执行的处理期间内的所有操作在通道上都是对称的; 也就是说,通道之间没有干扰 - 在处理期间我们不会显示图像; 我们仅在将它们转换回
QImage
后向他们显示
在这种情况下,我们可以简单地忽略颜色顺序的问题。 这将应用于我们稍后将编写的大多数插件。 但是,在某些情况下,您不能只是简单地忽略它。 例如,如果您使用 OpenCV 读取图像,将其转换为QImage
的实例,然后在 Qt 中显示,则以下代码将显示其红色和蓝色通道已交换的图像:
cv::Mat mat = cv::imread("/path/to/an/image.png"); QImage image( mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888 );
在将其转换为QImage
之前,应先交换 R 和 B 通道:
cv::Mat mat = cv::imread("/path/to/an/image.png"); cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB); QImage image( mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888 );
请记住,如果我们使用的过程不能使用 OpenCV 对称地处理颜色通道,则在执行该操作之前,必须确保颜色顺序为 BGR。
现在我们已经讨论了颜色的顺序,我们将回到创建Mat
对象的主题。 我们刚刚了解到,可以在创建Mat
对象时用恒定的颜色填充它,但是,在我们的应用中,我们应该创建一个Mat
对象,该对象与给定QImage
对象的图像相同。 让我们回头看看我们是如何做到的:
// image is the give QImage object cv::Mat mat = cv::Mat( image.height(), image.width(), CV_8UC3, image.bits(), image.bytesPerLine() );
除了我们已经讨论的前三个参数外,我们还传递由QImage
对象持有并由其bits
方法返回的数据指针作为第四个参数。 我们还传递了另一个额外的参数,即图像每行的字节数,以使 OpenCV 知道如何处理图像填充字节,以及如何以有效的方式将其存储在内存中。
如前所述,Mat
类的构造器太多了,在此不多讨论。 我们甚至可以创建尺寸更大的Mat
对象。 您可以参考这里上的文档以获取其构造器的完整列表。 在本章中,我们将不多讨论它们。
现在我们已经掌握了如何在 Qt 和 OpenCV 之间转换图像对象的知识,接下来的几节将继续介绍如何使用 OpenCV 编辑图像。
使用 Qt 的插件机制添加功能
在上一节中,我们向我们的应用添加了一个名为编辑的新菜单和工具栏,并向它们两个添加了操作以使打开的图像模糊。 让我们回顾一下添加此功能的过程。
首先,我们添加了菜单和工具栏,然后添加了动作。 添加动作后,我们将新的插槽连接到该动作。 在该插槽中,我们将打开的图像作为QPixmap
的实例,并将其转换为QImage
对象,然后转换为Mat
对象。 关键的编辑工作从这里开始-我们使用 OpenCV 修改Mat
实例以完成编辑工作。 然后,我们将Mat
分别转换回QImage
和QPixmap
,以显示编辑后的图像。
现在,如果我们想向我们的应用添加另一个编辑功能,我们应该怎么做? 当然,只重复前面添加模糊动作的过程就可以了,但是效率不高。 如果我们想象我们只是以添加模糊动作的相同方式向应用添加了另一个编辑动作,我们会发现大多数工作或代码都是相同的。 我们正在重复自己。 这不仅是一种不良的发展模式,而且是无聊的工作。
要解决此问题,我们应该仔细地进行重复的过程,将其分为多个步骤,然后找出哪些步骤完全相同,哪些步骤有所不同。
这样,我们可以找出添加其他编辑功能的关键点:
- 对于不同的编辑功能,操作的名称是不同的。
Mat
实例上的操作因不同的编辑功能而有所不同。
除前两个之外,其他所有步骤或逻辑在添加不同的编辑动作的过程中都是相同的。 也就是说,当我们要添加新的编辑功能时,我们只需要做两件事。 首先,我们将其命名,然后找出一种使用 OpenCV 对Mat
实例进行编辑操作的方法。 一旦清除了这两件事,就可以确定新的编辑功能。 接下来,我们需要将新功能集成到应用中。
那么,我们如何将其集成到应用中呢? 我们将使用 Qt 的插件机制来执行此操作,并且每个编辑功能都将是一个插件。
插件接口
Qt 插件机制是使 Qt 应用更可扩展的强大方法。 如前所述,我们将使用这种机制来抽象一种可以轻松添加新编辑功能的方式。 完成后,在添加新的编辑功能时,只需要注意编辑功能的名称和Mat
实例上的操作即可。
第一步是找出一个接口,以便在应用和插件之间提供通用协议,以便我们可以加载和调用插件,而不管插件是如何实现的。 在 C++ 中,接口是具有纯虚拟成员函数的类。 对于我们的插件,我们需要处理Mat
的动作名称和操作,因此我们在editor_plugin_interface.h
中声明我们的接口,如下所示:
#ifndef EDITOR_PLUGIN_INTERFACE_H #define EDITOR_PLUGIN_INTERFACE_H #include <QObject> #include <QString> #include "opencv2/opencv.hpp" class EditorPluginInterface { public: virtual ~EditorPluginInterface() {}; virtual QString name() = 0; virtual void edit(const cv::Mat &input, cv::Mat &output) = 0; }; #define EDIT_PLUGIN_INTERFACE_IID "com.kdr2.editorplugininterface" Q_DECLARE_INTERFACE(EditorPluginInterface, EDIT_PLUGIN_INTERFACE_IID); #endif
我们使用ifndef
/define
习惯用语(前两行和最后一行)来确保此头文件一次包含在源文件中。 在前两行之后,我们包括 Qt 和 OpenCV 提供的一些头文件,以介绍相关的数据结构。 然后,我们声明一个名为EditorPluginInterface
的类,这是我们的接口类。 在该类中,除了虚拟的空析构器之外,我们还可以看到两个纯虚拟成员函数:name
和edit
函数。 name
函数返回QString
,这将是编辑操作的名称。 edit
函数将Mat
的两个引用用作其输入和输出,并用于编辑操作。 每个插件都是该接口的子类,这两个函数的实现将确定操作名称和编辑操作。
在类声明之后,我们定义一个名为com.kdr2.editorplugininterface
的唯一标识符字符串作为接口的 ID。 该 ID 在应用范围内必须是唯一的,也就是说,如果编写其他接口,则必须为它们使用不同的 ID。 然后,我们使用Q_DECLARE_INTERFACE
宏将接口的类名与定义的唯一标识符相关联,以便 Qt 的插件系统可以在加载之前识别此接口的插件。
至此,已经确定了用于编辑功能的接口。 现在,让我们编写一个插件来实现此接口。
用ErodePlugin
腐蚀图像
要编写 Qt 插件,我们应该从头开始一个新的 Qt 项目。 在先前的编辑功能中,我们只是通过从 OpenCV 调用blur
函数来使图像模糊。 考虑到我们的主要目的是介绍 Qt 库的插件机制,我们仍将使用 OpenCV 库中的一个简单函数进行简单的编辑以使这一部分更加清楚。 在这里,我们将从 OpenCV 库中调用erode
函数,以侵蚀图像中的对象。
让我们命名插件ErodePlugin
并从头开始创建项目:
$ ls ImageEditor $ mkdir ErodePlugin $ ls ErodePlugin ImageEditor $ cd ErodePlugin $ touch erode_plugin.h erode_plugin.cpp $ qmake -project $ ls erode_plugin.h erode_plugin.cpp ErodePlugin.pro
首先,在终端中,将目录更改为ImageEditor
项目的父目录,创建一个名为ErodePlugin
的新目录,然后输入该目录。 然后,我们创建两个空的源文件erode_plugin.h
和erode_pluigin.cpp
。 稍后我们将在这两个文件中编写源代码。 现在,我们在终端中运行qmake -project
,这将返回一个名为ErodePlugin.pro
的 Qt 项目文件。 由于此项目是 Qt 插件项目,因此其项目文件具有许多不同的设置。 现在让我们看一下:
TEMPLATE = lib TARGET = ErodePlugin COPNFIG += plugin INCLUDEPATH += . ../ImageEditor
在项目文件的开头,我们使用lib
而不是app
作为其TEMPLATE
设置的值。 TARGET
设置没有什么不同,我们只使用项目名称作为其值。 我们还添加了特殊行CONFIG += plugin
来告诉qmake
该项目是 Qt 插件项目。 最后,在上一个代码块的最后一行中,我们将ImageEditor
项目的根目录添加为该项目包含路径的一项,以便编译器可以找到接口头文件editor_plugin_interface.h
, 在编译插件时已将其放在上一节的ImageEditor
项目中。
在此插件中,我们还需要 OpenCV 来实现我们的编辑功能,因此,我们需要像在 Qt 插件项目的设置中一样,添加 OpenCV 库的信息-更准确地说是库路径,并包括库的路径。 在ImageEditor
项目中:
unix: !mac { INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4 LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -l opencv_imgproc } 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 }
在项目文件的末尾,我们将头文件和 C++ 源文件添加到项目中:
HEADERS += erode_plugin.h SOURCES += erode_plugin.cpp
现在,我们插件的项目文件已经完成,让我们开始编写我们的插件。 就像我们设计的那样,为新的编辑功能编写插件只是为了提供我们在上一节中抽象的EditorPluginInterface
接口的实现。 因此,我们在erode_plugin.h
中声明了该接口的子类:
#include <QObject> #include <QtPlugin> #include "editor_plugin_interface.h" class ErodePlugin: public QObject, public EditorPluginInterface { Q_OBJECT Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID); Q_INTERFACES(EditorPluginInterface); public: QString name(); void edit(const cv::Mat &input, cv::Mat &output); };
如您所见,在包含必要的头文件之后,我们声明一个名为ErodePlugin
的类,该类继承自QObject
和EditorPluginInterface
。 后者是我们在上一节editor_plugin_interface.h
中定义的接口。 在这里,我们将插件实现作为QOBject
的子类,因为这是 Qt 元对象系统和插件机制的要求。 在类的主体中,我们使用 Qt 库定义的一些宏添加更多信息:
Q_OBJECT Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID); Q_INTERFACES(EditorPluginInterface);
在上一章中,我们介绍了Q_OBJECT
宏; 它与 Qt 元对象系统有关。 Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID)
行声明了此插件的元数据,在这里我们声明了在editor_plugin_interface.h
中定义为其IID
元数据的插件接口的唯一标识符。 然后,我们使用Q_INTERFACES(EditorPluginInterface)
行告诉 Qt 此类正在尝试实现的是EditorPluginInterface
接口。 有了前面的信息,Qt 插件系统就知道了有关该项目的所有信息:
- 这是一个 Qt 插件项目,因此该项目的目标将是一个库文件。
- 该插件是
EditorPluginInterface
的实例,其IID
是EDIT_PLUGIN_INTERFACE_IID
,因此 Qt 应用可以告诉它并加载此插件。
现在,我们可以专注于如何实现接口。 首先,我们在接口中声明两个纯粹的重要函数:
public: QString name(); void edit(const cv::Mat &input, cv::Mat &output);
然后,我们在erode_plugin.cpp
文件中实现它们。 对于name
函数,这很简单-我们只需返回QString
,Erode
作为插件的名称(以及编辑操作的名称)即可:
QString ErodePlugin::name() { return "Erode"; }
对于edit
函数,我们如下实现:
void ErodePlugin::edit(const cv::Mat &input, cv::Mat &output) { erode(input, output, cv::Mat()); }
这也很简单-我们只调用 OpenCV 库提供的erode
函数。 该函数的作用称为图像腐蚀。 它是数学形态学领域中的两个基本运算符之一。 侵蚀是缩小图像前景或 1 值对象的过程。 它可以平滑对象边界并去除半岛,手指和小物体。 在下一部分中将插件加载到应用中后,我们将看到此效果。
好。 我们插件项目的大部分工作都已完成,因此让我们对其进行编译。 编译方式与普通 Qt 应用的编译方式相同:
$ qmake -makefile $ make g++ -c -pipe -O2 ... # output trucated ln -s libErodePlugin.so.1.0.0 libErodePlugin.so ln -s libErodePlugin.so.1.0.0 libErodePlugin.so.1 ln -s libErodePlugin.so.1.0.0 libErodePlugin.so.1.0 $ ls -l *.so* lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 libErodePlugin.so -> libErodePlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 libErodePlugin.so.1 -> libErodePlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 libErodePlugin.so.1.0 -> libErodePlugin.so.1.0.0 -rwxr-xr-x 1 kdr2 kdr2 78576 Dec 12 16:24 libErodePlugin.so.1.0.0 $
首先,我们运行qmake -makefile
生成Makefile
,然后通过执行make
命令来编译源代码。 编译过程完成后,我们将使用ls -l *.so*
检查输出文件,并找到许多共享对象文件。 这些是我们将加载到应用中的插件文件。
检查输出文件时,您可能会发现许多扩展名为1.0.0
的文件。 这些字符串告诉我们有关库文件的版本号。 这些文件大多数是一个真实库文件的别名(以符号链接的形式)。 在下一部分中加载插件时,将复制真实库文件的副本,但不包含其版本号。
如果使用的平台不同于 GNU/Linux,则输出文件也可能会有所不同:在 Windows 上,文件将被命名为ErodePlugin.dll
,在 MacOS 上,文件将被命名为libErodePlugin.dylib
。
将插件加载到我们的应用中
在前面的部分中,我们为应用的编辑功能抽象了一个接口,然后实现了一个插件,该插件通过将 OpenCV 库中的erode
函数应用于打开的图像来满足该接口。 在本节中,我们会将插件加载到我们的应用中,以便我们可以使用它来侵蚀我们的图像。 之后,我们将查看一个名为Erode
的新动作,该动作可以在编辑菜单下和编辑工具栏上找到。 如果我们通过单击来触发动作,我们将看到Erode
在图像上的作用。
因此,让我们加载插件! 首先,我们修改ImageEditor
项目的项目文件,并将包含插件接口的头文件添加到HEADERS
设置的列表中:
HEADERS += mainwindow.h editor_plugin_interface.h
然后,将此文件包含在我们的mainwindow.cpp
源文件中。 我们还将使用另一个名为QMap
的数据结构来保存将要加载的所有插件的列表,因此我们也包含QMap
的头文件:
#include <QMap> #include "editor_plugin_interface.h"
然后,在MainWindow
类的声明主体中,声明两个成员函数:
void loadPlugins()
:用于加载出现在某个目录中的所有插件。void pluginPerform()
:这是一个公共插槽,它将连接到已加载插件创建的所有操作。 在此插槽中,我们应区分触发了哪个动作,导致该插槽被调用,然后我们找到与该动作相关的插件并执行其编辑操作。
添加这两个成员函数后,我们添加QMap
类型的成员字段以注册所有已加载的插件:
QMap<QString, EditorPluginInterface*> editPlugins;
该映射的键将是插件的名称,而值将是指向已加载插件实例的指针。
头文件中的所有工作都已完成,因此让我们实现loadPlugins
函数来加载我们的插件。 首先,我们应该在mainwindow.cpp
中包含必要的头文件:
#include <QPluginLoader>
然后,我们将提供loadPlugins
成员函数的实现,如下所示:
void MainWindow::loadPlugins() { QDir pluginsDir(QApplication::instance()->applicationDirPath() + "/plugins"); QStringList nameFilters; nameFilters << "*.so" << "*.dylib" << "*.dll"; QFileInfoList plugins = pluginsDir.entryInfoList( nameFilters, QDir::NoDotAndDotDot | QDir::Files, QDir::Name); foreach(QFileInfo plugin, plugins) { QPluginLoader pluginLoader(plugin.absoluteFilePath(), this); EditorPluginInterface *plugin_ptr = dynamic_cast<EditorPluginInterface*>(pluginLoader.instance()); if(plugin_ptr) { QAction *action = new QAction(plugin_ptr->name()); editMenu->addAction(action); editToolBar->addAction(action); editPlugins[plugin_ptr->name()] = plugin_ptr; connect(action, SIGNAL(triggered(bool)), this, SLOT(pluginPerform())); // pluginLoader.unload(); } else { qDebug() << "bad plugin: " << plugin.absoluteFilePath(); } } }
我们假设可执行文件所在的目录中有一个名为plugins
的子目录。 只需调用QApplication::instance()->applicationDirPath()
即可获取包含可执行文件的目录,然后将/plugins
字符串附加到其末尾以生成插件目录。 如上一节所述,我们的插件是库文件,它们的名称以.so
,.dylib
或.dll
结尾,具体取决于所使用的操作系统。 然后,我们在plugins
目录中列出所有具有这些扩展名的文件。
在将所有可能的插件文件列出为QFileInfoList
之后,我们遍历该列表以尝试使用foreach
加载每个插件。 foreach
是 Qt 定义的宏,并实现了for
循环。 在循环内部,每个文件都是QFileInfo
的一个实例。 我们通过调用abstractFilePath
方法获得其绝对路径,然后在该路径上构造QPluginLoader
的实例。
然后,我们有许多关键步骤需要解决。 首先,我们在QPluginLoader
实例上调用instance
方法。 如果已加载目标插件,则将返回指向QObject
的指针,否则将返回0
。 然后,我们将返回指针转换为指向我们的插件接口类型即EditorPluginInterface*
的指针。 如果该指针非零,则将是插件的实例! 然后,我们创建一个QAction
,其名称为已加载插件的名称,即plugin_ptr->name()
的结果。 你还记得是什么吗? 这是ErodePlugin
中的name
函数,我们在其中返回Erode
字符串:
QString ErodePlugin::name() { return "Erode"; }
现在已经创建了Erode
操作,通过使用该操作调用它们的addAction
方法,我们将其添加到编辑菜单和编辑工具栏。 然后,我们在editPlugins
映射中注册已加载的插件:
editPlugins[plugin_ptr->name()] = plugin_ptr;
稍后,我们将使用此映射在插件创建的所有动作的公共位置中按其名称查找插件。
最后,我们将使用操作连接一个插槽:
connect(action, SIGNAL(triggered(bool)), this, SLOT(pluginPerform()));
您可能很好奇,这一行代码处于循环中,并且我们将所有操作的触发信号连接到同一插槽; 这个可以吗? 是的,我们有一种方法可以区分插槽中触发了哪个操作,然后我们可以根据该操作执行操作。 让我们看看这是如何完成的。 在pluginPerform
插槽的实现中,我们检查是否有打开的图像:
if (currentImage == nullptr) { QMessageBox::information(this, "Information", "No image to edit."); return; }
然后,我们找到它刚刚触发的动作,以便它通过调用 Qt 库提供的sender()
函数来发送信号并调用插槽。 sender()
函数返回一个指向QObject
实例的指针。 在这里,我们知道我们仅将QAction
的实例连接到此插槽,因此我们可以使用qobject_cast
将返回的指针安全地强制转换为QAction
的指针。 现在,我们知道触发了哪个动作。 然后,我们获得动作的文本。 在我们的应用中,操作的文本是创建该操作的插件的名称。 通过使用此文本,我们可以从我们的注册映射中找到某个插件。 这是我们的操作方式:
QAction *active_action = qobject_cast<QAction*>(sender()); EditorPluginInterface *plugin_ptr = editPlugins[active_action->text()]; if(!plugin_ptr) { QMessageBox::information(this, "Information", "No plugin is found."); return; }
得到插件指针后,我们检查它是否存在。 如果没有,我们只是向用户显示一个消息框,然后从 slot 函数返回。
至此,我们有了用户已通过其操作触发的插件,因此现在我们来看一下编辑操作。 这段代码与blurImage
插槽函数中的代码非常相似。 首先,我们以QPixmap
的形式获取开始图像,然后依次将其转换为QImage
和Mat
。 一旦它成为Mat
的实例,我们就可以对其应用插件的edit
函数,即plugin_ptr->edit(mat, mat);
。 完成编辑操作后,我们将编辑后的Mat
分别转换回QImage
和QPixmap
,然后在图形场景中显示QPixmap
并更新状态栏上的信息:
QPixmap pixmap = currentImage->pixmap(); QImage image = pixmap.toImage(); image = image.convertToFormat(QImage::Format_RGB888); Mat mat = Mat( image.height(), image.width(), CV_8UC3, image.bits(), image.bytesPerLine()); plugin_ptr->edit(mat, mat); QImage image_edited( mat.data, mat.cols, mat.rows, mat.step, QImage::Format_RGB888); pixmap = QPixmap::fromImage(image_edited); imageScene->clear(); imageView->resetMatrix(); currentImage = imageScene->addPixmap(pixmap); imageScene->update(); imageView->setSceneRect(pixmap.rect()); QString status = QString("(editted image), %1x%2") .arg(pixmap.width()).arg(pixmap.height()); mainStatusLabel->setText(status);
已经添加了两个新函数,所以我们要做的最后一件事是在MainWindow
类的构造器中调用loadPlugins
函数,方法是在MainWindow::MainWindow(QWidget *parent)
的末尾添加以下行:
loadPlugins();
现在,我们已经从可执行文件所在目录的plugins
子目录中加载并设置了插件,现在让我们编译应用并对其进行测试。
首先,在终端中,将目录更改为ImageEditor
项目的根目录,然后发出qmake -makefile
和make
命令。 等待这些命令完成。 然后,通过运行./ImageEditor
命令启动我们的应用; 您将看到以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BAYkiAtT-1681871114320)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/0c494eac-f671-45a1-9719-42a2b103f11f.png)]
在运行应用之前,请不要忘记在 Linux 或 MacOS 上将LD_LIBRARY_PATH
或DYLD_LIBRARY_PATH
环境变量设置为 OpenCV 的lib
目录。
哦,什么都没改变-我们在编辑菜单或编辑工具栏上找不到Erode
操作。 这是因为我们没有将Erode
插件文件复制到plugins
目录中。 让我们现在开始:
$ ls ImageEditor ImageEditor.pro plugins ... $ ls -l ../ErodePlugin/*.so* lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 ../ErodePlugin/libErodePlugin.so -> libErodePlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 ../ErodePlugin/libErodePlugin.so.1 -> libErodePlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 23 Dec 12 16:24 ../ErodePlugin/libErodePlugin.so.1.0 -> libErodePlugin.so.1.0.0 -rwxr-xr-x 1 kdr2 kdr2 78576 Dec 12 16:24 ../ErodePlugin/libErodePlugin.so.1.0.0 $ cp ../ErodePlugin/libErodePlugin.so.1.0.0 plugins/libErodePlugin.so $ ls plugins/ libErodePlugin.so $
如果您使用的是 macOS,则在编译项目后,将找到一个名为ImageEditor.app
的目录,而不是ImageEditor
可执行文件。 这是因为在 MacOS 上,每个应用都是一个以.app
作为扩展名的目录。 真正的可执行文件位于ImageEditor.app/Contents/MacOS/ImageEdtior
,因此,在 MacOS 上,我们的插件目录为ImageEditor.app/Contents/MacOS/plugins
。 您应该创建该目录并在其中复制插件文件。
让我们再次运行我们的应用:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W0rzvvYc-1681871114320)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/94dcfa12-52aa-46ea-800d-500d0c8b9ccb.png)]
现在,我们可以在“编辑”菜单和“编辑”工具栏上看到“腐蚀”动作。 让我们打开一个图像以查看erode
的功能。
这是在执行任何操作之前由应用打开的图像:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFNLLNlN-1681871114321)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/efdd7765-a1d5-4a33-98a5-1a3c07fc5427.png)]
单击“侵蚀”操作后,将获得以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dmUfUzqf-1681871114321)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/677cb9fa-dea0-4421-b74d-3db4c111e4f0.png)]
如您所见,单击“腐蚀”操作后,图像的暗部被放大,白色对象缩小。 这是因为 OpenCV 将图像的深色部分视为背景,并且侵蚀了图像中的对象(浅色部分)。
我们已经使用 Qt 库提供的插件机制成功添加了新的编辑功能。 本节的重点是介绍该插件机制,而不是图像编辑功能,因此我们仅使用erode
函数来实现编辑功能,以简化图像编辑。 现在已经介绍了插件机制,我们可以继续使用 OpenCV 库和使用该库的图像编辑功能。
像专业人士一样编辑图像
在上一节中,我们研究了如何为应用添加图像编辑功能作为插件。 这样,我们就不需要照顾用户界面,打开和关闭图像以及热键。 相反,我们必须添加一个新的编辑功能,即编写EditorPluginInterface
接口的子类并实现其纯虚拟函数,然后将其编译为插件文件(共享库文件)并将其复制到我们应用的插件目录。 在本节中,我们将讨论使用 OpenCV 进行图像编辑。
首先,让我们从锐化图像开始。
锐化图像
图像锐化是由许多著名的图像编辑软件(例如 GIMP 和 Photoshop)实现的常见功能。 锐化图像的原理是我们从原始版本中减去图像的平滑版本,以得到这两个版本之间的差异,然后将该差异添加到原始图像中。 我们可以通过对图像的副本应用高斯平滑过滤器来获得平滑版本。 稍后我们将看到如何使用 OpenCV 进行此操作,但是第一步是创建一个新的 Qt 插件项目。
由于我们在上一节中创建了一个名为ErodePlugin
的 Qt 插件项目,因此创建类似的其他项目并不难。
首先,我们在终端中创建目录和必要的文件:
$ ls ErodePlugin ImageEditor $ mkdir SharpenPlugin $ ls ErodePlugin ImageEditor SharpenPlugin $ cd SharpenPlugin $ touch sharpen_plugin.h sharpen_plugin.cpp $ qmake -project $ ls sharpen_plugin.h sharpen_plugin.cpp SharpenPlugin.pro
然后,我们编辑SharpenPlugin.pro
项目文件并设置其配置:
TEMPLATE = lib TARGET = SharpenPlugin COPNFIG += plugin INCLUDEPATH += . ../ImageEditor unix: !mac { INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4 LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -l opencv_imgproc } 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 } HEADERS += sharpen_plugin.h SOURCES += sharpen_plugin.cpp
该项目文件的大部分内容与ErodePlugin
插件项目的项目文件相同,除了TARGET
,HEADERS
和SOURCES
的设置。 这三个设置的更改就其键和值而言很容易且不言自明。
现在,让我们看一下源文件。 第一个是头文件sharpen_plugin.h
:
#include <QObject> #include <QtPlugin> #include "editor_plugin_interface.h" class SharpenPlugin: public QObject, public EditorPluginInterface { Q_OBJECT Q_PLUGIN_METADATA(IID EDIT_PLUGIN_INTERFACE_IID); Q_INTERFACES(EditorPluginInterface); public: QString name(); void edit(const cv::Mat &input, cv::Mat &output); };
该文件与我们在 ErodePlugin 项目中编写的erode_plugin.h
头文件相同,只不过我们在此处使用了不同的类名SharpenPlugin
。 我们使该类成为QObject
和EditorPluginInterface
的后代。 在该类的主体中,我们使用多个 Qt 宏向 Qt 库的元对象和插件系统提供必要的信息,然后声明必须实现的两个方法才能满足EditorPluginInterface
接口。
我们完成了项目文件和头文件。 如您所见,它们的大多数内容与我们在 ErodePlugin 项目中的内容相同,除了一些名称更改,包括项目名称,目标名称和文件名。
现在,该看看sharpen_plugin.cpp
中方法的实现了。 毫不奇怪,对其所做的唯一更改就是名称的更改以及方法主体的更改。 首先让我们看一下name
方法:
QString SharpenPlugin::name() { return "Sharpen"; }
在这里,我们在第一行中将类名称更改为SharpenPlugin
,然后返回Sharpen
字符串作为其名称和标签。 那很简单。 现在,让我们继续进行edit
方法:
void SharpenPlugin::edit(const cv::Mat &input, cv::Mat &output) { int intensity = 2; cv::Mat smoothed; GaussianBlur(input, smoothed, cv::Size(9, 9), 0); output = input + (input - smoothed) * intensity; }
虽然仅在第一行中更改了类名,但我们在此方法的主体中进行了很多更改以进行锐化工作。 首先,我们定义两个变量。 intensity
变量是一个整数,它将指示我们将锐化图像的强度,而smoothed
是cv::Mat
的实例,将用于保存图像的平滑版本。 然后,我们调用GaussianBlur
函数对作为cv::Mat
实例传递到我们的方法的图像进行平滑处理,并将平滑后的版本存储在smoothed
变量中。
在图像处理中,高斯模糊是一种被广泛采用的算法,尤其是当您要减少图像的噪点或细节时。 它以出色的数学家和科学家卡尔·弗里德里希·高斯(Carl Friedrich Gauss)的名字命名,因为它使用高斯函数来模糊图像。 有时也称为高斯平滑。
您可以在这个页面中找到有关此算法的更多信息。 在 OpenCV 中,我们使用GaussianBlur
函数来实现此效果。 与大多数 OpenCV 函数一样,此函数接受许多参数。 第一个和第二个是输入和输出图像。 第三个参数是cv::Size
对象,代表核的大小。 第四个是double
类型的变量,它表示 X 方向上的高斯核标准差。 它还有两个带有默认值的额外参数。 我们在代码中使用其默认值以使该方法易于理解,但是您可以在这个页面上参考GaussianBlur
函数的文档,了解更多信息。
在获得原始图像的平滑版本之后,可以通过从原始版本中减去平滑版本input - smoothed
来找到原始版本和平滑版本之间的良好区别。 此表达式中的减法运算在 OpenCV 中称为按元素矩阵运算。 逐元素矩阵运算是计算机视觉中的数学函数和算法,可对矩阵的各个元素(即图像的像素)起作用。 重要的是要注意,可以逐个元素并行化操作,这从根本上意味着矩阵元素的处理顺序并不重要。 通过执行此减法,我们得到了区别-它也是cv::Mat
实例,因此如果您要查看它,可以在应用中显示它。 由于这种区别很小,因此即使显示出来,您也会看到黑色图像,尽管它不是完全黑色的-其中有一些无块像素。 为了锐化原始图像,我们可以通过使用附加的逐元素运算将这个区分矩阵叠加到原始图像上一次或多次。 在我们的代码中,次数是我们定义的intensity
变量。 首先,我们将intensity
标量乘以区分矩阵(这也是标量和矩阵之间的元素操作),然后将结果添加到原始图像矩阵中:
input + (input - smoothed) * intensity
最后,我们将结果矩阵分配给输出变量cv::Mat
的引用,以out
参数的方式返回锐化的图像。
所有代码已准备就绪,因此让我们在终端中编译我们的插件:
$ qmake -makefile $ make g++ -c -pipe -O2 ... # output truncated $ ls -l *so* lrwxrwxrwx 1 kdr2 kdr2 25 Dec 20 11:24 libSharpenPlugin.so -> libSharpenPlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 25 Dec 20 11:24 libSharpenPlugin.so.1 -> libSharpenPlugin.so.1.0.0 lrwxrwxrwx 1 kdr2 kdr2 25 Dec 20 11:24 libSharpenPlugin.so.1.0 -> libSharpenPlugin.so.1.0.0 -rwxr-xr-x 1 kdr2 kdr2 78880 Dec 20 11:24 libSharpenPlugin.so.1.0.0 $ cp libSharpenPlugin.so.1.0.0 ../ImageEditor/plugins/libSharpenPlugin.so $
编译好插件并将其复制到ImageEditor
应用的插件目录之后,我们可以运行该应用以测试我们的新插件:
$ cd ../ImageEditor/ $ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib/ $ ./ImageEditor
如果一切顺利,您将在“编辑”菜单和“编辑”工具栏下看到“锐化”操作。 让我们看看打开图像后的样子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DLOB1J1s-1681871114321)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/eca78b19-30d3-4dbc-a238-de11bebece4a.png)]
现在,让我们通过单击新插件提供的“锐化”操作来锐化图像后,看看图像是什么样子:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Fape6ODr-1681871114321)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/cc404f2f-65da-4482-ac1d-9a60f0966cfe.png)]
我们可以看到它们之间明显的区别。 请随意使用intensity
变量和GaussianBlur
函数的参数来获得自己喜欢的结果。
卡通效果
在上一节中,我们添加了新的编辑功能,以便可以在应用中锐化图像。 在本节中,我们将添加一个新的编辑功能,以便为图像创建有趣的卡通效果。 为了获得这种卡通效果,我们需要做两件事:首先,我们需要使图像具有卡通外观,因此我们需要找到一种减少其调色板的方法。 然后,我们将检测图像中的边缘并与它们一起产生粗体轮廓。 之后,我们将合并这些步骤的结果图像,然后获得实现了卡通效果的新图像。
幸运的是,所有这些都可以通过使用 OpenCV 库来完成。 因此,让我们开始新的插件项目,我们将其称为CartoonPlugin
。 创建插件项目的步骤和项目的目录结构与我们之前所做的非常相似,因此,为了使本章简洁明了,在此我们不会向您展示如何逐步创建项目。
要创建项目,我们将创建一个名为CartoonPlugin
的新目录,然后在该目录中创建项目文件和源文件。 该目录应如下所示:
$ ls cartoon_plugin.cpp cartoon_plugin.h CartoonPlugin.pro $
您可以从我们以前的插件项目之一复制项目文件,然后将TARGET
,HEADERS
和SOURCES
设置的值更改为此项目的正确值。 由于源文件的内容也与先前项目中的内容非常相似,因此您可以将任何已完成的插件项目中的源文件用作模板来简化开发过程-只需复制文件,更改文件名, 其中的插件类名称,以及name
和eidt
方法的实现。
在此项目中,我们使用CartoonPlugin
作为插件类名称,并在CartoonPlugin::name
方法中使用return "Cartoon";
作为插件类名称。 现在,我们要做的就是实现CartoonPlugin::edit
方法。 现在让我们继续进行这一关键部分。
第一项任务是减少调色板。 为此,我们可以使用 OpenCV 库提供的双边过滤器。 尽管双边过滤器效果很好,并通过平滑平坦区域并保持锐利边缘为普通的 RGB 图像提供了卡通外观,但是它比其他平滑算法(例如,我们之前使用的高斯模糊算法)慢得多。 但是,在我们的应用中,速度很重要-为了使代码易于理解,我们不会创建单独的辅助线程来进行编辑工作。 如果编辑过程太慢,它将冻结我们应用的用户界面-也就是说,在编辑时,我们的应用将不是交互式的,用户界面也不会被更新。
幸运的是,我们有两种方法可以加快这一过程,从而缩短冻结时间:
- 缩小原始图像,然后将过滤器应用于该缩小的版本。
- 代替一次对图像应用大的双边过滤器,我们可以多次应用小双边的过滤器。
让我们看看如何做到这一点:
int num_down = 2; int num_bilateral = 7; cv::Mat copy1, copy2; copy1 = input.clone(); for(int i = 0; i < num_down; i++) { cv::pyrDown(copy1, copy2); copy1 = copy2.clone(); } for(int i = 0; i < num_bilateral; i++) { cv::bilateralFilter(copy1, copy2, 9, 9, 7); copy1 = copy2.clone(); } for(int i = 0; i < num_down; i++) { cv::pyrUp(copy1, copy2); copy1 = copy2.clone(); }
首先,我们定义两个Mat
类对象copy1
和copy2
,然后将input
的副本分配给copy1
。
然后,我们使用cv::pyrDown
重复缩小copy1
的大小(两次通过int num_down = 2;
)。 在此循环中,我们对两个定义的矩阵copy1
和copy2
进行操作。 由于cv::pyrDown
函数不支持原地操作,因此对于输出,我们必须使用与输入矩阵不同的矩阵。 为了实现重复操作,我们应在每次操作后将所得矩阵的copy2
克隆为copy1
。
缩小操作后,我们在copy1
中获得了原始图像的降采样版本。 现在,就像缩小过程一样,我们反复对copy1
应用一个小的双边过滤器(通过int num_bilateral = 7;
进行七次)。 此函数也不支持原地,因此我们将copy1
用作其输入图像,并将copy2
用作其输出图像。 我们传递给cv::bilateralFilter
函数的最后三个参数指定像素邻域的直径,其值为9
,色彩空间中的过滤器σ
,其值也为9
,以及坐标中的过滤器σ
空间,其值分别为7
。 您可以参考这里了解如何在过滤器中使用这些值。
缩小调色板后,我们应该将向下采样的图像放大到其原始大小。 这是通过在copy1
上调用cv::pyrUp
的次数与在其上调用cv::pyrDown
相同的次数来完成的。
因为在缩小时将结果图像的大小计算为Size((input.cols + 1) / 2, (input.rows + 1) / 2)
,而在放大时将结果图像的大小计算为Size(input.cols * 2, (input.rows * 2)
,所以copy1
矩阵的大小可能与原始图像不同。 它可能等于或大于原始像素几个像素。 在此,如果copy1
在尺寸上与原始图片不同,则应将copy1
调整为原始图片的尺寸:
if (input.cols != copy1.cols || input.rows != copy1.rows) { cv::Rect rect(0, 0, input.cols, input.rows); copy1(rect).copyTo(copy2); copy1 = copy2; }
至此,我们得到了原始图像的副本,该副本的调色板减小且尺寸不变。 现在,让我们继续前进,检测边缘并生成一些大胆的轮廓。 OpenCV 提供了许多检测边缘的函数。 在这里,我们选择cv::adaptiveThreshold
函数并以cv::THRESH_BINARY
作为其阈值类型进行调用以执行边缘检测。 在自适应阈值算法中,不是使用全局值作为阈值,而是使用动态阈值,该阈值由当前像素周围较小区域中的像素确定。 这样,我们可以检测每个小区域中最显着的特征,并据此计算阈值。 这些函数正是我们应该在图像中的对象周围绘制粗体和黑色轮廓的地方。 同时,自适应算法也有其弱点-容易受到噪声的影响。 因此,最好在检测边缘之前对图像应用中值过滤器,因为中值过滤器会将每个像素的值设置为周围所有像素的中值,这样可以减少噪声。 让我们看看如何做到这一点:
cv::Mat image_gray, image_edge; cv::cvtColor(input, image_gray, cv::COLOR_RGB2GRAY); cv::medianBlur(image_gray, image_gray, 5); cv::adaptiveThreshold(image_gray, image_gray, 255, cv::ADAPTIVE_THRESH_MEAN_C, cv::THRESH_BINARY, 9, 2); cv::cvtColor(image_gray, image_edge, cv::COLOR_GRAY2RGB);
首先,我们通过调用cvtColor
函数将输入图像转换为灰度图像,然后将cv::COLOR_RGB2GRAY
作为颜色空间转换代码作为其第三个参数。 此函数也不能原地工作,因此我们使用另一个与输入矩阵不同的矩阵image_gray
作为输出矩阵。 此后,我们在image_gray
矩阵中获得原始图像的灰度版本。 然后,我们调用cv::medianBlur
将中值过滤器应用于灰度图像。 如您所见,在此函数调用中,我们将image_gray
矩阵用作其输入和输出矩阵。 这是因为此函数支持原地操作。 它可以原地处理输入矩阵的数据; 也就是说,它从输入读取数据,进行计算,然后将结果写入输入矩阵,而不会干扰图像。
应用中值过滤器后,我们在灰度图像上调用cv::adaptiveThreshold
以检测图像中的边缘。 我们在灰度图像上进行此操作,因此,在执行此操作后,灰度图像将变为仅包含边缘的二进制图像。 然后,我们将二进制边缘转换为 RGB 图像,并通过调用cvtColor
将其存储在image_edge
矩阵中。
现在,调色板已缩小并且边缘图像已准备就绪,让我们通过按位and
操作合并它们并将其分配给output
矩阵以返回它们:
output = copy1 & image_edge;
至此,所有开发工作已经完成。 现在,该编译并测试我们的插件了:
$ make g++ -c -pipe -O2 -Wall ... # output truncated $ cp libCartoonPlugin.so.1.0.0 ../ImageEditor/plugins/libCartoonPlugin.so $ ls ../ImageEditor/plugins/ libCartoonPlugin.so libErodePlugin.so libSharpenPlugin.so $ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib/ $ ../ImageEditor/ImageEditor
启动我们的应用并使用它打开图像后,我们得到以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TCY64v79-1681871114321)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/d7f7b010-cb0d-42bc-aac6-03d8b2dd9289.png)]
让我们单击卡通动作,看看会发生什么:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vlH2nvPu-1681871114322)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/2442be76-9f57-4de0-8c0f-2274129131b5.png)]
这还不错,您可以随意使用所有过滤器函数的参数来自己调整卡通效果。
在本节中,我们使用了 OpenCV 提供的许多过滤器函数。 在调用这些函数时,我指出了medianBlur
函数支持原地操作,而bilateralFilter
函数则不支持。 这是什么意思,我们如何知道某个函数是否支持原地操作?
如前所述,如果一个函数支持原地操作,则意味着该函数可以从输入图像读取,进行计算,然后将结果写入矩阵,该矩阵可以是我们用作输入的矩阵或与输入矩阵不同的矩阵。 当我们使用一个矩阵作为其输入和输出时,该函数仍然可以正常工作,并将结果放入输入矩阵中而不会破坏数据。 如果某个函数不支持原地运算,则必须使用与输入矩阵不同的矩阵作为其输出,否则数据可能会损坏。 实际上,在 OpenCV 的实现中,它会断言以确保在不支持原地操作的函数中,输入和输出不是同一矩阵,或者是共享同一数据缓冲区的不同矩阵。 如果某个函数支持原地操作,则可以使用它来提高程序的性能,因为这种方式可以节省内存。 由于 OpenCV 有充分的文档说明,因此可以参考文档以了解函数是否支持原地操作。 让我们看一下我们刚刚使用的medianBlur
函数的文档:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7OJWKfgQ-1681871114322)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/84881f18-d518-4f26-a740-7b33e9cf7d10.png)]
在前面的屏幕快照中,我突出显示了该函数支持原地操作的行。 一些(但不是全部)不支持原地操作的函数也有一条声明明确指出。 例如bilateralFilter()
函数,我们在本节中也使用了该函数:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dcJGMnzp-1681871114322)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/1e420196-7a2b-405a-9aa7-4f2c62f5b649.png)]
值得注意的是,如果文档中说某个函数支持原地操作,那么它将支持。 如果文档没有说明某个函数是否支持原地操作,则最好假定它不支持原地操作。
旋转图像
在前面的部分中,我们已将许多编辑功能作为插件添加,所有这些功能都利用了 OpenCV 提供的图像过滤器。 从本节开始,我们将添加一些利用 OpenCV 库的转换函数的功能。
根据 OpenCV 库的文档,OpenCV 中有两个图像转换类别:
- 几何变换
- 杂项变换(除几何变换外的所有变换)
在本节和下一部分中,我们将研究几何变换。 我们可以从它们的名称猜测得出,几何变换主要处理图像的几何属性,例如图像的大小,方向和形状。 它们不更改图像的内容,而是根据几何变换的性质,通过在周围移动图像的像素来更改图像的形式和形状。
让我们首先从简单的几何变换开始-旋转图像。 使用 OpenCV 旋转图像有多种方法。 例如,我们可以在矩阵上应用转置和翻转的复合操作,也可以使用适当的变换矩阵进行仿射变换。 在本节中,我们将使用后一种方法。
现在是时候开始一个新的动手项目来开发旋转插件了。 我们可以通过使用以前的插件项目作为模板来做到这一点。 以下是此过程的重点列表:
- 使用
RotatePlugin
作为项目名称。 - 创建项目文件和源文件(
.h
文件和.cpp
文件)。 - 更改项目文件中的相关设置。
- 使用
RotatePlugin
作为插件类名称。 - 在
name
方法中返回Rotate
作为插件名称。 - 更改
edit
方法的实现。
除了最后一步,每个步骤都非常简单明了。 因此,让我们跳过前五个步骤,直接进入最后一步-这是我们在此插件中实现edit
方法的方式:
void RotatePlugin::edit(const cv::Mat &input, cv::Mat &output) { double angle = 45.0; double scale = 1.0; cv::Point2f center = cv::Point(input.cols/2, input.rows/2); cv::Mat rotateMatrix = cv::getRotationMatrix2D(center, angle, scale); cv::Mat result; cv::warpAffine(input, result, rotateMatrix, input.size(), cv::INTER_LINEAR, cv::BORDER_CONSTANT); output = result; }
如前所述,我们使用仿射变换来进行旋转,这是通过调用 OpenCV 库提供的cv::warpAffine
函数来实现的。 此函数不支持原地操作,因此我们将定义一个新的临时矩阵result
来存储输出。
当我们在ImageEditor
应用中调用每个插件的edit
方法时,我们使用一个矩阵作为输入和输出参数,即plugin_ptr->edit(mat, mat);
,因此,在插件的edit
方法的实现中,参数input
和output
实际上是相同的矩阵。 这意味着我们不能将它们传递给不支持原地操作的函数。
warpAffine
函数将称为转换矩阵的矩阵作为其第三个参数。 该变换矩阵包含描述仿射变换应如何完成的数据。 手工编写此转换矩阵有点复杂,因此 OpenCV 提供了生成该转换矩阵的函数。 为了生成旋转的变换矩阵,我们可以使用cv::getRotationMatrix2D
函数,为其指定一个点作为轴点,一个角度和一个缩放比例。
在我们的代码中,我们将输入图像的中心点用作旋转的轴点,并使用正数 45 表示旋转将逆时针旋转 45 度这一事实。 由于我们只想旋转图像,因此我们使用 1.0 作为缩放比例。 准备好这些参数后,我们通过调用cv::getRotationMatrix2D
函数获得rotateMatrix
,然后将其传递给第三位置的cv::warpAffine
。
cv::warpAffine
的第四个参数是输出图像的大小。 我们在这里使用输入图像的大小来确保图像的大小在编辑过程中不会改变。 第五个参数是插值方法,因此在这里我们只使用cv::INTER_LINEAR
。 第六个参数是输出图像边界的像素外推方法。 我们在这里使用cv::BORDER_CONSTANT
,以便在旋转后,如果某些区域未被原始图像覆盖,则将用恒定的颜色填充它们。 我们可以将此颜色指定为第七个参数,否则默认使用黑色。
既然代码已经清晰了,让我们编译和测试插件:
$ make g++ -c -pipe -O2 -Wall ... # output truncated $ cp libRotatePlugin.so.1.0.0 ../ImageEditor/plugins/libRotatePlugin.so $ ls ../ImageEditor/plugins/ libCartoonPlugin.so libErodePlugin.so libRotatePlugin.so libSharpenPlugin.so $ export LD_LIBRARY_PATH=/home/kdr2/programs/opencv/lib/ $ ../ImageEditor/ImageEditor
打开图像后,我们应该获得以下输出:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nCuAvlgn-1681871114322)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/c3ae5b63-e213-4d57-9606-2b371e33ac32.png)]
让我们单击“旋转”操作,看看会发生什么:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uGz2wFTN-1681871114322)(https://gitcode.net/apachecn/apachecn-cv-zh/-/raw/master/docs/qt5-opencv4-cv-proj/img/1cc08f62-5aaa-496a-9455-c14459ecae92.png)]
如我们所见,图像正如我们预期的那样逆时针旋转 45 度。 随意更改中心点,角度和比例的值以查看会发生什么。
Qt5 和 OpenCV4 计算机视觉项目:1~5(3)https://developer.aliyun.com/article/1427054