欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

minigui/mgncs:利用LoadBitmapFromMem函数对摄像头MJPEG格式图像解码

程序员文章站 2022-05-30 15:12:32
...

可能与虚拟机有关,在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格式图像解码
找到问题原因解决办法就很简单,删除源码中这个判断语句重新编译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文件格式介绍》