关键词:C++、WIN、USB、OPENCV、MF、NV12、MJPEG、YUY2、H264 github:github.com/kivenyangmi…
一. 引言
今日闲暇之余,决定将去年烂尾的开发捡起来。这个项目文件主要是针对WIN系统中模仿Linux的V4L2获取USB相机,灵感来自微软公司的开源项目,我对其进行魔改。当然了还有我的两大得力助手,一个是deepseek另一个是chatGTP。这个项目可能没有什么实际意义,用来炫技或许还有点价值,因为你都选择WIN操作系统了,也就不在乎那点内存占用以及性能的问题了。
一、代码核心功能
由于也是参考的微软的代码,对于win32的内容本人也是不怎么精通,大部分功劳还是二位助手帮我修改,我进行调试反馈以及完善。这里主要是分为如下的四个大模块:
- 设备枚举
通过EnumerateVideoDevices函数,利用 MF 的MFEnumDeviceSourcesAPI 枚举系统中所有可用的视频捕获设备(如 USB 摄像头),并将设备列表存储在devices容器中。 - 媒体源配置
ConfigureSourceReader函数用于配置IMFSourceReader,获取设备的原生视频格式(如分辨率、像素格式),并设置视频流的输出参数。 - 视频帧格式转换
pData2Mat函数将不同像素格式(如 YUY2、NV12、MJPEG)的视频帧数据转换为 OpenCV 的cv::Mat格式,支持实时显示。 - 主流程
初始化 COM 和 MF 库,选择第一个摄像头设备,读取视频帧并循环显示。
二、关键代码解析
1. 设备枚举与初始化
在这里就是检查PC端是否存在USB相机以及存在几个USB相机,相关代码是借鉴(抄袭)微软的。
// 检测是否存在可用的摄像头 枚举所有视频捕获设备(USB 摄像头)
HRESULT EnumerateVideoDevices(std::vector<IMFActivate*>& devices) {
IMFAttributes* pAttrs = nullptr;
hr = MFCreateAttributes(&pAttrs, 1);
if (FAILED(hr)) return hr;
hr = pAttrs->SetGUID(
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE,
MF_DEVSOURCE_ATTRIBUTE_SOURCE_TYPE_VIDCAP_GUID
);
if (FAILED(hr)) { pAttrs->Release(); return hr; }
IMFActivate** ppDevices = nullptr;
UINT32 count = 0;
hr = MFEnumDeviceSources(pAttrs, &ppDevices, &count);
if (SUCCEEDED(hr)) {
for (UINT32 i = 0; i < count; i++) {
devices.push_back(ppDevices[i]);
}
CoTaskMemFree(ppDevices);
}
pAttrs->Release();
return hr;
}
2. 配置视频流格式
这里需要注意的是USB相机可能存在NV12、 mjpeg、yuy2、h264输入格式,前两个比较常见,yuy2在我的双目相机中发现了,h264在我的大疆相机中发现了。不同的输入流格式对应的不同的编号可供选择,这样再后续的获取图像指针数据的时候便于解码。
// 配置 SourceReader,并获取分辨率
HRESULT ConfigureSourceReader(IMFSourceReader* pReader, int index_pix, uint32_t& width, uint32_t& height) {
// —— 枚举原生类型 ——
printf("枚举原生媒体类型:\n");
for (DWORD i = 0; ; ++i) {
IMFMediaType* pNative = nullptr;
hr = pReader->GetNativeMediaType(
static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), i, &pNative
);
if (FAILED(hr)) {
printf(" [%u] —— 无更多类型 (hr=0x%08X)\n", i, hr);
break;
}
UINT32 nativeWidth = 0, nativeHeight = 0;
if (SUCCEEDED(MFGetAttributeSize(pNative, MF_MT_FRAME_SIZE, &nativeWidth, &nativeHeight))) {
printf(" [%u] 分辨率: %dx%d\n", i, nativeWidth, nativeHeight);
}
GUID subType = { 0 };
pNative->GetGUID(MF_MT_SUBTYPE, &subType);
WCHAR szGuid[64] = { 0 };
StringFromGUID2(subType, szGuid, ARRAYSIZE(szGuid));
wprintf(L" [%u] subtype = %s\n", i, szGuid);
pNative->Release();
}
// 获取设备原生支持的媒体类型
IMFMediaType* pNativeType = nullptr;
hr = pReader->GetNativeMediaType(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), index_pix, &pNativeType);
if (FAILED(hr)) {
printf("获取设备原生支持的媒体类型失败\n");
return hr;
}
// 直接使用原生格式配置
hr = pReader->SetCurrentMediaType(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), nullptr, pNativeType);
// 获取对应视频流格式的尺寸width, height 修改width, &height参数地址上的值
MFGetAttributeSize(pNativeType, MF_MT_FRAME_SIZE, &width, &height);
pNativeType->Release();
return hr;
}
3. 帧采集模块
关于初始化和释放相关变量这里我省去,核心是获取USB数据部分,这里为了避免一些输入和函数过程中的错误,编写了大量屎山般的if进行判断(原谅如此粗俗的话术)进行返回false。其中核心模块是将byte指针转为char指针。无论是NV12格式的数据以及mjpeg数据的格式似乎都是char,但转uchar的话需要解码。 亮点如下:
- 使用CComPtr自动管理COM对象生命周期
- 严格的缓冲区边界检查
- 安全内存拷贝函数防止溢出
bool get_pix(char** nv12prt, int bufferSize)
{
// 参数有效性检查
if (!nv12prt || !*nv12prt) {
printf("错误:输入指针无效\n");
return false;
}
DWORD streamIndex = 0, flags = 0;
LONGLONG timestamp = 0;
CComPtr<IMFSample> pSample = nullptr;
hr = pReader->ReadSample(static_cast<DWORD>(MF_SOURCE_READER_FIRST_VIDEO_STREAM), 0, &streamIndex, &flags, ×tamp, &pSample);
if (FAILED(hr) || (flags & MF_SOURCE_READERF_ENDOFSTREAM)) {
printf("USB数据帧获取为空\n");
return false;
}
if (!pSample) return false;
CComPtr<IMFMediaBuffer> pBuffer = nullptr;
hr = pSample->ConvertToContiguousBuffer(&pBuffer);
if (FAILED(hr)) return false;
BYTE* pData = nullptr; // 原始数据指针
DWORD maxLen = 0, currLen = 0;
// 添加缓冲区锁定保护
if (FAILED(pBuffer->Lock(&pData, &maxLen, &currLen))) return false;
// 添家缓冲区大小验证
if (currLen > static_cast<DWORD>(bufferSize)) {
printf("缓冲区溢出!需要%u字节,仅提供%u字节\n", currLen, bufferSize);
pBuffer->Unlock();
return false;
}
// 安全内存复制
if (memcpy_s(nv12prt, bufferSize, pData, currLen) != 0) {
printf("内存复制失败\n");
pBuffer->Unlock();
return false;
}
pBuffer->Unlock();
return true;
}
结余
五一假期即将来临,赶在放假前将该项目代码修改完成,于是趁着快下班了编写了这篇博客,在博客中必然存在不够完美的地方,还望各位大佬及时批评指正,编写该文供大家飨食。祝大家节日愉快!