发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装

发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装背景我司最近有款轮式巡检机器人用到了海康机器人的工业相机MV-CA060-10GC,我们的开发平台是树莓派(运行UbuntuServer1804),开发语言是Java,但该相机没有JavaSDK

背景

我司最近有款轮式巡检机器人用到了海康机器人的工业相机MV-CA060-10GC,我们的开发平台是树莓派(运行Ubuntu Server 1804),开发语言是Java,但该相机没有Java SDK,于是我决定自己开发一个。
好消息是海康机器人提供了C语言的SDK,这样我就能通过JNA直接调用,而不必写一行C代码。

问题的提出

开发过程中发现,有2个API在Windows下正常运行,在树莓派下却总是报错误的参数,错误码80000004
错误的参数80000004
第一个API负责获取原始图像,第二个负责将原始图像压缩编码成jpg或bmp等文件格式,都是必须要用的API

int MV_CC_GetOneFrameTimeout(  void                    *handle,
  unsigned char           *pData,  unsigned int            nDataSize,
  MV_FRAME_OUT_INFO_EX    *pFrameInfo,  int                     nMsec);
  
  
  int MV_CC_SaveImageEx2(  IN void* handle, 
  MV_SAVE_IMAGE_PARAM_EX    *pSaveParam);

向厂商的技术支持求助,技术支持问样例代码在树莓派上运行正常吗?我试了试,发现正常,技术支持说那就是你们代码写得有问题,仔细查查吧。

走过的弯路

我的Java代码应该不会有问题,否则Windows上就报错了,平台差异JNA已经帮我屏蔽了呀,除非JNA屏蔽得有问题。

换64位Ubuntu

注意到JNA在不通平台有相应的jnidispatch动态库,而树莓派是armhf
架构,会不会是因为JNA没有linux-armhfjnidispatch所致?
arm
windows
将Ubuntu server换成64位版,对应架构是linux-aarch64,所以是有专属的jnidispatch的,但错误依旧。

将Ubuntu换成Raspbian

怀疑是Ubuntu上的JDK有问题,那换树莓派官方发行版Raspbian应该没问题了吧?可惜,错误依旧。
另外Raspbian没有64位版,所以也没法进一步试

退而求其次,用JNI

项目进度逼迫之下,我用JNI方式实现了轮询式拍照,这是最简单的拍照方式,顺利实现功能。

JNI也有问题

但是测试中发现在Linux-arm下有一个BUG,如果开启抓取后几秒钟内没有调用GetOneFrameTimeout,则后续再调用就永远返回无数据(错误码80000007)且无法恢复,现在看来应该是缓冲区没管理好。
无数据
这个问技术支持,也没下文。

规避方法及其缺陷

最后自己想出规避办法,每次在拍照前开启抓取,开启后立即读取帧缓存,读完立即停止抓取。
但是在测试中又发现新问题,因为巡检机器人面对的环境光照强度差别较大,所以将相机配置成自动曝光后,每次开启抓取后等待曝光稳定就要好几秒,效率很低下。
总之,就用这么慢的相机,我们的机器人通过了中期验收

拍照改用callback方式

验收通过后,总结主要问题,拍照慢明显排第一。琢磨解决方法,既然第一个BUG无法解决,那么试试回调式拍照。
先在回调拍照的样例代码上做了几个实验,发现该方式有以下特点:

  1. 可以一直抓取,不用担心一段时间不读取引起无数据问题,因为SDK会启动一个线程不停地从相机帧缓存读取帧数据到用户缓存,用户回调函数对帧缓存处理完毕就会回收
  2. 因为一直抓取,所以机器人从一个位置移动到另一个位置后,等待曝光重新稳定的时间很短,从而提高巡检效率
  3. 回调函数在子线程里运行,而接收用户拍照请求的函数在主线程中执行,要想收到拍照指令立即返回照片,需要做线程同步

考虑重新用JNA做,因为JNI依赖的C做线程同步是很麻烦的,最好能用Java来做(synchronized关键字不要太简单)。

重构拍照流程

参照回调样例代码翻译成Java版,期间熟悉了JNA访问Union注册回调,总算编译通过了,但一运行MV_CC_SaveImageEx2函数就报错误的参数这个老问题

死磕JNA

既然报错误的参数,那就说明参数格式有误,而不是内存不足或功能不支持等其他错误,所以检查MV_CC_SaveImageEx2的2个参数,第一个handle嫌疑很小,如果有问题前面的开启抓取就会失败,另一个参数pSaveParam是我自己创建的,按理说不会有问题,难道是回调函数的入参有问题?

void(__stdcall* cbOutput)(  unsigned char           *pData,
  MV_FRAME_OUT_INFO_EX    *pFrameInfo,  void                    *pUser);

