以前做过的项目都是通过 ffmpeg c++ 来捕获摄像机的 RSTP 视频流来处理视频帧,抽空看了一下海康的SDK说明,使用 python ctypes方式实现了对海康SDK DLL的调用, 可以对视频预览、抓图、抽帧、云台控制、布防等任务,SDK使用C++库,速度也很快。如果不要求对视频帧进行实时智能算法分析的话,python的速度也能满足要求,而且开发效率高。
下面简介一下开发流程与关键步骤。
1、海康SDK开发包简介
在海康威视官网注册后,可以下载开发最新的SDK,其中包含说明书。
海康 SDK 构成与其它C/C++的SDK是类似的,主要由 头文件,库文件( ,lib, .so) 以及静态链接库DLL组成, linux下库文件只有.so。
python项目导入SDK 相当简单,只需要将相关DLL文件拷贝至python项目文件夹,主要是下面几个文件
- HCNetSDK.dll
- HCCore.dll
- HCPreview.dll
- PlayCtrl.dll
- HCCoreDevCfg.dll
或者将dll文件放入1个特定文件夹,然后将此文件夹路径加入到系统环境变量path中。总之,只要程序运行时可以找到 DLL 就可以。
2、调用SDK的基本流程
说明书上写得很详细,以实现预览的过程为例,流程如下:
3、主要步骤介绍
1)如何导入SDK DLL
python如何调用 C/C++ DLL,请参考本人文章:Python 使用 ctypes 调用 C/C++ DLL 动态链接库
由于海康的接口较多,对每个接口函数及参数重新申明,这是个体力活。因此建议大家在学习完ctypes以后,参考本文,自己用python ctypes 语法来封装你所需要的海康SDK 接口。
用 ctypes 调用sdk函数的方式,基本思路是:
- 先用ctype来声明 SDK接口函数的形参与返回值,目的是python代码类型与 C++类型能匹配上
- 建议采用签名函数方式,分别申明函数参数argstypes,返回值类型 restype,好处是使用结构清晰。
也可以使用 Cython 来调用海康SDK,调用速度可以直接与C++编程相比了。然而,Cython学习的难度明显高于 ctypes,如果C++平时用得少,不建议用 Cython.
用ctypes 声明 SDK接口函数的形参与返回值
首先要做的就是用python ctypes 将 DLL中的接口函数的形参重新定义。如 SDK中注册设备的方法。两个形参是结构指针,python中没有直接对应的类型,因此需要通过ctypes 类型重新定义:
LONG NET_DVR_Login_V40( LPNET_DVR_USER_LOGIN_INFO pLoginInfo, LPNET_DVR_DEVICEINFO_V40 lpDeviceInfo);
NET_DVR_Login_V40()参数用 python ctypes 申明
# NET_DVR_Login_V40()参数class NET_DVR_USER_LOGIN_INFO(Structure): _fields_ = [ ("sDeviceAddress", c_char * 129), # 设备地址,IP 或者普通域名 ("byUseTransport", c_byte), # 是否启用能力集透传:0- 不启用透传,默认;1- 启用透传 ("wPort", c_uint16), # 设备端口号,例如:8000 ("sUserName", c_char * 64), # 登录用户名,例如:admin ("sPassword", c_char * 64), # 登录密码,例如:12345 ("cbLoginResult", fLoginResultCallBack), # 登录状态回调函数,bUseAsynLogin 为1时有效 ("pUser", c_void_p), # 用户数据 ("bUseAsynLogin", c_uint32), # 是否异步登录:0- 否,1- 是 ("byProxyType", c_byte), # 0:不使用代理,1:使用标准代理,2:使用EHome代理 ("byUseUTCTime", c_byte), # 0-不进行转换,默认,1-接口上输入输出全部使用UTC时间,SDK完成UTC时间与设备时区的转换,2-接口上输入输出全部使用平台本地时间,SDK完成平台本地时间与设备时区的转换 ("byLoginMode", c_byte), # 0-Private 1-ISAPI 2-自适应 ("byHttps", c_byte), # 0-不适用tls,1-使用tls 2-自适应 ("iProxyID", c_uint32), # 代理服务器序号,添加代理服务器信息时,相对应的服务器数组下表值 ("byVerifyMode", c_byte), # 认证方式,0-不认证,1-双向认证,2-单向认证;认证仅在使用TLS的时候生效; ("byRes2", c_byte * 119)]LPNET_DVR_USER_LOGIN_INFO = POINTER(NET_DVR_USER_LOGIN_INFO)
设备参数结构体 V40
# 设备参数结构体 V40class NET_DVR_DEVICEINFO_V40(ctypes.Structure): _fields_ = [ ('struDeviceV30', NET_DVR_DEVICEINFO_V30), # 设备信息 ('bySupportLock', c_byte), # 设备支持锁定功能,该字段由SDK根据设备返回值来赋值的。bySupportLock为1时,dwSurplusLockTime和byRetryLoginTime有效 ('byRetryLoginTime', c_byte), # 剩余可尝试登陆的次数,用户名,密码错误时,此参数有效 ('byPasswordLevel', c_byte), # admin密码安全等级 ('byProxyType', c_byte), # 代理类型,0-不使用代理, 1-使用socks5代理, 2-使用EHome代理 ('dwSurplusLockTime', c_uint32), # 剩余时间,单位秒,用户锁定时,此参数有效 ('byCharEncodeType', c_byte), # 字符编码类型 ('bySupportDev5', c_byte), # 支持v50版本的设备参数获取,设备名称和设备类型名称长度扩展为64字节 ('bySupport', c_byte), # 能力集扩展,位与结果:0- 不支持,1- 支持 ('byLoginMode', c_byte), # 登录模式:0- Private登录,1- ISAPI登录 ('dwOEMCode', c_uint32), # OEM Code ('iResidualValidity', c_uint32), # 该用户密码剩余有效天数,单位:天,返回负值,表示密码已经超期使用,例如“-3表示密码已经超期使用3天” ('byResidualValidity', c_byte), # iResidualValidity字段是否有效,0-无效,1-有效 ('bySingleStartDTalkChan', c_byte), # 独立音轨接入的设备,起始接入通道号,0-为保留字节,无实际含义,音轨通道号不能从0开始 ('bySingleDTalkChanNums', c_byte), # 独立音轨接入的设备的通道总数,0-表示不支持 ('byPassWordResetLevel', c_byte), # 0-无效, # 1- 管理员创建一个非管理员用户为其设置密码,该非管理员用户正确登录设备后要提示“请修改初始登录密码”,未修改的情况下,用户每次登入都会进行提醒; # 2- 当非管理员用户的密码被管理员修改,该非管理员用户再次正确登录设备后,需要提示“请重新设置登录密码”,未修改的情况下,用户每次登入都会进行提醒。 ('bySupportStreamEncrypt', c_byte), # 能力集扩展,位与结果:0- 不支持,1- 支持 # bySupportStreamEncrypt & 0x1 表示是否支持RTP/TLS取流 # bySupportStreamEncrypt & 0x2 表示是否支持SRTP/UDP取流 # bySupportStreamEncrypt & 0x4 表示是否支持SRTP/MULTICAST取流 ('byMarketType', c_byte), # 0-无效(未知类型),1-经销型,2-行业型 ('byRes2', c_byte * 238) #保留,置为0 ]LPNET_DVR_DEVICEINFO_V40 = POINTER(NET_DVR_DEVICEINFO_V40)
调用sdk函数示例
下面用注册设备函数 NET_DVR_Login_V40 为例,展示初始化参数,赋值,调用dll函数步骤:def LoginDev(sdk): # 登录注册设备 ''' device_info = NET_DVR_DEVICEINFO_V30() lUserId = Objdll.NET_DVR_Login_V30( DEV_IP, DEV_PORT, DEV_USER_NAME, DEV_PASSWORD, byref(device_info)) ''' # 用户注册设备 # c++传递进去的是byte型数据,需要转成byte型传进去,否则会乱码 # 登录参数,包括设备地址、登录用户、密码等 struLoginInfo = NET_DVR_USER_LOGIN_INFO() struLoginInfo.bUseAsynLogin = 0 # 同步登录方式 struLoginInfo.sDeviceAddress = bytes("192.168.99.247", "ascii") # 设备IP地址 struLoginInfo.wPort = 8000 # 设备服务端口 struLoginInfo.sUserName = bytes("admin", "ascii") # 设备登录用户名 struLoginInfo.sPassword = bytes("123456", "ascii") # 设备登录密码 struLoginInfo.byLoginMode = 0 struDeviceInfoV40 = NET_DVR_DEVICEINFO_V40() UserID = sdk.NET_DVR_Login_V40( byref(struLoginInfo), byref(struDeviceInfoV40)) return (UserID, struDeviceInfoV40)
其它sdk函数调用过程也是类似的。
2) 开发框架说明
因为本文只是写1个demo程序,采用了 Python Tkinter 编写GUI界面,也可以使用PyQt 来写。
3) 几个技术点说明
设备注册后,必须要调用 NET_DVR_RealPlay_V40() 函数进行预览摄像头画面,后面的抓图、读帧以及云台控制均要求先执行这一步。
C++ SDK中该函数定义如下:
LONG NET_DVR_RealPlay_V40( LONG lUserID, LPNET_DVR_PREVIEWINFO lpPreviewInfo, REALDATACALLBACK fRealDataCallBack_V30, void *pUser);
可以有1个回调函数 REALDATACALLBACK
typedef void(CALLBACK *REALDATACALLBACK)( LONG lRealHandle, DWORD dwDataType, BYTE *pBuffer, DWORD dwBufSize, void *pUser);
注意要计划好显示视频的窗口控件,获取该窗口的句柄,传给NET_DVR_RealPlay_V40
,或回调函数。
捕获视频码流以及解码显示均由该回调函数完成,捕获原始的YUV视频帧也在此进行。
如果要对视频进行分析,有两种方法
1)通过sdk的抓图接口函数抓图进行分析,如 NET_DVR_CapturePicture
2) 实时性要求高,可将预览码流中的原始YUV帧l转换为RGB后,再进行
处理。
- 代码如下
# coding=utf-8import osimport platformimport tkinterfrom tkinter import *from tkinter import ttkfrom HCNetSDK import *from PlayCtrl import *from time import sleepimport ctypes# 登录的设备信息WINDOWS_FLAG = Truewin = None # 预览窗口funcRealDataCallBack_V30 = None # 实时预览回调函数,需要定义为全局的PlayCtrl_Port = c_long(-1) # 播放句柄Playctrldll = None # 播放库FuncDecCB = None # 播放库解码回调函数,需要定义为全局的# 获取当前系统环境def GetPlatform(): sysstr = platform.system() print('' + sysstr) if sysstr != "Windows": global WINDOWS_FLAG WINDOWS_FLAG = False# 设置SDK初始化依赖库路径def SetSDKInitCfg(): # 设置HCNetSDKCom组件库和SSL库加载路径 # print(os.getcwd()) if WINDOWS_FLAG: strPath = os.getcwd().encode('gbk') sdk_ComPath = NET_DVR_LOCAL_SDK_PATH() sdk_ComPath.sPath = strPath Objdll.NET_DVR_SetSDKInitCfg(2, byref(sdk_ComPath)) Objdll.NET_DVR_SetSDKInitCfg(3, create_string_buffer( strPath + b'\libcrypto-1_1-x64.dll')) Objdll.NET_DVR_SetSDKInitCfg( 4, create_string_buffer(strPath + b'\libssl-1_1-x64.dll')) else: strPath = os.getcwd().encode('utf-8') sdk_ComPath = NET_DVR_LOCAL_SDK_PATH() sdk_ComPath.sPath = strPath Objdll.NET_DVR_SetSDKInitCfg(2, byref(sdk_ComPath)) Objdll.NET_DVR_SetSDKInitCfg( 3, create_string_buffer(strPath + b'/libcrypto.so.1.1')) Objdll.NET_DVR_SetSDKInitCfg( 4, create_string_buffer(strPath + b'/libssl.so.1.1'))def LoginDev(sdk): # 登录注册设备 ''' device_info = NET_DVR_DEVICEINFO_V30() lUserId = Objdll.NET_DVR_Login_V30( DEV_IP, DEV_PORT, DEV_USER_NAME, DEV_PASSWORD, byref(device_info)) ''' # 用户注册设备 # c++传递进去的是byte型数据,需要转成byte型传进去,否则会乱码 # 登录参数,包括设备地址、登录用户、密码等 struLoginInfo = NET_DVR_USER_LOGIN_INFO() struLoginInfo.bUseAsynLogin = 0 # 同步登录方式 struLoginInfo.sDeviceAddress = bytes("192.168..200", "ascii") # 设备IP地址 struLoginInfo.wPort = 8000 # 设备服务端口 struLoginInfo.sUserName = bytes("admin", "ascii") # 设备登录用户名 struLoginInfo.sPassword = bytes("123456", "ascii") # 设备登录密码 struLoginInfo.byLoginMode = 0 struDeviceInfoV40 = NET_DVR_DEVICEINFO_V40() UserID = sdk.NET_DVR_Login_V40( byref(struLoginInfo), byref(struDeviceInfoV40)) return (UserID, struDeviceInfoV40)def DecCBFun(nPort, pBuf, nSize, pFrameInfo, nUser, nReserved2): # 解码回调函数 if pFrameInfo.contents.nType == 3: # 解码返回视频YUV数据,将YUV数据转成jpg图片保存到本地 # 如果有耗时处理,需要将解码数据拷贝到回调函数外面的其他线程里面处理,避免阻塞回调导致解码丢帧 sFileName = ('pic/test_stamp[%d].jpg' % pFrameInfo.contents.nStamp) nWidth = pFrameInfo.contents.nWidth nHeight = pFrameInfo.contents.nHeight nType = pFrameInfo.contents.nType dwFrameNum = pFrameInfo.contents.dwFrameNum nStamp = pFrameInfo.contents.nStamp print(nWidth, nHeight, nType, dwFrameNum, nStamp, sFileName) lRet = Playctrldll.PlayM4_ConvertToJpegFile( pBuf, nSize, nWidth, nHeight, nType, c_char_p(sFileName.encode())) if lRet == 0: print('PlayM4_ConvertToJpegFile fail, error code is:', Playctrldll.PlayM4_GetLastError(nPort)) else: print('PlayM4_ConvertToJpegFile success')def RealDataCallBack_V30(lPlayHandle, dwDataType, pBuffer, dwBufSize, pUser): # 码流回调函数 if dwDataType == NET_DVR_SYSHEAD: # 设置流播放模式 Playctrldll.PlayM4_SetStreamOpenMode(PlayCtrl_Port, 0) # 打开码流,送入40字节系统头数据 if Playctrldll.PlayM4_OpenStream(PlayCtrl_Port, pBuffer, dwBufSize, 1024*1024): # 设置解码回调,可以返回解码后YUV视频数据 global FuncDecCB FuncDecCB = DECCBFUNWIN(DecCBFun) Playctrldll.PlayM4_SetDecCallBackExMend( PlayCtrl_Port, FuncDecCB, None, 0, None) # 开始解码播放 if Playctrldll.PlayM4_Play(PlayCtrl_Port, cv.winfo_id()): print(u'播放库播放成功') else: print(u'播放库播放失败') else: print(u'播放库打开流失败') elif dwDataType == NET_DVR_STREAMDATA: Playctrldll.PlayM4_InputData(PlayCtrl_Port, pBuffer, dwBufSize) else: print(u'其他数据,长度:', dwBufSize)def OpenPreview(Objdll, lUserId, callbackFun): ''' 打开预览 ''' preview_info = NET_DVR_PREVIEWINFO() preview_info.hPlayWnd = 0 preview_info.lChannel = 1 # 通道号lk在 preview_info.dwStreamType = 0 # 主码流 preview_info.dwLinkMode = 0 # TCP preview_info.bBlocked = 1 # 阻塞取流 preview_info.dwDisplayBufNum = 15 # 缓冲区大小,15*1024*1024 # 开始预览并且设置回调函数回调获取实时流数据 lRealPlayHandle = Objdll.NET_DVR_RealPlay_V40( lUserId, byref(preview_info), callbackFun, None) return lRealPlayHandledef InputData(fileMp4, Playctrldll): while True: pFileData = fileMp4.read(4096) if pFileData is None: break if not Playctrldll.PlayM4_InputData(PlayCtrl_Port, pFileData, len(pFileData)): breakdef click_capture(): print("clicked capture button") # Objdll.NET_DVR_SetCapturePictureMode(1) pFileName = ctypes.c_char_p() pFileName.value = bytes("pic/image.jpg", "utf-8") # 开始抓图。 res = Objdll.NET_DVR_CapturePicture(lRealPlayHandle, pFileName) if res: print("Successfullly capture picture, ", pFileName.value)def click_left(): print("clicked button up") # 因无测试条件,暂略def click_right(): print("clicked button")def click_up(): print("clicked button")def click_down(): print("clicked button")if __name__ == '__main__': # 创建窗口 win = tkinter.Tk() # 固定窗口大小 win.resizable(0, 0) win.overrideredirect(True) sw = win.winfo_screenwidth() # 得到屏幕宽度 sh = win.winfo_screenheight() # 得到屏幕高度 # 窗口宽高 ww = 800 wh = 650 x = (sw - ww) / 2 y = (sh - wh) / 2 win.geometry("%dx%d+%d+%d" % (ww, wh, x, y)) # 创建一个Canvas,设置其背景色为白色 cv = Canvas( win, width=760, height=460, bg="white", ) cv.place(x=20, y=10) # 创建退出按键 btn_left = Button(win, text=" 左 ", command=click_left).place(x=100, y=530) btn_right = Button(win, text=" 右 ", command=click_right).place(x=180, y=530) btn_top = Button(win, text=" 上 ", command=click_up).place(x=145, y=495) btn_down = Button(win, text=" 下 ", command=click_down).place(x=145, y=565) btn_capture = Button(win, text=" 放大 ", command=click_capture).place(x=280, y=500) btn_capture = Button(win, text=" 缩小 ", command=click_capture).place(x=280, y=530) btn_capture = Button(win, text=" 截图 ", command=click_capture).place(x=280, y=560) lbl_ip = Label(win, text="IP地址", fg="#111").place(x=480, y=490) ent_ip = Entry(win).place(x=550, y=490) lbl_port = Label(win, text="端口", fg="#111").place(x=480, y=515) ent_port = Entry(win).place(x=550, y=515) lbl_name = Label(win, text="登录名", fg="#111").place(x=480, y=540) ent_name = Entry(win).place(x=550, y=540) lbl_password = Label(win, text="密码", fg="#111").place(x=480, y=565) password = StringVar() password_entry = ttk.Entry( win, textvariable=password, show='*' ) password_entry.place(x=550, y=565) separator = ttk.Separator(win, orient='horizontal') separator.place(x=10, y=600, width=790) btn_q = Button(win, text=' 退出 ', command=win.quit) btn_q.place(x=660, y=610) # 加载库,先加载依赖库 dname = 'D:\workplace\dependency\hik_lib\HCNetSDK.dll' # Objdll = ctypes.CDLL('lib/HCNetSDK.dll') # 加载网络库 Objdll = ctypes.cdll.LoadLibrary(dname) # Playctrldll = ctypes.CDLL('lib/PlayCtrl.dll') # 加载播放库 dname = 'D:\workplace\dependency\hik_lib\PlayCtrl.dll' Playctrldll = ctypes.cdll.LoadLibrary(dname) print("load dll successfully") # SetSDKInitCfg() # 设置组件库和SSL库加载路径 # 初始化DLL Objdll.NET_DVR_Init() print("init device successfully ") # 启用SDK写日志 Objdll.NET_DVR_SetLogToFile( 3, bytes('./SdkLog_Python/', encoding="utf-8"), False) print("config log to SdkLog_Python ") # 获取一个播放句柄 if not Playctrldll.PlayM4_GetPort(byref(PlayCtrl_Port)): print(u'获取播放库句柄失败') # 登录设备 (lUserId, device_info) = LoginDev(Objdll) if lUserId < 0: err = Objdll.NET_DVR_GetLastError() print('Login device fail, error code is: %d' % Objdll.NET_DVR_GetLastError()) # 释放资源 Objdll.NET_DVR_Cleanup() exit() print("login device ") # 定义码流回调函数 funcRealDataCallBack_V30 = REALDATACALLBACK(RealDataCallBack_V30) # 开启预览 lRealPlayHandle = OpenPreview(Objdll, lUserId, funcRealDataCallBack_V30) if lRealPlayHandle < 0: print('Open preview fail, error code is: %d' % Objdll.NET_DVR_GetLastError()) # 登出设备 Objdll.NET_DVR_Logout(lUserId) # 释放资源 Objdll.NET_DVR_Cleanup() exit() # show Windows win.mainloop() # 关闭预览 Objdll.NET_DVR_StopRealPlay(lRealPlayHandle) # 停止解码,释放播放库资源 if PlayCtrl_Port.value > -1: Playctrldll.PlayM4_Stop(PlayCtrl_Port) Playctrldll.PlayM4_CloseStream(PlayCtrl_Port) Playctrldll.PlayM4_FreePort(PlayCtrl_Port) PlayCtrl_Port = c_long(-1) # 登出设备 Objdll.NET_DVR_Logout(lUserId) # 释放资源 Objdll.NET_DVR_Cleanup()
说明
其它SDK功能函数, 可以参照上述思路,用ctype重新定义形参、返回值类型,然后重新申明 C++函数。需要注意 C++中的数组、指针、引用类型与ctypes 的转换。
来源地址:https://blog.csdn.net/captain5339/article/details/127435535