FFmpeg filter HOWTO

http://blog.yikuyiku.com/?p=3023

 

【 翻譯 】FFmpeg filter HOWTO

定義一個濾鏡

AVFilter

所有我們寫的濾鏡都要用一個AVFilter結構體講給ffmpeg聽。 這個結構體裏描述了ffmpeg從哪個方法進入我們的濾鏡。 這個結構體在libavfilter/avfilter.h裏如下定義:

01 typedef struct
02 {
03     char *name;         ///< 濾鏡名稱
04   
05     int priv_size;      ///< 給濾鏡分配的內存大小
06   
07     int (*init)(AVFilterContext *ctx, const char *args, void *opaque);
08     void (*uninit)(AVFilterContext *ctx);
09   
10     int (*query_formats)(AVFilterContext *ctx);
11   
12     const AVFilterPad *inputs;  ///< 一系列輸入 NULL terminated list of inputs. NULL if none
13     const AVFilterPad *outputs; ///< 一系列輸出 NULL terminated list of outputs. NULL if none
14 } AVFilter;

“query_formats”方法用於設置可以接受的輸入圖像格式和輸出的圖像格式(用於濾鏡鏈分辨哪些濾鏡可以組合在一起用)。

AVFilterPad

這個濾鏡用於描述濾鏡的輸入輸出,在libavfilter/avfilter.h中定義如下:

01 typedef struct AVFilterPad
02 {
03     char *name;
04     int type;
05   
06     int min_perms;
07     int rej_perms;
08   
09     void (*start_frame)(AVFilterLink *link, AVFilterPicRef *picref);
10     AVFilterPicRef *(*get_video_buffer)(AVFilterLink *link, int perms);
11     void (*end_frame)(AVFilterLink *link);
12     void (*draw_slice)(AVFilterLink *link, int y, int height);
13   
14     int (*request_frame)(AVFilterLink *link);
15   
16     int (*config_props)(AVFilterLink *link);
17 } AVFilterPad;

頭文件裏有十分具體的描述,這裏大概解釋如下:

輸入輸出pad都可以用的元素:
name pad的名字,所有的輸入pad名字不能重複,所有的輸出名字不能重複;
type 此元素目前只能爲“AV_PAD_VIDEO”值
config_props 鏈接此pad的配置方法的函數指針

僅限輸入pad使用的元素:
min_perms 接受輸入需要的最小權限
rej_perms 不接受的輸入權限
start_frame 一幀傳入時引用的方法的函數指針
draw_slice 每個slice已經傳入後引用的方法的函數指針
end_frame 一幀完整結束後引用的方法的函數指針
get_video_buffer 前一個濾鏡調用,用以爲一個圖像請求內存

僅限輸出pad使用的元素:
request_frame 請求濾鏡輸出一幀


圖像緩衝

引用計數

濾鏡系統使用引用計數。意味着存在一個buffer裏面存放着圖像數據,而所有的濾鏡都各自保有一個指向這個buffer的引用。當每一個濾鏡完事,它就釋放自己的那個引用。這樣當所有的引用都釋放以後,濾鏡系統就會自動把buffer釋放掉。

權限

由於可能有多個濾鏡都保有了buffer的指針,它們可能會同時操作buffer而造成衝突,因此ffmpeg引入了權限系統。
大多數情況下,當一個濾鏡準備輸出一幀時,它調用濾鏡鏈上下一個濾鏡的一個方法來請求一個buffer。這指定了它對這個buffer需要的最低權限,不過可能實際被賦予的權限可能比要求的要高。
在想要把buffer輸出給另一個濾鏡時,會新建一個新的指向這個圖像的引用,可能是一個權限標記的子集。這個新的引用屬於接受buffer的濾鏡。
舉例說:一個丟幀的濾鏡在輸出最後一幀時,他可能在輸出之後還是想要保持一個指向圖像的引用,以確保沒有其它濾鏡同時修改這個buffer。爲了達到這個目的,他可能請給自己求AV_PERM_READ|AV_PERM_WRITE|AV_PERM_PRESERVE權限,然後在給予其它濾鏡的引用裏去掉AV_PERM_WRITE權限。

可用的權限有:
AV_PERM_READ 可以讀取圖像數據
AV_PERM_WRITE 可以寫入圖像數據
AV_PERM_PRESERVE 保證圖像數據不會被其它濾鏡修改,意味着不會有其它濾鏡拿到AV_PERM_WRITE權限
AV_PERM_REUSE 濾鏡可能往一段buffer多次輸出,但圖像數據不得切換到不同的輸出
AV_PERM_REUSE2 濾鏡可能往一段buffer多次輸出,可能在不同的輸出之間修改圖像數據


濾鏡鏈

濾鏡的輸入輸出用“AVFilterLink”結構體和其它濾鏡相連接:

