minigui/mgncs:利用LoadBitmapFromMem函数对摄像头MJPEG格式图像解码
可能与虚拟机有关,在virtualbox虚拟机环境下,即使VIDIOC_S_FMT
设置了pixelformat
为RGB或YUV,通过v4l2视频驱动框架读取摄像头帧图像的格式总是MJPEG。
MJPG是什么格式?以下说明摘自百度百科:
MJPEG全名为 “Motion Joint Photographic Experts Group”,是一种视频编码格式,中文名称翻译为“技术即运动静止图像(或逐帧)压缩技术”。MJPEG广泛应用于非线性编辑领域可精确到帧编辑和多层图像处理,把运动的视频序列作为连续的静止图像来处理,这种压缩方式单独完整地压缩每一帧,在编辑过程中可随机存储每一帧,可进行精确到帧的编辑,此外M-JPEG的压缩和解压缩是对称的,可由相同的硬件和软件实现。但M-JPEG只对帧内的空间冗余进行压缩。不对帧间的时间冗余进行压缩,故压缩效率不高。采用M-JPEG数字压缩格式,当压缩比7:1时,可提供相当于Betacam SP质量图像的节目。https://baike.baidu.com/item/MJPEG
说白了,就是把视频的每一帧压缩成一个JPEG格式的图像。也就是说每一帧都是一个独立完整的JPEG,把它存成后缀为.jpg
或.jpeg
的文件,就可以用任意看图软件打开了。
所以对于MJPEG格式的视频,解码也不麻烦,只要把它当JPEG图像解码就好了。
minigui库中正好有LoadBitmapFromMem
函数用于对内存图像数据(bmp,png,jpg)解码,只要调用它,就可以直接将一帧图像转为BITMAP
,然后设置为窗口的背景(mWidget的NCSP_WIDGET_BKIMAGE
属性),就可以实现视频在窗口中的显示了,完美!
大致的解码片段就是酱紫:
void fl_camera_capture_mjpg(mWidget*self,fl_camera* camera,const void *imgdata, size_t size)
{
PBITMAP pbmp = (PBITMAP)calloc(1,sizeof(BITMAP));
assert(pbmp);
// 对MJPEG一帧图像解码为BITMAP
int ret = LoadBitmapFromMem(HDC_SCREEN,pbmp,imgdata,(int)size,"jpeg");
if(ret){
// 解码失败输出错误信息
fl_log_error("LoadBitmapFromMem from %s %d : %s",camera->dev_name,ret,mg_bmp_error(ret));
free(pbmp);
}
// 强制设置drawMode,相比调用NCSP_WIDGET_BKIMAGE_MODE减少一次屏幕刷新动作
self->bkimg.drawMode = NCS_DM_SCALED;
// 将收到的帧图像设置为窗口的背景图
_M(self,setProperty,NCSP_WIDGET_BKIMAGE,(DWORD)pbmp);
// 设置NCSP_WIDGET_BKIMAGE后,flag被自动置为IMG_FLAG_IGNORE
// 这里强制设置为IMG_FLAG_UNLOAD,让下次设置NCSP_WIDGET_BKIMAGE时,自动释放上一个pbmp对象
self->bkimg.flag = IMG_FLAG_UNLOAD;
}
理想是丰满的,现实却很骨感,代码写完了,第一次运行就报错了,错误就出在LoadBitmapFromMem
调用,错误码为ERR_BMP_IMAGE_TYPE
,也就是图像格式没有被minigui
识别。跟踪到minigui
对jpg图像解码部分的代码(libminigui-3.2.0/src/mybmp/jpeg.c
)就找到了原因,下面是jpeg.c
中__mg_init_jpg
函数的代码片段,见代码中本文作者添加的注释:
void* __mg_init_jpg (MG_RWops *fp, MYBITMAP* mybmp, RGB* pal)
{
int i;
unsigned char magic[5];
Uint16 magic_db;
/* This struct contains the JPEG decompression parameters
* and pointers to working space
* (which is allocated as needed by the JPEG library).
*/
struct jpeg_decompress_struct *cinfo;
struct my_error_mgr *jerr;
jpeg_init_info_t* init_info;
// 判断文件开始的两个字节(0,1)是否为JPEG文件的魔数`FFD8`
if (!MGUI_RWread (fp, magic, 2, 1))
goto err; /* not JPEG image*/
if (magic[0] != 0xFF || magic[1] != 0xD8)
goto err; /* not JPEG image*/
magic_db = MGUI_ReadLE16 (fp);
MGUI_RWread (fp, magic, 2, 1);
MGUI_RWread (fp, magic, 4, 1);
magic [4] = '\0';
// 判断接下来的2,3是FFDB,(DQT,Define Quantization Table, 定义量化表)
// 6,7,8,9字节是否为JIJF或Exif,如果不是就报错
// 错误就出在这个判断
if (magic_db != 0xDBFF
&& strncmp((char*)magic, "JFIF", 4) != 0
&& strncmp((char*)magic, "Exif", 4) != 0)
goto err; /* not JPEG image*/
MGUI_RWseek (fp, -10, SEEK_CUR);
....
}
判断开始两个字节是否为JPEG格式的魔数FFDB,这个没有错,但问题是根据JPEG标准的定义,接下来的判断就限定了只认JFIF和Exif两个格式,就不对了,Exif和JFIF格式是被广泛使用的JPEG的文件存储格式,但由此限定JPG只有这两种格式就狭隘了。
MJPEG格式属于视频流就没有文件存储定义,所以可以没有Exif和JFIF标记。
我收到的MJPEG帧图像就没有这个标记,不同的设备表现还不同,台式机上用的摄像头收到的MJPEG帧
开始2个字节FFD8后直接就是FFC0(SOFO,Start Of Frame, 帧图像开始)标记,所以在这一步报错了。
而在笔记本内置的摄像头上收到数据如下:(2,3字节为FFE0,6,7,8,9为AVI1)
找到问题原因解决办法就很简单,删除源码中这个判断语句重新编译libminigui
就OK
if (magic_db != 0xDBFF
&& strncmp((char*)magic, "JFIF", 4) != 0
&& strncmp((char*)magic, "Exif", 4) != 0)
goto err; /* not JPEG image*/
另外在__mg_check_jpg
函数中也是同样的判断逻辑,处理办法一样,一并修改掉。
2018/09/01 补记:
事后想想,本文的解决办法其实也不严谨,如何正确严谨的判断JPEG格式,请参见我新写的博文: