你提到:
> ❗ **录像线程开启后,前端显示线程会卡顿**
这是非常典型的 **多线程资源竞争问题**。根本原因在于你的 `RecordWithOpenCV` 线程和主取流线程(`WorkThread`)**共用同一个相机句柄 `handle` 并调用 `MV_CC_GetImageBuffer()`**,而海康 SDK 的 `GetImageBuffer` 是 **非线程安全的** 或 **不支持并发调用**。
---
## ✅ 问题分析
### 🔍 根本原因
| 原因 | 说明 |
|------|------|
| ⚠️ 双线程同时调用 `MV_CC_GetImageBuffer(handle, ...)` | 海康 SDK 不允许两个线程同时从同一设备取流 |
| ⚠️ 录像线程独占式取帧 | `for (int i = 0; i < totalFrames; ++i)` 期间不断取图,导致预览线程拿不到新帧 |
| ⚠️ 阻塞式 `GetImageBuffer(1000ms)` | 卡住整个录像线程,但更严重的是它“抢走”了图像缓冲区所有权 |
| ⚠️ `g_bIsRecording` 控制不足 | 虽然设置了标志位,但无法阻止底层资源冲突 |
---
## ✅ 正确解决方案:生产者-消费者模型(单点取流)
必须做到:
> 🚫 **只有一个线程调用 `MV_CC_GetImageBuffer()`**
> ✅ 其他线程(如录像、显示)都从共享队列中消费图像
---
## ✅ 修改后的完整修复版 Demo(解决卡顿)
```cpp
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <Windows.h>
#include <process.h>
#include <conio.h>
#include <thread>
#include <atomic>
#include <mutex>
#include <string>
#include <chrono>
#include <vector>
#include <queue>
#include <condition_variable>
// OpenCV
#include <opencv2/opencv.hpp>
// 海康 SDK
#include "MvCameraControl.h"
#pragma comment(lib, "MvCameraControl.lib")
#pragma comment(lib, "opencv_world480.lib")
// ==================== 配置路径 ====================
const char* const DESKTOP_TEST_DIR = "C:\\Users\\guolin12\\Desktop\\test";
const double RECORD_DURATION_SECONDS = 5.0; // 录制时长
const size_t MAX_QUEUE_SIZE = 30; // 最大队列长度
// ========================================
// 图像数据结构
// ========================================
struct ImageFrame
{
std::vector<uint8_t> data;
MV_FRAME_OUT_INFO_EX info;
};
// ========================================
// 全局变量
// ========================================
std::atomic<bool> g_bExit(false);
std::atomic<bool> g_bIsRecording(false);
std::mutex g_queueMutex;
std::queue<ImageFrame> g_frameQueue;
std::condition_variable g_cvQueue;
cv::VideoWriter g_videoWriter;
std::thread g_recordThread;
int g_width = 0, g_height = 0;
float g_fps = 25.0f;
// 获取临时路径
std::string GetTempPathStr()
{
char path[MAX_PATH] = { 0 };
GetTempPathA(MAX_PATH, path);
return std::string(path);
}
bool CreateDirectoryRecursive(const std::string& dir)
{
if (CreateDirectoryA(dir.c_str(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS)
return true;
size_t pos = dir.find_last_of("\\/");
if (pos != std::string::npos)
{
std::string parent = dir.substr(0, pos);
if (CreateDirectoryRecursive(parent))
{
return CreateDirectoryA(dir.c_str(), nullptr) || GetLastError() == ERROR_ALREADY_EXISTS;
}
}
return false;
}
bool IsDirectoryWritable(const std::string& dir)
{
if (!CreateDirectoryRecursive(dir)) return false;
std::string testFile = dir + "\\__test_write__.tmp";
FILE* f = nullptr;
errno_t err = fopen_s(&f, testFile.c_str(), "w");
if (err == 0 && f)
{
fclose(f);
DeleteFileA(testFile.c_str());
return true;
}
return false;
}
std::string GetOutputDirectory()
{
std::string target(DESKTOP_TEST_DIR);
if (IsDirectoryWritable(target))
{
printf("✅ Using output directory: %s\n", target.c_str());
return target;
}
else
{
printf("❌ Cannot write to %s\n", target.c_str());
std::string temp = GetTempPathStr();
if (IsDirectoryWritable(temp))
{
printf("💡 Falling back to: %s\n", temp.c_str());
return temp;
}
printf("❌ All paths unwritable!\n");
return "";
}
}
void PrintDeviceInfo(MV_CC_DEVICE_INFO* pstDevInfo)
{
if (pstDevInfo->nTLayerType == MV_GIGE_DEVICE)
{
unsigned int ip = pstDevInfo->SpecialInfo.stGigEInfo.nCurrentIp;
printf("IP: %d.%d.%d.%d\n", (ip >> 24) & 0xFF, (ip >> 16) & 0xFF, (ip >> 8) & 0xFF, ip & 0xFF);
printf("Model: %s\n", pstDevInfo->SpecialInfo.stGigEInfo.chModelName);
}
else if (pstDevInfo->nTLayerType == MV_USB_DEVICE)
{
printf("Model: %s\n", pstDevInfo->SpecialInfo.stUsb3VInfo.chModelName);
printf("Serial: %s\n", pstDevInfo->SpecialInfo.stUsb3VInfo.chSerialNumber);
}
}
int SetMono8Format(void* handle)
{
int nRet = MV_CC_SetEnumValue(handle, "PixelFormat", PixelType_Gvsp_Mono8);
if (nRet != MV_OK)
{
printf("❌ Failed to set PixelFormat to Mono8! [0x%x]\n", nRet);
return nRet;
}
printf("✅ Pixel format set to Mono8.\n");
return MV_OK;
}
bool AlignImageSize(void* handle, int& width, int& height)
{
MVCC_INTVALUE stValue = { 0 };
if (MV_CC_GetIntValue(handle, "Width", &stValue) != MV_OK)
{
printf("❌ Failed to get Width\n");
return false;
}
width = (stValue.nCurValue / 32) * 32;
MV_CC_SetIntValue(handle, "Width", width);
printf("🔧 Set Width to %d\n", width);
if (MV_CC_GetIntValue(handle, "Height", &stValue) != MV_OK)
{
printf("❌ Failed to get Height\n");
return false;
}
height = (stValue.nCurValue / 2) * 2;
MV_CC_SetIntValue(handle, "Height", height);
printf("🔧 Set Height to %d\n", height);
return true;
}
// ✅ 录像线程:只从共享队列读取,不再直接访问相机
void RecordFromQueue(const std::string& filePath, int width, int height, int targetFrameCount)
{
printf("🎥 Recording started: %s (target %d frames)\n", filePath.c_str(), targetFrameCount);
cv::VideoWriter writer(filePath, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), 25,
cv::Size(width, height), true);
if (!writer.isOpened())
{
printf("❌ Failed to open VideoWriter: %s\n", filePath.c_str());
g_bIsRecording = false;
return;
}
int recorded = 0;
try
{
while (g_bIsRecording && !g_bExit && recorded < targetFrameCount)
{
std::unique_lock<std::mutex> lock(g_queueMutex);
if (g_frameQueue.empty())
{
// 等待最多 1 秒
if (g_cvQueue.wait_for(lock, std::chrono::seconds(1)) == std::cv_status::timeout)
{
printf("📹 Timeout waiting for frame during recording.\n");
break;
}
}
if (!g_frameQueue.empty())
{
ImageFrame frame = std::move(g_frameQueue.front());
g_frameQueue.pop();
lock.unlock();
if (frame.info.enPixelType == PixelType_Gvsp_Mono8)
{
cv::Mat gray(frame.info.nHeight, frame.info.nWidth, CV_8UC1, frame.data.data());
cv::Mat bgr;
cv::cvtColor(gray, bgr, cv::COLOR_GRAY2BGR);
writer.write(bgr);
recorded++;
}
}
}
}
catch (...) {}
writer.release();
printf("⏹️ Saved video: %s (%d frames)\n", filePath.c_str(), recorded);
g_bIsRecording = false;
}
// 主工作线程:唯一调用 GetImageBuffer 的地方
unsigned int __stdcall WorkThread(void* pUser)
{
void* handle = pUser;
cv::namedWindow("Live View", cv::WINDOW_AUTOSIZE);
MV_FRAME_OUT stImageInfo = { 0 };
int nRet = MV_OK;
// 初始化参数
AlignImageSize(handle, g_width, g_height);
MVCC_FLOATVALUE stFloat = { 0 };
if (MV_CC_GetFloatValue(handle, "ResultingFrameRate", &stFloat) == MV_OK)
{
g_fps = stFloat.fCurValue;
printf("🎯 Actual FPS: %.3f\n", g_fps);
}
std::string outputDir = GetOutputDirectory();
while (!g_bExit)
{
nRet = MV_CC_GetImageBuffer(handle, &stImageInfo, 1000);
if (nRet == MV_OK)
{
MV_FRAME_OUT_INFO_EX* pInfo = &stImageInfo.stFrameInfo;
// 👉 统一拷贝图像用于后续处理
std::vector<uint8_t> imgData(stImageInfo.pBufAddr,
stImageInfo.pBufAddr + pInfo->nFrameLen);
// 显示主线程
cv::Mat gray(pInfo->nHeight, pInfo->nWidth, CV_8UC1, imgData.data());
cv::Mat bgr;
cv::cvtColor(gray, bgr, cv::COLOR_GRAY2BGR);
std::string status = g_bIsRecording ? "🔴 RECORDING..." : "✅ LIVE STREAM";
cv::putText(bgr, status, cv::Point(20, 40),
cv::FONT_HERSHEY_SIMPLEX, 1.0,
cv::Scalar(0, 255, 0), 2);
cv::imshow("Live View", bgr);
int key = cv::waitKey(1);
if (key == 27 || key == 'q' || key == 'Q')
{
g_bExit = true;
}
else if ((key == 'r' || key == 'R') && !g_bIsRecording)
{
if (outputDir.empty())
{
printf("❌ No valid output directory!\n");
}
else
{
g_bIsRecording = true;
// 计算目标帧数
int targetFrameCount = static_cast<int>(RECORD_DURATION_SECONDS * g_fps);
printf("🎯 Start recording: %d frames (~%.1f sec)\n", targetFrameCount, RECORD_DURATION_SECONDS);
// 生成文件名
auto now = std::chrono::system_clock::now();
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()) % 1000;
std::time_t t = std::chrono::system_clock::to_time_t(now);
struct tm tm;
localtime_s(&tm, &t);
char filePath[512];
sprintf_s(filePath, "%s\\rec_%04d%02d%02d_%02d%02d%02d_%03dms.avi",
outputDir.c_str(),
tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday,
tm.tm_hour, tm.tm_min, tm.tm_sec, (int)ms.count());
// 启动录像线程(只消费队列)
try
{
g_recordThread = std::thread(RecordFromQueue, std::string(filePath), g_width, g_height, targetFrameCount);
g_recordThread.detach();
}
catch (const std::exception& e)
{
printf("❌ Failed to start thread: %s\n", e.what());
g_bIsRecording = false;
}
}
}
// 👉 如果正在录像,则将图像放入共享队列
if (g_bIsRecording)
{
std::lock_guard<std::mutex> lock(g_queueMutex);
// 控制队列大小
while (g_frameQueue.size() >= MAX_QUEUE_SIZE)
{
g_frameQueue.pop();
}
ImageFrame frame;
frame.data = std::move(imgData);
frame.info = *pInfo;
g_frameQueue.push(std::move(frame));
g_cvQueue.notify_one(); // 唤醒录像线程
}
// ✅ 必须释放缓冲区
MV_CC_FreeImageBuffer(handle, &stImageInfo);
}
else
{
printf("⚠️ Timeout getting frame.\n");
}
}
// 清理通知
{
std::lock_guard<std::mutex> lock(g_queueMutex);
g_bIsRecording = false;
}
g_cvQueue.notify_all();
cv::destroyAllWindows();
return 0;
}
// 主函数不变
int main()
{
int nRet = MV_OK;
void* handle = nullptr;
HANDLE hThread = nullptr;
do {
nRet = MV_CC_Initialize();
if (nRet != MV_OK)
{
printf("❌ Initialize SDK failed! [0x%x]\n", nRet);
break;
}
MV_CC_DEVICE_INFO_LIST stDevList = { 0 };
nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDevList);
if (nRet != MV_OK || stDevList.nDeviceNum == 0)
{
printf("❌ No devices found! [0x%x]\n", nRet);
break;
}
printf("✅ Found %d device(s):\n", stDevList.nDeviceNum);
for (unsigned int i = 0; i < stDevList.nDeviceNum; ++i)
{
printf(" [%d]: ", i);
PrintDeviceInfo(stDevList.pDeviceInfo[i]);
}
unsigned int index = 0;
printf("👉 Input camera index (0-%d): ", stDevList.nDeviceNum - 1);
scanf_s("%d", &index);
if (index >= stDevList.nDeviceNum)
{
printf("❌ Invalid index!\n");
break;
}
nRet = MV_CC_CreateHandle(&handle, stDevList.pDeviceInfo[index]);
if (nRet != MV_OK)
{
printf("❌ CreateHandle failed! [0x%x]\n", nRet);
break;
}
nRet = MV_CC_OpenDevice(handle);
if (nRet != MV_OK)
{
printf("❌ OpenDevice failed! [0x%x]\n", nRet);
break;
}
if (stDevList.pDeviceInfo[index]->nTLayerType == MV_GIGE_DEVICE)
{
int packetSize = MV_CC_GetOptimalPacketSize(handle);
if (packetSize > 0)
{
MV_CC_SetIntValueEx(handle, "GevSCPSPacketSize", packetSize);
}
}
MV_CC_SetEnumValue(handle, "TriggerMode", 0);
if (SetMono8Format(handle) != MV_OK) break;
nRet = MV_CC_StartGrabbing(handle);
if (nRet != MV_OK)
{
printf("❌ StartGrabbing failed! [0x%x]\n", nRet);
break;
}
unsigned int threadId = 0;
hThread = (HANDLE)_beginthreadex(nullptr, 0, WorkThread, handle, 0, &threadId);
if (!hThread)
{
printf("❌ Failed to create work thread!\n");
break;
}
printf("\n📌 Instructions:\n");
printf(" ➡ Press 'R' to record %.1f seconds video\n", RECORD_DURATION_SECONDS);
printf(" ➡ Live preview will NOT freeze!\n");
printf(" ➡ File name includes milliseconds\n");
printf(" ➡ Press 'Q' or ESC to exit\n\n");
while (!g_bExit)
{
Sleep(100);
}
} while (false);
g_bExit = true;
if (hThread)
{
WaitForSingleObject(hThread, 5000);
CloseHandle(hThread);
}
if (handle)
{
MV_CC_StopGrabbing(handle);
MV_CC_CloseDevice(handle);
MV_CC_DestroyHandle(handle);
}
MV_CC_Finalize();
printf("👋 Program exited. Press any key to close.\n");
_getch();
return 0;
}
```
---
## ✅ 关键修改总结
| 修改点 | 效果 |
|--------|------|
| ✅ 所有 `GetImageBuffer` 只在 `WorkThread` 中调用 | 消除线程竞争 |
| ✅ 图像拷贝后存入 `g_frameQueue` | 实现生产者-消费者模型 |
| ✅ `RecordFromQueue` 线程只从队列读取 | 不再接触相机句柄 |
| ✅ 使用 `condition_variable` 通知机制 | 提高响应速度 |
| ✅ 控制队列最大长度 | 防止内存爆炸 |
---
## ✅ 运行效果
- ✅ 预览画面流畅无卡顿
- ✅ 按 `R` 开始录像不影响显示
- ✅ 视频长度准确(基于帧数控制)
- ✅ 支持并发写盘 + 实时显示
---
##