01 typedef struct AVFilterLink
02 {
03     AVFilterContext *src;       ///< source filter
04     unsigned int srcpad;        ///< index of the output pad on the source filter
05   
06     AVFilterContext *dst;       ///< dest filter
07     unsigned int dstpad;        ///< index of the input pad on the dest filter
08   
09     int w;                      ///< agreed upon image width
10     int h;                      ///< agreed upon image height
11     enum PixelFormat format;    ///< agreed upon image colorspace
12   
13     AVFilterFormats *in_formats;    ///< formats supported by source filter
14     AVFilterFormats *out_formats;   ///< formats supported by destination filter
15   
16     AVFilterPicRef *srcpic;
17   
18     AVFilterPicRef *cur_pic;
19     AVFilterPicRef *outpic;
20 };

成員“src”和“dst”分別指出濾鏡在鏈上的輸入和輸出的結束。“srcpad”指向鏈條上一個“源濾鏡”的輸出面的索引;類似的,“dstpad”指向目標濾鏡的輸入面的索引。
成員“in_formats”指向“源濾鏡”定義的它支持的格式,“out_formats”指向“目標濾鏡”支持的格式。結構體“AVFilterFormats”用於存儲支持格式的列表,它使用引用計數,跟蹤它的引用(參見libavfilter/avfilter.h中關於AVFilterFormats結構體的註釋,瞭解色度空間的協商機制是怎麼工作的,以及爲什麼協商是必要的)。結果就是一個濾鏡如果爲它之前和之後的濾鏡提供了指向相同支持格式的列表的指針,就意味着這個鏈條上的濾鏡就只能使用相同的格式了。
兩個濾鏡相連時,它們需要在它們處理的圖像數據的尺寸和圖像格式上達成一致。達成一致後,這些會作爲參數存儲在link結構體中。
成員“srcpic”是濾鏡系統內部使用的,不應該直接存取。
成員“cur_pic”是給目標濾鏡用的。當一個幀正在通過濾鏡鏈時(開始於start_frame(),結束於end_frame),這個成員包含了目標濾鏡對此幀的引用。
成員“outpic”會在接下來一個小教程中詳細介紹。


寫一個簡單的濾鏡

默認的濾鏡入口點

因爲大多數濾鏡都只有一個輸入一個輸出,且每接受一個幀只輸出一個幀,ffmpeg的濾鏡系統提供了一系列默認的切入點以簡化這種濾鏡的開發,以下是切入點和默認實現的作用:
request_frame() 從濾鏡鏈中前一個濾鏡那請求一個幀
query_formats() 設置所有面上都支持的格式列表,這樣別人就要按照這個列表來。默認包含大多數的YUV和RGB/BGR格式
start_frame() 請求一個buffer來保存輸出幀。一個指向此buffer的引用存儲在hook到濾鏡的輸出的link的“outpic”成員中。下一個濾鏡的start_frame()回調會被調用,傳入一個此buffer的引用。
end_frame() 調用下一個濾鏡的end_frame()回調函數。釋放指向輸出link的“outpic”成員的引用,如果那成員被設置了(比如說使用了默認的start_frame()方法)。釋放輸入link的“cur_pic”引用
get_video_buffer() 返回一個在要求的權限上加一個AV_PERM_READ權限的buffer
config_props() on output pad 把輸出的圖像尺寸設置成和輸入一樣

“vf_negate”濾鏡

介紹了數據結構和回調函數,讓我們來看一個真實的濾鏡。vf_negate濾鏡的效果是反轉視頻中的色彩。它就一個輸入一個輸出,並且每個輸入幀都輸出一個幀。非常典型,可以使用濾鏡系統那些默認的回調實現。

首先,讓我們看一眼在“libavfilter/vf_negate.c”文件最下面的“AVFilter”結構體:

01 AVFilter avfilter_vf_negate =
02 {
03     .name      = "negate",
04   
05     .priv_size = sizeof(NegContext),
06   
07     .query_formats = query_formats,
08   
09     .inputs    = (AVFilterPad[]) {{ .name            = "default",
10                                     .type            = AV_PAD_VIDEO,
11                                     .draw_slice      = draw_slice,
12                                     .config_props    = config_props,
13                                     .min_perms       = AV_PERM_READ, },
14                                   { .name = NULL}},
15     .outputs   = (AVFilterPad[]) {{ .name            = "default",
16                                     .type            = AV_PAD_VIDEO, },
17                                   { .name = NULL}},
18 };

可以看到濾鏡的名字是“negate”,需要sizeof(NegContext)字節的空間存儲上下文。在input和output列表的最後,都有一個name設置爲NULL的pad。可以看出這個濾鏡確實只有一個輸入一個輸出。如果你仔細觀察pad的定義,你會發現好多回調函數已經被指定好了。因爲我們這個濾鏡很簡單,所以大多數保持默認的就可以。

讓我們看看它自己定義的回調函數。

query_formats()

01 static int query_formats(AVFilterContext *ctx)
02 {
03     avfilter_set_common_formats(ctx,
04         avfilter_make_format_list(10,
05                 PIX_FMT_YUV444P,  PIX_FMT_YUV422P,  PIX_FMT_YUV420P,
06                 PIX_FMT_YUV411P,  PIX_FMT_YUV410P,
07                 PIX_FMT_YUVJ444P, PIX_FMT_YUVJ422P, PIX_FMT_YUVJ420P,
08                 PIX_FMT_YUV440P,  PIX_FMT_YUVJ440P));
09     return 0;
10 }

這個函數調用了avfilter_make_format_list()。這個方法第一個函數指定後面要列舉多少個格式,後面就把格式列出來。返回是一個包含指定格式的AVFilterFormats結構體。把這個結構體傳給avfilter_set_common_formats()方法把格式給設置上。如同你看到的,這個濾鏡支持一堆YUV平面的色彩空間格式,包括JPEG YUV色彩空間(其中那些包含字母J的)。

config_props() on an input pad
input填充的config_props()負責驗證是否支持輸入pad的屬性,也負責更新濾鏡的屬性上下文。
TODO: 快速解釋一下YUV色彩空間,色讀採樣,YUV和JEPG YUV範圍的不同。

讓我們看看濾鏡是怎麼存儲它的上下文的:

1 typedef struct
2 {
3     int offY, offUV;
4     int hsub, vsub;
5 } NegContext;

AVFilter結構體中的成員“priv_size”告訴濾鏡系統它需要多少字節來存儲這個結構體。成員“hsub”和“vsub”用於色度採樣,成員“offY”和“offUV”用於YUV和JPEG間範圍的不同。讓我們看看這些在輸入pad的config_props中是咋設置的:

01 static int config_props(AVFilterLink *link)
02 {
03     NegContext *neg = link->dst->priv;
04   
05     avcodec_get_chroma_sub_sample(link->format, &neg->hsub, &neg->vsub);
06   
07     switch(link->format) {
08     case PIX_FMT_YUVJ444P:
09     case PIX_FMT_YUVJ422P:
10     case PIX_FMT_YUVJ420P:
11     case PIX_FMT_YUVJ440P:
12         neg->offY  =
13         neg->offUV = 0;
14         break;
15     default:
16         neg->offY  = -4;
17         neg->offUV = 1;
18     }
19   
20     return 0;
21 }

它只是簡單調用了avcodec_get_chroma_sub_sample()方法去得到色度採樣的位移因子,然後把它們存到上下文中。然後它存儲了一些JPEG YUV的亮度/色度範圍不同的偏移補償。返回0表明成功,因爲沒有這個濾鏡無法處理的輸入格式。

draw_slice()

最後,濾鏡中最重要的方法,它實際處理圖像,draw_slice():

01 static void draw_slice(AVFilterLink *link, int y, int h)
02 {
03     NegContext *neg = link->dst->priv;
04     AVFilterPicRef *in  = link->cur_pic;
05     AVFilterPicRef *out = link->dst->outputs[0]->outpic;
06     uint8_t *inrow, *outrow;
07     int i, j, plane;
08   
09     /* luma plane */
10     inrow  = in-> data[0] + y * in-> linesize[0];
11     outrow = out->data[0] + y * out->linesize[0];
12     for(i = 0; i < h; i ++) {
13         for(j = 0; j < link->w; j ++)
14             outrow[j] = 255 - inrow[j] + neg->offY;
15         inrow  += in-> linesize[0];
16         outrow += out->linesize[0];
17     }
18   
19     /* chroma planes */
20     for(plane = 1; plane < 3; plane ++) {
21         inrow  = in-> data[plane] + (y >> neg->vsub) * in-> linesize[plane];
22         outrow = out->data[plane] + (y >> neg->vsub) * out->linesize[plane];
23   
24         for(i = 0; i < h >> neg->vsub; i ++) {
25             for(j = 0; j < link->w >> neg->hsub; j ++)
26                 outrow[j] = 255 - inrow[j] + neg->offUV;
27             inrow  += in-> linesize[plane];
28             outrow += out->linesize[plane];
29         }
30     }
31   
32     avfilter_draw_slice(link->dst->outputs[0], y, h);
33 }

“y”參數是當前slice的頂部,“h”參數是slice的高度。在這個區域以外的圖像被假設爲是無意義的(可能在未來的一些濾鏡中這個假設會被打破)。

變量“inrow”指向輸入slice的第一行,“outrow”指向輸出的第一行。然後,它先遍歷每一行,然後在每一行中遍歷每一個像素,用255去減像素值,加上在config_props()中爲不同格式的範圍做出的修正值。

然後它在色度平面上做了同樣的事。注意寬度和高度是調整到合適色度採樣的。

在圖像修改結束後,調用calling avfilter_draw_slice()方法把slice送給下一個濾鏡去處理。

翻譯自

http://wiki.multimedia.cx/index.php?title=FFmpeg_filter_HOWTO

 

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