使用 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的体系结构

image

1.2.2 DirectShow 功能

https://download.csdn.net/blog/column/9515931/123024463

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,它们利用硬件加速,性能更好。

posted @ 2026-03-24 15:09  twjy  阅读(3)  评论(0)    收藏  举报