FFmpeg源码分析:视频滤镜介绍(下)
at 2年前 ca FFmpeg pv 1463 by touch
FFmpeg在libavfilter模块提供音视频滤镜。所有的视频滤镜都注册在libavfilter/allfilters.c。我们也可以使用ffmpeg -filters命令行来查看当前支持的所有滤镜,前面-v代表视频。本篇文章主要介绍视频滤镜,包括:绘制文字、边缘检测、淡入淡出、高斯模糊、左右镜像、图层叠加、视频旋转。
关于视频滤镜的详细介绍,可查看官方文档:视频滤镜。视频滤镜介绍的上半部分,可以查看上一篇文章:视频滤镜介绍(上)。
1、drawtext
绘制文字,在视频画面上绘制文本文字。需要开启freetype第三方库--enable-freetype,详细介绍可查看:freetype官网。如果要设置字体大小、颜色,需要开启fontconfig第三方库--enable-fontconfig,详情可查看:fontconfig官网。如果要设置字体形状,需要开启libfribidi第三方库--enable-libfribidi,详情可查看:fribidi的GitHub网站。
参数选项如下:
box:是否使用背景颜色绘制矩形框, 1 (开启) 或0 (关闭),默认为关闭
boxborderw:绘制矩形边框的宽度,默认为0
boxcolor:绘制矩形边框的颜色,默认为白色
line_spacing:行距,默认为0
basetime:开始计时时间,单位微秒
fontcolor:字体颜色,默认为黑色
font:字体,默认为Sans
fontfile:字体文件,需要文件的绝对路径
alpha:文字的混合透明通道alpha值,范围为[0.0, 1.0],默认为1.0
fontsize:字体大小,默认为16
shadowcolor:阴影颜色,默认为黑色
shadowx、shadowy:文本阴影相对于文本的x、y偏移量
timecode:时间码,默认格式为 "hh:mm:ss[:;.]ff"
text:字符串文本,必须为UTF-8编码格式
textfile:文本文件,必须为UTF-8编码格式
main_h, h, H:输入高度
main_w, w, W:输入宽度
n:从哪一帧开始绘制文本,默认为0
t:时间戳表达式,单位为秒, NAN为未知值
text_h, th:文字高度
text_w, tw:文字宽度
x、y:文本在视频画面的xy坐标点,作为渲染文本的起始位置
绘制文本参考命令,指定文本、xy坐标点、字体大小、字体颜色:
ffmpeg -i in.mp4 -vf drawtext="text='Hello world:x=10:y=20:fontcolor=red" watermark.mp4
添加文字水印的效果如下图所示:

