一、背景说明

在 AI Photo Booth、自助拍照亭等项目中,相机已经不再是“本地 UI 点一下就完事”的工具,而是一个需要被远程调用、稳定运行、长期无人值守的硬件服务。

以佳能相机为例,官方仅提供了 EDSDK(Canon EOS Digital SDK),其本质是一个 COM 组件封装的原生 SDK,如果直接在 Web 服务或多线程环境中使用,非常容易遇到:

  • 相机卡死、无响应

  • 拍照 60 秒后才返回

  • DeviceBusy(0x0000A102)异常频发

  • 并发请求导致 SDK 崩溃

为了解决这些问题,我们将相机能力彻底服务化,设计并实现了一个独立的 CameraAgent,专门负责:

“用一条 STA 线程 + 串行队列,把不稳定的相机 SDK 变成稳定的 HTTP 服务”


二、核心设计思想

整个 CameraAgent 的设计,其实只围绕三条铁律

1️⃣ 所有 EDSDK 调用,必须在同一个 STA 线程执行

EDSDK 基于 Windows COM,有严格的线程亲和性,拍照、取事件、下载照片必须在同一 STA 线程

2️⃣ 相机事件不是“推送”,而是要主动拉

EdsGetEvent() 本质是“去相机取事件”,不主动调用就永远等不到拍照完成事件

3️⃣ 相机 ≠ Web 服务,必须做串行化

HTTP 是并发的,但相机不是。一个相机 = 一个串行执行队列


三、整体架构拆解

最终架构分为三层,职责非常清晰:

┌───────────────┐
│   前端 / MVP   │  (Java / Vue / 任意语言)
└───────▲───────┘
        │ HTTP
┌───────┴───────┐
│  CameraAgent  │  (OWIN / WebAPI)
│  只做接口转发 │
└───────▲───────┘
        │ 方法调用
┌───────┴───────┐
│  CameraCore   │  (C# Class Library)
│  STA线程 + 队列│
└───────▲───────┘
        │
      相机硬件
  • CameraCore:唯一直接操作 EDSDK 的地方

  • CameraAgent:HTTP 外壳,完全不碰 SDK

  • 上层系统:只关心接口是否成功,不关心相机细节


四、CameraCore 的关键实现思路

1. 单 STA 相机线程(核心)

CameraCore 启动时只做一件事:

启动一个 ApartmentState = STA 的后台线程

这个线程内部是一个永不退出的循环,主要干三件事:

  1. 调用 EdsGetEvent() 拉取相机事件

  2. 从任务队列中取一个相机任务(拍照 / 设置参数)

  3. 串行执行任务,捕获所有异常,保证线程不崩

这样可以确保:

  • 不会因为异常导致“相机线程直接死掉”

  • 不存在任何跨线程 EDSDK 调用


2. 请求队列 + Job 模型

所有拍照、参数设置请求,都被封装成一个 Job

HTTP请求 → CameraService → 创建 Job → 丢进队列

队列底层使用 BlockingCollection

  • 并发请求自动排队

  • 一个任务执行完,下一个才会开始

  • 上层永远不会“抢相机”

这也是 CameraAgent 能支撑 并发 HTTP 请求却不崩 的关键。


3. 拍照流程的真实时序

一次完整的拍照流程,并不是“调用一次 TakePicture”这么简单:

1. 设置 SaveTo = Host(照片直传电脑)
2. 初始化事件中转站(准备接收照片句柄)
3. 按下快门(PressShutter)
4. 循环:
   - EdsGetEvent()
   - 尝试拿到 DirItem(照片句柄)
5. 下载照片到指定路径
6. 释放句柄,返回结果

关键点在于第 4 步:

如果你不循环调用 EdsGetEvent,照片事件永远不会到你手里


五、LiveView(实时预览)的处理思路

LiveView 的本质是:

  • 相机持续输出低分辨率 JPEG 帧(EVF)

  • 需要频繁调用 EdsDownloadEvfImage

预览和拍照是互斥的,否则就会触发 DeviceBusy

解决方案只有一个:

拍照前:暂停预览
拍照中:只拍照
拍照后:恢复预览

因此在 CameraCore 中:

  • 预览由 LiveViewPipeline 统一管理

  • 拍照 Job 内部主动控制预览暂停 / 恢复

  • 所有操作仍然在同一 STA 线程中完成


六、参数读写与持久化(商用必备)

在真实项目中,光能拍照是不够的,还必须支持:

  • ISO / 快门 / 光圈 / 白平衡设置

  • 服务重启后参数自动恢复

实现方式也很直接:

  1. 对外统一参数 Key(ISO / Av / Tv)

  2. 映射到 EDSDK 的 PropID

  3. 所有 Set/Get 操作封装为 Job,丢进 STA 队列

  4. 成功设置后写入本地 config.json

  5. 相机重连 / 服务启动时自动 ApplyConfig

这样可以保证:

  • 参数设置幂等

  • 单个参数失败不影响整体

  • 长期运行稳定可控


七、为什么一定要拆成 CameraAgent?

很多人会问:

“为什么不直接在 Spring Boot / 前端里连相机?”

答案很简单:

  • EDSDK 只适合 C++ / C# + STA 线程

  • Web / Java / Node 天生是多线程并发模型

  • 强行混在一起,只会越来越不稳定

CameraAgent 的价值在于:

  • 把“脆弱的硬件 SDK”隔离在设备侧

  • 对外只暴露“稳定、可观测”的 HTTP 接口

  • 上层业务完全无感知、更容易扩展


八、总结

这套 CameraAgent 方案,本质上解决的是一个经典问题:

如何用工程化手段,把一个不稳定、强约束的硬件 SDK,变成一个稳定、可扩展的服务能力

关键不在于写了多少代码,而在于:

  • 严格遵守 SDK 的线程模型

  • 用队列对抗并发

  • 用事件泵对抗“假异步”

  • 用分层架构隔离复杂性

这套思路不仅适用于佳能相机,也同样适用于其他工业相机、扫描仪、串口设备等硬件集成场景。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