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)
minigui/mgncs:利用LoadBitmapFromMem函数对摄像头MJPEG格式图像解码_第1张图片
找到问题原因解决办法就很简单,删除源码中这个判断语句重新编译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格式,请参见我新写的博文:

《c/c++:判断数据(stream)是否为JPEG图像快速而准确的方法》

参考资料

《JPEG文件格式 JFIF & Exif》
《JPEG文件格式介绍》

你可能感兴趣的:(jpeg,embedded,minigui,MiniGUI)