海康威视(HikVision)在视频解决方案的地位毋庸置疑,起码属于国内业界顶尖水平。选择他家设备主要有两个原因:首要是因为他家产品贵,这确实是选择的首要原因(毕竟经费是……咳,打住);另一个重要原因是他的技术方案十分全面,包括技术支持、开发文档等。

在项目开发初期,一直使用 Python 开发、USB 摄像头调试,毕竟 Python 在机器学习有「先天」优势。然而把项目转移到 HikVision 的 IP 摄像头时,摄像头提供的 SDK 没有 Python 支持。虽然这不足为怪,但解决起来却比想象中要费劲。因此,记录下近期一些核心的学习资料,为后续回顾使用。

这份日记解决的问题,是把 SDK 从 C++ 迁移至 Python,并结合 OpenCV 进行二次开发

准备环境

开始之前,先了解我使用的开发环境,部分型号或版本可能不是必要条件。

名称版本 / 型号
系统Windows10
硬件HikVision IP 摄像头DS-2DC4420IW-D
软件Python3.6+
OpenCV3.4+
设备网络 SDK_Win645.3+
Visual Studio2015

后续所有内容默认必要的开发环境已经完备,包括安装、配置、简单调试等。其中在 Visual Studio 上配置 OpenCV 的方法网上有很多参考文章,请自行检索。

解决方案

把 SDK 从 C++ 搬到 Python 需要解决两个主要问题,一个是从 IP 摄像头中获取图像(码流),另一个是对云台进行控制(角度、倍率等)。为了解决以上两个问题,一共尝试了三个方案,实现过程从简单到复杂,在此总结一下其中各自的利弊。

1. RTSP 协议获取图像

在选择 HikVision 之前也关注了这方面的支持。RTSP 是流媒体协议,可以很方便地通过 URL 获取码流。它的协议如下1

// 说明:
// username:用户名,例如admin
// passwd:密码,例如12345
// ip:设备的ip地址,例如192.0.0.64
// port:端口号默认554,若为默认可以不写
// codec:有h264、MPEG-4、mpeg4这几种
// channel:通道号,起始为1
// subtype:码流类型,主码流为main,子码流为sub

rtsp://[username]:[passwd]@[ip]:[port]/[codec]/[channel]/[subtype]/av_stream

在 OpenCV 中,很简单就能读取 IP 摄像头。

import cv2

cam = cv2.VideoCapture("rtsp://[username]:[passwd]@[ip]:554/h264/ch1/sub/av_stream")
while True:
    ret, frame = cam.read()
    cv2.imshow("test", frame)
    key = cv2.waitKey(100) & 0xff
    if key == 27: # ESC
        break

cam.release()
cv2.destroyAllWindows()

然而这么做存在一个很大的缺陷,即通过流媒体协议获得的码流没有办法满足实时要求,返回图像存在 3-5 秒延时,在实际项目中并不可行。另一方面,仅仅取得码流对于实际项目也并不足够,还需要想方设法对云台进行控制。于是,找到了下一种解决方案。

2. ctypes 调用 C 方法

Python 3 已经内置了ctypes模块,SDK 也提供了所有功能的 Demo,所以使用ctypes库进行 Python、C 混合开发应该最简单、直接不过了。

使用ctypes库,目的是直接调用 SDK 提供的若干动态链接库.dll的方法,也就省去了大量的编译操作。在这里,比较直接相关的文章很少,特别感谢两位博客作者的文章2,提供了宝贵的思路。两篇文章相似,通过ctypes实现了摄像头登入、抓图、光学变倍等功能,也就意味着可以实现云台控制。

整个调用过程与 C++ 实现方式雷同,建议对照「设备网络SDK使用手册」使用。以「云台控制」为例,调用过程如下:

import os, ctypes
import cv2

# 遍历动态链接库目录
def add_dll(path, dll_list):
    files = os.listdir(path)
    for file in files:
        if not os.path.isdir(path + file):
            if file.endswith(".dll"):
                dll_list.append(path + file)
        else:
            add_dll(path + file + "/", dll_list)

