在 ImagePlay 中,算子属性(Operator Properties) 是控制图像处理节点(IPProcessStep
或 IPProcess
)行为的关键参数。这些属性允许用户动态调整算子的行为(如滤波器的半径、边缘检测的阈值等)
算子属性的核心作用
IPLProcessProperty
是一个抽象基类,用于定义图像处理算子(IPLProcess
)的可配置属性
其核心功能包括:
-
克隆与重置:支持深拷贝和恢复默认值。
-
参数化控制:每个图像处理算子(如高斯模糊、Canny边缘检测)通过属性暴露可调参数,用户可通过 GUI(如滑块、输入框)修改这些参数,实时改变处理效果。
-
数据流配置:部分属性可能影响输入/输出端口的行为(如启用/禁用某些端口)。
-
序列化支持:属性值通常需要保存到项目文件(或从文件加载),支持流程的持久化。
class IPLSHARED_EXPORT IPLProcessProperty
{
public:
struct SerializedData
{
std::string type;
std::string widget;
std::string widgetName;
std::string value;
};
struct DeserialationFailed : public std::runtime_error
{ DeserialationFailed(): std::runtime_error("") {} };
int position() const { return _position; }
const char* name() const { return _name; }
const char* title() const { return _title; }
const char* description() const { return _description; }
IPLProcessWidgetType widget() const { return _widget; }
virtual const char* type() const = 0;
virtual SerializedData serialize() const = 0;
virtual void deserialize(const SerializedData &data) = 0;
virtual IPLProcessProperty *clone() const = 0;
virtual void resetValue() = 0;
protected:
IPLProcessProperty(int position,
const char *name,
const char *title,
const char *description,
IPLProcess *process,
IPLProcessWidgetType widget = IPL_WIDGET_DEFAULT);
int _position; //!< Position in GUI
const char* _name; //!< ID for GUI
const char* _title; //!< Short title for GUI
const char* _description; //!< Short help for GUI
IPLProcess* _process;
IPLProcessWidgetType _widget;
};
关键成员解析
(1) 核心数据成员
成员 | 类型 | 说明 |
---|---|---|
_position | int | 属性在 GUI 中的布局顺序(如面板中的从上到下索引)。 |
_name | const char* | 属性的唯一标识符(程序内部使用,如 "kernel_size" )。 |
_title | const char* | 属性的显示名称(GUI 可见,如 "Blur Radius" )。 |
_description | const char* | 属性的详细描述(如工具提示文本)。 |
_process | IPLProcess* | 指向所属算子的指针,用于属性变更时通知算子更新。 |
_widget | IPLProcessWidgetType | 指定 GUI 控件类型(如 IPL_WIDGET_SLIDER 、IPL_WIDGET_CHECKBOX )。 |
(2) 纯虚函数(需子类实现)
函数 | 作用 |
---|---|
type() | 返回属性类型名称(如 int 、float )。 |
serialize() | 将属性值转换为 SerializedData ,用于保存到项目文件。 |
deserialize() | 从 SerializedData 加载属性值,可能抛出 DeserialationFailed 异常。 |
clone() | 深拷贝当前属性对象(多态复制,需子类实现)。 |
resetValue() | 重置属性为默认值。 |
(3) 异常类
struct DeserialationFailed : public std::runtime_error { ... };
-
反序列化失败时抛出,例如文件数据格式不匹配。
(4) GUI 动态生成控件
// 伪代码:根据属性类型创建控件
IPLProcessProperty* prop = process->property("kernel_size");
if (prop->type() == "int" && prop->widget() == IPL_WIDGET_SLIDER) {
QSlider* slider = new QSlider();
slider->setRange(prop->as<IPLIntProperty>()->min(),
prop->as<IPLIntProperty>()->max());
slider->setValue(prop->as<IPLIntProperty>()->value());
}
(5) 序列化数据结构
struct SerializedData {
std::string type; // 属性类型(如 "int")
std::string widget; // 控件类型(如 "slider")
std::string widgetName;// 控件名称(可选)
std::string value; // 属性值的字符串表示(如 "5")
};
设计模式与实现技巧
-
工厂模式:子类(如
IPLIntProperty
、IPLFloatProperty
)通过type()
和clone()
支持动态创建和复制。 -
观察者模式:属性值变化时,通过
_process
通知所属算子更新处理逻辑。 -
策略模式:
_widget
允许同一属性类型(如int
)对应不同 GUI 控件(滑块、输入框)。
在 ImagePlay 中,算子属性可能支持以下数据类型:
类型 | 示例 | GUI 控件 |
---|---|---|
整数 (int ) | 模糊核大小、阈值 | 滑块 (QSlider ) |
浮点数 (double ) | Sigma 值、透明度 | 双精度滑块 (QDoubleSpinBox ) |
布尔值 (bool ) | 启用/禁用功能 | 复选框 (QCheckBox ) |
字符串 (string ) | 文件路径、模式选择 | 文本框 (QLineEdit ) |
枚举值 (enum ) | 颜色空间转换类型 | 下拉菜单 (QComboBox ) |
图像 (cv::Mat ) | 掩模图像、模板 | 特殊文件选择器 |
属性UI控件
class IPPropertyWidget : public QWidget
{
Q_OBJECT
public:
explicit IPPropertyWidget(IPLProcessProperty* processProperty, QWidget* parent=0) : QWidget(parent)
{
_processProperty = processProperty;
}
~IPPropertyWidget()
{
_processProperty = NULL;
}
virtual void saveValue() = 0;
virtual void resetValue() = 0;
IPLProcessProperty* processProperty() { return _processProperty; }
signals:
void changed();
private:
IPLProcessProperty* _processProperty;
};
IPPropertyWidget
是一个 抽象基类(继承自 QWidget
),作为 属性控件 的通用接口,核心功能包括:
-
桥梁作用:将
IPLProcessProperty
(属性数据)与 Qt 控件(GUI 显示)连接。 -
值同步:提供
saveValue()
和resetValue()
方法,确保 GUI 控件值与属性数据双向同步。 -
变更通知:通过
changed()
信号通知外部属性值已修改。
设计模式与实现逻辑
-
策略模式
每个IPLProcessProperty
类型(如IPLIntProperty
)对应一个具体的IPPropertyWidget
子类(如IPPropertySliderInt
),实现不同的控件交互方式。 -
观察者模式
changed()
信号允许外部对象(如主窗口)监听属性变更,触发实时处理。
工作流程
-
控件创建
-
当用户打开属性面板时,根据
IPLProcessProperty
的类型和widget()
标志创建对应的IPPropertyWidget
子类实例。
void IPProcessPropertiesWidget::init(IPProcessStep* processStep) { _processStep = processStep; // remove all children while (layout()->count() > 0) { QLayoutItem* item = layout()->takeAt(0); if(item != NULL ) { delete item->widget(); delete item; } } // delete layout(); auto* processSettings = _processStep->process()->properties(); if(processSettings->size() == 0) { addPropertyWidget("This step has no properties.", "", NULL); } // sort the properties by user set position std::vector<IPLProcessProperty*> orderedProperties; orderedProperties.reserve(processSettings->size()); for (auto &entry: *processSettings) orderedProperties.push_back(entry.second.get()); std::sort(orderedProperties.begin(), orderedProperties.end(), IPProcessPropertiesWidget::sortByPosition); // create all property widgets for (auto &property: orderedProperties) { // generate GUI based on the property type if (property->widget() == IPL_WIDGET_HIDDEN) {} //Don't process hidden widgets else if (property->widget() == IPL_WIDGET_LABEL) //Labels are type independent { QFormLayout* layout = (QFormLayout*) this->layout(); QLabel* lblDescription = new QLabel(property->description()); lblDescription->setWordWrap(true); lblDescription->setStyleSheet("color: #666; font-size: 10px"); layout->addRow("", lblDescription); // add widget to list //_propertyWidgets.append(lblDescription); } else if (auto p = dynamic_cast<IPLProcessPropertyInt*>(property)) { IPPropertyWidget *widget = NULL; QString rawName, name; switch(p->widget()) { case IPL_WIDGET_SLIDER: widget = new IPPropertySliderInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_SLIDER_ODD: widget = new IPPropertySliderIntOdd(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_SLIDER_EVEN: widget = new IPPropertySliderIntEven(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_RADIOBUTTONS: widget = new IPPropertyRadioInt(p, this); rawName = property->title(); // name:value1|value2 name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; case IPL_WIDGET_COMBOBOX: widget = new IPPropertyCombobox(p, this); rawName = property->title(); // name:value1|value2 name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; case IPL_WIDGET_GROUP: widget = new IPPropertyGroup(p, this); rawName = property->title(); // name:value1|value2 name = rawName.split(":").at(0); addPropertyWidget(name, "", widget); // connect widget events to grid execution connect((IPPropertyGroup*)widget, &IPPropertyGroup::groupChanged, this, &IPProcessPropertiesWidget::showPropertyGroup); break; case IPL_WIDGET_BUTTON: widget = new IPPropertyButtonInt(p, this); addPropertyWidget(name, property->description(), widget); break; default: //IPL_WIDGET_SPINNER IPPropertySpinnerInt* widget = new IPPropertySpinnerInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyUnsignedInt*>(property)) { IPPropertyWidget *widget = NULL; QString rawName, name; switch(p->widget()) { //TODO: Implement Sliders, Checkboxes etc. case IPL_WIDGET_CHECKBOXES: widget = new IPPropertyCheckboxInt(p, this); rawName = property->title(); // name:value1|value2 name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; case IPL_WIDGET_BUTTON: widget = new IPPropertyButtonUnsignedInt(p, this); addPropertyWidget(name, property->description(), widget); break; default: //IPL_WIDGET_SPINNER IPPropertySpinnerUnsignedInt* widget = new IPPropertySpinnerUnsignedInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyDouble*>(property)) { IPPropertyWidget *widget = NULL; switch(p->widget()) { default: //IPL_WIDGET_SLIDER widget = new IPPropertySliderDouble(p, this); addPropertyWidget(property->title(), property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyString*>(property)) { IPPropertyWidget *widget = NULL; QString rawName, name; QString defaultDirectory = _mainWindow->defaultImagePath(); switch(p->widget()) { case IPL_WIDGET_FILE_OPEN: widget = new IPPropertyFileOpen(p, defaultDirectory, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_FILE_SAVE: widget = new IPPropertyFileSave(p, this); // name:value1|value2 rawName = property->title(); name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; case IPL_WIDGET_FOLDER: widget = new IPPropertyFolder(p, this); addPropertyWidget(property->title(), property->description(), widget); break; default: //IPL_WIDGET_TEXTFIELD widget = new IPPropertyString(p, this); addPropertyWidget(property->title(), property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyColor*>(property)) { IPPropertyWidget *widget = NULL; IPLColorPickProvider *provider = dynamic_cast<IPLColorPickProvider*>(_mainWindow->imageViewer()); switch(p->widget()) { case IPL_WIDGET_COLOR_HSL: widget = new IPPropertyColorHSL(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_COLOR_HSV: widget = new IPPropertyColorHSV(p, this); addPropertyWidget(property->title(), property->description(), widget); break; default: //IPL_WIDGET_COLOR_RGB widget = new IPPropertyColorRGB(p, this, provider); addPropertyWidget(property->title(), property->description(), widget); break; } //if (auto picker = dynamic_cast<IPLColorPickHandler*>(widget)) // connect to image viewer for color picking // _mainWindow->imageViewer()->setColorPickHandler(picker); } else if (auto p = dynamic_cast<IPLProcessPropertyBool*>(property)) { IPPropertyWidget *widget = NULL; switch(p->widget()) { case IPL_WIDGET_BUTTON: widget = new IPPropertyButtonBool(p, this); addPropertyWidget(property->title(), property->description(), widget); break; default: //IPL_WIDGET_CHECKBOXES widget = new IPPropertyCheckbox(p, property->title(), this); addPropertyWidget("", property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyVectorInt*>(property)) { IPPropertyWidget *widget = NULL; switch(p->widget()) { case IPL_WIDGET_BINARY_MORPHOLOGY: widget = new IPPropertyBinaryMorphologyInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_BINARY_MORPHOLOGY_TRISTATE: widget = new IPPropertyBinaryMorphologyTristateInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; case IPL_WIDGET_GRAYSCALE_MORPHOLOGY: widget = new IPPropertyGrayscaleMorphologyInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; default: //IPL_WIDGET_KERNEL widget = new IPPropertyKernelInt(p, this); addPropertyWidget(property->title(), property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyVectorDouble*>(property)) { IPPropertyWidget *widget = NULL; QString rawName, name; switch(p->widget()) { case IPL_WIDGET_MATRIX: widget = new IPPropertyMatrixDouble(p, this); rawName = property->title(); // name:rows|cols name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; default: //IPL_WIDGET_MATRIX widget = new IPPropertyMatrixDouble(p, this); rawName = property->title(); // name:rows|cols name = rawName.split(":").at(0); addPropertyWidget(name, property->description(), widget); break; } } else if (auto p = dynamic_cast<IPLProcessPropertyPoint*>(property)) { IPPropertyWidget *widget = NULL; IPLCoordinatePickProvider *provider = dynamic_cast<IPLCoordinatePickProvider*>(_mainWindow->imageViewer()); switch(p->widget()) { default: //IPL_WIDGET_POINT widget = new IPPropertyPoint(p, this, provider); addPropertyWidget(property->title(), property->description(), widget); break; } //if (auto picker = dynamic_cast<IPLCoordinatePickHandler*>(widget)) // connect to image viewer for coordinate picking // _mainWindow->imageViewer()->setCoordinatePickHandler(picker); } } }
-
-
用户交互
-
用户操作控件(如拖动滑块)→ 触发
changed()
信号 → 主窗口调用saveValue()
并更新处理结果。
-
-
数据持久化
-
保存项目时,通过
_processProperty->serialize()
存储属性值。
-
属性控件呈现
///双击算子节点
/*!
* \brief IPProcessStep::mouseDoubleClickEvent
* \param event
*/
void IPProcessStep::mouseDoubleClickEvent(QGraphicsSceneMouseEvent*)
{
_mainWindow->setActiveProcessStep(this);
}
void MainWindow::setActiveProcessStep(IPProcessStep* step)
{
if(!step)
{
_activeProcessStep = NULL;
_lastActiveProcessStep = NULL;
return;
}
if(_allowChangeActiveProcessStep)
{
_activeProcessStep = step;
if(_synchronizeViews)
{
_allowChangeActiveProcessStep = false;
_imageViewer->setActiveStep(_activeProcessStep->stepID());
_allowChangeActiveProcessStep = true;
}
if(_lastActiveProcessStep)
{
hideProcessSettings();
_lastActiveProcessStep->setEditing(false);
}
// activate and show settings
showProcessSettings(_activeProcessStep);
_activeProcessStep->setEditing(true);
// save last active step
_lastActiveProcessStep = _activeProcessStep;
}
}
///主窗口MainWindow中显示
void MainWindow::showProcessSettings(IPProcessStep* processStep)
{
ui->dockSettings->setVisible(true);
ui->dockProcesses->setVisible(false);
// add description
if(processStep->process()->description().length() > 0)
{
ui->lblProcessDescription->setText(QString::fromStdString(processStep->process()->description()));
ui->lblProcessDescription->setVisible(true);
}
else
{
ui->lblProcessDescription->setVisible(false);
}
// change title
QString title = QString::fromStdString(processStep->process()->title());
ui->lblProcessSettings->setText(title);
// hide help button because it is currently not used
//ui->btnHelpPage->hide();
// add inputs/outputs
if(processStep->process()->inputs()->size() > 0 || processStep->process()->outputs()->size() > 0)
{
QString msgInputsOutputs;
if(processStep->process()->inputs()->size() > 0)
{
msgInputsOutputs.append("<b>Inputs:</b>");
for(int i=0; i < (int)processStep->process()->inputs()->size(); i++)
{
IPLProcessIO input = processStep->process()->inputs()->at(i);
QString msgString("<br />%1: %2 (<i>%3</i>)");
msgInputsOutputs.append(msgString
.arg(input.index)
.arg(QString::fromStdString(input.name))
.arg(QString::fromStdString(dataTypeName(input.type))));
}
}
if(processStep->process()->outputs()->size() > 0)
{
if(processStep->process()->inputs()->size() > 0)
msgInputsOutputs.append("<br />");
msgInputsOutputs.append("<b>Outputs:</b>");
for(int i=0; i < (int)processStep->process()->outputs()->size(); i++)
{
IPLProcessIO output = processStep->process()->outputs()->at(i);
QString msgString("<br />%1: %2 (<i>%3</i>)");
msgInputsOutputs.append(msgString
.arg(output.index)
.arg(QString::fromStdString(output.name))
.arg(QString::fromStdString(dataTypeName(output.type))));
}
}
ui->lblProcessInputsOutputs->setText(msgInputsOutputs);
ui->lblProcessInputsOutputs->setVisible(true);
}
else
{
ui->lblProcessInputsOutputs->setVisible(false);
}
ui->processPropertiesWidget->init(processStep);
ui->processMessageWidget->init(processStep);
}