ARTICLE · 1242765
换个检测算法就要重编译?把算法做成插件

一套统一接口 + 动态加载,让检测算法像 USB 一样即插即用
【产线真相】客户加一个检测项,我改主程序、重编译、停机半天
接上篇。图预处理干净了,接下来该检测了。但先讲一个让我吃过亏的现场。
第一版 VisionStation,我把检测逻辑直接写在主流程里:
// ❌ 第一版:算法写死在主流程里
Result detect(const cv::Mat& img){
if (cfg_.mode == "threshold") {
return thresholdDetect(img); // 阈值检测
} else if (cfg_.mode == "blob") {
return blobDetect(img); // 斑点检测
} else if (cfg_.mode == "template") {
return templateMatch(img);
}
// 客户说:下个月想加"深度学习缺陷检测"……
// 我心里一沉:又得改主程序、重编译、去现场停机替换。
}
问题不是这段代码"写错了",而是它违反了一条工程铁律:新增功能,不应该修改已有的、稳定的代码。
每加一个算法:
主程序要改 → 要重新编译 → 要重新测试全链路 → 要去现场停机替换; 一个算法崩了,整个软件一起崩; 不同客户想用不同算法 → 你得维护 N 个分支版本。
这不是算法问题,是架构问题。
解法:把"算法"和"算法怎么用"切开
一句话:主程序只认识"接口",不认识"具体算法"。
主程序(稳定,不轻易动)
│ 只依赖这个接口
▼
┌───────────────────┐
│ IDetectPlugin │ ← 检测插件契约:输入图,输出结果
└───────────────────┘
▲ 谁实现了它,谁就能被装进来
┌───────┼────────┬─────────┐
▼ ▼ ▼ ▼
阈值插件 斑点插件 模板插件 深度学习插件 ← 都是独立 .dll/.so
想加算法 → 写一个新插件,主程序一行不改; 想换算法 → 换一个 dll 文件,不用重编译; 某个算法崩了 → 可以被隔离处理,主程序还能活。
这套思路,工作区里 Aether 项目的插件系统已经完整印证过(IoC + 插件热插拔),你可以对照着看,这里我们做工业视觉这个场景下的最小版。
【能跑起来的最小实现】一个插件契约 + 一个动态加载器
先定"契约"——所有检测插件都必须实现它:
// algo/include/IDetectPlugin.h
class IDetectPlugin {
public:
virtual ~IDetectPlugin() = default;
virtual QString name()const= 0; // 算法名(配置里用它选算法)
virtual QString version() const= 0;
virtual Result detect(const cv::Mat& img)= 0; // 核心:一图进,结果出
};
// 让 Qt 认识这个接口(用于 QPluginLoader 动态加载)
#define IDetectPlugin_iid "com.visionstation.IDetectPlugin/1.0"
Q_DECLARE_INTERFACE(IDetectPlugin, IDetectPlugin_iid)
一个最普通的"阈值检测"插件:
// plugins/threshold/ThresholdPlugin.h
class ThresholdPlugin : public QObject, public IDetectPlugin {
Q_OBJECT
Q_PLUGIN_METADATA(IID IDetectPlugin_iid)
Q_INTERFACES(IDetectPlugin)
public:
QString name() const override { return "threshold"; }
Result detect(const cv::Mat& img) override{
cv::Mat bin;
cv::threshold(img, bin, thresh_, 255, cv::THRESH_BINARY);
double ratio = (double)cv::countNonZero(bin) / bin.total();
return Result{ ratio > 0.02, ratio, {} }; // 简单示例规则
}
private:
double thresh_{128.0};
};
主程序这一侧,动态加载所有插件,按名字挑一个用——主程序全程没见过"ThresholdPlugin"这个类名:
// 插件管理器:扫描目录,加载所有检测插件
class PluginManager {
public:
void loadAll(const QString& dir){
for (const QString& f : QDir(dir).entryList({"*.dll", "*.so"})) {
auto* loader = new QPluginLoader(QDir(dir).filePath(f), this);
QObject* obj = loader->instance();
if (auto* p = qobject_cast<IDetectPlugin*>(obj)) {
plugins_[p->name()] = p; // 登记: 名字 → 插件
} else {
qWarning() << "不是检测插件:" << f << loader->errorString();
delete loader;
}
}
}
IDetectPlugin* byName(const QString& name) const{
auto it = plugins_.find(name);
return it == plugins_.end() ? nullptr : it.value();
}
private:
QHash<QString, IDetectPlugin*> plugins_;
};
用的时候:
PluginManager pm;
pm.loadAll("./plugins"); // 扫插件目录
auto* algo = mgr.get("threshold"); // 配置里写的名字
if (!algo) { /* 找不到就降级/报错,绝不崩 */ }
Result r = algo->detect(cleanImage);
对比一下第一版那段 if/else,你就能感觉到差别:
// ❌ 算法写死:新增要改主程序、要重编译
if (mode == "threshold") return thresholdDetect(img);
// ✅ 算法插件:新增 = 扔一个 dll 进目录
auto* algo = plugins.get(cfg_.algoName);
return algo->detect(img);
从此,"加一个算法"从"改工程 + 编译 + 现场停机",变成了"拷贝一个文件进插件目录"。
插件不只用在"算法"上
把"接口 + 动态加载"这套思路想透,你会发现它能用在设备的很多地方:
相机插件:不同品牌各自一个实现(第 3 篇的 ICamera也能这么装进来);通信插件:Modbus / Profinet / 串口,按现场选一个; 算法插件:本篇主角; 输出插件:结果存本地 / 上传 MES / 发报警。
一旦"会变化的东西"都被隔离在插件里,主程序就稳了。 这也正是工作区里 Aether 那类插件化项目的核心价值:主程序负责"不变的骨架",插件负责"会变的部分"。
一个常被忽略的点:接口版本
插件一多,迟早会遇到"主程序升级了,老插件还能不能用"。给接口一个版本号,是最省事的保险:
virtual QString apiVersion() const= 0; // 例如 "1.0"
主程序加载时先校验版本,不匹配就跳过并提示,而不是加载进来直接崩。小小一行,省下一次现场事故。
【避坑指南】
坑1:主程序和插件编译器/运行时不匹配。
现象:插件加载失败、一调用就崩。 原因:C++ 的 ABI 不一致(不同编译器版本、不同 STL、Debug/Release 混用)。 正解:主程序和所有插件用同一套工具链构建;接口里只用稳定的类型(QString/基础类型/自定义 POD)。
坑2:接口里塞了大量内部类型。
现象:主程序一升级,所有插件都要重编。 原因:接口耦合了实现细节。 正解:接口尽量"窄"——输入图 + 输出结果,中间细节插件内部自己管。
坑3:一个插件崩溃,拖跨整个软件。
现象:某个第三方算法一崩,整条产线停机。 原因:插件和主程序在同一进程。 正解:关键场景下把算法放到独立进程里跑,用 IPC 通信;崩了只重启进程(第 10、11 篇接着讲)。
坑4:Plugin 目录写死绝对路径。
现象:换个机器就找不到插件。 原因:路径写死。 正解:插件目录相对可执行文件位置或配置文件定位。
坑5:不做加载失败兜底。
现象:插件缺失 → 启动即崩。 原因:没判 loadAll/get的失败。正解:加载失败要记录日志、给出提示,主程序仍能启动。
下一篇:让上位机联上真正的设备
算法插件化之后,VisionStation 已经"会看"了。但一台设备软件光会看不够,它还得和产线对话——告诉 PLC 结果、接收触发信号。
下一篇,我们啃通信:Modbus-TCP 从"连不上"到"断线自动重连",把上位机和 PLC/控制卡之间的握手、心跳、补传讲透。
先问你一句:
你的项目里,加一个新检测算法,需要重新编译主程序吗? 如果需要——那你其实已经把"算法"和"工程"焊死在一起了。
评论区聊聊你们是怎么管多算法的。















































