# 载入动态链接库
def callCpp(func_name, *args):
    for so_lib in so_list:
        try:
            lib = ctypes.cdll.LoadLibrary(so_lib)
            try:
                value = eval("lib.%s" % func_name)(*args)
                # print("调用的库:" + so_lib)
                # print("执行成功,返回值:" + str(value))
                return value
            except:
                continue
        except:
            print("库文件载入失败:" + so_lib)
            continue
    print("没有找到接口!")
    return False

# 云台控制操作
def NET_DVR_PTZControl_Other(lUserID, lChannel, dwPTZCommand, dwStop):
    res = callCpp("NET_DVR_PTZControl_Other", lUserID, lChannel, dwPTZCommand, dwStop)
    if res:
        print("控制成功")
    else:
        print("控制失败: " + str(callCpp("NET_DVR_GetLastError")))

与使用手册对比,调用的过程还是很直接明了的:填入库方法名,通过callCpp函数实现调用。设备网络SDK使用手册

如果涉及结构体,只需定义继承自ctypes.Structure的类即可。以「设备抓图」为例:

# 抓图数据结构体
class NET_DVR_JPEGPARA(ctypes.Structure):
    _fields_ = [
        ("wPicSize", ctypes.c_ushort), # WORD
        ("wPicQuality", ctypes.c_ushort)] # WORD

# jpeg 抓图
# hPlayWnd 显示窗口可以为 none;存在缺点采集图片速度慢
def NET_DVR_CaptureJPEGPicture():
    sJpegPicFileName = bytes("pytest.jpg", "ascii")
    lpJpegPara = NET_DVR_JPEGPARA()
    lpJpegPara.wPicSize = 2
    lpJpegPara.wPicQuality = 1
    res = callCpp("NET_DVR_CaptureJPEGPicture", lUserID, lChannel, ctypes.byref(lpJpegPara), sJpegPicFileName)
    if res == False:
        error_info = callCpp("NET_DVR_GetLastError")
        print("抓图失败:" + str(error_info))
    else:
        print("抓图成功")

「云台控制」实现算是满足了,然而「实时预览」却不那么容易解决。如参考文章提到,使用「设备抓图」来获取图像实现的速度稍慢,效果不理想;而要实现「实时预览」功能,则需要使用ctypes处理函数回调。虽然理论上可行,但在直接调用 SDK 的「实时预览」相关方法时却始终无法实现,可能是传递的数据原因,也可能不是,反正最终依然无法解决。因此,又再考虑了另外一种备选方案。

3. swig 封装库

既然已经到走投无路的阶段,只能考虑利用 C++ 实现功能,再自行封装 Python 库了。这种实现方案确实很费劲,操作十分繁琐,但毕竟 SDK 有给出 C++ 的实现,算是最靠谱的方案,所以万不得已的我尝试了这种解决方案。要在 Windows 操作系统下完成封装,能够找到最相关的参考文章似乎只有一篇3,也可算雪中送炭了。这篇文章写得十分「言简意赅」,在此十分有必要对操作过程稍微扩展一下,请结合原文阅读。

  1. 安装 Swig。Swig 用于封装库,在 Windows 系统下,点击下载对应的「swigwin」版本,解压后将目录添加到系统「环境变量」。系统环境变量
  2. 下载 OpenCV-swig 接口文件。该文件用于预编译 OpenCV 相关函数,是一系列.i后缀的文件。点击下载并解压。
  3. 将上述接口文件中 lib 文件夹的所有文件拷贝到项目所在目录,并与三个源文件放置在一起。(注:源文件包括HKIPcamera.cppHKIPcamera.hHKIPcamera.i,请回顾出处原文。代码基本与原文一致,需要新增功能也在HKIPcamera.cpp中实现,所以不在此展示。)
  4. 通过命令行使用 swig 生成HKIPcamera_wrap.cxx文件。cdHKIPcamera.i源文件文件夹下,并修改 OpenCV 路径。如:

    E:
    cd OpenCV Project\HKIPCamera\HKIPCamera
    swig -I"D:\Open CV\opencv\build\include" -python -c++ HKIPCamera.i
  5. 修改plaympeg4.h文件。这一问题在原文章中有所提及:在extern "C" __declspec(dllexport)"C"__之间需要增加空格,否则会导致编译报错。
  6. 下载 boost 库。boost 库提供了一系列扩展的 C++ 方法,文件稍大。点击下载(Windows 平台),并将其头文件和库文件添加到项目中。如:

    // 头文件目录
    D:\Boost\boost_1_68_0
    // 库文件目录
    D:\Boost\boost_1_68_0\libs
  7. 编译动态链接库。参考 Windows 下使用 Swig 的一篇文章4,操作基本一致。需要注意的是:

    • 这里生成动态链接库.dll需要另外创建一个新的「Win32 Console Application」工程,且反选「生命周期(SDL)检查」,否则会导致编译失败。Win32 Console Application 工程项目
    • 在「属性管理器」添加 Python 头文件目录和库目录。再将HKIPcamera.h文件添加到头文件、HKIPcamera_wrap.cxxHKIPcamera.cpp添加到源文件 、HKIPcamera.i 添加到工程目录下,进行编译。
    • 将生成的.dll文件改名为_HKIPcamera.pyd,并与HKIPcamera.py放置在同一文件目录下,即可在 Python 中引用。

