2. 适配层抽象接口设计:枚举相机操作、通用参数结构、工厂模式与插件化架构

适配层的核心价值在于为上层业务屏蔽不同厂商相机 SDK 的差异,通过统一的抽象接口管理所有相机操作。本章将详细讲解如何设计一套可扩展的抽象接口,包括操作枚举、通用参数结构,以及基于工厂模式与插件化架构的实现方案。

2.1 设计原则与目标

适配层抽象接口的设计需遵循以下原则:

  • 最小化约定:接口只暴露必要的相机操作,避免过度抽象导致灵活性降低。
  • 单向依赖:上层业务依赖抽象层,抽象层不依赖具体厂商 SDK 实现。
  • 可演进:新增相机厂商时,无需修改已有接口定义,仅需追加新插件。
  • 参数标准化:将各厂商差异化的参数(如曝光时间、白平衡模式)映射为统一的枚举或结构体。

2.2 枚举相机操作(Camera Operation Enum)

将所有相机操作抽象为枚举类型,便于统一调度。操作涵盖相机的生命周期管理、参数设置、图像采集与控制指令。

典型操作枚举定义(C++ 示例):
enum class CameraOperation {
    // 生命周期
    OPEN = 0,
    CLOSE,
    START_PREVIEW,
    STOP_PREVIEW,
    
    // 参数设置
    SET_EXPOSURE_TIME,
    SET_GAIN,
    SET_WHITE_BALANCE,
    SET_FRAME_RATE,
    SET_RESOLUTION,
    SET_TRIGGER_MODE,
    
    // 数据获取
    CAPTURE_IMAGE,
    START_RECORDING,
    STOP_RECORDING,
    GET_FRAME,
    
    // 控制命令
    SOFTWARE_TRIGGER,
    HARDWARE_TRIGGER_ENABLE,
    HARDWARE_TRIGGER_DISABLE,
    RESET_DEVICE,
    GET_DEVICE_INFO
};

2.3 通用参数结构(Generic Parameter Structure)

不同厂商的参数类型和取值范围各异,必须设计一种可自描述的参数结构。通过参数 ID(对应枚举)、数据类型标记、值联合体或变体来实现。

字段名称类型说明
paramIdCameraOperation对应操作枚举
dataTypeParamDataTypeINT32、FLOAT64、STRING、BOOLEAN 等
valueParamValue(union/variant)实际参数值
min / maxdouble / int取值范围(可选)
descriptionstd::string参数描述,用于日志与调试
// 参数数据类型枚举
enum class ParamDataType {
    INT32,
    FLOAT64,
    BOOLEAN,
    STRING,
    INT32_RANGE,
    FLOAT64_RANGE
};

// 参数值变体示例(C++17 std::variant)
using ParamValue = std::variant<int32_t, double, bool, std::string,
                                 std::pair<int32_t, int32_t>,
                                 std::pair<double, double>>;

// 通用参数结构
struct CameraParam {
    CameraOperation paramId;
    ParamDataType   dataType;
    ParamValue      value;
    std::string     description;
};
设计考量:使用 std::variant 或 boost::variant 比裸 union 更安全,能自动处理类型匹配与异常。在 C 语言环境中可使用带类型标记的 union,并配以严格的校验函数。

2.4 工厂模式:创建具体适配器实例

工厂模式用于根据厂商标识动态创建对应的适配器对象,上层只依赖抽象基类 ICameraAdapter。

  • 抽象基类:ICameraAdapter 声明 open()、close()、setParameter()、capture() 等纯虚函数。
  • 具体适配器:HikCameraAdapter、BaslerCameraAdapter、DahengCameraAdapter 等继承并实现基类接口。
  • 工厂函数:根据 vendor 字符串(如 "HikVision")或相机设备信息返回适配器指针。
class ICameraAdapter {
public:
    virtual ~ICameraAdapter() = default;
    virtual bool open(const std::string& deviceId) = 0;
    virtual bool close() = 0;
    virtual bool setParameter(const CameraParam& param) = 0;
    virtual bool getParameter(CameraParam& param) = 0;
    virtual std::shared_ptr<Frame> capture() = 0;
    // ...
};

// 工厂类
class CameraAdapterFactory {
public:
    static std::unique_ptr<ICameraAdapter> create(const std::string& vendor) {
        if (vendor == "HikVision") {
            return std::make_unique<HikCameraAdapter>();
        } else if (vendor == "Basler") {
            return std::make_unique<BaslerCameraAdapter>();
        } else if (vendor == "Daheng") {
            return std::make_unique<DahengCameraAdapter>();
        }
        return nullptr;
    }
};

2.5 插件化架构:动态加载适配器

在大型项目中,各厂商适配器应以独立动态库(.so / .dll)形式存在,通过插件接口注册到统一的管理器中。这样新增厂商时无需重新编译主程序。

  • 插件接口:定义一个 ICameraPlugin 抽象类,包含 getVendorName() 和 createAdapter() 方法。
  • 注册机制:动态库加载后调用 registerPlugin() 将自身注册到全局插件列表。
  • 加载策略:启动时遍历指定目录下的插件库,依次加载并注册。
