海康威视相机 + opencv初体验
简单实现打开相机的连接,打开,控制相机的帧率,曝光时间等,使用opencv实时处理图片并输出
平台
windows10 + vs2019 + MV-CA050-10GM + HIKCAMERA SDK版本v3.3.0 + OpenCV3.3.1
环境配置
下载
HIKCAMERA SDK下载:https://www.hikrobotics.com/service/download/0/0
下载安装后,会有一个客户端和SDK
我们开发所需要的SDK就在Development文件夹里边,里边有我们所需的头文件、库文件、文档、教程demo等等,是非常好学习资料。
OpenCV3.3.1下载:
配置
配置比较简单,将我们所需的可执行文件、库文件、头文件、附加依赖项包含进来就可以了
可执行文件
这一步也可以在环境变量里添加到PATH里面,主要是为了让OpenCV正常运行。出现错误,重启一次就OK。
头文件
库文件
附加依赖项
开发流程
Development文件夹里边里边的开发指南写的相当完善:
设备连接
对设备进行操作,实现图像采集、参数配置等功能,需要先连接设备(打开设备),具体流程如下图所示。
详细步骤
- (可选)调用 MV_CC_EnumDevices() 枚举子网内指定传输协议对应的所有设备。 可通过nTLayerType在结构 MV_CC_DEVICE_INFO() 中获取设备信息。
- (可选)在打开指定设备前,调用 MV_CC_IsDeviceAccessible() 检查指定设备是否可访问。
- 调用 MV_CC_CreateHandle() 创建设备句柄。
- 调用 MV_CC_OpenDevice() 打开设备。
- (可选)调用 MV_CC_GetAllMatchInfo()以获取设备信息。
- 调用 MV_CC_CloseDevice() 关闭设备。
- 调用 MV_CC_DestroyHandle() 销毁句柄并释放资源。
主动取流流程
SDK提供主动获取图像的接口,用户可以在开启取流后直接调用此接口获取图像,也可以使用异步方式(线程、定时器等)获取图像。
主动获取图像有两种方式(两种方式不能同时使用):
- 方式一:调用 MV_CC_StartGrabbing() 开始采集,需要自己开启一个buffer,然后在应用层循环调用 MV_CC_GetOneFrameTimeout() 获取指定像素格式的帧数据,获取帧数据时上层应用程序需要根据帧率控制好调用该接口的频率。
- 方式二:调用 MV_CC_StartGrabbing() 开始采集,然后在应用层调用 MV_CC_GetImageBuffer() 获取指定像素格式的帧数据,然后调用 MV_CC_FreeImageBuffer() 释放buffer,获取帧数据时上层应用程序需要根据帧率控制好调用该接口的频率。
详细步骤
- (可选)调用 MV_CC_EnumDevices() 枚举子网内指定传输协议对应的所有设备。 可通过nTLayerType在结构 MV_CC_DEVICE_INFO() 中获取设备信息。
- (可选)打开指定设备前,调用 MV_CC_IsDeviceAccessible() 检查指定设备是否可访问。
- 调用 MV_CC_CreateHandle() 创建设备句柄。
- 调用 MV_CC_OpenDevice() 打开设备。
- (可选)执行以下一个或多个操作以获取/设置相机不同类型的参数。
• 获取/设置Int类型节点值 调用 MV_CC_GetIntValue() / MV_CC_SetIntValue()
• 获取/设置Float类型节点值 调用 MV_CC_GetFloatValue() / MV_CC_SetFloatValue()
• 获取/设置Enum类型节点值 调用 MV_CC_GetEnumValue() / MV_CC_SetEnumValue()
• 获取/设置Bool类型节点值 调用 MV_CC_GetBoolValue() / MV_CC_SetBoolValue()
• 获取/设置String类型节点值 调用 MV_CC_GetStringValue() / MV_CC_SetStringValue()
• 设置Command类型节点值 调用 MV_CC_SetCommandValue() - 图像采集:
• (可选)调用 MV_CC_SetImageNodeNum() 设置图像缓存节点个数。当获取的图像数超过这个设定值,最早的图像数据会被自动丢弃。
• 调用 MV_CC_StartGrabbing() 开始取流。
• 对于原始图像数据,可调用 MV_CC_ConvertPixelType() 转换图像的像素格式,也可调用 MV_CC_SaveImage() 转换成JPEG或BMP格式的图片,并保存成图片文件。
• 在应用程序层中重复调用 MV_CC_GetOneFrameTimeout() 来获取图片数据。 - 调用 MV_CC_StopGrabbing() 停止采集。
- 调用 MV_CC_CloseDevice() 关闭设备。
- 调用 MV_CC_DestroyHandle() 销毁句柄并释放资源。
下面尝试使用第一种方式进行主动获取图片
代码
#include <stdio.h>
#include <process.h>
#include <conio.h>
#include "windows.h"
#include "MvCameraControl.h"
#include "opencv2/imgproc/imgproc.hpp"
#include "opencv2/highgui/highgui.hpp"
#include "string.h"
#include "time.h"
HWND g_hwnd = NULL;
bool g_bExit = false;
unsigned int g_nPayloadSize = 0;
// 以时间作为图片名 精确到毫秒
std::string GetTimeAsFileName()
{
SYSTEMTIME st = { 0 };
GetLocalTime(&st); //获取当前时间 可精确到ms
char filename[100] = { 0 };
sprintf(filename, "pictures_src/%d%02d%02d_%02d%02d%02d%03d.bmp", st.wYear, st.wMonth, st.wDay, st.wHour, st.wMinute, st.wSecond, st.wMilliseconds);
return filename;
}
// ch:等待按键输入 | en:Wait for key press
void WaitForKeyPress(void)
{
while (!_kbhit())
{
Sleep(10);
}
_getch();
}
bool PrintDeviceInfo(MV_CC_DEVICE_INFO* pstMVDevInfo)
{
if (NULL == pstMVDevInfo)
{
printf("The Pointer of pstMVDevInfo is NULL!\n");
return false;
}
if (pstMVDevInfo->nTLayerType == MV_GIGE_DEVICE)
{
int nIp1 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0xff000000) >> 24);
int nIp2 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x00ff0000) >> 16);
int nIp3 = ((pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x0000ff00) >> 8);
int nIp4 = (pstMVDevInfo->SpecialInfo.stGigEInfo.nCurrentIp & 0x000000ff);
// ch:打印当前相机ip和用户自定义名字 | en:print current ip and user defined name
printf("CurrentIp: %d.%d.%d.%d\n", nIp1, nIp2, nIp3, nIp4);
printf("UserDefinedName: %s\n\n", pstMVDevInfo->SpecialInfo.stGigEInfo.chUserDefinedName);
printf("DeviceVersion: %s\n\n", pstMVDevInfo->SpecialInfo.stGigEInfo.chDeviceVersion);
printf("ManufacturerName: %s\n\n", pstMVDevInfo->SpecialInfo.stGigEInfo.chManufacturerName);
}
else if (pstMVDevInfo->nTLayerType == MV_USB_DEVICE)
{
printf("UserDefinedName: %s\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.chUserDefinedName);
printf("Serial Number: %s\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.chSerialNumber);
printf("Device Number: %d\n\n", pstMVDevInfo->SpecialInfo.stUsb3VInfo.nDeviceNumber);
}
else
{
printf("Not support.\n");
}
return true;
}
int RGB2BGR(unsigned char* pRgbData, unsigned int nWidth, unsigned int nHeight)
{
if (NULL == pRgbData)
{
return MV_E_PARAMETER;
}
for (unsigned int j = 0; j < nHeight; j++)
{
for (unsigned int i = 0; i < nWidth; i++)
{
unsigned char red = pRgbData[j * (nWidth * 3) + i * 3];
pRgbData[j * (nWidth * 3) + i * 3] = pRgbData[j * (nWidth * 3) + i * 3 + 2];
pRgbData[j * (nWidth * 3) + i * 3 + 2] = red;
}
}
return MV_OK;
}
// convert data stream in Mat format
bool Convert2Mat(MV_FRAME_OUT_INFO_EX* pstImageInfo, unsigned char* pData, cv::Mat& mat_img)
{
cv::Mat srcImage;
if (pstImageInfo->enPixelType == PixelType_Gvsp_Mono8)
{
srcImage = cv::Mat(pstImageInfo->nHeight, pstImageInfo->nWidth, CV_8UC1, pData);
}
else if (pstImageInfo->enPixelType == PixelType_Gvsp_RGB8_Packed)
{
RGB2BGR(pData, pstImageInfo->nWidth, pstImageInfo->nHeight);
srcImage = cv::Mat(pstImageInfo->nHeight, pstImageInfo->nWidth, CV_8UC3, pData);
}
else
{
printf("unsupported pixel format\n");
return false;
}
if (NULL == srcImage.data)
{
return false;
}
//save converted image in a local file
std::string filename = GetTimeAsFileName();
try {
#if defined (VC9_COMPILE)
cvSaveImage(filename, &(IplImage(srcImage)));
#else
//cv::imwrite(filename, srcImage);
#endif
}
catch (cv::Exception& ex) {
fprintf(stderr, "Exception saving image to bmp format: %s\n", ex.what());
}
mat_img = srcImage.clone();
srcImage.release();
return true;
}
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
g_hwnd = NULL;
break;
}
return DefWindowProc(hWnd, msg, wParam, lParam);
}
static unsigned int __stdcall CreateRenderWindow(void* pUser)
{
HINSTANCE hInstance = ::GetModuleHandle(NULL); //获取应用程序的模块句柄
WNDCLASSEX wc;
wc.cbSize = sizeof(wc);
wc.style = CS_HREDRAW | CS_VREDRAW; //窗口的风格
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = ::LoadIcon(NULL, IDI_APPLICATION); //图标风格
wc.hIconSm = ::LoadIcon(NULL, IDI_APPLICATION);
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //背景色
wc.hCursor = ::LoadCursor(NULL, IDC_ARROW); //鼠标风格
wc.lpfnWndProc = WndProc; //自定义消息处理函数
wc.lpszMenuName = NULL;
wc.lpszClassName = "RenderWindow"; //该窗口类的名称
if (!RegisterClassEx(&wc))
{
return 0;
}
DWORD style = WS_OVERLAPPEDWINDOW;
DWORD styleEx = WS_EX_APPWINDOW | WS_EX_WINDOWEDGE;
RECT rect = { 0, 0, 640, 480 };
AdjustWindowRectEx(&rect, style, false, styleEx);
HWND hWnd = CreateWindowEx(styleEx, "RenderWindow", "Display", style, 0, 0,
rect.right - rect.left, rect.bottom - rect.top, NULL, NULL, hInstance, NULL);
if (hWnd == NULL)
{
return 0;
}
::UpdateWindow(hWnd);
::ShowWindow(hWnd, SW_SHOW);
g_hwnd = hWnd;
MSG msg = { 0 };
while (msg.message != WM_QUIT)
{
if (PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
static unsigned int __stdcall WorkThread(void* pUser)
{
int nRet = MV_OK;
MV_FRAME_OUT_INFO_EX stImageInfo = { 0 };
MV_DISPLAY_FRAME_INFO stDisplayInfo = { 0 };
unsigned char* pData = (unsigned char*)malloc(sizeof(unsigned char) * (g_nPayloadSize));
if (pData == NULL)
{
return 0;
}
unsigned int nDataSize = g_nPayloadSize;
cv::namedWindow("test_img", cv::WINDOW_FREERATIO);
cv::namedWindow("edge_img", cv::WINDOW_FREERATIO);
for (int i = 0; ; i++)
{
cv::waitKey(0);
nRet = MV_CC_GetOneFrameTimeout(pUser, pData, nDataSize, &stImageInfo, 1000);
if (nRet == MV_OK)
{
printf("Get One Frame: Width[%d], Height[%d], nFrameNum[%d]\n",
stImageInfo.nWidth, stImageInfo.nHeight, stImageInfo.nFrameNum);
if (g_hwnd)
{
stDisplayInfo.hWnd = g_hwnd;
stDisplayInfo.pData = pData;
stDisplayInfo.nDataLen = stImageInfo.nFrameLen;
stDisplayInfo.nWidth = stImageInfo.nWidth;
stDisplayInfo.nHeight = stImageInfo.nHeight;
stDisplayInfo.enPixelType = stImageInfo.enPixelType;
MV_CC_DisplayOneFrame(pUser, &stDisplayInfo);
// 数据去转换
bool bConvertRet = false;
cv::Mat mat_img, threshold_img, edge, gray;
bConvertRet = Convert2Mat(&stImageInfo, pData, mat_img);
// print result
if (bConvertRet)
{
printf("OpenCV format convert finished.\n");
cv::threshold(mat_img, threshold_img, 100, 255, cv::THRESH_BINARY);
//cv::cvtColor(mat_img, gray, cv::COLOR_BGR2GRAY);
cv::Canny(mat_img, edge, 100, 200, 3);
cv::imshow("test_img", threshold_img);
cv::imshow("edge_img", edge);
/* free(pData);
pData = NULL;*/
}
else
{
printf("OpenCV format convert failed.\n");
free(pData);
/* pData = NULL;
break;*/
}
}
}
else
{
printf("No data[0x%x]\n", nRet);
}
if (g_bExit)
{
break;
}
}
cv::destroyAllWindows();
free(pData);
return 0;
}
int main()
{
int nRet = MV_OK; //
void* handle = NULL;
do
{
// 获取SDK版本信息
unsigned int sdk_V = MV_CC_GetSDKVersion();
printf("SDK version is [0x%x]\n", sdk_V);
// 获取支持的传输层
int tls = MV_CC_EnumerateTls();
printf("surpport tls is: [%d]\n", tls);
// 根据厂商名字枚举设备
/* MV_CC_DEVICE_INFO_LIST stDeviceList_ex = { 0 };
nRet = MV_CC_EnumDevicesEx(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList_ex, "hikvision");
if (MV_OK != nRet)
{
printf("Enum Devices with ManufacturerName fail! nRet [0x%x]\n", nRet);
break;
}*/
// ch:枚举设备 | en:Enum device
MV_CC_DEVICE_INFO_LIST stDeviceList = { 0 };
nRet = MV_CC_EnumDevices(MV_GIGE_DEVICE | MV_USB_DEVICE, &stDeviceList);
if (MV_OK != nRet)
{
printf("Enum Devices fail! nRet [0x%x]\n", nRet);
break;
}
if (stDeviceList.nDeviceNum > 0)
{
for (unsigned int i = 0; i < stDeviceList.nDeviceNum; i++)
{
printf("[device %d]:\n", i);
MV_CC_DEVICE_INFO* pDeviceInfo = stDeviceList.pDeviceInfo[i];
/* bool access = MV_CC_IsDeviceAccessible(pDeviceInfo, 1);
if (!access)
{
printf("[device %d]:could not access... nRet:[0x%x]\n", i, nRet);
}*/
if (NULL == pDeviceInfo)
{
break;
}
PrintDeviceInfo(pDeviceInfo);
}
}
else
{
printf("Find No Devices!\n");
break;
}
printf("Please Input camera index:");
unsigned int nIndex = 0;
scanf_s("%d", &nIndex);
if (nIndex >= stDeviceList.nDeviceNum)
{
printf("Input error!\n");
break;
}
// ch:选择设备并创建句柄 | en:Select device and create handle
nRet = MV_CC_CreateHandle(&handle, stDeviceList.pDeviceInfo[nIndex]);
if (MV_OK != nRet)
{
printf("Create Handle fail! nRet [0x%x]\n", nRet);
break;
}
// ch:打开设备 | en:Open device
nRet = MV_CC_OpenDevice(handle);
if (MV_OK != nRet)
{
printf("Open Device fail! nRet [0x%x]\n", nRet);
break;
}
// 设置相机常见参数
nRet = MV_CC_SetFrameRate(handle, 10.f);
if (MV_OK != nRet)
{
printf("set FrameRate fail! nRet [0x%x]\n", nRet);
break;
}
nRet = MV_CC_SetExposureTime(handle, 2000.f);
if (MV_OK != nRet)
{
printf("set Exposure Time fail! nRet [0x%x]\n", nRet);
break;
}
// 保存相机参数
printf("Start export the camera properties to the file\n");
printf("Wait......\n");
nRet = MV_CC_FeatureSave(handle, "FeatureFile.ini");
if (MV_OK != nRet)
{
printf("Save Feature fail! nRet [0x%x]\n", nRet);
break;
}
printf("Finish export the camera properties to the file\n\n");
// ch:探测网络最佳包大小(只对GigE相机有效) | en:Detection network optimal package size(It only works for the GigE camera)
if (stDeviceList.pDeviceInfo[nIndex]->nTLayerType == MV_GIGE_DEVICE)
{
int nPacketSize = MV_CC_GetOptimalPacketSize(handle);
if (nPacketSize > 0)
{
nRet = MV_CC_SetIntValue(handle, "GevSCPSPacketSize", nPacketSize);
if (nRet != MV_OK)
{
printf("Warning: Set Packet Size fail nRet [0x%x]!", nRet);
}
}
else
{
printf("Warning: Get Packet Size fail nRet [0x%x]!", nPacketSize);
}
}
// ch:设置触发模式为off | en:Set trigger mode as off
nRet = MV_CC_SetEnumValue(handle, "TriggerMode", 0);
if (MV_OK != nRet)
{
printf("Set Trigger Mode fail! nRet [0x%x]\n", nRet);
break;
}
// ch:获取数据包大小 | en:Get payload size
MVCC_INTVALUE stParam = { 0 };
nRet = MV_CC_GetIntValue(handle, "PayloadSize", &stParam);
if (MV_OK != nRet)
{
printf("Get PayloadSize fail! nRet [0x%x]\n", nRet);
break;
}
g_nPayloadSize = stParam.nCurValue;
unsigned int nThreadID = 0;
void* hCreateWindow = (void*)_beginthreadex(NULL, 0, CreateRenderWindow, handle, 0, &nThreadID);
if (NULL == hCreateWindow)
{
break;
}
// ch:开始取流 | en:Start grab image
nRet = MV_CC_StartGrabbing(handle);
if (MV_OK != nRet)
{
printf("Start Grabbing fail! nRet [0x%x]\n", nRet);
break;
}
nThreadID = 0;
void* hThreadHandle = (void*)_beginthreadex(NULL, 0, WorkThread, handle, 0, &nThreadID);
if (NULL == hThreadHandle)
{
break;
}
printf("Press a key to stop grabbing.\n");
WaitForKeyPress();
g_bExit = true;
WaitForSingleObject(hThreadHandle, INFINITE);
CloseHandle(hThreadHandle);
// ch:停止取流 | en:Stop grab image
nRet = MV_CC_StopGrabbing(handle);
if (MV_OK != nRet)
{
printf("Stop Grabbing fail! nRet [0x%x]\n", nRet);
break;
}
// ch:关闭设备 | Close device
nRet = MV_CC_CloseDevice(handle);
if (MV_OK != nRet)
{
printf("ClosDevice fail! nRet [0x%x]\n", nRet);
break;
}
// ch:销毁句柄 | Destroy handle
nRet = MV_CC_DestroyHandle(handle);
if (MV_OK != nRet)
{
printf("Destroy Handle fail! nRet [0x%x]\n", nRet);
break;
}
} while (0);
if (nRet != MV_OK)
{
if (handle != NULL)
{
MV_CC_DestroyHandle(handle);
handle = NULL;
}
}
printf("Press a key to exit.\n");
WaitForKeyPress();
return 0;
}
代码效果
选择好设备序号,我目前只有一个相机,所以设备号为0,按下回车,把鼠标放在窗口上边,按一下enter或空格取一张图片,display显示原图,剩下两个窗口分别显示二值化图和通过canny提取的边缘轮廓图。