首发于 基础设施

海康威视摄像机SDK的使用

写在前面

文章未经允许,不可转载

海康威视提供的SDK,支持C++、Java、C#,但是就是不支持Python,调用SDK并不是件很愉快的事情。毕竟和图像相关的程序,Python编写起来更加方便。如果不特定选择海康威视这个品牌, 大华摄像机 是支持Python,而且还支持go,第一次看到的时候,我还是吃了一惊,心想厉害呀!

本文章将会有如下内容:

+ 解码yuv

+ 编写类调用海康威视相机

+ void *

+ redis

+ 多线程

+ 制作Python的海康威视库(不是重点)

首发于个人博客


opencv解码

摄像机是图像采集的设备,采集得到是YUV格式的图像数据,如果要对图像进行处理,第一步就是转换为rgb的图像。 方法嘛,网上很多文章都已经写了,有自己编程,也有用opencv直接转换的,我个人推荐使用opencv库进行转换。

位于回调函数DecCBFun中,代码片段如下:

// 解码部分不控制速度,只要从回调函数中返回,解码器就会解码下一部分数据
// 解码回调后,需要及时返回;如果等到上层所有操作都结束再返回,可能会影响到播放库主线程执行
long lFrameType = pFrameInfo->nType;
if (lFrameType == T_YV12)
    // 原始yuv图像
    cv::Mat YUVImg(pFrameInfo->nHeight + pFrameInfo->nHeight / 2, pFrameInfo->nWidth, CV_8UC1, pBuf);
    cv::Mat BGRImg;
    cv::cvtColor(YUVImg, BGRImg, cv::COLOR_YUV2BGR_YV12);  // 随着图片像素增加,cup占用率也增加

多个摄像机调用,编写类

对于帮助文档中的例子,或者网上绝大多数文章,都仅仅只是一个摄像头的调用,其实在工作中不只是一个摄像头的调用。 如果要调用多个摄像头,很自然想到建立一个 HKCamera类,每一个实例对应一个摄像机。 如果要实现一个HKCamera类,遇到最大的问题回调函数没有this指针,指定定义为静态函数。

如果是一个摄像机,可以直接定义一个静态指针,可以完成任务。

class HKCamera{
    private:
        static HKCamera* pThis  // 回调函数是静态函数,可以访问静态成员
    public:
        HKCamera()
            pThis = this;
A * A::pThis=NULL

但是是多个摄像机,就有点不方便了,记住一点, 友好的回调函数一定会设计一个类似 void* duser 的指针 ,这个保留指针的存在,就可以把this传进去,就可以大功告成了。

帮助文档的错误

海康威视的文档让我试错了很久,还把64位的SDK换成了32位的SDK,下面讲讲其中的过程。 整体思路确定下来,然后看海康威视的文档,发现文档中并没有提供 void* duser 的指针, 但是有 long nUser 。 如果把this转换为long类型,然后再转换回来,那么也可以达到目标,但是long类型是4个字节,用64位的SDK,this指针占用8个字节,无论如何也不能轻易转换。

所以就有和官方人员的邮件交流,我提的问题大致如下:

获取实时解码流,根据帮助文档的例子,在类中定义了静态函数,,
static void CALLBACK g_RealDataCallBack_V30(LONG lRealHandle, DWORD dwDataType, BYTE* pBuffer, DWORD dwBufSize, void* dwUser);
static void CALLBACK DecCBFun(long nPort, char* pBuf, long nSize, FRAME_INFO* pFrameInfo, long nUser, long nReserved2);
首先把this指针传入g_RealDataCallBack_V30,因为g_RealDataCallBack_V30中有void * dwUser参数
lRealPlayHandle = NET_DVR_RealPlay_V40(this->lUserID, &struPlayInfo, g_RealDataCallBack_V30, this)
在g_RealDataCallBack_V30函数体内调用PlayM4_SetDecCallBackMend,问题就出在了这里
if (!PlayM4_SetDecCallBackMend(pHk->lPort, DecCBFun, nUser))
BOOL PlayM4_SetDecCallBackMend(  long nPort,  DecCBFun  fDecCBFun, long  nUser);
其中回调函数的定义:
typedef void(CALLBACK *DecCBFun)(
  long        nPort,
  char        *pBuf,
  long        nSize,
 FRAME_INFO  *pFrameInfo,
  long        nUser,
  long        nReserved2
以上的函数并没有定义传入 void * 参数,而是用 long nUser替代,我无法把g_RealDataCallBack_V30中获得的this指针传入PlayM4_SetDecCallBackMend中
尝试如下:
将指针类型用reinterpret_cast为long类型,然后传入
在windows 64位系统下,long只占4个字节,而指针占有8个字节,那个转换类型会发生很大问题
也就是无法通过reinterpret_cast把8直接指针类型转换为4字节long类型。请问有什么办法解决这个问题码?当初为什么要不long nUser定义为 void *nUser呢?

和官方交流了很多个来回,然后弄清楚了问题 不要使用官方文档中的plaympeg4.h 头文件,而是换成 PlayM4.h , 果不其然,这个头文件相应引入的函数中存在了熟悉的 void* nUser 的指针

评论区里说还是没有 void * nUser,请再仔细看看,应该是有的

PLAYM4_API BOOL __stdcall PlayM4_SetDecCallBackMend(LONG nPort,void (CALLBACK* DecCBFun)(long nPort,char * pBuf,long nSize,FRAME_INFO * pFrameInfo, void* nUser,void* nReserved2), void* nUser);

建议所有的大公司一定要重视帮助文档的正确性,保证同步测试。

指针随想

看到海康威视这么大的公司,都会犯这个低级错误,一直在文档中用 plaympeg4.h ,可能原因是工业界为了保证稳定性,很多时候还是用32位进行编译,而这个问题呢,就没有被发觉。

redis

解决多个摄像机的问题,还有一个简单的方法,就是用刚才说的定义一个静态指针的方法,而在回调函数中把解码得到的数据,存入到redis中,将整体编译成一个可执行文件。 然后开启多个进程运行这个程序就好了,类似这样的代码

std::vector<uchar> img_encode;
cv::imencode(".jpg", BGRImg, img_encode, img_quality_params);  // 以特定格式编码,存入vector
std::string img_str(img_encode.begin(), img_encode.end());  // bytes数据组成string
// 将二进制数据存入redis中
redisReply* reply;
reply = (redisReply*)redisCommand(pHk->redisCon, "RPUSH %s %b", pHk->redisDetectKey.c_str(), img_str.c_str(), (size_t)img_str.length());
freeReplyObject(reply);
// 保证redis中列表数据不会超过2个,不用写成事务形式。线程安全由redis保证
reply = (redisReply*)redisCommand(pHk->redisCon, "LTRIM %s -2 -1", pHk->redisDetectKey.c_str());
freeReplyObject(reply);

多线程

在回调函数中,尽可能不要停留,所以用多线程中锁,尽快返回YUV数据

// 解码部分不控制速度,只要从回调函数中返回,解码器就会解码下一部分数据
// 解码回调后,需要及时返回;如果等到上层所有操作都结束再返回,可能会影响到播放库主线程执行
long lFrameType = pFrameInfo->nType;
if (lFrameType == T_YV12)
    // 原始yuv图像
    cv::Mat YUVImg(pFrameInfo->nHeight + pFrameInfo->nHeight / 2, pFrameInfo->nWidth, CV_8UC1, pBuf);