夜雨聆风学习资料网

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/控制卡之间的握手、心跳、补传讲透。

先问你一句:

你的项目里,加一个新检测算法,需要重新编译主程序吗? 如果需要——那你其实已经把"算法"和"工程"焊死在一起了。

评论区聊聊你们是怎么管多算法的。

相关学习资料