使用 USB 工业相机 + DirectShow + Qt 进行工业相机二次开发项目(简易版)
一、技术简介
1.1 USB 工业相机
支持 UVC(USB Video Class)协议,即插即用,无需额外驱动,这使得 DirectShow 能直接识别和控制。
1.2 DirectShow 框架
DirectShow 是微软在 Windows 平台上开发的流媒体处理框架,基于 COM(组件对象模型) 架构,主要用于音视频的捕获、播放、转换和流处理。它曾是 Windows 多媒体开发的核心技术之一,广泛用于视频采集、DVD 播放、非线性编辑等场景。但当前 DirectShow 已被标记为旧技术。微软官方建议:新项目应使用 Media Foundation :MediaPlayer、IMFMediaEngine。
1.2.1 DirectShow的体系结构

1.2.2 DirectShow 功能
DirectShow 位于应用层中,DirectShow 使用一种叫 Filter Graph 的模型来管理整个数据流的处理过程,参与数据处理的各个功能模块叫 Filter,各个Filter在Filter Graph中按一定的顺序连接成一条“流水线”协同工作。按照功能来分,Filter大致分为三类:Source Filters. Transform Filters 和 Rendering Filters。要完成特定的多媒体功能,必须用相应的Filter组成特定的Filter Graph。
通过Filter Graph Manager来控制整个的数据处理过程。DirectShow 能在Filter Graph运行的时候接收到各种事件,并通过消息的方式发送到应用程序。
1.3 项目配置
1.3.1 Qt 项目配置(.pro 文件)
# 包含 DirectShow、BaseClasses
INCLUDEPATH += $$PWD/DirectShow/Include \
$$PWD/BaseClasses
LIBS += $$PWD/BaseClasses/Release_Unicode/STRMBASE.lib
LIBS += -lstrmiids -lole32 -loleaut32
# 如果是 MSVC 环境,可能需要加上
LIBS += -lquartz
1.3.2 包含头文件
#include <dshow.h>
#include <windows.h>
#include <qedit.h>
windows10 需要手写qedit.h
二、基础功能搭建
利用 Windows DirectShow API 实现了 USB 摄像头的设备枚举、视频预览、帧数据捕获和同步拍照功能。其核心是构建一个 DirectShow 过滤器图(Filter Graph),通过 SampleGrabber 获取每一帧图像数据,并利用 Qt 的线程同步机制提供阻塞式拍照接口。
2.1 所需接口
private:
// COM 接口指针
IGraphBuilder* m_pGraph; // Filter Graph Manager
ICaptureGraphBuilder2* m_pCaptureBuilder; // Capture Graph Builder
IBaseFilter* m_pSrcFilter; // 相机源 Filter
IVideoWindow* m_pVideoWindow; // 视频窗口接口
std::vector<std::wstring> m_deviceNames; // 存储设备友好名称
std::vector<std::wstring> m_devicePaths; // 存储设备路径(用于精确选择)
2.1.1 初始化
首先在应用程序启动时初始化 COM 库(通常在 main 函数中)。
#include <windows.h>
#include <dshow.h>
#include <iostream>
// 初始化 COM
CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
// ... 程序结束前
CoUninitialize();
2.1.2 枚举设备(获取 Source Filter)
工业相机通常与普通摄像头(如笔记本自带摄像头)共存在系统中,我们需要通过代码筛选出指定的 USB 工业相机。
核心思路:创建系统设备枚举器,查找视频输入设备类别(CLSID_VideoInputDeviceCategory),并读取设备的友好名称。
#include <dshow.h>
#include <string>
#include <vector>
struct CameraInfo {
std::string devicePath;
std::string deviceName;
};
std::vector<CameraInfo> CameraManager::enumerateCameras() {
std::vector<CameraInfo> cameras;
// 创建系统设备枚举器
ICreateDevEnum *pDevEnum = nullptr;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER, IID_ICreateDevEnum, (void**)&pDevEnum);
// 创建视频输入设备枚举器
IEnumMoniker *pEnum = nullptr;
pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0);
if (pEnum) {
IMoniker *pMoniker = nullptr;
while (pEnum->Next(1, &pMoniker, NULL) == S_OK) {
IPropertyBag *pPropBag = nullptr;
pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pPropBag);
VARIANT var;
VariantInit(&var);
// 获取设备名称
pPropBag->Read(L"FriendlyName", &var, 0);
CameraInfo info;
info.deviceName = std::string(var.bstrVal, var.bstrVal + wcslen(var.bstrVal));
// 获取设备路径(用于后续绑定)
pPropBag->Read(L"DevicePath", &var, 0);
if (var.vt == VT_BSTR) {
info.devicePath = std::string(var.bstrVal, var.bstrVal + wcslen(var.bstrVal));
}
cameras.push_back(info);
VariantClear(&var);
pPropBag->Release();
pMoniker->Release();
}
pEnum->Release();
}
pDevEnum->Release();
return cameras;
}
2.1.3 根据设备路径创建源 Filter
使用 IBaseFilter 绑定到选中的设备。
bool CameraPreview::selectDevice(int index) {
if (index < 0 || index >= (int)m_devicePaths.size()) return false;
return selectDevice(m_devicePaths[index]);
}
bool CameraPreview::selectDevice(const std::wstring& devicePath) {
// 释放已有的源 Filter(如果有)
if (m_pSrcFilter) {
m_pSrcFilter->Release();
m_pSrcFilter = nullptr;
}
// 再次枚举以获取对应的 Moniker
ICreateDevEnum* pDevEnum = nullptr;
CoCreateInstance(CLSID_SystemDeviceEnum, NULL, CLSCTX_INPROC_SERVER,
IID_ICreateDevEnum, (void**)&pDevEnum);
IEnumMoniker* pEnum = nullptr;
pDevEnum->CreateClassEnumerator(CLSID_VideoInputDeviceCategory, &pEnum, 0);
IMoniker* pMoniker = nullptr;
bool found = false;
while (pEnum->Next(1, &pMoniker, NULL) == S_OK) {
IPropertyBag* pPropBag = nullptr;
pMoniker->BindToStorage(0, 0, IID_IPropertyBag, (void**)&pPropBag);
VARIANT varPath;
VariantInit(&varPath);
pPropBag->Read(L"DevicePath", &varPath, 0);
if (varPath.vt == VT_BSTR && devicePath == varPath.bstrVal) {
// 找到匹配的设备,绑定为源 Filter
HRESULT hr = pMoniker->BindToObject(0, 0, IID_IBaseFilter, (void**)&m_pSrcFilter);
if (SUCCEEDED(hr)) found = true;
VariantClear(&varPath);
pPropBag->Release();
pMoniker->Release();
break;
}
VariantClear(&varPath);
pPropBag->Release();
pMoniker->Release();
}
pEnum->Release();
pDevEnum->Release();
return found;
}
2.2 构建 Filter Graph(预览流程)
这是 DirectShow 开发的核心,需要构建一个Filter Graph(过滤器图)。通常包括三个核心组件:
- Source Filter:代表你的 USB 相机。
- Sample Grabber(可选):用于抓取图像数据帧,以便转换成 QImage 显示在 Qt 界面上。
- Video Renderer:用于渲染视频到窗口。
为了方便管理,推荐使用 IVideoWindow 接口将视频直接嵌入到 Qt 的 QWidget 窗口中,或者使用 ISampleGrabber 抓取数据转为 QImage 重绘。
2.2.1 创建过滤器图
使用 ICaptureGraphBuilder2 自动连接源 Filter 到视频渲染器。
bool CameraPreview::buildPreview(HWND hwnd) {
if (m_pSrcFilter == nullptr) return false;
// 1. 创建 Filter Graph Manager
HRESULT hr = CoCreateInstance(CLSID_FilterGraph, NULL, CLSCTX_INPROC_SERVER,
IID_IGraphBuilder, (void**)&m_pGraph);
if (FAILED(hr)) return false;
// 2. 创建 Capture Graph Builder
hr = CoCreateInstance(CLSID_CaptureGraphBuilder2, NULL, CLSCTX_INPROC_SERVER,
IID_ICaptureGraphBuilder2, (void**)&m_pCaptureBuilder);
if (FAILED(hr)) return false;
// 3. 将 Capture Graph Builder 与 Filter Graph Manager 关联
m_pCaptureBuilder->SetFiltergraph(m_pGraph);
// 4. 将源 Filter 添加到 Graph 中
hr = m_pGraph->AddFilter(m_pSrcFilter, L"Video Source");
if (FAILED(hr)) return false;
// 5. 使用 Capture Graph Builder 自动连接预览链路
// 这会自动添加必要的渲染器(Video Renderer),并连接数据流。
hr = m_pCaptureBuilder->RenderStream(&PIN_CATEGORY_PREVIEW, &MEDIATYPE_Video,
m_pSrcFilter, NULL, NULL);
if (FAILED(hr)) return false;
// 6. 从 Graph 获取 IVideoWindow 接口,用于设置窗口属性
hr = m_pGraph->QueryInterface(IID_IVideoWindow, (void**)&m_pVideoWindow);
if (FAILED(hr)) return false;
// 7. 将视频窗口嵌入到 Qt 控件中
hr = m_pVideoWindow->put_Owner((OAHWND)hwnd);
if (FAILED(hr)) return false;
// 设置窗口样式(子窗口,无边框)
hr = m_pVideoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);
if (FAILED(hr)) return false;
// 调整窗口大小适应控件(初始时设置)
RECT rc;
GetClientRect(hwnd, &rc);
hr = m_pVideoWindow->SetWindowPosition(0, 0, rc.right, rc.bottom);
if (FAILED(hr)) return false;
return true;
}
2.2.2 将视频嵌入 QWidget 窗口
// 定义IVideoWindow 接口
CComPtr<IVideoWindow> m_pVideoWindow;
// 获取 Qt 窗口的句柄
HWND hWnd = (HWND)this->ui->videoWidget->winId();
// 设置视频窗口的父窗口为 Qt 控件
pVideoWindow->put_Owner((OAHWND)hWnd);
pVideoWindow->put_WindowStyle(WS_CHILD | WS_CLIPSIBLINGS);
// 设置窗口大小适应控件
RECT rect;
GetClientRect(hWnd, &rect);
pVideoWindow->SetWindowPosition(0, 0, rect.right, rect.bottom);
2.2.3 拍照
设置帧抓取回调函数:使用 Sample Grabber 模式,从视频流中截取一帧,保存为图像文件。工业应用常用此方法在最高分辨率下获取静态图像。
class SampleGrabberCallback : public ISampleGrabberCB {
public:
// 引用计数
ULONG STDMETHODCALLTYPE AddRef() override { return 1; } // 简单实现,实际应使用原子操作
ULONG STDMETHODCALLTYPE Release() override { return 1; }
HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void **ppvObject) override {
if (riid == IID_ISampleGrabberCB || riid == IID_IUnknown) {
*ppvObject = static_cast<ISampleGrabberCB*>(this);
AddRef();
return S_OK;
}
return E_NOINTERFACE;
}
// SampleCB: 接收媒体样本(IMediaSample),通常使用更底层的 BufferCB 即可
HRESULT STDMETHODCALLTYPE SampleCB(double SampleTime, IMediaSample *pSample) override {
return S_OK; // 不实现,改用 BufferCB
}
// BufferCB: 接收原始缓冲区数据
HRESULT STDMETHODCALLTYPE BufferCB(double SampleTime, BYTE *pBuffer, long BufferLen) override {
// 在此处处理帧数据,pBuffer 是原始图像数据(格式由设置的媒体类型决定)
// 注意:这个回调运行在 DirectShow 的工作线程中,不要执行耗时操作或直接更新 UI
// 建议将数据复制出来,通过信号/消息机制发送到主线程
// 示例:复制数据到一个全局变量(注意线程安全)
std::lock_guard<std::mutex> lock(mutex);
frameData.assign(pBuffer, pBuffer + BufferLen);
frameReady = true;
// 如果需要通知主线程,可以发送 Qt 信号(需通过 QMetaObject::invokeMethod 或自定义事件)
// 例如:emit newFrame(QImage(...)); 但需注意线程安全
return S_OK;
}
// 用于外部访问帧数据
std::vector<BYTE> getFrameData() {
std::lock_guard<std::mutex> lock(mutex);
frameReady = false;
return frameData;
}
private:
std::vector<BYTE> frameData;
std::mutex mutex;
bool frameReady = false;
};
在buildPreview函数中添加:
// ---------- 创建 Sample Grabber 并添加 ----------
IBaseFilter* pSampleGrabberFilter = nullptr;
ISampleGrabber* pSampleGrabber = nullptr;
hr = CoCreateInstance(CLSID_SampleGrabber, NULL, CLSCTX_INPROC_SERVER, IID_IBaseFilter, (void**)&pSampleGrabberFilter);
if (FAILED(hr)) return false;
hr = m_pGraph->AddFilter(pSampleGrabberFilter, L"Sample Grabber");
if (FAILED(hr)) {
pSampleGrabberFilter->Release();
return false;
}
// 获取 ISampleGrabber 接口
hr = pSampleGrabberFilter->QueryInterface(IID_ISampleGrabber, (void**)&pSampleGrabber);
if (FAILED(hr)) {
pSampleGrabberFilter->Release();
return false;
}
// 设置媒体类型:我们想要未压缩的 RGB24 格式,便于转换为 QImage
AM_MEDIA_TYPE mt;
ZeroMemory(&mt, sizeof(AM_MEDIA_TYPE));
mt.majortype = MEDIATYPE_Video;
mt.subtype = MEDIASUBTYPE_RGB24; // 也可以使用 MEDIASUBTYPE_RGB32 等
// 其他字段设为默认,Sample Grabber 会自动协商格式
hr = pSampleGrabber->SetMediaType(&mt);
if (FAILED(hr)) {
// 可能设备不支持 RGB24,可以尝试其他格式(如 MEDIASUBTYPE_RGB32 或 MEDIASUBTYPE_YUY2)
mt.subtype = MEDIASUBTYPE_RGB32;
hr = pSampleGrabber->SetMediaType(&mt);
if (FAILED(hr)) {
pSampleGrabber->Release();
pSampleGrabberFilter->Release();
return false;
}
}
// 设置回调接口
SampleGrabberCallback* pCallback = new SampleGrabberCallback();
hr = pSampleGrabber->SetCallback(pCallback, 1); // 1 表示使用 BufferCB,0 表示使用 SampleCB
if (FAILED(hr)) {
delete pCallback;
pSampleGrabber->Release();
pSampleGrabberFilter->Release();
return false;
}
2.3 启动与停止预览
IMediaControl* pControl; // 用于向Filter Graph Manager发送Command
void CameraPreview::startPreview() {
if (m_pGraph) {
IMediaControl* pControl = nullptr;
HRESULT hr = m_pGraph->QueryInterface(IID_IMediaControl, (void**)&pControl);
if (SUCCEEDED(hr)) {
pControl->Run();
pControl->Release();
}
}
}
void CameraPreview::stopPreview() {
if (m_pGraph) {
IMediaControl* pControl = nullptr;
HRESULT hr = m_pGraph->QueryInterface(IID_IMediaControl, (void**)&pControl);
if (SUCCEEDED(hr)) {
pControl->Stop();
pControl->Release();
}
}
}
2.4 资源释放
在析构函数中释放所有 COM 接口,并停止 Graph。
CameraPreview::~CameraPreview() {
if (m_pVideoWindow) {
m_pVideoWindow->put_Visible(OAFALSE);
m_pVideoWindow->put_Owner(NULL);
m_pVideoWindow->Release();
}
if (m_pGraph) {
IMediaControl* pControl = nullptr;
if (SUCCEEDED(m_pGraph->QueryInterface(IID_IMediaControl, (void**)&pControl))) {
pControl->Stop();
pControl->Release();
}
m_pGraph->Release();
}
if (m_pCaptureBuilder) m_pCaptureBuilder->Release();
if (m_pSrcFilter) m_pSrcFilter->Release();
}
2.5 与 Qt 集成示例
在 Qt 的窗口类中(例如 MainWindow),放置一个 QWidget 作为视频容器,然后调用上述类。
// mainwindow.cpp 片段
#include "camerapreview.h"
void MainWindow::initCamera() {
CameraPreview* cam = new CameraPreview(this);
// 枚举设备(可选择第一个)
auto names = cam->enumerateDevices();
if (names.empty()) return;
// 选择第一个设备
cam->selectDevice(0);
// 获取视频容器的句柄
HWND hwnd = (HWND)ui->videoWidget->winId();
// 构建预览图
if (cam->buildPreview(hwnd)) {
cam->startPreview();
}
}
三、工业相机参数调节(亮度、对比度、曝光)
工业相机二次开发的关键在于能够调节底层参数。DirectShow 提供了 IAMCameraControl(硬件控制)和 IAMVideoProcAmp(视频处理)接口)。
void CameraManager::setCameraProperty(IBaseFilter *pCaptureFilter) {
IAMCameraControl *pCameraControl = nullptr;
HRESULT hr = pCaptureFilter->QueryInterface(IID_IAMCameraControl, (void**)&pCameraControl);
if (SUCCEEDED(hr)) {
long min, max, step, def, flags;
// 获取曝光范围(通常 CameraControl_Exposure)
pCameraControl->GetRange(CameraControl_Exposure, &min, &max, &step, &def, &flags);
// 设置曝光值(假设设置到最大值的一半)
pCameraControl->Set(CameraControl_Exposure, (max + min)/2, CameraControl_Flags_Manual);
pCameraControl->Release();
}
// 调节亮度、对比度等
IAMVideoProcAmp *pProcAmp = nullptr;
hr = pCaptureFilter->QueryInterface(IID_IAMVideoProcAmp, (void**)&pProcAmp);
if (SUCCEEDED(hr)) {
// 设置亮度
pProcAmp->Set(VideoProcAmp_Brightness, 128, VideoProcAmp_Flags_Manual);
// 设置对比度
pProcAmp->Set(VideoProcAmp_Contrast, 50, VideoProcAmp_Flags_Manual);
pProcAmp->Release();
}
}
四、问题
4.1 编译BaseClass 需要使用VS 2013。
4.2 使用#include <qedit.h>
报错:D:\company\code\CameraPreview\thirdpart\usb_camera\DirectShow\Include\qedit.h:492: error: C1083: 无法打开包括文件: “dxtrans.h”: No such file or directory
原因
新版 Windows SDK 已经移除了 dxtrans.h(DXTrans)
解决
直接自己定义(需要使用ISampleGrabber、ISampleGrabberCB等)
给“类”和“接口”定义一个唯一ID(GUID)。在 Windows 的 COM 模型中,一切都是“接口 + GUID”
| 概念 | 作用 |
|---|---|
| CLSID | 标识一个“类”(组件) |
| IID | 标识一个“接口” |
SampleGrabber功能:在数据流中“截胡”,把视频帧交给你代码处理
/******************qedit.h 定义********************/
#include <dshow.h>
// {6B652FFF-11FE-4FCE-92AD-0266B5D7C1F2}
DEFINE_GUID(CLSID_SampleGrabber, 0x6B652FFF, 0x11FE, 0x4FCE, 0x92, 0xAD, 0x02, 0x66, 0xB5, 0xD7, 0xC1, 0xF2);
// {C1F400A0-3F08-11D3-9F0B-006008039E37}
DEFINE_GUID(IID_ISampleGrabber, 0xC1F400A0, 0x3F08, 0x11D3, 0x9F, 0x0B, 0x00, 0x60, 0x08, 0x03, 0x9E, 0x37);
// {0579154A-2B53-4994-B0D0-E773148EFF85}
DEFINE_GUID(IID_ISampleGrabberCB, 0x0579154A, 0x2B53, 0x4994, 0xB0, 0xD0, 0xE7, 0x73, 0x14, 0x8E, 0xFF, 0x85);
五、其他
5.1 录像(保存视频流)
需要构建 File Writer 或使用 ASF Writer 进行编码封装。对于工业相机,如果只需要原始 MJPG 或未压缩数据,可以使用 AVI Mux + File Writer 的组合。
5.2 多相机同时工作
工业应用中常需要一台 PC 连接多台相机。DirectShow 支持同时打开多个设备实例,只需创建多个独立的 IGraphBuilder 实例,并分别绑定不同设备即可。
5.3 分辨率与帧率
工业相机通常支持非标准分辨率。使用 IAMStreamConfig 接口可以枚举所有支持的分辨率和帧率组合,确保设置正确。
5.4 错误处理
实际生产代码中应对每个 HRESULT 进行检查,并添加必要的日志。
5.5 设备热插拔
DirectShow 不直接支持热插拔通知,如需监听设备变化,需要自行处理 WM_DEVICECHANGE 消息。
5.6 性能优化
若预览时 CPU 占用过高,可考虑使用 VMR-9 或 EVR 渲染器替代默认的 Video Renderer,它们利用硬件加速,性能更好。

浙公网安备 33010602011771号