// 插件接口定义
class ICameraPlugin {
public:
    virtual ~ICameraPlugin() = default;
    virtual std::string getVendorName() const = 0;
    virtual std::unique_ptr<ICameraAdapter> createAdapter() = 0;
};

// 插件注册管理器
class PluginManager {
public:
    static PluginManager& instance() {
        static PluginManager mgr;
        return mgr;
    }
    
    void registerPlugin(std::shared_ptr<ICameraPlugin> plugin) {
        plugins_[plugin->getVendorName()] = plugin;
    }
    
    std::unique_ptr<ICameraAdapter> createAdapter(const std::string& vendor) {
        auto it = plugins_.find(vendor);
        if (it != plugins_.end()) {
            return it->second->createAdapter();
        }
        return nullptr;
    }
    
private:
    std::unordered_map<std::string, std::shared_ptr<ICameraPlugin>> plugins_;
};

// 动态库导出宏示例(Windows)
#define EXPORT_PLUGIN extern "C" __declspec(dllexport)

EXPORT_PLUGIN void registerHikPlugin() {
    auto plugin = std::make_shared<HikCameraPlugin>();
    PluginManager::instance().registerPlugin(plugin);
}

2.6 适配层架构总览(内联 SVG 结构图)

下图展示了适配层的整体架构:上层业务通过统一的抽象接口与适配层交互,适配层内部由工厂模式和插件化机制管理多个厂商适配器,最终调用各厂商 SDK 完成具体操作。

相机适配层抽象接口架构图 上层业务(图像采集、控制逻辑) 抽象接口层 ICameraAdapter(open, close, setParameter, capture, ...) 工厂与插件管理层 CameraAdapterFactory + PluginManager HikCameraAdapter BaslerCameraAdapter DahengCameraAdapter 其他厂商... Hik SDK(私有 API) Basler Pylon 大恒 GalView(GxIAPI) 厂商私有 SDK 说明:枚举操作 + 通用参数结构保证统一接口;工厂模式负责创建具体适配器;插件化架构支持动态扩展厂商支持。

2.7 实现流程与关键步骤

  1. 定义枚举与参数结构:在公共头文件中声明 CameraOperation 和 CameraParam,确保所有厂商适配器都引用同一份定义。
  2. 实现抽象基类:编写 ICameraAdapter,包含纯虚函数;将参数传递统一为 CameraParam 类型。
  3. 编写第一个适配器(如 HikCameraAdapter):继承 ICameraAdapter,内部调用 Hik SDK 函数;注意将 SDK 特有的错误码映射为统一的错误枚举。
  4. 集成工厂模式:在 CameraAdapterFactory::create() 中根据厂商字符串返回对应的适配器实例。
  5. 实现插件化加载:定义 ICameraPlugin 接口,编写动态库装载函数,通过 PluginManager 管理所有已注册插件。
  6. 端到端验证:编写测试程序,通过 createAdapter("Basler") 获取适配器,调用统一接口完成打开、设置参数、采集一帧并关闭操作。

2.8 调试与常见错误

  • 参数转换错误:不同厂商的曝光时间单位不同(微秒 vs 毫秒),务必在适配器内部进行单位转换,并用日志记录原始值与转换后的值。
  • 枚举值溢出:新增操作枚举时,若在已有枚举后追加,需检查所有适配器中已实现的 switch/case 是否遗漏处理新枚举。
  • 插件加载失败:动态库依赖缺失(如缺少厂商 SDK 运行时 Runtime)将导致 dlopen/LoadLibrary 返回空;应打印详细错误信息并增加预检步骤。
  • 工厂返回空指针:传入未注册的厂商名时工厂应返回 nullptr,上层调用方需做空指针检查,不可直接使用。
// 调试阶段建议在工厂函数中添加厂商列表获取方法:
std::vector<std::string> CameraAdapterFactory::listAvailableVendors() {
    std::vector<std::string> vendors;
    for (const auto& pair : plugins_) {
        vendors.push_back(pair.first);
    }
    return vendors;
}

2.9 性能与稳定性优化

  • 缓存工厂实例:每次调用 createAdapter() 都创建新对象开销大;可考虑对象池或复用相同设备 ID 的适配器实例。
  • 参数校验前置:在抽象层实现参数取值范围校验,避免无效值传递给底层 SDK 导致崩溃。
  • 超时与重试机制:枚举操作中 START_PREVIEW、CAPTURE_IMAGE 等可能阻塞,应在适配器内设置超时,超时后返回错误码并清理资源。
  • 插件加载隔离:为每个插件库启用独立的错误处理域(如 C++ 的 try-catch 跨 DLL 边界需注意异常传播问题),防止一个插件的崩溃影响整个进程。
稳定性警告:不同厂商 SDK 内部可能存在线程模型冲突(如主线程回调 vs 专用回调线程)。适配层必须明确约定回调线程归属,必要时在适配器内部进行线程切换,避免死锁或数据竞争。