FFmpeg在libavfilter模块提供音视频滤镜。所有的视频滤镜都注册在libavfilter/allfilters.c。我们也可以使用ffmpeg -filters命令行来查看当前支持的所有滤镜,前面-v代表视频。本篇文章主要介绍视频滤镜,包括:黑色检测、视频叠加、色彩均衡、去除水印、抗抖动、矩形标注、九宫格。
关于视频滤镜的详细介绍,可查看官方文档:视频滤镜。音频滤镜可参考前面两篇文章:音频滤镜介绍(上)和音频滤镜介绍(下)。
1、blackdetect
黑色检测,用于检测纯黑的视频间隔时间。此滤波器将其分析结果输出到日志和元数据。如果找到至少具有指定最小持续时间的黑色片段,则打印一行日志,其中包含开始和结束时间戳以及持续时间。参数选项如下:
- black_min_duration, d:最短的检测黑色时长,单位为s,默认为2.0
- picture_black_ratio_th, pic_th:设置黑色图像的比率,默认为0.98
- pixel_black_th, pix_th:设置黑色像素的阈值,默认为0.10
2、blend
混合,把两个视频的所有帧混合在一起,又称为视频叠加。第一个视频在顶层,第二个视频在底层,默认为最长的视频时长作为输出时长。
- 2.1 从顶层到底层的线性水平过渡:
blend=all_expr='A*(X/W)+B*(1-X/W)'
- 2.2 从右到左覆盖,可用于转场动画过渡效果:
blend=all_expr='if(gte(N*SW+X,W),A,B)'
- 2.3 从上到下覆盖,可用于转场动画过渡效果:
blend=all_expr='if(gte(Y-N*SH,0),A,B)'
- 2.4 沿对角线分割视频,两边分别显示顶层与底层 :
blend=all_expr='if(gt(X,Y*(W/H)),A,B)'
视频混合的代码位于libavfilter/vf_blend.c,主要是遍历像素矩阵,计算顶层像素乘以一个透明度与底层像素乘以透明度的相反数之和,关键代码如下:
static void blend_normal_8bit(const uint8_t *top, ptrdiff_t top_linesize,
const uint8_t *bottom, ptrdiff_t bottom_linesize,
uint8_t *dst, ptrdiff_t dst_linesize,
ptrdiff_t width, ptrdiff_t height,
FilterParams *param, double *values, int starty)
{
const double opacity = param->opacity;
int i, j;
for (i = 0; i < height; i++) {
for (j = 0; j < width; j++) {
dst[j] = top[j] * opacity + bottom[j] * (1. - opacity);
}
dst += dst_linesize;
top += top_linesize;
bottom += bottom_linesize;
}
}
完整命令如下:
ffmpeg -i hello.mp4 -i world.mp4 -filter_complex blend=all_expr='A*(X/W)+B*(1-X/W)' blend.mp4
两个视频混合效果如下图所示:
3、colorbalance
色彩均衡,调整视频帧的RGB分量占比。此滤波器允许在阴影、中间色调或高光区域调整输入帧,以获得红青色、洋红或蓝黄平衡效果。正调整值将平衡移向原色,负调整值则移向补色。参数选项如下:
- rs、gs、bs:调整红、绿、蓝阴影 (最暗像素)
- rm、gm、bm:调整红、绿、蓝基调 (中间像素)
- rh、gh、bh:调整红、绿、蓝高亮(最亮像素)
4、delogo
去除水印,通过对周围像素的简单插值来抑制水印。只需要设置一个覆盖水印的矩形。矩形绘制在最外面的像素上,该像素将被替换为插值。每个方向紧靠矩形外的下一个像素的值,用于计算矩形内的插值像素值。参数如下:
- x、y:水印的左上角坐标
- w、h:水印的宽高
-
show:是否显示覆盖水印区域,默认为0
去除水印代码位于libavfilter/vf_delogo.c,核心代码如下:
static void apply_delogo(uint8_t *dst, int dst_linesize,
uint8_t *src, int src_linesize,
int w, int h, AVRational sar,
int logo_x, int logo_y, int logo_w, int logo_h,
unsigned int band, int show, int direct)
{
int x, y;
uint64_t interp, weightl, weightr, weightt, weightb, weight;
uint8_t *xdst, *xsrc;
uint8_t *topleft, *botleft, *topright;
unsigned int left_sample, right_sample;
int xclipl, xclipr, yclipt, yclipb;
int logo_x1, logo_x2, logo_y1, logo_y2;
xclipl = FFMAX(-logo_x, 0);
xclipr = FFMAX(logo_x+logo_w-w, 0);
yclipt = FFMAX(-logo_y, 0);
yclipb = FFMAX(logo_y+logo_h-h, 0);
logo_x1 = logo_x + xclipl;
logo_x2 = logo_x + logo_w - xclipr - 1;
logo_y1 = logo_y + yclipt;
logo_y2 = logo_y + logo_h - yclipb - 1;
topleft = src+logo_y1 * src_linesize+logo_x1;
topright = src+logo_y1 * src_linesize+logo_x2;
botleft = src+logo_y2 * src_linesize+logo_x1;
if (!direct)
av_image_copy_plane(dst, dst_linesize, src, src_linesize, w, h);
dst += (logo_y1 + 1) * dst_linesize;
src += (logo_y1 + 1) * src_linesize;
for (y = logo_y1+1; y < logo_y2; y++) {
left_sample = topleft[src_linesize*(y-logo_y1)] +
topleft[src_linesize*(y-logo_y1-1)] +
topleft[src_linesize*(y-logo_y1+1)];
right_sample = topright[src_linesize*(y-logo_y1)] +
topright[src_linesize*(y-logo_y1-1)] +
topright[src_linesize*(y-logo_y1+1)];
for (x = logo_x1+1,
xdst = dst+logo_x1+1,
xsrc = src+logo_x1+1; x < logo_x2; x++, xdst++, xsrc++) {
if (show && (y == logo_y1+1 || y == logo_y2-1 ||
x == logo_x1+1 || x == logo_x2-1)) {
*xdst = 0;
continue;
}
// 基于像素的相对距离进行权重插值,考虑SAR
weightl = (uint64_t)(logo_x2-x) * (y-logo_y1) * (logo_y2-y) * sar.den;
weightr = (uint64_t)(x-logo_x1) * (y-logo_y1) * (logo_y2-y) * sar.den;
weightt = (uint64_t)(x-logo_x1) * (logo_x2-x) * (logo_y2-y) * sar.num;
weightb = (uint64_t)(x-logo_x1) * (logo_x2-x) * (y-logo_y1) * sar.num;
interp =
left_sample * weightl
+
right_sample * weightr
+
(topleft[x-logo_x1] +
topleft[x-logo_x1-1] +
topleft[x-logo_x1+1]) * weightt
+
(botleft[x-logo_x1] +
botleft[x-logo_x1-1] +
botleft[x-logo_x1+1]) * weightb;
weight = (weightl + weightr + weightt + weightb) * 3U;
interp = (interp + (weight >> 1)) / weight;
// 判断是否在水印区域内
if (y >= logo_y+band && y < logo_y+logo_h-band &&
x >= logo_x+band && x < logo_x+logo_w-band) {
*xdst = interp;
} else {
unsigned dist = 0;
if (x < logo_x+band)
dist = FFMAX(dist, logo_x-x+band);
else if (x >= logo_x+logo_w-band)
dist = FFMAX(dist, x-(logo_x+logo_w-1-band));
if (y < logo_y+band)
dist = FFMAX(dist, logo_y-y+band);
else if (y >= logo_y+logo_h-band)
dist = FFMAX(dist, y-(logo_y+logo_h-1-band));
*xdst = (*xsrc*dist + interp*(band-dist))/band;
}
}
dst += dst_linesize;
src += src_linesize;
}
}
去水印前后效果,如下图所示:
5、drawbox
绘制矩形,在视频画面绘制矩形框,可用于标注ROI兴趣区域。在人脸检测与人脸识别场景,检测到人脸时会用矩形框标注出来。参数选项如下:
- x、y:矩形的xy坐标点
- width, w、height, h:矩形的宽高
- color, c:矩形边框的颜色
- thickness, t:矩形边框的厚度,默认为3
指定xy坐标、矩形宽高、边框颜色为红色且透明度为50%,命令如下:
drawbox=x=10:y=20:w=200:h=60:color=red@0.5
6、drawgrid
绘制x宫格,可用于绘制四宫格、九宫格,模拟画面拼接,或者画面分割。参数选项如下:
- x、y:九宫格的xy坐标点
- width, w、height, h:每行宫格的宽高
- color, c:九宫格边框颜色
- thickness, t:九宫格边框厚度,默认为1
- iw、ih:输入宽高
绘制3x3的九宫格、边框厚度为2、颜色为蓝色且透明度50%,命令如下:
drawgrid=w=iw/3:h=ih/3:t=2:c=blue@0.5
7、lut、lutyuv和lutrgb
调整yuv或rgb,调整过程:计算查找表,用于绑定每个像素输入值到 输出值,并将其应用到输入视频。相关代码位于vf_lut.c,分为四种类型进行处理:packed 8bits、packed 16bits、planar 8bits、planar 16bits,关键代码如下:
static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
......
if (av_frame_is_writable(in)) {
direct = 1;
out = in;
} else {
// 从缓冲区获取视频帧数据
out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
if (!out) {
av_frame_free(&in);
return AVERROR(ENOMEM);
}
av_frame_copy_props(out, in);
}
// 分为packed 8bits、packed 16bits、planar 8bits、planar 16bits
if (s->is_rgb && s->is_16bit && !s->is_planar) {
/* packed, 16-bits */
PACKED_THREAD_DATA
ctx->internal->execute(ctx, lut_packed_16bits, &td, NULL,
FFMIN(in->height, ff_filter_get_nb_threads(ctx)));
} else if (s->is_rgb && !s->is_planar) {
/* packed 8 bits */
PACKED_THREAD_DATA
ctx->internal->execute(ctx, lut_packed_8bits, &td, NULL,
FFMIN(in->height, ff_filter_get_nb_threads(ctx)));
} else if (s->is_16bit) {
/* planar 16 bits depth */
PLANAR_THREAD_DATA
ctx->internal->execute(ctx, lut_planar_16bits, &td, NULL,
FFMIN(in->height, ff_filter_get_nb_threads(ctx)));
} else {
/* planar 8bits depth */
PLANAR_THREAD_DATA
ctx->internal->execute(ctx, lut_planar_8bits, &td, NULL,
FFMIN(in->height, ff_filter_get_nb_threads(ctx)));
}
if (!direct)
av_frame_free(&in);
return ff_filter_frame(outlink, out);
}
将彩色视频转换为黑白视频,设置U和V分量为128,参考命令如下:
ffmpeg -i in.mp4 -vf lutyuv='u=128:v=128' gray.mp4
黑白视频的效果如下图所示:
对音视频感兴趣的伙伴,可以到GitHub学习:https://github.com/xufuji456/FFmpegAndroid