Qt5+Opencv插件式开发

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

5、整体效果图

在这里插入图片描述

  • 4
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值