2、edgedetect
边缘检测,用于检测与绘制边缘,使用Canny边缘检测算法。关于Canny边缘检测算法原理,详情可查看:Canny边缘检测的维基百科。边缘检测步骤如下:
(1) 应用高斯滤波器平滑图像以去除噪声
(2) 计算图像的梯度与方向
(3) 应用梯度幅度阈值或下限截止抑制来消除对边缘检测的虚假响应
(4) 应用双阈值来确定潜在边缘
(5) 滞后跟踪边缘:抑制所有其他弱且未连接到强边缘的边缘
参数选项如下:
low、high:Canny边缘检测阈值,范围 [0,1],默认最小值为20/255, 默认最大值为50/255
mode:绘制模式,默认为wire,所有模式如下所示:
‘wires’:黑色背景中绘制白灰连线
‘colormix’:混合颜色,类似绘画卡通效果
‘canny’:每个平面都进行Canny检测
planes:是否开启平面过滤,默认开启
2.1 边缘检测算法
边缘检测的代码位于libavfilter/vf_edgedetect.c,操作步骤包括高斯滤波、sobel算子、消除响应、双阈值寻找潜在边缘、滞后跟踪边缘,核心代码如下:
static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
......
// 从缓冲区获取视频帧数据
out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
for (p = 0; p < edgedetect->nb_planes; p++) {
......
// 高斯滤波,图像降噪处理
gaussian_blur(ctx, width, height,
tmpbuf, width,
in->data[p], in->linesize[p]);
// sobel算子: 计算图像梯度和方向
sobel(width, height,
gradients, width,
directions,width,
tmpbuf, width);
memset(tmpbuf, 0, width * height);
// 应用梯度阈值消除虚假响应
non_maximum_suppression(width, height,
tmpbuf, width,
directions,width,
gradients, width);
// 应用高低位双阈值来确定潜在边缘
double_threshold(edgedetect->low_u8, edgedetect->high_u8,
width, height,
out->data[p], out->linesize[p],
tmpbuf, width);
// 颜色混合,滞后跟踪边缘
if (edgedetect->mode == MODE_COLORMIX) {
color_mix(width, height,
out->data[p], out->linesize[p],
in->data[p], in->linesize[p]);
}
}
if (!direct)
av_frame_free(&in);
return ff_filter_frame(outlink, out);
}static void gaussian_blur(AVFilterContext *ctx, int w, int h,
uint8_t *dst, int dst_linesize,
const uint8_t *src, int src_linesize)
{
int i, j;
memcpy(dst, src, w); dst += dst_linesize; src += src_linesize;
if (h > 1) {
memcpy(dst, src, w); dst += dst_linesize; src += src_linesize;
}
for (j = 2; j < h - 2; j++) {
dst[0] = src[0];
if (w > 1)
dst[1] = src[1];
for (i = 2; i < w - 2; i++) {
/* Gaussian mask of size 5x5 with sigma = 1.4 */
dst[i] = ((src[-2*src_linesize + i-2] + src[2*src_linesize + i-2]) * 2
+ (src[-2*src_linesize + i-1] + src[2*src_linesize + i-1]) * 4
+ (src[-2*src_linesize + i ] + src[2*src_linesize + i ]) * 5
+ (src[-2*src_linesize + i+1] + src[2*src_linesize + i+1]) * 4
+ (src[-2*src_linesize + i+2] + src[2*src_linesize + i+2]) * 2
+ (src[ -src_linesize + i-2] + src[ src_linesize + i-2]) * 4
+ (src[ -src_linesize + i-1] + src[ src_linesize + i-1]) * 9
+ (src[ -src_linesize + i ] + src[ src_linesize + i ]) * 12
+ (src[ -src_linesize + i+1] + src[ src_linesize + i+1]) * 9
+ (src[ -src_linesize + i+2] + src[ src_linesize + i+2]) * 4
+ src[i-2] * 5
+ src[i-1] * 12
+ src[i ] * 15
+ src[i+1] * 12
+ src[i+2] * 5) / 159;
}
if (w > 2)
dst[i] = src[i];
if (w > 3)
dst[i + 1] = src[i + 1];
dst += dst_linesize;
src += src_linesize;
}
if (h > 2) {
memcpy(dst, src, w); dst += dst_linesize; src += src_linesize;
}
if (h > 3)
memcpy(dst, src, w);
}static void sobel(int w, int h,
uint16_t *dst, int dst_linesize,
int8_t *dir, int dir_linesize,
const uint8_t *src, int src_linesize)
{
int i, j;
for (j = 1; j < h - 1; j++) {
dst += dst_linesize;
dir += dir_linesize;
src += src_linesize;
for (i = 1; i < w - 1; i++) {
const int gx =
-1*src[-src_linesize + i-1] + 1*src[-src_linesize + i+1]
-2*src[ i-1] + 2*src[ i+1]
-1*src[ src_linesize + i-1] + 1*src[ src_linesize + i+1];
const int gy =
-1*src[-src_linesize + i-1] + 1*src[ src_linesize + i-1]
-2*src[-src_linesize + i ] + 2*src[ src_linesize + i ]
-1*src[-src_linesize + i+1] + 1*src[ src_linesize + i+1];
dst[i] = FFABS(gx) + FFABS(gy);
dir[i] = get_rounded_direction(gx, gy);
}
}
}边缘检测效果如下图所示:
fade=t=in:s=0:n=30
3、fade
淡入淡出效果,应用淡入淡出效果到视频中。参数选项如下:
type, t:效果类型,"in" 代表淡入效果, "out" 代表淡出效果,默认为淡入效果
start_frame, s:效果开始的帧数,默认为0
nb_frames, n:效果持续的帧数,默认为25
alpha:是否开启alpha,如果开启只会应用效果到alpha通道,默认关闭
start_time, st:效果开始的时间,默认从0开始
duration, d:效果持续的时间
color, c:淡入淡出效果颜色,默认为黑色
以帧为单位的参考命令:
fade=t=in:st=0:d=5.0
4、gblur
高斯模糊,使用高斯模糊来打马赛克。关于高斯模糊算法,主要思想是对像素邻近区域进行加权平均,详情可查看:高斯模糊算法的维基百科。参数选项如下:
sigma:水平方向sigma, 高斯模糊标准差,默认为 0.5
steps:高斯近似值的步数,默认为1
planes:选择哪一平面进行滤波,默认所有平面
sigmaV:垂直方向sigma,如果为-1则与水平方向相同,默认为-1
4.1 高斯模糊算法
高斯模糊的代码位于vf_gblur.c, 关键代码如下:
static void gaussianiir2d(AVFilterContext *ctx, int plane)
{
GBlurContext *s = ctx->priv;
const int width = s->planewidth[plane];
const int height = s->planeheight[plane];
const int nb_threads = ff_filter_get_nb_threads(ctx);
ThreadData td;
if (s->sigma <= 0 || s->steps < 0)
return;
td.width = width;
td.height = height;
// 水平方向滤波
ctx->internal->execute(ctx, filter_horizontally, &td, NULL, FFMIN(height, nb_threads));
// 垂直方向滤波
ctx->internal->execute(ctx, filter_vertically, &td, NULL, FFMIN(width, nb_threads));
// 缩放后处理
ctx->internal->execute(ctx, filter_postscale, &td, NULL, FFMIN(width * height, nb_threads));
}static int filter_horizontally(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs)
{
......
// 对每个slice条带进行滤波
s->horiz_slice(buffer + width * slice_start, width, slice_end - slice_start,
steps, nu, boundaryscale);
emms_c();
return 0;
}而horiz_slice是个函数指针(在初始化进行赋值),指向horiz_slice_c()函数:
static void horiz_slice_c(float *buffer, int width, int height, int steps,
float nu, float bscale)
{
int step, x, y;
float *ptr;
for (y = 0; y < height; y++) {
for (step = 0; step < steps; step++) {
ptr = buffer + width * y;
ptr[0] *= bscale;
// 向右滤波
for (x = 1; x < width; x++)
ptr[x] += nu * ptr[x - 1];
ptr[x = width - 1] *= bscale;
// 向左滤波
for (; x > 0; x--)
ptr[x - 1] += nu * ptr[x];
}
}
}内部调用到的do_vertical_columns()函数如下:
static void do_vertical_columns(float *buffer, int width, int height,
int column_begin, int column_end, int steps,
float nu, float boundaryscale, int column_step)
{
const int numpixels = width * height;
int i, x, k, step;
float *ptr;
for (x = column_begin; x < column_end;) {
for (step = 0; step < steps; step++) {
ptr = buffer + x;
for (k = 0; k < column_step; k++) {
ptr[k] *= boundaryscale;
}
// 向下滤波
for (i = width; i < numpixels; i += width) {
for (k = 0; k < column_step; k++) {
ptr[i + k] += nu * ptr[i - width + k];
}
}
i = numpixels - width;
for (k = 0; k < column_step; k++)
ptr[i + k] *= boundaryscale;
// 向上滤波
for (; i > 0; i -= width) {
for (k = 0; k < column_step; k++)
ptr[i - width + k] += nu * ptr[i + k];
}
}
x += column_step;
}
}static void postscale_c(float *buffer, int length,
float postscale, float min, float max)
{
for (int i = 0; i < length; i++) {
// 缩放
buffer[i] *= postscale;
// 裁剪
buffer[i] = av_clipf(buffer[i], min, max);
}
}5、hflip
ffmpeg -i in.mp4 -vf "hflip" out.mp4
6、hstack
ffmpeg -i one.mp4 -i two.mp4 -vf "hstack" out.mp4
7、rotate
旋转,以弧度表示的任意角度来旋转视频,可以顺时针方向旋转,也可以逆时针方向旋转。参数选项如下:
angle, a:使用弧度角表示要旋转的角度,默认为0,如果为负数则表示逆时针旋转
out_w, ow:输出的视频宽度,默认与输入视频相同 ,即"iw"
out_h, oh:输出的视频高度,默认与输入视频相同,即"ih"
bilinear:双线性插值,默认开启,0代表关闭,1代表开启
fillcolor, c:填充颜色,默认为黑色
n:输入视频帧的序号
t:输入视频帧的时间,单位为秒
hsub、vsub:水平垂直方向的子采样,比如像素格式为"yuv422p",那么hsub=2、vsub=1
in_w, iw、in_h, ih:输入视频的宽高
out_w, ow、out_h, oh:输出视频的宽高
rotw(a)、roth(a):旋转视频的最小宽高
以顺时针旋转90°为例,旋转前后的效果如下图所示:
8、xfade
转场过渡动画,应用于从一个视频到另一个视频的转场过渡。需要注意的是,所有输入视频的帧率、像素格式、分辨率、时间基要保持一致。
支持的转场动画包括淡入淡出、上下左右擦除、上下左右划出、圆形剪裁、矩形剪裁、圆形打开、溶解、模糊、缩放等,默认为淡入淡出。如下列表所示:
‘custom’
‘fade’
‘wipeleft’
‘wiperight’
‘wipeup’
‘wipedown’
‘slideleft’
‘slideright’
‘slideup’
‘slidedown’
‘circlecrop’
‘rectcrop’
‘distance’
‘fadeblack’
‘fadewhite’
‘radial’
‘smoothleft’
‘smoothright’
‘smoothup’
‘smoothdown’
‘circleopen’
‘circleclose’
‘vertopen’
‘vertclose’
‘horzopen’
‘horzclose’
‘dissolve’
‘pixelize’
‘diagtl’
‘diagtr’
‘diagbl’
‘diagbr’
‘hlslice’
‘hrslice’
‘vuslice’
‘vdslice’
‘hblur’
‘fadegrays’
‘wipetl’
‘wipetr’
‘wipebl’
‘wipebr’
‘squeezeh’
‘squeezev’
‘zoomin’
参数选项如下所示:
duration:转场动画时长,范围为[0, 60],默认为1
offset:转场动画相对第一个视频的偏移时间,单位为秒,默认为0
9、overlay
视频叠加,把另一个图层叠加到视频上面,可以做文字水印、图片水印、GIF水印等。参数选项如下:
x、y:设置叠加图层的xy坐标点
format:输出视频的像素格式,默认为yuv420,完整列表如下:
‘yuv420’
‘yuv420p10’
‘yuv422’
‘yuv422p10’
‘yuv444’
‘rgb’
‘gbrp’(平面RGB)
‘auto’(自动选择)
alpha:设置透明度格式,straight或premultiplied,默认为straight
main_w, W、main_h, H:输入视频的宽高
overlay_w, w:overlay_h, h:叠加图层的宽高
n:视频帧的偏移数量,默认为0
pos:输入帧在文件的位置
t:时间戳
添加图片水印命令如下:
ffmpeg -i in.mp4 -i logo.png -filter_complex overlay=10:20 out.mp4
如果要配置左上角、右上角、左下角、右下角方位,可以使用如下方法:
private static String obtainOverlay(int offsetX, int offsetY, int location) {
switch (location) {
case 2: // 右上角
return "overlay='(main_w-overlay_w)-" + offsetX + ":" + offsetY + "'";
case 3: // 左下角
return "overlay='" + offsetX + ":(main_h-overlay_h)-" + offsetY + "'";
case 4: // 右下角
return "overlay='(main_w-overlay_w)-" + offsetX + ":(main_h-overlay_h)-" + offsetY + "'";
case 1: // 左上角
default:
return "overlay=" + offsetX + ":" + offsetY;
}
}
AG注释添加图片水印效果如下(使用雷神制作的logo,以此致敬与缅怀雷神):

添加GIF动画水印的参考命令如下(-ignore_loop 0表示循环播放GIF):
ffmpeg -i in.mp4 -ignore_loop 0 -i in.gif -filter_complex overlay=10:20 out.mp4
版权声明
本文仅代表作者观点,不代表码农殇立场。
本文系作者授权码农殇发表,未经许可,不得转载。
