#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>
// 海康威视 MVS SDK 头文件
#include "MvCameraControl.h"
#pragma comment(lib, "MvCameraControl.lib") // 链接海康 SDK 库
#pragma comment(lib, "opencv_world480.lib") // 请根据实际 OpenCV 版本修改此行
// ==================== 配置参数 ====================
const char* const DESKTOP_TEST_DIR = "C:\\Users\\guolin12\\Desktop\\test";
const double RECORD_DURATION_SECONDS = 10.0;
const size_t MAX_QUEUE_SIZE = 30;
// ========================================
// 自定义图像帧结构体
// ========================================
struct ImageFrame {
std::vector<uint8_t> data;
MV_FRAME_OUT_INFO_EX info;
std::chrono::steady_clock::time_point timestamp;
};
// ========================================
// 全局变量
// ========================================
std::atomic<bool> g_bExit(false);
std::atomic<bool> g_bIsRecording(false);
std::atomic<unsigned int> g_pixelFormat(PixelType_Gvsp_Mono8);
std::mutex g_queueMutex;
std::queue<ImageFrame> g_frameQueue;
std::condition_variable g_cvQueue;
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);
}
}
// 支持的格式优先级:RGB8 > BayerRG8 > Mono8
static const unsigned int SUPPORTED_PIXEL_FORMATS[] = {
PixelType_Gvsp_RGB8_Packed,
PixelType_Gvsp_BayerRG8,
PixelType_Gvsp_Mono8
};
static const int NUM_SUPPORTED_FORMATS = 3;
int SetPreferredPixelFormat(void* handle, unsigned int& selectedFormat) {
for (int i = 0; i < NUM_SUPPORTED_FORMATS; ++i) {
unsigned int fmt = SUPPORTED_PIXEL_FORMATS[i];
int nRet = MV_CC_SetEnumValue(handle, "PixelFormat", fmt);
if (nRet == MV_OK) {
const char* name = "";
switch (fmt) {
case PixelType_Gvsp_Mono8: name = "Mono8"; break;
case PixelType_Gvsp_BayerRG8: name = "BayerRG8"; break;
case PixelType_Gvsp_RGB8_Packed: name = "RGB8"; break;
default: name = "Unknown";
}
printf("✅ Set PixelFormat to %s\n", name);
selectedFormat = fmt;
return MV_OK;
}
}
printf("❌ No supported pixel format available!\n");
return 0;
}
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;
}
cv::Mat ConvertToBGR(const uint8_t* pData, int width, int height, unsigned int pixelFormat) {
cv::Mat bgr;
switch (pixelFormat) {
case PixelType_Gvsp_Mono8:
{
cv::Mat gray(height, width, CV_8UC1, const_cast<uint8_t*>(pData));
cv::cvtColor(gray, bgr, cv::COLOR_GRAY2BGR);
break;
}
case PixelType_Gvsp_BayerRG8:
{
cv::Mat bayer(height, width, CV_8UC1, const_cast<uint8_t*>(pData));
cv::cvtColor(bayer, bgr, cv::COLOR_BayerRG2BGR);
break;
}
case PixelType_Gvsp_RGB8_Packed:
{
cv::Mat rgb(height, width, CV_8UC3, const_cast<uint8_t*>(pData));
cv::cvtColor(rgb, bgr, cv::COLOR_RGB2BGR);
break;
}
default:
printf("⚠️ Unsupported pixel format: 0x%x\n", pixelFormat);
bgr = cv::Mat::zeros(height, width, CV_8UC3);
break;
}
return bgr;
}
// ========================================
// 录像线程函数
// ========================================
void RecordFromQueue(const std::string& filePath, int width, int height, int targetFrameCount, float outputFps) {
printf("🎥 Recording started: %s\n", filePath.c_str());
printf("🎯 Target frames: %d, Output FPS: %.3f → Duration: %.2f sec\n",
targetFrameCount, outputFps, targetFrameCount / outputFps);
cv::VideoWriter writer(filePath, cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), outputFps,
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;
auto start = std::chrono::steady_clock::now();
try {
while (g_bIsRecording && !g_bExit && recorded < targetFrameCount) {
std::unique_lock<std::mutex> lock(g_queueMutex);
auto timeout = std::chrono::steady_clock::now() + std::chrono::seconds(3);
if (g_frameQueue.empty()) {
if (g_cvQueue.wait_until(lock, timeout) == 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();
cv::Mat bgr = ConvertToBGR(frame.data.data(), frame.info.nWidth, frame.info.nHeight, frame.info.enPixelType);
if (bgr.empty()) continue;
writer.write(bgr);
recorded++;
}
}
}
catch (const std::exception& e) {
printf("❌ Exception during recording: %s\n", e.what());
}
catch (...) {
printf("❌ Unknown error during recording.\n");
}
writer.release();
auto durationMs = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start).count();
printf("⏹️ Saved video: %s\n", filePath.c_str());
printf("📊 Wrote %d frames at %.3f FPS → Play time: %.2f sec\n",
recorded, outputFps, recorded / outputFps);
printf("⏱️ Actual process time: %.3f sec\n", durationMs / 1000.0);
g_bIsRecording = false;
}
// ========================================
// 主工作线程
// ========================================
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);
// 👉 最大可能的数据大小(按 RGB 保守估计)
size_t maxRotatedDataSize = g_width * g_height * 3;
std::vector<uint8_t> rotatedBuffer(maxRotatedDataSize);
MVCC_FLOATVALUE stFloat = { 0 };
if (MV_CC_GetFloatValue(handle, "ResultingFrameRate", &stFloat) == MV_OK) {
g_fps = stFloat.fCurValue;
printf("🎯 Actual Camera FPS: %.3f\n", g_fps);
}
else {
printf("⚠️ Could not get FPS, using default %.2f\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;
// 👉 计算源图像数据大小
auto GetImageSize = [](int w, int h, unsigned int fmt) -> size_t {
switch (fmt) {
case PixelType_Gvsp_Mono8:
case PixelType_Gvsp_BayerGR8:
case PixelType_Gvsp_BayerRG8:
case PixelType_Gvsp_BayerGB8:
case PixelType_Gvsp_BayerBG8:
return static_cast<size_t>(w) * h;
case PixelType_Gvsp_RGB8_Packed:
return static_cast<size_t>(w) * h * 3;
case PixelType_Gvsp_RGBA8_Packed:
return static_cast<size_t>(w) * h * 4;
default:
return static_cast<size_t>(w) * h;
}
};
size_t srcLen = GetImageSize(pInfo->nWidth, pInfo->nHeight, pInfo->enPixelType);
// 👉 初始化旋转参数
MV_CC_ROTATE_IMAGE_PARAM rotateParam ;
rotateParam.enPixelType = static_cast<MvGvspPixelType>(pInfo->enPixelType);
rotateParam.nWidth = pInfo->nWidth;
rotateParam.nHeight = pInfo->nHeight;
rotateParam.pSrcData = stImageInfo.pBufAddr;
rotateParam.nSrcDataLen = static_cast<unsigned int>(srcLen);
rotateParam.pDstBuf = rotatedBuffer.data();
rotateParam.nDstBufSize = static_cast<unsigned int>(rotatedBuffer.size());
rotateParam.enRotationAngle = MV_IMAGE_ROTATE_90; // 90度旋转:新宽=原高,新高=原宽
// 👉 执行旋转
nRet = MV_CC_RotateImage(handle, &rotateParam);
if (nRet != MV_OK) {
printf("❌ MV_CC_RotateImage failed! [0x%x]\n", nRet);
MV_CC_FreeImageBuffer(handle, &stImageInfo);
continue;
}
// ✅ 关键:使用 SDK 返回的实际输出尺寸和长度
int outWidth = rotateParam.nWidth;
int outHeight = rotateParam.nHeight;
size_t dataSize = static_cast<size_t>(rotateParam.nDstBufLen);
// ✅ 安全检查
if (dataSize == 0 || dataSize > rotatedBuffer.size()) {
printf("❌ Invalid rotated image size: %zu bytes\n", dataSize);
MV_CC_FreeImageBuffer(handle, &stImageInfo);
continue;
}
// ✅ 提取有效数据
std::vector<uint8_t> imgData(rotatedBuffer.begin(), rotatedBuffer.begin() + dataSize);
// ✅ 转换为 BGR 显示
cv::Mat bgr = ConvertToBGR(imgData.data(), outWidth, outHeight, pInfo->enPixelType);
if (!bgr.empty()) {
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);
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 {
std::thread t(RecordFromQueue,
std::string(filePath),
outWidth,
outHeight,
targetFrameCount,
g_fps);
t.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;
frame.info.nWidth = outWidth; // 更新为旋转后尺寸
frame.info.nHeight = outHeight;
frame.timestamp = std::chrono::steady_clock::now();
g_frameQueue.push(std::move(frame));
g_cvQueue.notify_one();
}
MV_CC_FreeImageBuffer(handle, &stImageInfo);
}
else {
printf("⚠️ Timeout getting frame from camera.\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); // 关闭触发,连续采集
unsigned int chosenFormat = 0;
if (SetPreferredPixelFormat(handle, chosenFormat) != MV_OK) break;
g_pixelFormat = chosenFormat;
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(" ➡ Output FPS matches actual camera FPS\n");
printf(" ➡ File name includes milliseconds to avoid overwrite\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;
}
旋转是在哪一步进行,需要增加按钮使能控制是否旋转,并代码补全注释
最新发布