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函數中也是同樣的判斷邏輯,處理辦法一樣,一併修改掉。

參考資料

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章