1.背景
很久以前,实验室就买了个海康威视的工业相机,型号:MV-CA013-21UC,产品页面是这里。传感器本体如下。 镜头以及传感器如下。 最终的组合体如下。 这种工业相机是需要通过专门的SDK进行数据采集的,不能像普通的USB相机一样利用OpenCV或者V4L2直接获取。之前一直比较忙,没时间整理一下它的使用方法。这篇博客就简单记录一下。
2.硬件介绍
2.1 成像传感器
以下数据均来自产品页面官网。
- 传感器型号:Onsemi(安森美) PYTHON1300
- 传感器类型:CMOS
- 传感器快门:全局快门
- 像元尺寸:4.8 µm × 4.8 µm
- 像素个数:130万
- 感光面积:1/2”
- 动态范围:59.6 dB
- 曝光时间:65 µs - 10 sec
- 像素格式:Mono 8/10/12; Bayer RG 8/10/10Packed/12/12Packed; YUYV422Packed, YUYV422_YUYV_Packed; RGB8, BGR8
- 最大帧率:210FPS @ 1280 × 1024
- 典型功耗:3W @ 5V DC (USB供电)
2.2 镜头
MV-CA013-21UC镜头接口是C-Mount。我们选用的是一款可变焦、可调光圈的摄像头,如下图所示。 焦距变化范围为2.8-12mm,最大光圈支持F1.6(镜头上的C表示Close,即完全关闭,光线无法通过)。镜头上的“TELE-WIDE”调节环用于变化焦距,“OPEN-CLOSE”调节环调节光圈,“NEAR-FAR”调节焦距。采用物理转动的方式调节,可以无级调节,没有档位。当然了,这种摄像头都不支持自动对焦、调光圈。另外可能需要说明的一点是变焦和调焦是两个不同的概念。变焦是指改变镜头光学系统的焦距,进而改变一些光学特性;而调焦则是指在焦距固定的情况下,调整像平面,使得其刚好处于焦点位置(此时画面最清晰,也就是所谓的“对上焦了”)。很多人容易弄混这两个概念。比如很多手机都是定焦镜头,但我们依然要对焦(调焦)。
3.快速上手
去海康机器人官网的下载中心,下载机器视觉工业相机客户端MVS软件,一路安装即可,如下。 可以看到,MVS不仅安装了应用软件,还安装了相机的驱动,而这也是之后二次开发必须的。安装好以后,连接上相机,打开MVS软件,如下。 正常情况下就能在左侧的设备列表里找到我们刚刚连接的设备了。双击即可进入设备查看界面。点击工具栏的“播放按钮”即可实时预览相机画面,如下。 如果此时预览画面一片黑,请确认数据流是否在正常读取。如果是的话,那么可能是镜头盖没打开,或者光圈关闭了。在预览的时候,点击工具栏的“相机”按钮即可抓拍图像,如下。 此外,在窗口的右边还有非常多的相机属性设置,这里就不再介绍了,可以按需调整,如下。
至此,我们就完成了最基本的相机使用,验证了整个流程是正常的。
4.更进一步
除了通过官方提供的MVS软件获取相机数据,我们还可以通过官方提供的SDK以代码的方式进行二次开发。在安装好MVS软件以后,在你的开始菜单中应该会多出来一个叫做MVS SDK Examples的文件夹,如下。 我们可以根据自己的需求去学习对应的示例,基本就可以把一些想要的功能实现了。此外,在MVS的安装目录下,还有一个Documentations的文件夹里面有更多开发文档,如下所示。 其中可以重点关注的就是《工业相机SDK开发指南(C)》,这里面详细记录了一些基本的开发流程、API,可以学习阅读或者作为工具查询,如下。 在这里,我们先给出最简单的例子,在后面部分再进一步分析API。
4.1 官方Python示例
直接运行Python示例文件夹下的BasicDemo.py,有如下界面。 点击“Enum Devices”查询可用设备列表,然后点击“Open Device”打开设备,点击“Start Grabbing”获取实时预览数据,如下。
4.2 自己的例子
事实上,上面的例子代码有些复杂。因为涉及到了界面还有各种参数的读取、设置等。如果只是想“打开相机-获取数据-展示影像”,代码会简单的多。这部分主要参考了Python的GrabImage.py示例、C++的GrabImage_Display示例,并进行了一定程度的简化,把一些容错操作去掉了。尽可能用最少的代码展示核心步骤。
import sys
import threading
import msvcrt
import cv2
import numpy as np
from ctypes import *
# 指定SDK路径,根据自身情况修改
SDK_path = "./SDK"
sys.path.append(SDK_path)
from MvCameraControl_class import *
# 用于控制退出循环的变量
g_bExit = False
# 将二进制的影像数据转换成numpy的矩阵,方便后处理
def buffer2numpy(data, nWidth, nHeight):
data_ = np.frombuffer(data, count=int(nWidth * nHeight * 3), dtype=np.uint8, offset=0)
data_r = data_[0:nWidth * nHeight * 3:3]
data_g = data_[1:nWidth * nHeight * 3:3]
data_b = data_[2:nWidth * nHeight * 3:3]
data_r_arr = data_r.reshape(nHeight, nWidth)
data_g_arr = data_g.reshape(nHeight, nWidth)
data_b_arr = data_b.reshape(nHeight, nWidth)
numArray = np.zeros([nHeight, nWidth, 3], "uint8")
numArray[:, :, 0] = data_r_arr
numArray[:, :, 1] = data_g_arr
numArray[:, :, 2] = data_b_arr
return numArray
# 定义一个线程,专门用于接收数据流,和主线程分开
def work_thread(cam=0, pData=0, nDataSize=0):
stOutFrame = MV_FRAME_OUT()
buf_cache = None
img_buff = None
while True:
# 获取影像缓冲数据
ret = cam.MV_CC_GetImageBuffer(stOutFrame, 1000)
if None != stOutFrame.pBufAddr and 0 == ret:
# 输出影像长、宽等信息
print("get one frame: Width[%d], Height[%d], nFrameNum[%d]" % (
stOutFrame.stFrameInfo.nWidth, stOutFrame.stFrameInfo.nHeight, stOutFrame.stFrameInfo.nFrameNum))
# 将缓冲数据的内存地址赋给buf_cache
buf_cache = (c_ubyte * stOutFrame.stFrameInfo.nFrameLen)()
cdll.msvcrt.memcpy(byref(buf_cache), stOutFrame.pBufAddr, stOutFrame.stFrameInfo.nFrameLen)
n_save_image_size = stOutFrame.stFrameInfo.nWidth * stOutFrame.stFrameInfo.nHeight * 3 + 2048
if img_buff is None:
img_buff = (c_ubyte * n_save_image_size)()
# 转换像素格式为RGB
stConvertParam = MV_CC_PIXEL_CONVERT_PARAM()
memset(byref(stConvertParam), 0, sizeof(stConvertParam))
stConvertParam.nWidth = stOutFrame.stFrameInfo.nWidth
stConvertParam.nHeight = stOutFrame.stFrameInfo.nHeight
stConvertParam.pSrcData = cast(buf_cache, POINTER(c_ubyte))
stConvertParam.nSrcDataLen = stOutFrame.stFrameInfo.nFrameLen
stConvertParam.enSrcPixelType = stOutFrame.stFrameInfo.enPixelType
nConvertSize = stOutFrame.stFrameInfo.nWidth * stOutFrame.stFrameInfo.nHeight * 3
stConvertParam.enDstPixelType = PixelType_Gvsp_RGB8_Packed
stConvertParam.pDstBuffer = (c_ubyte * nConvertSize)()
stConvertParam.nDstBufferSize = nConvertSize
cam.MV_CC_ConvertPixelType(stConvertParam)
# 将转换后的RGB数据拷贝给img_buff
cdll.msvcrt.memcpy(byref(img_buff), stConvertParam.pDstBuffer, nConvertSize)
# 将二进制的img_buff转换为numpy矩阵
numArray = buffer2numpy(img_buff, stOutFrame.stFrameInfo.nWidth, stOutFrame.stFrameInfo.nHeight)
numArray_bgr = cv2.cvtColor(numArray, cv2.COLOR_RGB2BGR)
cv2.imshow("frame", numArray_bgr)
cv2.waitKey(10)
# 最后释放缓冲区
cam.MV_CC_FreeImageBuffer(stOutFrame)
if g_bExit == True:
break
if __name__ == "__main__":
# step1 获取可用设备列表并指定设备类型
deviceList = MV_CC_DEVICE_INFO_LIST()
tlayerType = MV_USB_DEVICE
ret = MvCamera.MV_CC_EnumDevices(tlayerType, deviceList)
# 默认使用第一个可用设备
print("Find %d devices!" % deviceList.nDeviceNum)
input("Use the first one as default, press any key to continue/stop ...")
# step2 创建相机实例并绑定句柄
cam = MvCamera()
stDeviceList = cast(deviceList.pDeviceInfo[0], POINTER(MV_CC_DEVICE_INFO)).contents
ret = cam.MV_CC_CreateHandle(stDeviceList)
# step3 打开设备
ret = cam.MV_CC_OpenDevice(MV_ACCESS_Exclusive, 0)
# step4 开始取流
ret = cam.MV_CC_StartGrabbing()
try:
hThreadHandle = threading.Thread(target=work_thread, args=(cam, None, None))
hThreadHandle.start()
except:
print("error: unable to start thread")
msvcrt.getch()
g_bExit = True
hThreadHandle.join()
# step5 停止取流
ret = cam.MV_CC_StopGrabbing()
print("stopped steaming")
# step6 关闭设备
ret = cam.MV_CC_CloseDevice()
print("closed device")
# step7 销毁句柄
ret = cam.MV_CC_DestroyHandle()
print("destroyed handle")
上述代码需要官方提供的SDK,我们在代码的一开始就指定了它的路径,默认就是在MVS安装目录下的Development/Samples/Python/MvImport
,根据你自己的情况修改即可。此外,还需要在本机安装MVS(因为需要一些驱动等DLL,这些是不包含在SDK里的,否则可能会报找不到DLL的错误)。完成以后,直接运行脚本,就可以获取相机的数据并可视化,如下图所示。事实上,在上面的代码中,我们是通过OpenCV实现的可视化。换句话说,把可视化的影像保存下来就是imshow()
换成imwrite()
的事情,十分简单了。
为了方便使用,也将上述代码和SDK上传到了Github,点击查看。如果有时候出现了打开设备失败的情况,请确认是不是有其它程序正在占用相机。因为SDK的默认逻辑是同一时刻只能由一个程序访问相机(独占模式)。
5.开发文档学习
在上面的部分,我们通过MVS软件看到的相机数据,通过官方和自己编写的代码获取了数据。但都比较感性,也不够系统。在这一部分,我们将重点介绍官方开发文档《工业相机SDK开发指南(C)》中推荐的数据获取流程与方式。
5.1 设备连接接口流程
对设备进行操作,实现图像采集、参数配置等功能,需要先连接设备(打开设备),具体流程如下图所示。 完整流程如下:
- 1.(可选)调用
MV_CC_EnumDevices()
枚举子网内指定传输协议对应的所有设备。 可通过nTLayerType
在结构MV_CC_DEVICE_INFO()
中获取设备信息。 - 2.(可选)在打开指定设备前,调用
MV_CC_IsDeviceAccessible()
检查指定设备是否可访问。 - 3.调用
MV_CC_CreateHandle()
创建设备句柄。 - 4.调用
MV_CC_OpenDevice()
打开设备。 - 5.(可选)调用
MV_CC_GetAllMatchInfo()
以获取设备信息。 - 6.调用
MV_CC_CloseDevice()
关闭设备。 - 7.调用
MV_CC_DestroyHandle()
销毁句柄并释放资源。
在上面的Python示例脚本中,我们就使用了上面提到的函数。
5.2 主动取流流程
SDK提供主动获取图像的接口,可以在开启取流后直接调用此接口获取图像,也可以使用异步方式(线程、定时器等)获取图像。主动获取图像有两种方式(两种方式不能同时使用):
-
方式一:调用
MV_CC_StartGrabbing()
开始采集,需要自己开启一个buffer
,然后在应用层循环调用MV_CC_GetOneFrameTimeout()
获取指定像素格式的帧数据,获取帧数据时上层应用程序需要根据帧率控制好调用该接口的频率。 -
方式二:调用
MV_CC_StartGrabbing()
开始采集,然后在应用层调用MV_CC_GetImageBuffer()
获取指定像素格式的帧数据,然后调用MV_CC_FreeImageBuffer()
释放buffer
,获取帧数据时上层应用程序需要根据帧率控制好调用该接口的频率。
这两种取流方式都需要先调用MV_CC_StartGrabbing()
启动图像采集,只是后续使用的函数不同。两种主动取图方式都支持设置超时时间,SDK内部等待直到有数据时返回,可以增加取流平稳性,适合用于对平稳性要求较高的场合。
这两种主动取图方式的区别在于:
-
a、
MV_CC_GetImageBuffer()
需要与MV_CC_FreeImageBuffer()
配套使用,当处理完取到的数据后,需要用MV_CC_FreeImageBuffer()
接口将pstFrame
内的数据指针权限进行释放。 -
b、
MV_CC_GetImageBuffer()
与MV_CC_GetOneFrameTimeout()
相比,有着更高的效率。且其取流缓存的分配是由SDK内部自动分配的,而MV_CC_GetOneFrameTimeout()
接口是需要客户自行分配。
注意事项:
- a、两种主动取图方式不能同时使用,且不能与后面的回调取图方式同时使用,三种取图方式只能使用其中一种。
- b、
pData
返回的是一个地址指针,建议将pData
里面的数据copy出来另建线程使用。
一个完整的取流过程如下所示。 详细步骤如下:
- 1.(可选)调用
MV_CC_EnumDevices()
枚举子网内指定传输协议对应的所有设备。可通过nTLayerType
在结构MV_CC_DEVICE_INFO()
中获取设备信息。 - 2.(可选)打开指定设备前,调用
MV_CC_IsDeviceAccessible()
检查指定设备是否可访问。 - 3.调用
MV_CC_CreateHandle()
创建设备句柄。 - 4.调用
MV_CC_OpenDevice()
打开设备。 - 5.(可选)执行以下一个或多个操作以获取/设置相机不同类型的参数。
- 获取/设置
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()
- 获取/设置
- 6.图像采集:
- (可选)调用
MV_CC_SetImageNodeNum()
设置图像缓存节点个数。当获取的图像数超过这个设定值,最早的图像数据会被自动丢弃。 - 调用
MV_CC_StartGrabbing()
开始取流。 - 对于原始图像数据,可调用
MV_CC_ConvertPixelType()
转换图像的像素格式,也可调用MV_CC_SaveImage()
转换成JPEG
或BMP
格式的图片,并保存成图片文件。 - 在应用程序层中重复调用
MV_CC_GetOneFrameTimeout()
来获取图片数据。
- (可选)调用
- 7.调用
MV_CC_StopGrabbing()
停止采集。 - 8.调用
MV_CC_CloseDevice()
关闭设备。 - 9.调用
MV_CC_DestroyHandle()
销毁句柄并释放资源。
更多更详细的内容还是参阅官方文档。为了更方便,我也把开发文档传到了上面的Github项目里。
本文作者原创,未经许可不得转载,谢谢配合