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 AVFrame
的AVFrameSideData **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有其它需求的話,歡迎討論,謝謝!
以上內容是本人業餘時間興趣之作,限於水平,差錯難免,僅代表個人觀點,和本人任職公司無關。
本文首發於微信公衆號:那遁去的一