换相机品牌只改一行注册:工业相机客户端的分层怎么拆
工业相机客户端有个绕不开的麻烦:每个品牌的 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 上没有硬件也能跑通全部测试。
同一套策略后来原封不动复用在接入大模型助手这件事上(另一篇文章写这个)。