HikVision SDK: C++ 至 Python
海康威视(HikVision)在视频解决方案的地位毋庸置疑,起码属于国内业界顶尖水平。选择他家设备主要有两个原因:首要是因为他家产品贵,这确实是选择的首要原因(毕竟经费是……咳,打住);另一个重要原因是他的技术方案十分全面,包括技术支持、开发文档等。
在项目开发初期,一直使用 Python 开发、USB 摄像头调试,毕竟 Python 在机器学习有「先天」优势。然而把项目转移到 HikVision 的 IP 摄像头时,摄像头提供的 SDK 没有 Python 支持。虽然这不足为怪,但解决起来却比想象中要费劲。因此,记录下近期一些核心的学习资料,为后续回顾使用。
这份日记解决的问题,是把 SDK 从 C++ 迁移至 Python,并结合 OpenCV 进行二次开发。
准备环境
开始之前,先了解我使用的开发环境,部分型号或版本可能不是必要条件。
名称 | 版本 / 型号 | |
---|---|---|
系统 | Windows | 10 |
硬件 | HikVision IP 摄像头 | DS-2DC4420IW-D |
软件 | Python | 3.6+ |
OpenCV | 3.4+ | |
设备网络 SDK_Win64 | 5.3+ | |
Visual Studio | 2015 |
后续所有内容默认必要的开发环境已经完备,包括安装、配置、简单调试等。其中在 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
函数实现调用。
如果涉及结构体,只需定义继承自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,也可算雪中送炭了。这篇文章写得十分「言简意赅」,在此十分有必要对操作过程稍微扩展一下,请结合原文阅读。
- 安装 Swig。Swig 用于封装库,在 Windows 系统下,点击下载对应的「swigwin」版本,解压后将目录添加到系统「环境变量」。
- 下载 OpenCV-swig 接口文件。该文件用于预编译 OpenCV 相关函数,是一系列
.i
后缀的文件。点击下载并解压。 - 将上述接口文件中 lib 文件夹的所有文件拷贝到项目所在目录,并与三个源文件放置在一起。(注:源文件包括
HKIPcamera.cpp
、HKIPcamera.h
和HKIPcamera.i
,请回顾出处原文。代码基本与原文一致,需要新增功能也在HKIPcamera.cpp
中实现,所以不在此展示。) 通过命令行使用 swig 生成
HKIPcamera_wrap.cxx
文件。cd
到HKIPcamera.i
源文件文件夹下,并修改 OpenCV 路径。如:E: cd OpenCV Project\HKIPCamera\HKIPCamera swig -I"D:\Open CV\opencv\build\include" -python -c++ HKIPCamera.i
- 修改
plaympeg4.h
文件。这一问题在原文章中有所提及:在extern "C" __declspec(dllexport)
的"C"
和__
之间需要增加空格,否则会导致编译报错。下载 boost 库。boost 库提供了一系列扩展的 C++ 方法,文件稍大。点击下载(Windows 平台),并将其头文件和库文件添加到项目中。如:
// 头文件目录 D:\Boost\boost_1_68_0 // 库文件目录 D:\Boost\boost_1_68_0\libs
编译动态链接库。参考 Windows 下使用 Swig 的一篇文章4,操作基本一致。需要注意的是:
- 这里生成动态链接库
.dll
需要另外创建一个新的「Win32 Console Application」工程,且反选「生命周期(SDL)检查」,否则会导致编译失败。 - 在「属性管理器」添加 Python 头文件目录和库目录。再将
HKIPcamera.h
文件添加到头文件、HKIPcamera_wrap.cxx
和HKIPcamera.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 头文件和库文件引入,可以省去每次重复配置的麻烦。
- 由于 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); } }
如有问题,欢迎留言或邮件咨询
修改于 June 23, 2019 21:36:45 (5 年前) - « 上一篇:JSBox - 触发器
- 下一篇:JSBox - Music Widget »
- 修改