1、插件介绍
插件(Plug-in,又称addin、add-in、addon或add-on,又译外挂)是一种遵循一定规范的应用程序接口编写出来的程序。其只能运行在程序规定的系统平台下(可能同时支持多个平台),而不能脱离指定的平台单独运行。因为插件需要调用原纯净系统提供的函数库或者数据。很多软件都有插件,插件有无数种。例如在IE中,安装相关的插件后,WEB浏览器能够直接调用插件程序,用于处理特定类型的文件。
2、项目介绍
在开发机器视觉项目通常使用QtWidget作为GUI框架,使用Opencv或者Halcon作为视觉算法框架。当开发大型项目时,通常分为了许多模块,例如:算法模块、界面模块等。为了方便管理项目,需要将界面和算法分隔开,在界面中加载算法框架,这样就得用到插件,也就是将每一个算法转化为dll,在主界面中加载这些算法dll。
开发环境:Qt 5.14.2
操作系统:Win11
Opencv:4.51
3、开发流程
首先创建一个子项目OpenVision:
然后在OpenVision项目下右键->新子项目->Application->Qt Widgets Application,名称为MainApp,用来读取图片和加载插件。
每一个插件就是一个dll文件,为了好管理,我在OpenVision项目下右键->新子项目->其他项目->子目录项目,创建名称为ImageProcessForPlugins的子项目,所有的插件项目都在ImageProcessForPlugins子目录下创建。
最后新建一个插件Library,在ImageProcessForPlugins上鼠标右键->新子项目->Library,创建名称为FilterPlugin(滤波算法插件)和MorphologyExPlugin(形态学算法插件)。
具体的项目结构如图:
项目结构整清楚以后,就可以把有关的类库引进来,由于很多地方都会用到OpenCV库,所以我创建了一个3rdparty目录,存放opencv库目录和头文件目录,以及算法接口抽象基类,用于子类实现并且使用。
opencv451.pri内容如下:
INCLUDEPATH += D:/Opencv4.51/opencv/build/include
LIBS += D:\Opencv4.51\opencv\build\lib\libopencv_*.a
CvPluginInterface.h内容如下:
#ifndef CVPLUGIN_H
#define CVPLUGIN_H
#include <QObject>
#include <QWidget>
#include <QString>
#include "opencv2/opencv.hpp"
class CvPluginInterface
{
public:
//很重要 虚析构函数
virtual ~CvPluginInterface() {}
//最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是纯虚函数”
//virtual QString title() = 0;
//virtual QString version() = 0;
//virtual QString description() = 0;
//virtual QString help() = 0;
virtual QWidget *setUi() = 0; //算法界面的获取
virtual void processImage(const cv::Mat &inputImage, cv::Mat &outputImage) = 0;//算法执行
virtual QString name() = 0; //算法名称
};
#define CVPLUGININTERFACE_IID "com.motionandvision.cvplugininterface"
//此宏用于把标识符与类名接口关联起来,将类定义为接口
Q_DECLARE_INTERFACE(CvPluginInterface, CVPLUGININTERFACE_IID)
#endif // CVPLUGIN_H
3.1、滤波算法插件
如下是滤波算法界面的ui:
将界面中的slider和spinBox进行初始化:
void FilterWidget::initUI()
{
//均值滤波
this->ui->spinBoxBlur->setRange(1,50);
this->ui->sliderBlur->setRange(1,50);
this->ui->sliderBlur->setValue(1);
//高斯滤波
ui->spinBoxGaussianBlur1->setRange(3,50);
ui->spinBoxGaussianBlur2->setRange(0,50);
ui->spinBoxGaussianBlur3->setRange(0,50);
ui->sliderGaussianBlur1->setRange(3,50);
ui->sliderGaussianBlur2->setRange(0,50);
ui->sliderGaussianBlur3->setRange(0,50);
ui->sliderGaussianBlur1->setValue(3);
ui->sliderGaussianBlur2->setValue(1);
ui->sliderGaussianBlur3->setValue(1);
//双边滤波
ui->spinBilateralBlur1->setRange(1,50);
ui->spinBilateralBlur2->setRange(0,255);
ui->spinBilateralBlur3->setRange(0,100);
ui->sliderBilateralBlur1->setRange(1,50);
ui->sliderBilateralBlur2->setRange(0,255);
ui->sliderBilateralBlur3->setRange(0,100);
ui->sliderBilateralBlur1->setValue(3);
ui->sliderBilateralBlur2->setValue(20);
ui->sliderBilateralBlur3->setValue(1);
//中值滤波
ui->spinBoxMedianBlur->setRange(1,50);
ui->sliderMedianBlur->setRange(1,50);
ui->sliderMedianBlur->setValue(1);
}
信号和槽之间的绑定:
void FilterWidget::initConnectSLot()
{
//均值滤波
connect(this->ui->sliderBlur,&QSlider::valueChanged,this->ui->spinBoxBlur,&QSpinBox::setValue);
connect(ui->sliderBlur,SIGNAL(valueChanged(int)),ui->spinBoxBlur,SLOT(setValue(int)));
//高斯滤波
connect(this->ui->sliderGaussianBlur1,&QSlider::valueChanged,this->ui->spinBoxGaussianBlur1,&QSpinBox::setValue);
connect(this->ui->spinBoxGaussianBlur1,SIGNAL(valueChanged(int)),this->ui->sliderGaussianBlur1,SLOT(setValue(int)));
connect(this->ui->sliderGaussianBlur2,&QSlider::valueChanged,this->ui->spinBoxGaussianBlur2,&QSpinBox::setValue);
connect(this->ui->spinBoxGaussianBlur2,SIGNAL(valueChanged(int)),this->ui->sliderGaussianBlur2,SLOT(setValue(int)));
connect(this->ui->sliderGaussianBlur3,&QSlider::valueChanged,this->ui->spinBoxGaussianBlur3,&QSpinBox::setValue);
connect(this->ui->spinBoxGaussianBlur3,SIGNAL(valueChanged(int)),this->ui->sliderGaussianBlur3,SLOT(setValue(int)));
//双边滤波
connect(this->ui->sliderBilateralBlur1,&QSlider::valueChanged,this->ui->spinBilateralBlur1,&QSpinBox::setValue);
connect(this->ui->spinBilateralBlur1,SIGNAL(valueChanged(int)),this->ui->sliderBilateralBlur1,SLOT(setValue(int)));
connect(this->ui->sliderBilateralBlur2,&QSlider::valueChanged,this->ui->spinBilateralBlur2,&QSpinBox::setValue);
connect(this->ui->spinBilateralBlur2,SIGNAL(valueChanged(int)),this->ui->sliderBilateralBlur2,SLOT(setValue(int)));
connect(this->ui->sliderBilateralBlur3,&QSlider::valueChanged,this->ui->spinBilateralBlur3,&QSpinBox::setValue);
connect(this->ui->spinBilateralBlur3,SIGNAL(valueChanged(int)),this->ui->sliderBilateralBlur3,SLOT(setValue(int)));
//中值滤波
connect(this->ui->sliderMedianBlur,&QSlider::valueChanged,this->ui->spinBoxMedianBlur,&QSpinBox::setValue);
connect(this->ui->spinBoxMedianBlur,SIGNAL(valueChanged(int)),this->ui->sliderMedianBlur,SLOT(setValue(int)));
}
在滤波算法插件的文件需要继承算法抽象基类,重写它的接口,以及设置插件的IID。
滤波算法的实现:
void FilterPlugin::processImage(const Mat &inputImage, Mat &outputImage)
{
//0->均值滤波 1->高斯滤波 2->双边滤波 3->中值滤波
int type = m_widget->ui->toolBox->currentIndex();
switch (type)
{
case 0:
{
int sizeBlur = m_widget->ui->sliderBlur->value();
blur(inputImage,outputImage,Size(sizeBlur,sizeBlur));
}
break;
case 1:
{
int sizeGaussBlurSize = m_widget->ui->sliderGaussianBlur1->value();
//高斯滤波内核参数不能为偶数
if(sizeGaussBlurSize % 2 == 0)
{
break;
}
int sigmax = m_widget->ui->sliderGaussianBlur2->value();
int sigmay = m_widget->ui->sliderGaussianBlur3->value();
GaussianBlur(inputImage,outputImage,Size(sizeGaussBlurSize,sizeGaussBlurSize),sigmax,sigmay);
}
break;
case 2:
{
int d = m_widget->ui->sliderBilateralBlur1->value();
int sigmaColor = m_widget->ui->sliderBilateralBlur2->value();
int sigmaSpace = m_widget->ui->sliderBilateralBlur3->value();
bilateralFilter(inputImage,outputImage,d,sigmaColor,sigmaSpace);
}
break;
case 3:
{
int sizeMediaBlur = m_widget->ui->sliderMedianBlur->value();
if(sizeMediaBlur % 2 == 0)
break;
medianBlur(inputImage,outputImage,sizeMediaBlur);
}
break;
}
}
3.2、形态学算法插件
形态学算法插件的ui如下:
将界面中的slider和spinBox进行初始化:
ui->sliderX->setRange(1,50);
ui->sliderY->setRange(1,50);
ui->morphIterSpin->setRange(1,20);
ui->sliderX->setValue(1);
ui->sliderY->setValue(1);
ui->morphIterSpin->setValue(1);
信号和槽的绑定:
connect(this->ui->sliderX,&QSlider::valueChanged,this->ui->spinBoxX,&QSpinBox::setValue);
connect(this->ui->sliderY,&QSlider::valueChanged,this->ui->spinBoxY,&QSpinBox::setValue);
connect(this->ui->morphIterSpin,&QSlider::valueChanged,this->ui->morphIterSpinBox,&QSpinBox::setValue);
connect(this->ui->spinBoxX,SIGNAL(valueChanged(int)),this->ui->sliderX,SLOT(setValue(int)));
connect(this->ui->spinBoxY,SIGNAL(valueChanged(int)),this->ui->sliderY,SLOT(setValue(int)));
connect(this->ui->morphIterSpinBox,SIGNAL(valueChanged(int)),this->ui->morphIterSpin,SLOT(setValue(int)));
形态学算法的实现:
void MorphologyExPlugin::processImage(const Mat &inputImage, Mat &outputImage)
{
int op = m_widget->ui->morphTypesCombo->currentIndex();
int shape = m_widget->ui->morphShapesCombo->currentIndex();
int sliderX = m_widget->ui->sliderX->value();
int sliderY = m_widget->ui->sliderY->value();
int iterations = m_widget->ui->morphIterSpin->value();
morphologyEx(inputImage,outputImage,op,getStructuringElement(shape,Size(sliderX,sliderY)),Point(-1,-1),iterations);
}
3.3、主界面的实现
首先实现QImage与cv::Mat矩阵之间的转化接口:
/**
* @brief 将 OpenCV 的 cv::Mat 类型图像转换为 QImage 类型
* @param mat 待转换的图像,支持 CV_8UC1、CV_8UC3、CV_8UC4 三种OpenCV 的数据类型
* @param clone true 表示与 Mat 不共享内存,更改生成的 mat 不会影响原始图像,false 则会与 mat 共享内存
* @param rb_swap 只针对 CV_8UC3 格式,如果 true 则会调换 R 与 B RGB->BGR,如果共享内存的话原始图像也会发生变化
* @return 转换后的 QImage 图像
*/
QImage MainWindow::cvMat2QImage(const Mat &mat, bool clone, bool rb_swap)
{
const uchar *pSrc = (const uchar*)mat.data;
// 8-bits unsigned, NO. OF CHANNELS = 1
if(mat.type() == CV_8UC1)
{
//QImage image(mat.cols, mat.rows, QImage::Format_Grayscale8);
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8);
if(clone) return image.copy();
return image;
}
// 8-bits unsigned, NO. OF CHANNELS = 3
else if(mat.type() == CV_8UC3)
{
// Create QImage with same dimensions as input Mat
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_RGB888);
if(clone)
{
if(rb_swap) return image.rgbSwapped();
return image.copy();
}
else
{
if(rb_swap)
{
cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB);
}
return image;
}
}
else if(mat.type() == CV_8UC4)
{
qDebug() << "CV_8UC4";
QImage image(pSrc, mat.cols, mat.rows, mat.step, QImage::Format_ARGB32);
if(clone) return image.copy();
return image;
}
else
{
qDebug() << "ERROR: Mat could not be converted to QImage.";
return QImage();
}
}
/**
* @brief 将 QImage 的类型图像转换为 cv::Mat 类型
* @param image 待转换的图像,支持 Format_Indexed8/Format_Grayscale、24 位彩色、32 位彩色格式,
* @param clone true 表示与 QImage 不共享内存,更改生成的 mat 不会影响原始图像,false 则会与 QImage 共享内存
* @param rg_swap 只针对 RGB888 格式,如果 true 则会调换 R 与 B RGB->BGR,如果共享内存的话原始图像也会发生变化
* @return 转换后的 cv::Mat 图像
*/
Mat MainWindow::QImage2cvMat(QImage &image, bool clone, bool rb_swap)
{
cv::Mat mat;
qDebug() << image.format();
switch(image.format())
{
case QImage::Format_ARGB32:
case QImage::Format_RGB32:
case QImage::Format_ARGB32_Premultiplied:
mat = cv::Mat(image.height(), image.width(), CV_8UC4, (void *)image.constBits(), image.bytesPerLine());
if(clone) mat = mat.clone();
break;
case QImage::Format_RGB888:
mat = cv::Mat(image.height(), image.width(), CV_8UC3, (void *)image.constBits(), image.bytesPerLine());
if(clone) mat = mat.clone();
if(rb_swap) cv::cvtColor(mat, mat, cv::COLOR_BGR2RGB);
break;
case QImage::Format_Indexed8:
case QImage::Format_Grayscale8:
mat = cv::Mat(image.height(), image.width(), CV_8UC1, (void *)image.bits(), image.bytesPerLine());
if(clone) mat = mat.clone();
break;
}
return mat;
}
加载插件,并将算法插件显示在QTreeWidget上:
//加载插件
void MainWindow::loadPlugins()
{
QTreeWidgetItem *group = new QTreeWidgetItem(this->ui->treeWidget);
group->setText(0,"图像处理算法插件");
//加载所有插件并构建菜单
QDir pluginDir(PLUGINS_DIR);
QFileInfoList pluginFiles = pluginDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files,QDir::Name);
foreach(auto pluginFile,pluginFiles)
{
//判断文件是否为dll文件
if(QLibrary::isLibrary(pluginFile.absoluteFilePath()))
{
QPluginLoader pluginLoader(pluginFile.absoluteFilePath(),this);
if(CvPluginInterface *plugin = dynamic_cast<CvPluginInterface *>(pluginLoader.instance()))
{
QTreeWidgetItem *subItem = new QTreeWidgetItem(group);
subItem->setText(0,plugin->name());
subItem->setData(0,Qt::UserRole,pluginFile.absoluteFilePath());
connect(this->ui->treeWidget, SIGNAL(itemDoubleClicked(QTreeWidgetItem*,int)), this, SLOT(onPluginTriggered(QTreeWidgetItem* ,int)));
}
}
}
group->setExpanded(true);
}
结果如下:
双击QTreeWidgetItem显示算法参数配置界面:
void MainWindow::onPluginTriggered(QTreeWidgetItem *item, int)
{
if(item->text(0) == "图像处理算法插件")
return;
if(currentPluginGui != nullptr)
{
this->ui->verticalLayout_8->removeWidget(currentPluginGui);
delete currentPluginGui;
delete currentPlugin;
}
currentPluginFile = item->data(0,Qt::UserRole).toString();
qDebug()<<"插件路径:"<<currentPluginFile;
currentPlugin = new QPluginLoader(currentPluginFile,this);
CvPluginInterface *plugin = dynamic_cast<CvPluginInterface *>(currentPlugin->instance());
if(plugin)
{
//添加界面
currentPluginGui = plugin->setUi();
this->ui->verticalLayout_8->addWidget(currentPluginGui);
connect(currentPlugin->instance(),SIGNAL(updateNeeded()),this,SLOT(onCurrentPluginUpdateNeeded()));
}
}
在主界面配置算法参数,显示效果:
void MainWindow::onCurrentPluginUpdateNeeded()
{
if(!originalMat.empty())
{
if(currentPlugin != nullptr)
{
CvPluginInterface *plugin = dynamic_cast<CvPluginInterface *>(currentPlugin->instance());
if(plugin)
{
QElapsedTimer time;
time.start();
plugin->processImage(originalMat,processedMat);
int millsecs = time.elapsed();
this->ui->textBrowser->append(plugin->name()+"--耗时:"+QString::number(millsecs)+"毫秒");
}
}
else
{
processedMat = originalMat.clone();
}
processedImage = cvMat2QImage(processedMat);
processedPixmap.setPixmap(QPixmap::fromImage(processedImage));
}
}
4、完整源码
链接:https://pan.baidu.com/s/1rs818AGZyzLRJQ0UTFWM_g
提取码:diwh