示例:在模块中实现图像二值化
最终效果如下所示
依赖文件包
本示例中使用OpenCV4.0.0作为二值化工具。若使用本软件配套的开发Demo工程,则无需额外配置;若是新建的工程,请自行按照OpenCV官方文档进行工程配置。
声明所需成员变量和成员函数
在算法类ModuleDemo
中的module_demo.h头文件中添加从基类继承的函数以及为了实现二值化所需要的函数和成员变量
public:
ModuleDemo();
//必须提供一个析构函数供主程序调用
~ModuleDemo();
//从基类继承的纯虚函数,请参考基类头文件 abstract_module.h
void release();
void initResultsType(MyResult *result, DrawableResult *draw_result);
int prepareParam(int runtime_mode);
int process(const GtMat *mat, MyResult *result, DrawableResult *draw_result, int runtime_mode);
int output(MyResult *result, DrawableResult *draw_result);
int saveParam(const QString &path, const QString &module_id, QJsonObject &json, bool param_only);
int readParam(const QJsonObject &json, const QString &path, const QString &module_id, bool param_only);
//二值化的实现函数
void doBinary(cv::Rect rect, cv::Mat mat);
private:
float binary_threshold_; //二值化阈值
cv::Rect roi_rect_; //roi区域
cv::Mat input_mat_; //运行时的输入
cv::Mat refer_mat_; //配置参数时的基准图像
cv::Mat binary_mat_; //二值化之后的图像
在界面类UIDemo
中的ui_demo.h头文件中添加所需成员函数和变量
public:
//继承自AbstractModuleWidget纯虚函数
QWidget * getConfigWidget(); //创建用户自己编辑的widget
void releaseConfigWidget(); //删除widget
void saveParamsToModule(); //保存参数到算法模块
bool setAbstractModulePtr(AbstractModule *m); //获得算法模块的指针,用于调用算法模块的函数
void initParamUi();
//继承自AbstractModuleWidget的虚函数,根据用户需求选择实现
void initDisplay(GtDisplay *display); //拿到GtDisplay指针,用于调用GtDisplay的函数
void doAfterReferImageChanged(); //当基准图像被改变时,该函数被调用
private:
WidgetDemo * widget_ = nullptr; //用户编辑的界面窗口指针
这里需要说明的是,这个UIDemo
类是本平台提供的AbstractModuleWidget
基类的子类,而不是qt的窗口类。WidgetDemo
这个类才是用户自己创建的Qt QWidget
类,用户在这个类中编辑自己的窗口界面。UIDemo
类通过成员变量widget_
从WidgetDemo
中获得参数,通过继承的接口实现平台所需功能(如保存、传递参数);或者通过该指针根据保存的参数修改界面中的对象。
在WidgetDemo
类的头文件中添加所需的函数和变量
public:
WidgetDemo(QWidget *parent = Q_NULLPTR);
~WidgetDemo();
//获得算法模块对象,通过该指针将界面配置的参数传递至算法模块
bool setMdlPtr(AbstractModule *m);
//获得基准图像窗口,通过该指针对窗口进行操作
bool initDisplay(GtDisplay *display);
//将界面获得的参数保存到算法模块,在关闭配置界面并保存时被调用
void saveParam();
//当基准图像变化时被调用
void doAfterReferImageChanged();
//初始化窗口界面
void initParamUi();
public slots:
//设置二值化阈值
void setThreshold();
//接收roi的位置和大小数据
void slotUpdateItemData(bool valid, double reserve0, double reserve1,
double reserve2, double reserve3, double reserve4, QString &name);
private:
Ui::UIDemo ui;
GtDisplay *display_; //图像显示窗口的指针
ModuleDemo *mdl_; //算法模块指针
QGraphicsPixmapItem* roi_overlay_; //覆盖roi区域的qt对象
void initConnects(); //构造信号槽
void updateROI(); //更新roi区域
public的成员函数多数与ui类对应,因此ui类中这些函数的实现只需调用widget类中对应的函数即可。
在算法模块中实现二值化功能
1. 构造函数
using namespace cv;
using namespace std;
ModuleDemo::ModuleDemo():
roi_rect_(Rect(100, 100, 150, 100)), //初始化roi大小和位置
binary_threshold_(0.5) //初始化二值化阈值
{
}
2. 释放函数
void ModuleDemo::release()
{
delete this;
}
3. 初始化输出结果
void ModuleDemo::initResultsType(MyResult * result, DrawableResult * draw_result)
{
//声明添加单一文本结果x坐标和y坐标,会显示在结果表格中
result->addSingleResult("x", BASIC_INT);
result->addSingleResult("y", BASIC_INT);
//声明绘制结果中总共有1中绘制类型
draw_result->setTypeNumber(1);
//声明第0个绘制类型的为ROTATE_RECT类型,该类型的结果最多有1个
draw_result->setElementType(0, DrawElement::ROTATE_RECT, 1);
}
我们的目标是在文本输出表格中显示roi框的左上角顶点坐标,以及在主界面右侧显示图像的区域画出roi框。所以我们首先对MyResult
类的对象指针result
调用addSingleResult()
。传入的参数为数据名称和数据类型。接下来声明绘制结果,对DrawableResult
类的对象指针draw_result
调用setTypeNumber()
设置绘制结果类型的个数,这个个数会为draw_result
的elements
指针申请内存,用于存放我们的绘制结果中所有绘制元素的种类。调用setElementType()
设置绘制类型和该类型的最大绘制数。
4. 参数检查函数
int ModuleDemo::prepareParam(int runtime_mode)
{
return 0;
}
因为本模块运行时需要的参数都会被初始化,不会出现参数为空的情况,即使不经过UI页面配置也可以运行,所以在这里我们不检查参数。
需要说明的是,虽然运行时所需参数都会被初始化,但我们不能保证初始化的参数都是合法的,比如说,我们初始化的roi_rect_
左上角定点位于(100, 100),长宽分别为150和100。如果运行时的输入图像足够小导致它超出了图像区域,那么该参数就是不合法的,但这无法在prepareParam(int runtime_mode)
这个函数中做检测,因为我们无法得知运行时的输入图片是怎样的。因此,我们应该在运行时也设置必要的参数检测保证自己的模块不会崩溃。
5. 运行时函数
int ModuleDemo::process(const GtMat * mat, MyResult * result, DrawableResult * draw_result, int runtime_mode)
{
//检查输入是否为空
if (mat == nullptr)
{
return 1010;
}
//将输入数据从GtMat格式转换为OpenCV的Mat格式
input_mat_ = Mat(mat->rows, mat->cols, CV_8UC1, mat->image_data, mat->cols);
//将选定区域二值化
doBinary(roi_rect_, input_mat_);
//返回0表示该轮运行成功
return 0;
}
GtMat
是本平台通用的图像数据结构,用户可以将该数据结构转化为自己需要的形式。在本例中,我们将它转化为OpenCV的Mat格式。
void ModuleDemo::doBinary(Rect rect, Mat mat)
{
//检查roi区域是否在输入图像范围内
if (rect.x < 0 ||
rect.y < 0 ||
rect.x + rect.width > mat.rows ||
rect.y + rect.height > mat.cols)
{
return;
}
Mat temp = mat(roi_rect_).clone();
//二值化
threshold(temp, binary_mat_, 255 * binary_threshold_, 255, THRESH_BINARY);
}
doBinary(Rect rect, Mat mat)
首先检测输入的合法性,当roi区域超出图像的范围时,我们粗暴地return
。如果合法,那么我们就将该区域二值化,并保存在binary_mat_
这个成员变量中。
6. 输出函数
int ModuleDemo::output(MyResult * result, DrawableResult * draw_result)
{
//设置文本输出
result->setResult("x", roi_rect_.x);
result->setResult("y", roi_rect_.y);
//设置绘制输出
draw_result->elements[0].element_number = 1;
DrawElement::RotateRectDrawable* rect = (DrawElement::RotateRectDrawable*)(draw_result->elements[0].data_ptr);
rect[0].cx = roi_rect_.x + roi_rect_.width / 2;
rect[0].cy = roi_rect_.y + roi_rect_.height / 2;
rect[0].width = roi_rect_.width;
rect[0].height = roi_rect_.height;
rect[0].angle = 0;
return 0;
}
文本输出设置很好理解,在此不做过多解释。
DrawableResult
这个结构体拥有一个elements
指针,用来保存绘制输出中所有绘制元素。在初始化输出结果函数initResultsType(MyResult * result, DrawableResult * draw_result)
中,我们设置了绘制元素种类的个数为1,并指定它为ROTATE_RECT
。因此,这里的elements
指针只含有1个元素,它为ROTATE_RECT
。我们在声明的时候设置该种类可以绘制的最大个数为1,在这里我们用element_number
设置实际绘制的个数。接下来,我们从draw_result
中拿到该种类的数据指针,将其强制转换成它的子类RotateRectDrawable
,并将位置信息写入其中。至此,绘制结果设置完毕。
7. 保存与读取工程
int ModuleDemo::saveParam(const QString & path, const QString & module_id, QJsonObject & json, bool param_only)
{
json["x"] = roi_rect_.x;
json["y"] = roi_rect_.y;
json["width"] = roi_rect_.width;
json["height"] = roi_rect_.height;
json["threshold"] = binary_threshold_;
return 0;
}
int ModuleDemo::readParam(const QJsonObject & json, const QString & path, const QString & module_id, bool param_only)
{
roi_rect_.x = json.value("x").toInt();
roi_rect_.y = json.value("y").toInt();
roi_rect_.width = json.value("width").toInt();
roi_rect_.height = json.value("height").toInt();
binary_threshold_ = json.value("threshold").toDouble();
return 0;
}
我们需要保存的参数是roi_rect_
和binary_threshold_
。本平台统一采用json的格式保存和读取数据,同时提供保存和读取的路径,方便有需求的用户保存文件。module_id
为平台框架为每个模块分发的编号,提供给用户在给保存的文件起名时使用,防止多个相同的模块使用相同的文件名。布尔型参数param_only
在点击主页面的保存和打开工程时传入的值为false
;当用户对界面进行二次开发时调用WorkflowList
类中的getMdlParam()
来读取工程文件的参数和调用setMdlParam()
来修改工程文件时,传入的参数为false
。
8. 读取基准图像
void ModuleDemo::setReferImage(const GtMat * image)
{
if (image == nullptr)
{
refer_mat_ = Mat();
}
else
{
refer_mat_ = Mat(image->rows, image->cols, CV_8UC1, image->image_data, image->cols);
}
}
与读取运行时的输入图像类似,基准图像也是以GtMat
的数据结构传入。在本例中,我们将它以Mat
的格式保存在refer_mat_
成员变量中。setReferImage()
函数会在基准图像改变时和读取工程时被调用。
在界面模块中实现调整算法模块的参数
1. 在QT Designer中创建Widget的UI文件并添加到工程中
WidgetDemo::WidgetDemo(QWidget *parent):QWidget(parent)
{
ui.setupUi(this);
}
ModuleDemo
类的对象指针
2. 获得算法模块bool WidgetDemo::setMdlPtr(AbstractModule * m)
{
mdl_ = dynamic_cast<ModuleDemo *>(m);
if (mdl_ != nullptr)
{
return true;
}
else
{
return false;
}
}
这样我们就能调用算法模块的函数了。本例中,WidgetDemo
已经被声明成了ModuleDemo
的友元类,因此可以直接对其私有成员变量赋值。
3. 获得显示窗口的指针
bool WidgetDemo::initDisplay(GtDisplay * display)
{
display_ = display;
return true;
}
这样我们就可以向显示窗口中添加Qt的GraphicsItem
类了。
4. 初始化参数配置界面
void WidgetDemo::initParamUi()
{
//将滑动条的位置设置为初始化的阈值
ui.binaryThreshold->setValue(int(mdl_->binary_threshold_ * 100));
//在显示窗口中添加可拖动矩形框
display_->addRectItem("roi", QRectF(QPointF(mdl_->roi_rect_.x, mdl_->roi_rect_.y),
QSizeF(mdl_->roi_rect_.width, mdl_->roi_rect_.height)));
//新建一个覆盖在roi上的图层,并将它添加到显示窗口中
roi_overlay_ = new QGraphicsPixmapItem;
display_->addUserDefinedItem(roi_overlay_);
//初始化信号槽
initConnects();
}
该函数会在参数配置界面打开时被调用。GtDisplay
提供了很多向显示窗口添加对象的接口,包括现在我们所使用的addRectItem()
和addUserDefinedItem()
。前者是预先定义好的可拖动item,在添加的同时,需要设置它的名字,位置和大小;后者接受一个QGraphicsItem
指针,将其放入显示窗口的QGraphicsScene
中。
void WidgetDemo::initConnects()
{
connect(ui.binaryThreshold, SIGNAL(valueChanged(int)), this, SLOT(setThreshold()));
connect(display_, &GtDisplay::signalUpdateDefaultItem, this, &WidgetDemo::slotUpdateItemData);
}
GtDisplay
的signalUpdateDefaultItem
信号会在点击可拖动窗口释放后发出,并传递出若干参数,包括窗口的位置,大小以及旋转角度。
5. 槽函数
void WidgetDemo::setThreshold()
{
mdl_->binary_threshold_ = float(ui.binaryThreshold->value()) / 100.0;
updateROI();
}
void WidgetDemo::slotUpdateItemData(bool valid, double reserve0, double reserve1, double reserve2, double reserve3, double reserve4, QString & name)
{
if (!valid)
{
return;
}
else
{
if (name == "roi")
{
mdl_->roi_rect_.x = reserve0;
mdl_->roi_rect_.y = reserve1;
mdl_->roi_rect_.width = reserve2;
mdl_->roi_rect_.height = reserve3;
updateROI();
}
}
}
当信号发出后,槽函数被调用,这样我们就可以根据信号来调整算法模块中的参数了。
6. 更新函数
void WidgetDemo::updateROI()
{
//调用算法模块的二值化函数
mdl_->doBinary(mdl_->roi_rect_, mdl_->refer_mat_);
//获得二值化后的roi区域图像
Mat binary = mdl_->binary_mat_.clone();
//将二值化图像从Mat格式转化成Qt的格式
QPixmap pixmap = QPixmap::fromImage(QImage((uchar*)binary.data,
binary.cols,
binary.rows,
binary.cols,
QImage::Format_Indexed8));
//将二值化图像添加到覆盖图层上
roi_overlay_->setPixmap(pixmap);
//将覆盖图层位置设置为roi的位置
roi_overlay_->setPos(mdl_->roi_rect_.x, mdl_->roi_rect_.y);
//更新覆盖图层
roi_overlay_->update();
}
更新函数调用了算法模块的二值化函数,并将结果覆盖在了roi区域。每个槽函数在最后都会调用更新函数,所以每当参数改变时,我们能立即看到二值化的效果。
7. 当基准图像被改变时
void WidgetDemo::doAfterReferImageChanged()
{
updateROI();
}
这个函数会在设置新的基准图像后被调用,一般我们会在其中写入清理之前显示结果的实现。
注册算法类和界面类
本平台的特点之一就是可以根据类名去动态加载DLL。为了做到这一点,我们要求二次开发的模块必须继承我们提供的基类。另外在算法类头文件中需要#include "module_plugin_register.h"
和在界面类头文件中#include "widget_plugin_register.h"
。
同时,在头文件最后使用我们提供的宏REGISTER_CLASS(ModuleDemo, AbstractModule);
和REGISTER_WIDGET(UIDemo, AbstractModuleWidget);
在开发者版本的GtHawkeye软件包中,与exe可执行文件同路径下有一个名为"Data"的文件夹,我们需要将算法模块和界面模块编译好的DLL文件根据debug或release版本添加到"Data"文件夹中"plugins"的"debug-plugin"或"release-plugin"文件夹中。
最后,在"Data"文件夹中,有一个名为"json"的文件夹,其中"tools.json"文件中保存着所有DLL的类名。按照如下格式填写,平台就能识别新的DLL了。
[
{
"name": "Demo",
"child_mdl":
[
{
"module_dll_name": "ModuleDemo",
"widget_dll_name": "UIDemo",
"nick_name": "Demo",
"chinese_description": "Demo"
}
]
}
]