面向机器视觉自动化场景的基础 Demo,实现:插件化加载相机、相机取流、图像渲染显示、缩放拖拽查看图像(图像查看器具体实现可以看我上一篇文章)。核心思路:抽象相机接口,相机实现编译为独立 DLL 插件,主程序动态加载插件,获取相机帧数据,交给 Qt 的QGraphicsView图像控件做图像展示、缩放、拖拽交互。技术栈:Qt6 + C++17 + OpenCV(相机模拟读取),跨平台支持 目前Windows实现。一、整体分层架构
项目分为三层:相机 API 抽象层、相机插件实现层、上层应用层。相机 API 抽象层:定义纯虚相机接口,不绑定任何相机 SDK,所有相机都必须实现这套接口;定义帧数据结构体、错误码、像素格式。相机插件实现层:独立动态库 (DLL/SO),实现抽象层接口,使用 OpenCV VideoCapture 模拟 USB 相机取流;对外导出 C 风格创建销毁函数,供主程序加载。应用层:插件管理器、图像查看控件 ImageView、主窗口 Widget。插件管理器动态加载相机插件;ImageView 负责接收相机帧,渲染图像,实现鼠标滚轮缩放、拖拽平移。app├─include│ ├─imageview.h //图像查看控件头文件│ ├─pluginmanager.h //插件管理器│ ├─singleton.h //单例模板(插件管理器)│ └─widget.h //主窗口└─src ├─imageview.cpp ├─main.cpp ├─pluginmanager.cpp └─widget.cpp
相机插件是独立子工程,编译输出 dll,放置程序运行目录plugins文件夹下,主程序运行时扫描该目录加载插件。二、各核心模块详细解析
模块一:相机抽象接口层
这是整个插件机制的核心,定义ICameraApi纯虚基类,所有相机插件都继承实现该接口。1.核心结构体
CameraInfo:相机设备信息,设备 ID、名称、分辨率(模拟)CameraErrorCode:枚举错误码,打开失败、找不到设备、权限不足等PixelFormat:像素格式:灰度 8、RGB24、RGBA32CameraFrame:相机输出帧结构体,包含像素格式、宽高、步长 stride、原始图像数据指针pBits。FrameCallbackFunc:帧回调,相机输出图像帧时回调,回传帧数据 + 用户上下文CameraErrorCallbackFunc:错误回调,相机异常时上报错误码与信息2.ICameraApi 虚接口
class ICameraApi{public:virtual ~ICameraApi() = default;virtual CameraErrorCode OpenCamera(const std::string& strCameraDeviceId)= 0;virtualboolIsCameraOpened()const= 0;virtual CameraErrorCode CloseCamera()= 0;virtual CameraErrorCode StartCapture(FrameCallbackFunc frameCallback, CameraErrorCallbackFunc errorCallback, void* userCtx)= 0;virtual CameraErrorCode StopCapture()= 0;virtual std::string GetCameraDeviceId()const= 0;};
3.插件导出 C 接口
extern "C" {CAMERA_PLUGIN_API ICameraApi* CreateCameraApi();CAMERA_PLUGIN_API voidDestroyCameraApi(ICameraApi* pCameraApi);}
关键点:使用extern "C"防止 C++ 名字修饰,主程序可以通过 QLibrary 按函数名字符串找到创建 / 销毁函数。CAMERA_PLUGIN_API宏处理 Windows 导出 dll 符号、Linux 符号可见性。模块二:相机插件实现层(独立 dll 工程)
UsbCameraPlugin:继承ICameraApi,作为插件对外暴露类,内部持有ImplCamera实现实例。ImplCamera:真正业务实现类,使用 OpenCVcv::VideoCapture读取 USB 相机;内部开启子线程CaptureLoop循环读取相机图像。OpenCamera:调用 OpenCV 打开设备 0;StartCapture:启动采集线程CaptureLoop;CaptureLoop循环:线程内循环read()读取 cv::Mat,把 mat 数据封装成CameraFrame结构体,调用上层注册的帧回调,把原始图像数据指针传递出去;⚠️注意:这里直接传递 mat.data 指针,mat 是线程栈局部对象,回调处理必须立刻拷贝数据,不能保存裸指针,代码中上层回调做了QImage.copy()拷贝,规避野指针风险。
插件导出函数:CreateCameraApi() new 出UsbCameraPlugin实例;DestroyCameraApi()负责 delete 释放,主程序调用这两个函数创建销毁相机对象。后续扩展其他相机(海康、大恒等工业相机):只需要新增插件工程,实现 ICameraApi 接口,编译出新 dll 丢 plugins 目录,主程序无需修改任何代码。#pragma once#include<icamera_api.h>#include<memory>class ImplCamera;class UsbCameraPlugin : public ICameraApi{public:explicitUsbCameraPlugin();virtual ~UsbCameraPlugin() override;virtual CameraErrorCode OpenCamera(const std::string& strCameraDeviceId)override;virtualboolIsCameraOpened()constoverride;virtual CameraErrorCode CloseCamera()override;virtual CameraErrorCode StartCapture(FrameCallbackFunc frameCallback, CameraErrorCallbackFunc errorCallback, void* userCtx)override;virtual CameraErrorCode StopCapture()override;virtual std::string GetCameraDeviceId()constoverride;protected: std::unique_ptr<ImplCamera> m_implCamera;};
#include<opencv2/opencv.hpp>#include<icamera_api.h>#include<thread>class ImplCamera{public:explicitImplCamera();virtual ~ImplCamera();CameraErrorCode OpenCamera(const std::string& strCameraDeviceId);boolIsCameraOpened() const;CameraErrorCode CloseCamera();CameraErrorCode StartCapture(FrameCallbackFunc frameCallback, CameraErrorCallbackFunc errorCallback, void* userCtx);CameraErrorCode StopCapture();std::string GetCameraDeviceId()const;private: std::string m_strCameraDeviceId = "0"; cv::VideoCapture m_videoCapture; std::thread m_captureThread;bool m_isCapturing = false; FrameCallbackFunc m_frameCallback = nullptr; CameraErrorCallbackFunc m_errorCallback = nullptr;void* m_userCtx = nullptr;voidCaptureLoop();};
模块三:PluginManager 插件管理器(单例)
#pragma once#include<QObject>#include<QLibrary>#include<icamera_api.h>#include"singleton.h"using CameraCreate = ICameraApi * (*)();using CameraDestroy = void (*)(ICameraApi*);struct PluginInfo{ QLibrary* library = nullptr; CameraCreate funCreate = nullptr; CameraDestroy funDestroy = nullptr; ICameraApi* camera = nullptr; std::string strCameraDeviceId;};class IPlugin{public: virtualvoidISetImage(const QImage& image)= 0;};class PluginManager : public QObject{using Indexs = std::unordered_map<std::string, int>;Q_OBJECTDECL_SINGLETON(PluginManager)public: virtual ~PluginManager();voidInitPlugin();ICameraApi* GetCameraApi(const std::string& strCameraDeviceId);staticvoidCallBackFrameArrived(const CameraFrame* frame, void* userCtx);private: std::vector<PluginInfo> m_vecPluginInfos; Indexs m_mapCameraDeviceIdToIndex;};
职责:扫描插件目录、加载 dll 插件、管理插件生命周期、提供帧回调转发、根据设备 ID 获取相机实例。1.InitPlugin()初始化插件
定位程序运行目录下plugins文件夹;Windows 配置 DLL 搜索路径;遍历目录下所有 dll 文件,使用 QtQLibrary动态加载库;通过resolve查找CreateCameraApi、DestroyCameraApi两个 C 导出函数;调用 CreateCameraApi 创建 ICameraApi 实例,保存插件信息(库句柄、创建销毁函数、相机实例、设备 ID)存入容器m_vecPluginInfos。GetCameraApi(std::string deviceId)根据设备 ID 查找已经加载好的相机插件实例,内部带缓存 map 加速查找。静态回调 CallBackFrameArrived
将CameraFrame原始帧数据转成QImage;userCtx 传入的是 ImageView 对象指针;线程边界重点:相机采集子线程,不能直接操作 UI 控件! 代码中发送 Qt 信号signalImage(image),利用 Qt 信号槽跨线程自动排队,回到 UI 线程执行图像更新。源码中:ISetImage发射 signalImage 信号,槽函数slotImage在 UI 线程执行,完成图像更新。析构释放
程序退出时,遍历所有插件:关闭相机、调用插件 DestroyCameraApi 销毁实例、卸载 QLibrary 库、释放内存。PluginManager::GetInstance()->InitPlugin(); //程序启动加载插件PluginManager::GetInstance()->ReleaseInstance(); //程序退出释放
相机初始化逻辑
ImageView 的initEnvironment()函数内部:ICameraApi* pCamera = PluginManager::GetInstance()->GetCameraApi("0");if(pCamera){ if(pCamera->OpenCamera("0") == CameraErrorCode::CAMERA_ERROR_NONE) { pCamera->StartCapture(&PluginManager::CallBackFrameArrived, nullptr, this); }}
控件初始化的时候直接获取设备 ID=0 的相机插件实例,打开相机,启动采集,把自身this作为 userCtx 传给相机回调。相机每来一帧,回调到CallBackFrameArrived,回传给 ImageView。三、完整数据流流程
1.程序启动 main.cpp└─PluginManager::InitPlugin() →扫描plugins目录,加载usb_camera_plugin.dll └─调用CreateCameraApi() 创建UsbCameraPlugin实例2. Widget窗口创建 →创建ImageView控件 └─ImageView::initEnvironment() └─PluginManager获取deviceId="0"相机实例 └─OpenCamera打开相机 └─StartCapture注册回调,传入&PluginManager::CallBackFrameArrived,userCtx=this(ImageView对象)3.插件内部ImplCamera采集线程循环 └─cv::VideoCapture读取图像 cv::Mat └─封装CameraFrame,调用注册的FrameCallback(CallBackFrameArrived)4. CallBackFrameArrived(相机子线程运行) └─把CameraFrame转QImage,调用ImageView::ISetImage(image.copy()) └─ISetImage发射signalImage信号(跨线程信号)5.槽函数 slotImage(UI主线程执行) └─QImage转QPixmap,交给ImageViewItem绘制显示到界面
四、架构设计优势
插件解耦:不同品牌相机只需要实现 ICameraApi 接口,编译独立插件 dll,主程序不用重新编译,插拔式替换相机驱动。线程隔离:相机采集在后台子线程,UI 在主线程;原始帧回调不直接操作 UI,通过 Qt 信号槽线程安全转发。抽象统一:不管什么相机,输出统一 CameraFrame 结构体,上层图像显示控件完全不需要关心底层相机来源。可扩展性:后续可扩展:相机参数配置、图像标注、保存图片、多相机支持。五、现存可以优化点(源码潜在问题)
代码还需要优化,目前只是架构实现和demo展示,相机线程退出逻辑可以增加互斥锁保护成员变量等等;没有做多相机设备枚举逻辑,目前硬编码设备 ID 为 "0",这个可以具体的去拓展;PS:需要具体代码的可以留言私信(支持定制哦),后续会新增一些标注等操作,敬请期待!!!