在上一篇文章中,讲解了如何利用ffmpeg实现摄像头直播,本文将在此基础上,实现一个可以选择各种视频滤镜的摄像头直播示例。本文包含以下内容
1、AVFilter的基本介绍
2、如何利用ffmpeg命令行工具实现各种视频滤镜
3、如何利用libavfilter编程实现在摄像头直播流中加入各类不同滤镜的功能
具有较强的综合性。
AVFilter的功能十分强大,可以实现对多媒体数据的各种处理,包括时间线编辑、视音频特效滤镜的添加或信号处理,还可以实现多路媒体流的合并或叠加,其丰富程度令人叹为观止。这里主要以视频滤镜为例进行介绍。使用AVFilter可以为单路视频添加单个或多个滤镜,也可以为多路视频分别添加不同的滤镜并且在最后将多路视频合并为一路视频,AVFilter为实现这些功能定义了以下几个概念:
Filter:代表单个filter
FilterPad:代表一个filter的输入或输出端口,每个filter都可以有多个输入和多个输出,只有输出pad的filter称为source,只有输入pad的filter称为sink
FilterLink:若一个filter的输出pad和另一个filter的输入pad名字相同,即认为两个filter之间建立了link
FilterChain:代表一串相互连接的filters,除了source和sink外,要求每个filter的输入输出pad都有对应的输出和输入pad
FilterGraph:FilterChain的集合
基本和DirectShow类似,也与视频后期调色软件中的节点等概念类似。具体来看,以下面的命令为例
[in]split[main][tmp];[tmp]crop=iw:ih/2,vflip[flip];[main][flip]overlay=0:H/2[out]
在该命令中,输入流[in]首先被分[split]为两个流[main]和[tmp],然后[tmp]流经过了剪切[crop]和翻转[vflip]两个滤镜后变为[flip],这时我们将[flip]叠加[overlay]到最开始的[main]上形成最后的输出流[out],最后呈现出的是镜像的效果。下图清晰地表示了以上过程
我们可以认为图中每一个节点就是一个Filter,每一个方括号所代表的就是FilterPad,可以看到split的输出pad中有一个叫tmp的,而crop的输入pad中也有一个tmp,由此在二者之间建立了link,当然input和output代表的就是source和sink,此外,图中有三条FilterChain,第一条由input和split组成,第二条由crop和vflip组成,第三条由overlay和output组成,整张图即是一个拥有三个FilterChain的FilterGraph。
上面的图是人工画出来的,也可以在代码中调用avfilter_graph_dump函数自动将FilterGraph画出来,如下
可以看到,多出来了一个scale滤镜,这是由ffmpeg自动添加的用于格式转换的滤镜。
在命令行中使用AVFilter需要遵循专门的语法,简单来说,就是每个Filter之间以逗号分隔,每个Filter自己的属性之间以冒号分隔,属性和Filter以等号相连,多个Filter组成一个FilterChain,每个FilterChain之间以分号相隔。AVFilter在命令行工具中对应的是-vf或-af或-filter_complex,前两个分别对应于单路输入的视频滤镜和音频滤镜,最后的filter_complex则对应于有多路输入的情况。除了在FFMpeg命令行工具中使用外,在FFplay中同样也可以使用AVFilter。其他一些关于单双引号、转义符号等更详细的语法参考Filter Documentation
下面举几个例子
1、叠加水印
ffmpeg -i test.flv -vf movie=test.jpg[wm];[in][wm]overlay=5:5[out] out.flv
将test.jpg作为水印叠加到test.flv的坐标为(5,5)的位置上,效果如下
2、镜像
ffmpeg -i test.flv -vf crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right];[left]pad=iw*2[a];[a][right]overlay=w out.flv
输入[in]和输出[out]可以省略不写,pad用于填充画面,效果如下
3、调整曲线
ffmpeg -i test.flv -vf curves=vintage out.flv
类似Photoshop里面的曲线调整,这里的vintage是ffmpeg自带的预设,实现复古画风,还可以直接加载其他的Photoshop预设文件并在其基础上加以调整,如下
ffmpeg -i test.flv -vf curves=psfile='test.acv':green='0.45/0.53' out.flv
其中的acv预设文件实现的是加强对比度,再次基础上调整绿色的显示效果,以上两个命令的最终效果如下
4、多路输入拼接
ffmpeg -i test1.mp4 -i test2.mp4 -i test3.mp4 -i test4.mp4 -filter_complex "[0:v]pad=iw*2:ih*2[a];[a][1:v]overlay=w[b];[b][2:v]overlay=0:h[c];[c][3:v]overlay=w:h" out.mp4
正如前面所说的,当有多个输入时,需要使用filter_complex,效果如下
通过以上几个例子,基本可以明白在命令行中使用AVFilter时需要遵循的语法。
要使用libavfilter,首先要注册相关组件
avfilter_register_all();
首先需要构造出一个完整可用的FilterGraph,需要用到输入流的解码参数,参见上一篇文章,如下
AVFilterContext *buffersink_ctx;//看名字好像AVFilterContext是什么很厉害的东西,但其实只要认为它是AVFilter的一个实例就OK了
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
AVFilter *buffersrc=avfilter_get_by_name("buffer");//Filter的具体定义,只要是libavfilter中已注册的filter,就可以直接通过查询filter名字的方法获得其具体定义,所谓定义即filter的名称、功能描述、输入输出pad、相关回调函数等
AVFilter *buffersink=avfilter_get_by_name("buffersink");
AVFilterInOut *outputs = avfilter_inout_alloc();//AVFilterInOut对应buffer和buffersink这两个首尾端的filter的输入输出
AVFilterInOut *inputs = avfilter_inout_alloc();
filter_graph = avfilter_graph_alloc();
/* buffer video source: the decoded frames from the decoder will be inserted here. */
snprintf(args, sizeof(args),
"video_size=%dx%d:pix_fmt=%d:time_base=%d/%d:pixel_aspect=%d/%d",
ifmt_ctx->streams[0]->codec->width, ifmt_ctx->streams[0]->codec->height, ifmt_ctx->streams[0]->codec->pix_fmt,
ifmt_ctx->streams[0]->time_base.num, ifmt_ctx->streams[0]->time_base.den,
ifmt_ctx->streams[0]->codec->sample_aspect_ratio.num, ifmt_ctx->streams[0]->codec->sample_aspect_ratio.den);
ret = avfilter_graph_create_filter(&buffersrc_ctx, buffersrc, "in",
args, NULL, filter_graph);//根据指定的Filter,这里就是buffer,构造对应的初始化参数args,二者结合即可创建Filter的示例,并放入filter_graph中
if (ret < 0) {
printf("Cannot create buffer source\n");
return ret;
}
/* buffer video sink: to terminate the filter chain. */
ret = avfilter_graph_create_filter(&buffersink_ctx, buffersink, "out",
NULL, NULL, filter_graph);
if (ret < 0) {
printf("Cannot create buffer sink\n");
return ret;
}
/* Endpoints for the filter graph. */
outputs->name = av_strdup("in");//对应buffer这个filter的output
outputs->filter_ctx = buffersrc_ctx;
outputs->pad_idx = 0;
outputs->next = NULL;
inputs->name = av_strdup("out");//对应buffersink这个filter的input
inputs->filter_ctx = buffersink_ctx;
inputs->pad_idx = 0;
inputs->next = NULL;
if ((ret = avfilter_graph_parse_ptr(filter_graph, filter_descr,
&inputs, &outputs, NULL)) < 0)//filter_descr是一个filter命令,例如"overlay=iw:ih",该函数可以解析这个命令,然后自动完成FilterGraph中各个Filter之间的联接
return ret;
if ((ret = avfilter_graph_config(filter_graph, NULL)) < 0)//检查当前所构造的FilterGraph的完整性与可用性
return ret;
avfilter_inout_free(&inputs);
avfilter_inout_free(&outputs);
上面介绍的是FilterGraph的构造方法之一,即根据filter命令使用avfilter_graph_parse_ptr自动进行构造,当然也可以由我们自己将各个filter一一联接起来,如下,这里假设我们已经有了buffersrc_ctx、 buffersink_ctx和一个filter_ctx
// connect inputs and outputs
if (err >= 0) err = avfilter_link(buffersrc_ctx, 0, filter_ctx, 0);
if (err >= 0) err = avfilter_link(filter_ctx, 0, buffersink_ctx, 0);
if (err < 0) {
av_log(NULL, AV_LOG_ERROR, "error connecting filters\n");
return err;
}
err = avfilter_graph_config(filter_graph, NULL);
if (err < 0) {
av_log(NULL, AV_LOG_ERROR, "error configuring the filter graph\n");
return err;
}
return 0;
不过在filter较多的情况下,还是直接使用avfilter_graph_parse_ptr比较方便
在构造好FilterGraph之后,就可以开始使用了,使用流程也很简单,先将一个AVFrame帧推入FIlterGraph中,在将处理后的AVFrame从FilterGraph中拉出来即可,这里以上一篇文章的编解码核心模块的代码为例看一下实现过程。可以看到,是将解码得到的pFrame推入filter_graph,将处理后的数据写入picref中,他也是一个AVFrame。需要注意的是,这里依然要将picref转换为YUV420的帧之后再进行编码,一方面是因为我们这里用的是摄像头数据,是RGB格式的,另一方面,诸如curves这样的filter是在RGB空间进行处理的,最后得到的也是对应像素格式的帧,所以需要进行转换。其他部分基本和原来一样。
//start decode and encode
int64_t start_time=av_gettime();
while (av_read_frame(ifmt_ctx, dec_pkt) >= 0){
if (exit_thread)
break;
av_log(NULL, AV_LOG_DEBUG, "Going to reencode the frame\n");
pframe = av_frame_alloc();
if (!pframe) {
ret = AVERROR(ENOMEM);
return -1;
}
//av_packet_rescale_ts(dec_pkt, ifmt_ctx->streams[dec_pkt->stream_index]->time_base,
// ifmt_ctx->streams[dec_pkt->stream_index]->codec->time_base);
ret = avcodec_decode_video2(ifmt_ctx->streams[dec_pkt->stream_index]->codec, pframe,
&dec_got_frame, dec_pkt);
if (ret < 0) {
av_frame_free(&pframe);
av_log(NULL, AV_LOG_ERROR, "Decoding failed\n");
break;
}
if (dec_got_frame){
#if USEFILTER
pframe->pts = av_frame_get_best_effort_timestamp(pframe);
if (filter_change)
apply_filters(ifmt_ctx);
filter_change = 0;
/* push the decoded frame into the filtergraph */
if (av_buffersrc_add_frame(buffersrc_ctx, pframe) < 0) {
printf("Error while feeding the filtergraph\n");
break;
}
picref = av_frame_alloc();
/* pull filtered pictures from the filtergraph */
while (1) {
ret = av_buffersink_get_frame_flags(buffersink_ctx, picref, 0);
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
break;
if (ret < 0)
return ret;
if (picref) {
img_convert_ctx = sws_getContext(picref->width, picref->height, (AVPixelFormat)picref->format, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL);
sws_scale(img_convert_ctx, (const uint8_t* const*)picref->data, picref->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
sws_freeContext(img_convert_ctx);
pFrameYUV->width = picref->width;
pFrameYUV->height = picref->height;
pFrameYUV->format = PIX_FMT_YUV420P;
#else
sws_scale(img_convert_ctx, (const uint8_t* const*)pframe->data, pframe->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize);
pFrameYUV->width = pframe->width;
pFrameYUV->height = pframe->height;
pFrameYUV->format = PIX_FMT_YUV420P;
#endif
enc_pkt.data = NULL;
enc_pkt.size = 0;
av_init_packet(&enc_pkt);
ret = avcodec_encode_video2(pCodecCtx, &enc_pkt, pFrameYUV, &enc_got_frame);
av_frame_free(&pframe);
if (enc_got_frame == 1){
//printf("Succeed to encode frame: %5d\tsize:%5d\n", framecnt, enc_pkt.size);
framecnt++;
enc_pkt.stream_index = video_st->index;
//Write PTS
AVRational time_base = ofmt_ctx->streams[videoindex]->time_base;//{ 1, 1000 };
AVRational r_framerate1 = ifmt_ctx->streams[videoindex]->r_frame_rate;// { 50, 2 };
AVRational time_base_q = { 1, AV_TIME_BASE };
//Duration between 2 frames (us)
int64_t calc_duration = (double)(AV_TIME_BASE)*(1 / av_q2d(r_framerate1)); //内部时间戳
//Parameters
//enc_pkt.pts = (double)(framecnt*calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
enc_pkt.pts = av_rescale_q(framecnt*calc_duration, time_base_q, time_base);
enc_pkt.dts = enc_pkt.pts;
enc_pkt.duration = av_rescale_q(calc_duration, time_base_q, time_base); //(double)(calc_duration)*(double)(av_q2d(time_base_q)) / (double)(av_q2d(time_base));
enc_pkt.pos = -1;
//Delay
int64_t pts_time = av_rescale_q(enc_pkt.dts, time_base, time_base_q);
int64_t now_time = av_gettime() - start_time;
if (pts_time > now_time)
av_usleep(pts_time - now_time);
ret = av_interleaved_write_frame(ofmt_ctx, &enc_pkt);
av_free_packet(&enc_pkt);
}
#if USEFILTER
av_frame_unref(picref);
}
}
#endif
}
else {
av_frame_free(&pframe);
}
av_free_packet(dec_pkt);
}
这里我们还可以实现一个按下不同的数字键就添加不同的滤镜的功能,如下
可以看到,首先写好一些要用的filter命令,然后在多线程的回调函数里监视用户的按键情况,根据不同的按键使用对应的filter命令初始化filter_graph,这里“null”也是一个filter命令,用于将输入视频原样输出
#if USEFILTER
int filter_change = 1;
const char *filter_descr="null";
const char *filter_mirror = "crop=iw/2:ih:0:0,split[left][tmp];[tmp]hflip[right]; \
[left]pad=iw*2[a];[a][right]overlay=w";
const char *filter_watermark = "movie=test.jpg[wm];[in][wm]overlay=5:5[out]";
const char *filter_negate = "negate[out]";
const char *filter_edge = "edgedetect[out]";
const char *filter_split4 = "scale=iw/2:ih/2[in_tmp];[in_tmp]split=4[in_1][in_2][in_3][in_4];[in_1]pad=iw*2:ih*2[a];[a][in_2]overlay=w[b];[b][in_3]overlay=0:h[d];[d][in_4]overlay=w:h[out]";
const char *filter_vintage = "curves=vintage";
typedef enum{
FILTER_NULL =48,
FILTER_MIRROR ,
FILTER_WATERMATK,
FILTER_NEGATE,
FILTER_EDGE,
FILTER_SPLIT4,
FILTER_VINTAGE
}FILTERS;
AVFilterContext *buffersink_ctx;
AVFilterContext *buffersrc_ctx;
AVFilterGraph *filter_graph;
AVFilter *buffersrc;
AVFilter *buffersink;
AVFrame* picref;
#endif
DWORD WINAPI MyThreadFunction(LPVOID lpParam)
{
#if USEFILTER
int ch = getchar();
while (ch != '\n')
{
switch (ch){
case FILTER_NULL:
{
printf("\nnow using null filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = "null";
getchar();
ch = getchar();
break;
}
case FILTER_MIRROR:
{
printf("\nnow using mirror filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_mirror;
getchar();
ch = getchar();
break;
}
case FILTER_WATERMATK:
{
printf("\nnow using watermark filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_watermark;
getchar();
ch = getchar();
break;
}
case FILTER_NEGATE:
{
printf("\nnow using negate filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_negate;
getchar();
ch = getchar();
break;
}
case FILTER_EDGE:
{
printf("\nnow using edge filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_edge;
getchar();
ch = getchar();
break;
}
case FILTER_SPLIT4:
{
printf("\nnow using split4 filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_split4;
getchar();
ch = getchar();
break;
}
case FILTER_VINTAGE:
{
printf("\nnow using vintage filter\nPress other numbers for other filters:");
filter_change = 1;
filter_descr = filter_vintage;
getchar();
ch = getchar();
break;
}
default:
{
getchar();
ch = getchar();
break;
}
}
#else
while ((getchar())!='\n')
{
;
#endif
}
exit_thread = 1;
return 0;
}
除了在API层面调用AVFilter之外,还可以自己写一个FIlter,实现自己想要的功能,比如前面用到的反相功能,就是用255减去原来的像素数据值实现的,在后面的文章中会专门介绍如何自己编写一个Filter。
此外,针对多输入的Filter使用也是一个比较难的点,期待大家的交流。
关注下方公众号,回复“ffmpeg直播特效”,查看源码地址
关注公众号,掌握更多多媒体领域知识与资讯
文章帮到你了?可以扫描如下二维码进行打赏~,打赏多少您随意~