海康威视工业相机的基本使用与SDK数据获取

May 25,2022   7783 words   28 min


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()转换成JPEGBMP格式的图片,并保存成图片文件。
    • 在应用程序层中重复调用MV_CC_GetOneFrameTimeout()来获取图片数据。
  • 7.调用MV_CC_StopGrabbing()停止采集。
  • 8.调用MV_CC_CloseDevice()关闭设备。
  • 9.调用MV_CC_DestroyHandle()销毁句柄并释放资源。

更多更详细的内容还是参阅官方文档。为了更方便,我也把开发文档传到了上面的Github项目里。

本文作者原创,未经许可不得转载,谢谢配合

返回顶部