ffmpeg中的roi encoding介紹

ROI (region of interest) encoding是一項基於感興趣區域的視頻編碼技術,對圖像中感興趣的區域減少量化參數值(qp:quantization parameter),從而分配更多碼率以提升畫面質量,而對不感興趣的區域則增加量化參數值(qp),從而分配更少碼率(這部分區域的畫面質量會因此有所下降),這樣,在不損失圖像整體質量的前提下,可以節省網絡帶寬佔用和視頻存儲空間,或者,在不增加網絡帶寬佔用和存儲空間的前提下,可以提高視頻的整體質量。這在監控、窄帶高清等領域都有較大的應用。

ROI encoding並不是一個新技術,諸如libx264、libvpx等軟件編碼器早已經提供了相應的支持,基於Intel GPU的libva也早已實現了相應接口。但是,每個編碼器提供的接口都不一樣,這給開發者帶來了一定的麻煩。所以,建立在這些編碼器之上的FFmpeg,完全可以定義一個統一的接口,然後,在FFmpeg的內部,將這個統一接口的參數翻譯爲相應編碼器的API調用。我在2019年爲FFmpeg增加了這樣的接口,以支持ROI encoding,如下圖所示。
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-agjVmHw3-1587431473547)(https://graph.baidu.com/resource/2220b9367743e59b0732301582037105.png)]

此接口的關鍵數據結構是:

typedef struct AVRegionOfInterest {
    uint32_t self_size;
    int top;
    int bottom;
    int left;
    int right;
    AVRational qoffset;
} AVRegionOfInterest;

其中,self_size必須等於sizeof(AVRegionOfInterest),爲了以後萬一struct AVRegionOfInterest需要增加數據成員的時候,新老版本兼容用。top、bottom、left和right的單位是像素,以圖像左上角爲座標原點,定義一個矩形像素區域,根據當前所用編碼器的要求,此區域可能會被對齊處理。qoffset則是對量化參數值(qp)在此矩形區域中的調整,qoffset的取值範圍是[-1.0, 1.0],在傳遞給編碼器的時候,還會先乘以當前編碼器qp的取值範圍,使得它和qp在數值上具有可加性,最後的加法發生在編碼器內部。如果qoffset的值是0,則表示不改變qp值。如果qoffset是正數,將使得最終qp值變大,從而使得圖像質量變差;如果qoffset是1.0,則按最差質量編碼。如果qoffset是負數,將使得qp值減小,從而圖像質量變好;如果qoffset爲-1.0,這個區域內的圖像將按編碼器的最佳圖像質量編碼。之所以用AVRational而不是float來定義qoffset,是因爲IEEE float無法精確表示小數值,不利於跨平臺一致性。

可以支持多個這樣的roi區域,每個區域的qoffset值可以都不相同,多個矩形區域也可以組成複雜形狀。在FFmpeg中用 struct AVRegionOfInterest的數組來表示多個區域,有些編碼器對區域個數有限制,此時,假如存在區域重疊的情況,那麼,重疊區域的qoffset採用更小數組索引的qoffset值,換句話說,數組中應先放高優先級的roi數據,再放低優先級的roi數據。這個數組最後被放到struct AVFrameAVFrameSideData **side_data中,其type爲AV_FRAME_DATA_REGIONS_OF_INTEREST。所以,對於使用FFmpeg API的開發者來說,要用上roi encoding功能,只需設置好AVFrame中的side_data即可。下面,分兩種情況介紹如何設置。

  • 可變區域的roi encoding

這種情況下,每個frame中的roi區域都可能會發生變化,包括區域個數、區域位置和大小、還有qoffset的值。所以,每次都需要重新申請side_data空間,並且填好數值。主要代碼如下所示,省略了錯誤返回值的處理。

while () {
    // 首先解碼得到AVFrame
    AVFrame *frame = decode();

    // 申請side_data空間,假設有2個區域。
    // 不需要顯式釋放申請到的空間,
    // 在AVFrame的清理函數中會自動處理。
    AVFrameSideData *sd =
           av_frame_new_side_data(frame,
                 AV_FRAME_DATA_REGIONS_OF_INTEREST,
                 2*sizeof(AVRegionOfInterest));

    // 獲取剛申請到的內存地址
    AVRegionOfInterest* roi =
		         (AVRegionOfInterest*)sd->data;

    // 設置第一個區域
    roi[0].self_size = sizeof(*roi);
    roi[0].top       = ...;
    roi[0].bottom    = ...;
    roi[0].left      = ...;
    roi[0].right     = ...;
    roi[0].qoffset   = ...;

    // 設置第二個區域
    roi[1].self_size = sizeof(*roi);
    roi[1].top       = ...;
    roi[1].bottom    = ...;
    roi[1].left      = ...;
    roi[1].right     = ...;
    roi[1].qoffset   = ...;

    // 然後調用視頻編碼器進行編碼
    encode(frame);
}
  • 固定區域的roi encoding

這種情況下,因爲roi區域不變,所以,我們沒有必要每次都重新申請side_data並且填入數值,只要在一開始的時候準備好,後續繼續使用即可,可以用下面的代碼來實現。

// 首先準備好存放roi數據的buffer,假設只有1個roi區域
AVBufferRef *roi_buf_ref =
       av_buffer_alloc(sizeof(AVRegionOfInterest));

// 獲取剛申請到的內存地址
AVRegionOfInterest* roi =
          (AVRegionOfInterest*)roi_buf_ref->data;

// 填入數值
*roi = (AVRegionOfInterest) {
        .self_size = sizeof(*roi),
        .top       = ...,
        .bottom    = ...,
        .left      = ...,
        .right     = ...,
        .qoffset   = ...,
        };

while () {
    // 首先解碼得到AVFrame
    AVFrame *frame = decode();

	// 將之前準備好的roi數據(指針)放入frame的side_data中
    AVBufferRef *ref = av_buffer_ref(roi_buf_ref);
    av_frame_new_side_data_from_buf(frame,
                  AV_FRAME_DATA_REGIONS_OF_INTEREST,
                  ref);

    // 然後調用視頻編碼器進行編碼
    encode(frame);
}

// 最後,釋放一開始申請的內存
av_buffer_unref(&roi_buf_ref);

如果您對FFmpeg中ROI encoding有其它需求的話,歡迎討論,謝謝!

以上內容是本人業餘時間興趣之作,限於水平,差錯難免,僅代表個人觀點,和本人任職公司無關。

本文首發於微信公衆號:那遁去的一

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