ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用Qt打造仿VisionMaster通用视觉平台:架构与实现

用Qt打造仿VisionMaster通用视觉平台:架构与实现 简介一套基于QT开发的仿海康VisionMaster通用视觉平台源码面向机器视觉算法工程师、QT界面开发者及视觉平台二次开发人员。压缩包共1643个文件以h头文件、cc/cpp源文件为主辅以ui界面文件、qrc资源、svg/png图标、dll/lib动态库及cmake构建脚本整体约34.49MB目录结构清晰。已有1799人学习下载。源码已配置好Debug/Release构建切换、统一输出目录生成并给出CommonUsing公共库的调用方式可参考XvCore工程快速搭建视觉平台框架目录涵盖XvCore、XvData、XvDisplay、XvUtils、XvTokenMsg等核心模块适合学习QT大型项目架构、视觉算子封装、显示交互与跨平台工程管理也可作为企业级视觉软件二次开发的起点。 自从接触机器视觉项目以来我一直对海康VisionMaster这类通用视觉平台的交互方式和调度逻辑很感兴趣。这类商业软件封装得很完整但反过来也意味着你想改一个细节、想定制一个流程往往无从下手。所以我花了不少业余时间用Qt做了一款仿VisionMaster的通用视觉平台。它不是像素级复刻而是把视觉项目中真正高频用到的模块组织方式、流程编排思路和结果展示逻辑抽了出来做成了一个可以跑起来的源码项目。这篇文章就把我的设计思路、模块划分、核心实现和踩过的坑完整写出来给想做类似工具的朋友一个参考。1. 为什么做这个仿VisionMaster的平台先聊几句背景。VisionMaster的交互核心其实不是某个算法有多强而是它那种流程组流程模块的树状结构配合右侧参数面板和下方图像结果显示区的总体布局。用过的人应该都有印象左侧是流程树中间是图像显示区右边是模块参数底部还有输出结果的表格或者状态栏。这种布局对现场调试人员非常友好因为你能直观地看到每一步图像处理到底发生了什么参数改了之后结果怎样。我的目标很简单做一个开源的、可扩展的、界面逻辑接近VisionMaster的通用视觉平台让研究算法的人不用纠结UI怎么搭让做项目的人能快速把某个算法模块拖进流程里跑通验证让二次开发变成加一个模块类的事而不是从零搭建一套框架。技术上选Qt而不是C#或者WPF原因有几个。第一Qt的跨平台特性让我们在Windows上调试完可以直接扔到Linux工控机上跑不需要改代码。第二Qt的Graphics View框架非常适合做流程节点编辑器模块之间连线、拖拽、状态高亮这些交互做起来很顺手。第三Qt的信号槽机制天然适合视觉流程这种前一步输出喂给后一步输入的链式调用场景。这个平台我给它定义的能力边界是支持把图像处理算法封装成独立模块模块之间通过明确定义的输入输出端口连接支持多流程并行每个流程内部可以串联多个算法支持图像实时显示、缩放、像素值查看等基础调试功能支持模块参数通过右侧属性面板实时修改支持当前流程运行结果的日志输出和状态提示至于具体的算法库我接了OpenCV也留了接口给海康SDK、Halcon这类商业算法库做适配。这套框架本身不绑定某个具体厂商的SDK这是它跟VisionMaster很不一样的地方——后者是绑定硬件生态的而我这个纯粹是软件层面的通用编排平台。2. 整体架构和工程目录从界面框架到算法插件的分层这个项目虽然叫源码但并不是一个玩具demo。我按照实际项目的标准做了分层目录结构大致如下VisionMasterClone/ ├── CMakeLists.txt ├── main.cpp ├── core/ │ ├── model/ │ │ ├── FlowModel.h/cpp # 流程组、流程、模块节点的数据模型 │ │ ├── Port.h/cpp # 输入输出端口定义 │ │ └── Connection.h/cpp # 连边定义 │ ├── algorithm/ │ │ ├── AlgorithmBase.h/cpp # 算法模块基类 │ │ ├── AlgorithmFactory.h/cpp # 算法模块工厂 │ │ ├── ImageLoader.h/cpp # 图像读取模块 │ │ ├── GrayConvert.h/cpp # 灰度转换模块 │ │ ├── Threshold.h/cpp # 阈值分割模块 │ │ ├── BlobAnalysis.h/cpp # 斑点分析模块 │ │ └── TemplateMatch.h/cpp # 模板匹配模块 │ └── executor/ │ ├── FlowExecutor.h/cpp # 流程执行引擎 │ └── ExecutionContext.h/cpp # 执行上下文用来在不同模块间传递数据 ├── ui/ │ ├── MainWindow.h/cpp # 主窗口负责组装整个界面 │ ├── FlowTreeView.h/cpp # 左侧流程树 │ ├── ImageWidget.h/cpp # 中间图像显示控件 │ ├── PropertyPanel.h/cpp # 右侧属性面板 │ ├── ModuleLibraryPanel.h/cpp # 模块库面板 │ └── NodeGraphicsItem.h/cpp # 流程节点在场景中的图形表现 └── utils/ ├── Logger.h/cpp # 日志工具 └── ImageUtils.h/cpp # 图像格式转换工具整个架构的核心思路是数据模型、算法逻辑、界面表现三者解耦。FlowModel只管流程的拓扑结构和节点属性不关心算法怎么执行算法模块只接收输入数据、产出输出数据不知道自己被画在界面的哪个位置界面层通过观察者模式监听模型变化刷新显示。这样做的好处非常明显。假设你要把OpenCV的算法换成Halcon的只需要重写算法模块内部实现流程结构、界面面板、执行引擎全部不用动。再比如你要新增一个轮廓提取模块只需要继承AlgorithmBase实现initialize、run、getParameters这几个纯虚函数然后注册到AlgorithmFactory这个模块就能被拖到流程里右侧参数面板会自动根据参数定义生成对应的编辑控件完全不需要手动写面板代码。3. 核心数据结构和流程模型这个平台的主干流程模型是整个平台真正的心脏。VisionMaster里最经典的概念是流程组FlowGroup里面可以有多个流程Flow每个流程里面又挂一串模块Module模块之间可以有连线表示数据依赖。我沿用了这套概念但做了一定的简化流程组只作为容器存在真正运行和调度的是流程而流程内部则是一个有向无环图DAG。数据模型的核心类设计大概是这样的class FlowGroup { public: QString name; QListFlow* flows; int currentFlowIndex -1; }; class Flow { public: QString name; QListModuleNode* modules; QListConnection* connections; }; class ModuleNode { public: int id; QString moduleName; QString displayName; QPointF position; // 节点在场景中的位置 AlgorithmBase* algorithm; // 算法实例 QListPort* inputPorts; QListPort* outputPorts; QMapQString, QVariant params; // 模块参数 }; class Connection { public: int fromModuleId; int fromPortIndex; int toModuleId; int toPortIndex; }; class Port { public: QString name; QString dataType; // Image, Region, Double 等 PortDirection direction; };所有的数据都通过ExecutionContext传递class ExecutionContext { public: QMapQString, QVariant dataStore; };之所以用QMap加字符串键来传递中间结果而不是用强类型指针是为了后续能方便地支持把任意模块的输出接到任意模块的输入你只需要保证端口声明的数据类型匹配就行。比如灰度转换模块的输出类型声明为Image输入类型也是Image那么一个阈值分割模块就能直接把Image输出接到灰度转换的Image输入上。这种基于字符串类型的松耦合设计让模块之间不需要互相include头文件是这套平台能支持算法插件化的基础。流程的执行顺序采用拓扑排序。我每次运行流程前会先对DAG做一次拓扑排序如果检测到环就直接报错并弹出提示然后按拓扑序依次执行模块。每次执行之前也会把ExecutionContext清空保证不存在上一帧的残留数据污染下一帧的运算结果。4. 算法模块的封装方式让一个算法变成可复用积木既然叫通用视觉平台算法模块的封装方式决定了平台的上限。我设计AlgorithmBase的时候参考了VisionMaster算法模块输入参数输出状态的模型class AlgorithmBase : public QObject { Q_OBJECT public: explicit AlgorithmBase(QObject* parent nullptr); virtual bool initialize() { return true; } virtual bool run(ExecutionContext context) 0; virtual QListPort getInputPorts() const; virtual QListPort getOutputPorts() const; virtual QMapQString, QVariant getDefaultParameters() const; void setParameter(const QString key, const QVariant value); QVariant getParameter(const QString key) const; virtual QString getDisplayName() const { return AlgorithmBase; } virtual QString getDescription() const { return ; } void addOutputImage(const QString key, const cv::Mat img); void addOutputScalar(const QString key, double value); signals: void runFinished(bool success, QString message); protected: QMapQString, QVariant m_parameters; QMapQString, QVariant m_outputs; };每个算法模块在run函数里拿到ExecutionContext然后从里面取输入数据、执行自己的算法逻辑、再把结果写回ExecutionContext。因为模块之间是用字符串键关联的所以模块内部需要按约定好的标识符去取数。我内置了几个常见的算法模块作为示例第一个是图像读取模块ImageLoader。它负责从文件读取图像输出一张Image类型的数据。它有一个参数imagePath属性面板里会显示一个文件选择按钮。这个模块是整个流程的起点几乎每个测试流程都会先拖一个。第二个是灰度转换模块GrayConvert。输入是彩色图输出是灰度图。虽然OpenCV里就一行cvtColor但是作为一个流程节点它很有教学意义——你能清楚看到图像从彩色变成灰度之后后面的阈值模块参数会发生什么样的变化。第三个是阈值分割模块Threshold。参数是阈值低值、高值还有二值化模式。输入是灰度图输出是二值图。这个模块在视觉定位、缺陷检测里用得非常多也是理解参数面板实时调参这个交互逻辑的好例子。第四个是斑点分析模块BlobAnalysis。输入是二值图通过连通域分析输出每个斑点Blob的面积、中心坐标、外接矩形信息。这个模块的输出是结构化的数值我会在界面上用一张表格来展示让使用者直观体会到图像处理的结果不只是图像还有数据这件事。第五个是模板匹配模块TemplateMatch。用的是OpenCV的matchTemplate方法参数包括模板图路径、匹配方法、阈值。输出包括最佳匹配位置、得分并且会在图像上用绿框把匹配区域画出来。这个模块比较接近VisionMaster里最常见的定位应用演示效果好。模块写完之后要注册到AlgorithmFactory里流程树里的模块库面板拖拽创建节点时就是从这个工厂取算法的static QMapQString, AlgorithmCreator getRegistry() { static QMapQString, AlgorithmCreator registry; return registry; } #define REGISTER_ALGORITHM(ClassName) \ static bool _registered_##ClassName \ AlgorithmFactory::registerAlgorithm(#ClassName, \ []() - AlgorithmBase* { return new ClassName(); });REGISTER_ALGORITHM这个宏放在每个模块的cpp文件末尾类名就是它在模块库里的显示名称。加新模块的时候创建类、实现run、写参数定义、宏注册四步就结束了。界面层的修改量是零。5. 界面交互怎么实现从流程树拖拽到图像显示这一节是很多人关心的毕竟仿VisionMaster这类项目界面的还原度和交互流畅度直接决定了成品有没有那味儿。主窗口的整体布局我用了QSplitter嵌套这样用户能自由拖拽调整每个区域的大小而不是固定比例。从上到下大致是顶部工具栏运行流程、运行所有流程、停止、保存工程、打开工程、重置视图左侧区域左边是模块库右边是当前工程的流程树视图用Tab页签切换中间区域图像显示区支持多标签页显示不同的输出图带缩放和拖拽功能右侧区域属性面板显示当前选中模块的参数、模块帮助说明、状态信息底部区域日志输出面板显示运行日志和异常堆栈模块库面板其实是QListWidget每个条目对应一个算法模块名称设置了拖拽标志。拖拽的MIME类型我自定义为application/x-vm-module数据内容是模块名称字符串。当用户松开鼠标放到流程画布上的时候在dropEvent里创建ModuleNode并实例化对应算法。流程画布本身用的是QGraphicsView和QGraphicsScene。每个模块节点对应一个NodeGraphicsItem绘制成圆角矩形的卡片上面是模块名称下方左侧是输入端口小圆形右侧是输出端口。端口被拖拽时会从端口位置拉出一条线到目标端口这个暂态的连线用一个QGraphicsPathItem表示。松开时判断目标位置是否落在另一个端口的吸附范围里如果是就创建Connection否则丢弃连线。图像显示控件的实现需要注意性能问题。OpenCV的cv::Mat是BGR格式的而Qt的QImage通常是RGB或者ARGB。所以每一帧显示之前要做一次像素格式转换QImage cvMatToQImage(const cv::Mat mat) { switch (mat.type()) { case CV_8UC3: { cv::Mat rgb; cvtColor(mat, rgb, cv::COLOR_BGR2RGB); return QImage(rgb.data, rgb.cols, rgb.rows, rgb.step, QImage::Format_RGB888).copy(); } case CV_8UC1: { return QImage(mat.data, mat.cols, mat.rows, mat.step, QImage::Format_Grayscale8).copy(); } default: return QImage(); } }注意这里用了copy()因为QImage默认不会复制像素数据而cv::Mat在流程结束后可能被释放如果不copy就会出现花屏、闪退这类很难排查的野指针问题。我第一次写的时候没注意调试了一个多小时。属性面板这里有个比较巧妙的设计。我不想为每个模块单独写一个参数编辑控件所以定义了一套参数描述结构包括参数名、类型、取值范围、默认值、是否启用、分组。界面根据这个描述自动生成对应的QWidgetstruct ParamSpec { QString name; // 参数名 QString displayName; // 显示名 ParamType type; // Int, Double, Bool, String, Enum, FilePath QVariant defaultValue; double min 0.0, max 0.0; QStringList enumOptions; QString description; };属性面板拿到当前选中模块的m_parameters和ParamSpec列表后遍历生成表单行。数字类型用QDoubleSpinBox或QSlider布尔类型用QCheckBox枚举类型用QComboBox文件类型用QLineEdit加浏览按钮。用户改动任何一个控件的值立即setParameter并触发模块状态刷新。日志面板主要是用Qt的qInstallMessageHandler重定向了qDebug、qWarning、qCritical输出同时自定义了一个signals来让算法模块可以输出业务日志比如找到3个斑点平均面积800.5px。这一块对后续排查问题帮助很大比打断点看变量要直观得多。6. 流程执行引擎和结果展示把模块串起来跑执行引擎是整个平台最底层也是最重要的调度中心。它的实现并不复杂但细节决定了稳定度。我设计了一个FlowExecutor类核心方法是runFlow(Flow* flow)。流程如下第一步清空ExecutionContext初始化所有节点状态为Waiting第二步统计每个节点的入度用Kahn算法做拓扑排序第三步按排序结果逐个调用node-algorithm-run(context)第四步每个模块运行完成立刻观察它的输出端口有没有连线如果有连线目标模块且目标模块的所有直接上游都跑完了就标记目标模块为Ready并入队第五步全部跑完统计每个模块的运行耗时、成功失败状态这里的入度判断是支持多输入模块的关键。比如一个图像差分模块有两个输入端口只有当两条输入链路的模块都跑完差分模块才执行。我通过一个QSet 来记录每个模块还有哪些上游没完成每完成一个上游就从QSet里移除等QSet空了才运行。由于算法模块运行可能涉及OpenCV的耗时操作我另开了QThread去执行整个流程避免UI卡死。QThread通过signals把每步进度、输出图像、输出数据发回UI线程。每一帧流程图跑完后我会把当前流程里所有模块的输出图像和结构化数据收集起来填到界面底部的表格里字段包括模块名称、输出类型、耗时、是否成功、输出说明。如果你点某一行的输出说明中间图像区会跳到对应的输出图标签页。结果缓存这块还做了个细节每个模块的m_outputs里会记录上次运行结果所以在流程没修改的情况下再次运行会复用上一帧的图像数据而不是重新计算。这个功能在调试的时候特别实用——你在调后面的模块参数时不需要每次把前面的所有模块全部重跑一遍。为了演示整条链路的效果我内置了一个常见定位流程的示例ImageLoader - GrayConvert - Threshold - BlobAnalysis - TemplateMatch跑起来之后左侧流程树能看到5个节点中间显示区能看到彩色图、灰度图、二值图、标记了匹配框的彩色图底部的表格列出每个模块的耗时和结果。第一次跑通的时候那种从一个文件路径到一串坐标数据的完整感确实有点VisionMaster的味道了。7. 序列化与工程保存工程文件结构怎么设计做视觉平台工程文件的保存和打开是早晚要面对的问题。我在设计工程文件时参考了VisionMaster的工程概念也参考了Qt自身用JSON描述界面的思路最终选了JSON格式。为什么用JSON不用XML因为JSON在Qt里用QJsonDocument解析特别顺手可读性好也方便在git里做diff评审。工程文件里需要保存的信息包括工程名称、版本号每个流程组的名称、包含的流程列表每个流程的名称、拓扑信息、节点的坐标每个模块的实例名、算法类型、参数值连线两端对应的模块和端口序列化实现的核心是这个结构QJsonObject serializeModule(ModuleNode* node) { QJsonObject obj; obj[id] node-id; obj[moduleName] node-moduleName; obj[displayName] node-displayName; obj[positionX] node-position.x(); obj[positionY] node-position.y(); QJsonObject params; for (auto it node-params.begin(); it ! node-params.end(); it) { params[it.key()] QJsonValue::fromVariant(it.value()); } obj[params] params; return obj; }反序列化的时候也简单读取JSON后在工厂里查算法类型创建实例填参数放到指定坐标。加载完工程自动触发一次流程树的刷新和画布场景的重建。这里有一个比较容易被忽略的点模块实例的id在工程文件里是全局唯一的不能只在一个流程内唯一。不然当你打开一个带多个流程组的工程时跨流程的连线和模块查找会出现id冲突。我用了一个全局自增的int计数器每次创建模块就分配一个新id保存的时候原样写进去。打开工程之后我还会自动做一次参数合法性检查。比如某个模块的模板图路径指向的文件不存在那么在执行前就会给出警告而不是让用户跑到一半才看到红色报错。这种提前暴露配置问题的体验对工程化使用非常重要。8. 踩坑记录Qt开发视觉平台的几个经典问题这部分是全文最有血泪价值的地方。我踩过的坑不少是Qt和OpenCV混用时的经典问题提前写出来能帮大家省下好几个晚上。第一个坑就是之前提到的cv::Mat转QImage的copy问题。不只是显示图像如果你把cv::Mat放进QPixmap或者保存到缓冲区里都要留意QImage的像素数据生命周期。解决方案很简单要么确保源cv::Mat的生命周期比QImage长要么直接copy。稳妥起见我在工具函数里统一copy换来的是每帧显示多一次内存拷贝。对一般工业相机分辨率500万像素以内来说这个开销可以忽略。第二个坑是QGraphicsScene的层级问题。节点和连线如果直接塞在同一个scene里连线可能被节点盖住或者拖拽视图时节点偏离连线。解决办法是给连线和节点设置不同的ZValue连线用一条低于节点ZValue的ZValue并且连线的起点终点坐标取端口中心的scene坐标而不是节点矩形边缘的坐标。这样视觉上更干净。第三个坑是流程运行时的线程安全。早期版本我直接在UI线程里跑OpenCV算法结果大图处理时界面完全卡死鼠标都拖不动。后来改成QThread之后又遇到新问题算法模块在子线程中运行而它内部如果需要访问界面上的QWidget比如参数面板里的图像预览就会导致Timers can only be used with threads started with QThread这类崩溃。最终的规范是算法模块禁止访问任何QWidget界面对象所有UI更新都通过信号发回主线程。第四个坑是坐标轮换。在VisionMaster里图像坐标的参考系是像素而在Qt的QGraphicsView里你可能为了方便做了缩放和偏移。如果在界面坐标和像素坐标之间乱切换模板匹配和斑点分析出来的坐标就会出现系统性偏差。我的做法是只把scene坐标当作显示坐标真正的算法输入输出永远是原始像素坐标界面只在显示的时候做缩放变换绝不让缩放值污染算法内部的数据。第五个坑是高DPI缩放。在Windows上如果你的Qt程序开了高DPI缩放而OpenCV的窗口或图像显示区域没有做适配会出现鼠标点击位置和图像实际区域错位的情况。这个问题在工业现场的1080P和2K分辨率的显示器上特别常见。我在main函数里显式设置了Qt::AA_EnableHighDpiScaling然后在ImageWidget里通过devicePixelRatio换算鼠标坐标保证缩放后点击的区域和图像像素一一对应。第六个坑是算法模块的参数变更后需要重新初始化。有些算法比如模板匹配的模板图是在initialize阶段加载的如果你在参数面板里改了模板图路径而流程没重新执行initialize那么模块实际用的还是旧模板。我在setParameter之后做了一个dirty标记下次run之前会检查dirty标记并重新initialize。这个细节在用户交互上是千人千面级别的体验差异很多商业软件都处理得不好值得自己实现时注意。9. 可扩展方向和相关资源推荐这套平台目前的定位是视觉项目快速验证框架已经覆盖了流程编排、算法模块化、结果显示、工程保存这些核心场景。如果你想往深了做有几个方向我个人认为性价比很高。第一个方向是接入相机采集。目前的图像来源是文件读取但工业现场基本都是相机实时采集。建议用海康的MVS SDK或者大华的SDK把采集封装成一个相机采集模块输出图像到流程里同时用信号槽把采集帧率显示到界面上。这个模块的注册过程和ImageLoader完全一样唯一的难点是SDK的回调线程和Qt事件循环之间的同步。我建议在回调里只做图像的拷贝和发出信号不要在相机线程里调用任何UI操作。第二个方向是接入星环形通信或者TCP Modbus把视觉定位的结果发给PLC或者机器人控制器。视觉平台如果只能处理图像那还只是半成品真正落地必须接到自动化设备上。我在工程里预留了一个结果输出模块的扩展点输出端口的类型已经支持DoubleArray和String你需要做的就是编写自己的通信模块从ExecutionContext里把结果键取出来编码成协议发送出去。第三个方向是做多相机标定和坐标系转换。VisionMaster卖得贵很大一部分原因是它把相机标定、手眼标定、畸变校正、坐标系映射这些视觉工程里最脏最累的活都打包好了。如果你想用这套框架做有竞争力的产品可以把OpenCV的calibrateCamera和solvePnP包成算法模块这样流程链里就能完成图像坐标-世界坐标的转换。第四个方向是模板管理功能。VisionMaster有独立的模板库可以保存不同产品的模板标记图运行时按产品型号自动切换模板。我在TemplateMatch模块里其实已经预留了模板参数的文件路径字段但还没做成可视化模板管理界面。如果你做项目多这个功能非常值钱能明显拉高平台的易用性。第五个方向是算法模块的Python脚本化。Qt本身可以通过QJSEngine或者内嵌Python解释器PyBind11把一个Python脚本模块做成通用节点让用户在不改C代码的情况下写几行Python实现自定义算法直接拖进流程里运行。这个功能一旦做了平台的可玩性和适应性会上升一个档次也很适合演示给非开发人员看。关于源码我强烈建议拿到后先去读一遍core/executor和core/algorithm这两个目录它们是整个平台的骨架。读的时候关注三个问题节点状态是怎么流转的数据是怎么在模块间传递的新增一个模块需要动哪些文件理清这三个问题再去看ui层的代码就轻松多了。10. 几个实际使用中的心得最后分享几个从实际调试中得到的经验算是给打算入坑类似项目的人提前打个预防针。把视觉算法封装成流程节点这件事看起来简单实际上最大的坑在模块参数语义的治理。一个阈值分割模块参数建议叫thresholdValue还是threshold_low命名混乱的后果是当你做工程文件迁移时所有参数都会对不上。我最终的规范是所有参数名统一用驼峰命名布尔参数用is前缀单位统一的参数在类型里就标注好。项目早期只有两三个模块的时候无所谓但你一旦扩张到十来个模块就会发现命名规范比想象中重要得多。画布节点之间的连线最好默认颜色表示数据类型一致。我在Connetion的绘制函数里做了一次类型检查如果两端端口声明的dataType不一致连线会用红色显示并且执行引擎会拒绝运行。这个功能看起来不起眼但它能在流程搭建的早期阶段就拦截掉大量错误而不是等到运行时报一堆令人费解的OpenCV异常。还有日志的重要性不要低估。我在平台里给每个模块都加了耗时统计运行完会把各模块的耗时按占比排序打印出来。现场调优的时候哪里是瓶颈一目了然。比如一个流程总耗时150毫秒如果模板匹配占了120毫秒那你就知道该在模板匹配上动刀而不是浪费时间去优化一个只占5毫秒的图像读取。界面上我还做了个快捷键习惯F5运行当前流程CtrlF5运行所有流程空格暂停图像刷新。这个不算什么复杂功能但习惯之后调试效率有明显提升。如果你准备用这个代码做二次开发我建议第一件事不是去看某个算法实现而是先在ModuleLibraryPanel里面添加一个自定义模块试着把它的输入输出接到现有模块上跑通然后再去改算法实现。当你亲手把一个模块拖到画布上连线、运行、看到输出数据的那一刻这套框架的边界和扩展方式你基本就摸清了。做这种仿商业软件的开源项目最大的成就感不是界面多像VisionMaster而是你真正理解了一个通用视觉平台背后数据如何流动、模块如何编排、算法如何解耦这件事。等你自己在这套框架上不费力气地加了一个新算法模块时回头看那些被封装在黑盒里的商业软件至少对它们的运作方式心里会有底得多。本文还有配套的精品资源点击获取
返回列表