这里复杂的地方在于,需要配置的环境很多,而且步骤 1-4 属于封装过程,获得.py.cxx文件;步骤 5-7 属于编译过程,获得.dll文件。目前两个过程需要分别在两个工程项目中完成。

理论上,这一解决方案能够同时解决以上两个主要问题,所以该问题的学习过程算是到此结束了。

总结

RTSP 方案基本满足不了项目要求,因此不作考虑。如果项目中不需要「实时预览」图像回调,使用ctypes就能够满足需求;但如果需要结合图像作二次开发,最终还是需要自行封装库文件来实现 Python 内引用。自行封装的过程略微繁杂,但理清思路以后就只需要在 C++ 实现功能,尚属「一劳永逸」。当然,图像回调功能自行封装实现、云台控制使用ctypes实现也完全可行,不存在明显的局限。

至此,「把 SDK 从 C++ 迁移至 Python,并结合 OpenCV 进行二次开发」算是顺利得到解答,这篇日记的目的也达到了。

说明

本日记中省略了许多配置上的细节,其中有几点容易忽视,在此稍作说明。

  • 调试 SDK 时,原有的动态链接库文件需要全部拷贝到系统环境中,与 SDK「说明事项」一致,「HCNetSDKCom 文件夹名不能修改」。说明事项
  • 工程项目中新建「属性表」并使用相对路径把 SDK 头文件和库文件引入,可以省去每次重复配置的麻烦。SDK 头文件SDK 库文件
  • 由于 wrapper 文件使用了Python.h,因此同样需要把 Python 头文件目录和库目录添加到工程项目中,与步骤 7 描述一致。
  • 封装和编译两个过程目前需要分别在两个工程项目中完成。
  • 由于 Python 只能接收Mat类型,原文3中解码回调DecCBFun部分可以改写为:

    void CALLBACK DecCBFun(long nPort, char * pBuf, long nSize, FRAME_INFO * pFrameInfo, long nReserved1, long nReserved2)
    {
        long lFrameType = pFrameInfo->nType;
    
        if (lFrameType == T_YV12)
        {
            if (g_BGRImage.empty())
            {
                g_BGRImage.create(pFrameInfo->nHeight, pFrameInfo->nWidth, CV_8UC3);
            }
    
            Mat YUVImage(pFrameInfo->nHeight + pFrameInfo->nHeight / 2, pFrameInfo->nWidth, CV_8UC1, (unsigned char*)pBuf);
            cvtColor(YUVImage, g_BGRImage, COLOR_YUV2BGR_YV12);
    
            EnterCriticalSection(&g_cs_frameList);
            g_frameList.push_back(g_BGRImage);
            LeaveCriticalSection(&g_cs_frameList);
        }
    }

如有问题,欢迎留言或邮件咨询

  • « 上一篇:JSBox - 触发器