徐仁康首页项目文章标签关于

换相机品牌只改一行注册:工业相机客户端的分层怎么拆

2026-09-28 · C++ · Qt · 架构设计

工业相机客户端有个绕不开的麻烦:每个品牌的 SDK 长得都不一样,但界面要做的事几乎相同——枚举设备、连接、读写参数、拉流显示。

放任不管的话,代码很快会变成「海康的判断写在参数面板里、虚拟相机的判断写在图像显示里」,接第三个品牌时就得满仓库改。这篇文章说的是怎么把这件事收住:把品牌差异关进一个抽屉,抽屉外面的人不知道里面装的是什么。

分层

资源层   主题样式 / 图标 / 参数 Schema(编译进可执行文件,交互层只按名字引用)

交互层   设备控制栏 │ 参数面板 │ 图像显示
              │ 仅凭序列号寻址,不出现任何厂商类型
门面层   CameraContext
              │ 多机映射 · 生命周期托管 · 当前会话状态
契约层   CameraInterface(17 个纯虚方法)
         ZCCameraParam 统一参数模型 │ CameraImageQueue 缓冲队列 │ 统一错误码
              ▲ 实现契约(依赖倒置)
实现层   HikCamera(海康 MVS)│ VirtualCamera(仿真)
         CameraFactory:按厂商名登记「创建器 + 枚举器」

依赖只允许自上而下。唯一的反向关系是实现层对契约层的实现,属于依赖倒置——所以契约层不需要认识任何一个具体相机。

层 职责 不做什么
交互层 设备列表、参数面板、图像显示 不判断厂商、不直接调 SDK
门面层 序列号寻址、生命周期、会话状态 不认识具体适配器
契约层 定义硬件无关的能力边界与数据模型 不依赖 Qt Widgets
实现层 各品牌 SDK 适配、图像格式归一化 不接触界面

三条能被 grep 当场验证的约束

架构文档里写的规则,如果没法验证,三个月后一定会烂掉。所以这里的三条核心约束都写成了可以当场跑的命令(路径是仓库里的):

# 1. 门面与契约层不得依赖任何具体适配器
grep -rn "HikCamera.h\|VirtualCamera.h" src/CameraInterface/
# 期望:无输出

# 2. 面板之间不得互相持有引用,联动一律走事件总线和门面
grep -rn "ControlWidget\*" src/ParamWidget/ src/ViewWidget/
# 期望:无输出

# 3. 交互层不得出现厂商名判断
grep -rniE "hikrobot|\"Hik\"|MV_CC_" src/ControlWidget/ src/ParamWidget/ src/ViewWidget/
# 期望:无输出

第 1 条带来两个直接收益:新增品牌不必改门面;门面也得以在没有 SDK 的机器上编译——这点对 CI 很关键,构建服务器上不会装海康的驱动。

图像链路:四段解耦

采集回调、缓冲队列、采集线程、界面渲染分处四段,任何一段变慢都不会阻塞其余部分。

 SDK 回调线程          缓冲队列           采集线程           UI 线程
   MV_CC 回调
     │ 像素格式归一化(Mono / Bayer / YUV / BGR → Mono8 / RGB8)
     ▼
   HikConvert2Mat
     │ Put()
     ├─ 有空闲缓冲 → 原地 copyTo 复用内存
     └─ 无空闲且队列满 → 丢弃最旧帧,回收其缓冲
     ▼
   工作队列 ──→ Take() ─→ cvMat2QImage(深拷贝)
                            ├─ 信号投递 → 图像显示
                            └─ Recycle() 归还缓冲 ──┐
   空闲缓冲池 ◄──────────────────────────────────────┘

四个点值得展开:

  • 保新弃旧:预览只关心最新帧,队列满时丢最旧的而不是最新的。这条如果写反,界面会持续播放几秒前的画面,还查不出原因。
  • 缓冲所有权:归还动作发生在消费者完成深拷贝之后,保证同一时刻一块缓冲只有一个所有者。
  • 稳态零分配:分辨率和像素格式不变时 copyTo 原地写入,堆分配只发生在首帧。跑长时间预览时这条决定了内存曲线是平的还是锯齿。
  • 超时保护:Take 带超时,超时返回明确的错误码,不会把空帧当成有效图像送进渲染。

参数体系:三段各自独立演进

参数在不同型号之间差异极大,所以拆成三段:

参数 Schema (JSON)  →  解析器  →  统一模型  →  界面
group / params         逐字段校验   元信息+值    按类型挂载控件
name / type / tips     错误定位到参数 +权限位域
  • Schema 描述有哪些参数、属于哪个分组、什么类型、提示文案
  • 统一模型 CameraParam 把六种参数类型压成一个外观类,上层只认 displayText() 和三个权限标志
  • 权限闭环:设备的 GenICam 访问模式(RO / RW / WO / NI / NA)在适配器内部被翻译成「有效 / 可读 / 可写」三态,界面据此决定单元格能不能编辑。参数面板里没有任何硬编码的权限判断,换个品牌也不用动
  • 界面依据类型在运行时挂载对应的编辑控件,界面层对参数有多少个完全无感知

扩展成本

新增一个品牌:继承 CameraInterface 实现 17 个纯虚方法,然后在工厂里追加一行 registerVendor<YourCamera>("厂商名")。除此之外,门面、参数面板、图像链路一行都不用改。

新增一个型号:只改 Resource/VirtualCameraParam.json 里增删分组和参数,不编译、不发版。

这两个数字——「1 行」和「0 行」——是这套分层唯一值得炫耀的地方。硬件差异被压在实现层,大概占全部代码的 14%,其余部分对品牌完全无感。

顺带解决的构建问题

海康 SDK 的导入库是第三方二进制,不能随仓库分发。所以 CMakeLists.txt 里做成了可选依赖:找不到 SDK 时不编适配器、工厂也不注册该品牌,程序退化成纯虚拟相机形态依然能构建成功。CI 上没有硬件也能跑通全部测试。

同一套策略后来原封不动复用在接入大模型助手这件事上(另一篇文章写这个)。

← 返回文章列表