在IDEA下检查3个入参,pData确实是图像数据,能看到相邻2个像素的值很接近;pUser也确实是我传的handle;就剩下pFrameInfo嫌疑最大。
导出x64下和arm下pFrameInfo内存内容,比对发现问题!
在这里插入图片描述
注意红线标注的数字,是enPixelType字段(C下是枚举类型,JNA推断其存储类型int)的一种取值PixelType_Gvsp_RGB8_Packed,x64下正常,arm下却被赋值给nFrameNum字段,很诡异。
因为pFrameInfonFrameLenenPixelType都会分别赋值给pSaveParamnDataLenenPixelType字段,所以拦截掉这2处赋值,替换为x64下的正常值,错误依旧。
最后没办法,想着只能是pSaveParam本身哪里有问题,于是比对该结构体在x64下和arm下的内存内容,结果发现一处异常!
在这里插入图片描述
发现x64比arm多了12字节,而不是我以为的8字节(2个pointer,x64下每个pointer 8字节),原来额外多出来的4字节是因为pImageBuffer前面为保证8字节对齐而填充了4字节,白高兴一场。

灵光一现

想看一下样例c代码中pSaveParam的内存内容,跟JNA申请的native内存里,是否一样。用gdb调试样例C代码,在回调执行编码函数处打断点,断住后

(gdb) p sizeof(MV_FRAME_OUT_INFO_EX)
$2 = 56

pSaveParam的size竟然是56,而同样跑在arm下,JNA版却是52,为何会不一样?逐个检查结构体的每个字段,最终找到异常字段enPixelType,其size是8!

(gdb) p sizeof(MvGvspPixelType)
$16 = 8

至于为什么是8?我做了一系列实验寻找答案,这应该是一种未定义的行为,要尽量避免

BUG的根源

就是厂商开发人员将PixelType_Gvsp_Undefined的值定义成-1导致的

enum MvGvspPixelType
{ 
   
    // Undefined pixel type
#ifdef WIN32
    PixelType_Gvsp_Undefined                =   0xFFFFFFFF, 
#else
    PixelType_Gvsp_Undefined                =   -1, 
#endif
}

猜测厂商开发人员一开始所有平台都是-1,后来发现win32平台编译不过,必须改成0xffffffff才能过,为了变动最小,于是加个条件编译宏,殊不知微软的编译报错才是用心良苦。

解决办法

知道是因为enum size变8字节导致JNA解析方法跟内存内容对不上,进而出现错误,所以解决办法就是更改JNA的解析方法,同时不影响x64
一开始尝试自定义type mapper,但随着了解的深入,发现不用大动干戈,只要为这个特殊的enum单独定义converter就行,但是这还不够简单,更简单的方法是让该enum实现NativeMapped接口,这样JNA就能将该enum当作intlong之类的已知对应native类型的类来看待了。
完整的解决方案,见我的这个repo

尾声

解决此类问题的通用办法

如果相同的JNA代码,在一个平台OK,另一个不OK,那么一定是native部分出了问题。
native部分又分两种情况,一种是我们的JNA wrapper类没有为特定平台做适配,另一种是特定平台本身C代码有问题。怎么判定到底是那一种?
方法就是:比对JNA分配的内存C分配的内存,看哪一个跟头文件中结构体的定义有出入,问题就在哪边。

enum变8字节过程的一种猜测

为何枚举值定义成-1会触发这种异常行为?猜测是编译器实现enum时,会先检查所有枚举值中的最大值,如果是32位的,就至少用32位来存
再检查是否有负数,有的话说明是有符号的,那枚举值的字面量就只能在-231到231-1之间了
再检查有没有字面量最高位为1的,如果有,编译器就认为用户想要这个字面量为正值,这样就超出了int32的表示范围,那就只能扩成int64

解决方案的一处小问题

在Linux-amd64平台上测试,MvGvspPixelType的size也是8字节

test.c: In function ‘main’:
test.c:8:38: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘long unsigned int’ [-Wformat=]
     printf("sizeof enum PixelType = %d\n", sizeof(enum PixelType));
                                     ~^
                                     %ld
test.c:10:24: warning: format ‘%llx’ expects argument of type ‘long long unsigned int’, but argument 2 has type ‘long int’ [-Wformat=]
     printf("RGB = 0x%llx\n", RGB);
                     ~~~^
                     %lx
whp@whp-STCK1A32WFC:~$ ./a.out
sizeof enum PixelType = 8
MONO = 0xffffffff
RGB = 0x80000000

所以用芯片类型来区分enum szie是不合适的,有空改下代码

今天的文章发现海康机器人工业相机SDK的两个BUG,顺便发布我的Java封装分享到此就结束了,感谢您的阅读。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/68085.html

(0)
编程小号编程小号

相关推荐

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注