diff options
author | Thilo Borgmann <thilo.borgmann@mail.de> | 2022-04-25 00:07:04 +0200 |
---|---|---|
committer | Thilo Borgmann <thilo.borgmann@mail.de> | 2022-04-25 20:52:15 +0200 |
commit | b23208826bfb9ac2cba9736e066ea6b82f6207b9 (patch) | |
tree | bb8c70f277efbbfdbd2c43107a49d3159a42f742 | |
parent | 22df52c444252d1df29244ca99b51911a1e0eb58 (diff) | |
download | ffmpeg-b23208826bfb9ac2cba9736e066ea6b82f6207b9.tar.gz |
lavfi: Add blurdetect filter
-rw-r--r-- | Changelog | 1 | ||||
-rw-r--r-- | doc/filters.texi | 52 | ||||
-rw-r--r-- | libavfilter/Makefile | 1 | ||||
-rw-r--r-- | libavfilter/allfilters.c | 1 | ||||
-rw-r--r-- | libavfilter/version.h | 2 | ||||
-rw-r--r-- | libavfilter/vf_blurdetect.c | 394 | ||||
-rw-r--r-- | tests/fate/filter-video.mak | 3 | ||||
-rw-r--r-- | tests/ref/fate/filter-refcmp-blurdetect-yuv | 10 |
8 files changed, 463 insertions, 1 deletions
@@ -13,6 +13,7 @@ version 5.1: - pixelize video filter - colormap video filter - colorchart video source filter +- blurdetect filter version 5.0: diff --git a/doc/filters.texi b/doc/filters.texi index c8699b9099..499f3adcd9 100644 --- a/doc/filters.texi +++ b/doc/filters.texi @@ -7997,6 +7997,58 @@ tblend=all_mode=grainextract @subsection Commands This filter supports same @ref{commands} as options. +@anchor{blurdetect} +@section blurdetect + +Determines blurriness of frames without altering the input frames. + +Based on Marziliano, Pina, et al. "A no-reference perceptual blur metric." +Allows for a block-based abbreviation. + +The filter accepts the following options: + +@table @option +@item low +@item high +Set low and high threshold values used by the Canny thresholding +algorithm. + +The high threshold selects the "strong" edge pixels, which are then +connected through 8-connectivity with the "weak" edge pixels selected +by the low threshold. + +@var{low} and @var{high} threshold values must be chosen in the range +[0,1], and @var{low} should be lesser or equal to @var{high}. + +Default value for @var{low} is @code{20/255}, and default value for @var{high} +is @code{50/255}. + +@item radius +Define the radius to search around an edge pixel for local maxima. + +@item block_pct +Determine blurriness only for the most significant blocks, given in percentage. + +@item block_width +Determine blurriness for blocks of width @var{block_width}. If set to any value smaller 1, no blocks are used and the whole image is processed as one no matter of @var{block_height}. + +@item block_height +Determine blurriness for blocks of height @var{block_height}. If set to any value smaller 1, no blocks are used and the whole image is processed as one no matter of @var{block_width}. + +@item planes +Set planes to filter. Default is first only. +@end table + +@subsection Examples + +@itemize +@item +Determine blur for 80% of most significant 32x32 blocks: +@example +blurdetect=block_width=32:block_height=32:block_pct=80 +@end example +@end itemize + @section bm3d Denoise frames using Block-Matching 3D algorithm. diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 38ca379e5a..1db097b464 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -195,6 +195,7 @@ OBJS-$(CONFIG_BLACKDETECT_FILTER) += vf_blackdetect.o OBJS-$(CONFIG_BLACKFRAME_FILTER) += vf_blackframe.o OBJS-$(CONFIG_BLEND_FILTER) += vf_blend.o framesync.o OBJS-$(CONFIG_BLEND_VULKAN_FILTER) += vf_blend_vulkan.o framesync.o vulkan.o vulkan_filter.o +OBJS-$(CONFIG_BLURDETECT_FILTER) += vf_blurdetect.o edge_common.o OBJS-$(CONFIG_BM3D_FILTER) += vf_bm3d.o framesync.o OBJS-$(CONFIG_BOXBLUR_FILTER) += vf_boxblur.o boxblur.o OBJS-$(CONFIG_BOXBLUR_OPENCL_FILTER) += vf_avgblur_opencl.o opencl.o \ diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 36fa3ae8d7..2ad523fd0f 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -183,6 +183,7 @@ extern const AVFilter ff_vf_blackdetect; extern const AVFilter ff_vf_blackframe; extern const AVFilter ff_vf_blend; extern const AVFilter ff_vf_blend_vulkan; +extern const AVFilter ff_vf_blurdetect; extern const AVFilter ff_vf_bm3d; extern const AVFilter ff_vf_boxblur; extern const AVFilter ff_vf_boxblur_opencl; diff --git a/libavfilter/version.h b/libavfilter/version.h index 9add1658e5..8f1b16969a 100644 --- a/libavfilter/version.h +++ b/libavfilter/version.h @@ -31,7 +31,7 @@ #include "version_major.h" -#define LIBAVFILTER_VERSION_MINOR 36 +#define LIBAVFILTER_VERSION_MINOR 37 #define LIBAVFILTER_VERSION_MICRO 100 diff --git a/libavfilter/vf_blurdetect.c b/libavfilter/vf_blurdetect.c new file mode 100644 index 0000000000..0199bd13de --- /dev/null +++ b/libavfilter/vf_blurdetect.c @@ -0,0 +1,394 @@ +/* + * Copyright (c) 2021 Thilo Borgmann <thilo.borgmann _at_ mail.de> + * + * This file is part of FFmpeg. + * + * FFmpeg is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * FFmpeg is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with FFmpeg; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * No-reference blurdetect filter + * + * Implementing: + * Marziliano, Pina, et al. "A no-reference perceptual blur metric." Proceedings. + * International conference on image processing. Vol. 3. IEEE, 2002. + * https://infoscience.epfl.ch/record/111802/files/14%20A%20no-reference%20perceptual%20blur%20metric.pdf + * + * @author Thilo Borgmann <thilo.borgmann _at_ mail.de> + */ + +#include "libavutil/avassert.h" +#include "libavutil/imgutils.h" +#include "libavutil/opt.h" +#include "libavutil/pixelutils.h" +#include "libavutil/motion_vector.h" +#include "libavutil/qsort.h" +#include "avfilter.h" +#include "formats.h" +#include "internal.h" +#include "video.h" +#include "edge_common.h" + +static int comp(const float *a,const float *b) +{ + return FFDIFFSIGN(*a, *b); +} + +typedef struct BLRContext { + const AVClass *class; + + int hsub, vsub; + int nb_planes; + + float low, high; + uint8_t low_u8, high_u8; + int radius; // radius during local maxima detection + int block_pct; // percentage of "sharpest" blocks in the image to use for bluriness calculation + int block_width; // width for block abbreviation + int block_height; // height for block abbreviation + int planes; // number of planes to filter + + double blur_total; + uint64_t nb_frames; + + float *blks; + uint8_t *filterbuf; + uint8_t *tmpbuf; + uint16_t *gradients; + char *directions; +} BLRContext; + +#define OFFSET(x) offsetof(BLRContext, x) +#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM +static const AVOption blurdetect_options[] = { + { "high", "set high threshold", OFFSET(high), AV_OPT_TYPE_FLOAT, {.dbl=30/255.}, 0, 1, FLAGS }, + { "low", "set low threshold", OFFSET(low), AV_OPT_TYPE_FLOAT, {.dbl=15/255.}, 0, 1, FLAGS }, + { "radius", "search radius for maxima detection", OFFSET(radius), AV_OPT_TYPE_INT, {.i64=50}, 1, 100, FLAGS }, + { "block_pct", "block pooling threshold when calculating blurriness", OFFSET(block_pct), AV_OPT_TYPE_INT, {.i64=80}, 1, 100, FLAGS }, + { "block_width", "block size for block-based abbreviation of blurriness", OFFSET(block_width), AV_OPT_TYPE_INT, {.i64=-1}, -1, INT_MAX, FLAGS }, + { "block_height", "block size for block-based abbreviation of blurriness", OFFSET(block_height), AV_OPT_TYPE_INT, {.i64=-1}, -1, INT_MAX, FLAGS }, + { "planes", "set planes to filter", OFFSET(planes), AV_OPT_TYPE_INT, {.i64=1}, 0, 15, FLAGS }, + { NULL } +}; + +AVFILTER_DEFINE_CLASS(blurdetect); + +static av_cold int blurdetect_init(AVFilterContext *ctx) +{ + BLRContext *s = ctx->priv; + + s->low_u8 = s->low * 255. + .5; + s->high_u8 = s->high * 255. + .5; + + return 0; +} + +static int blurdetect_config_input(AVFilterLink *inlink) +{ + AVFilterContext *ctx = inlink->dst; + BLRContext *s = ctx->priv; + const int bufsize = inlink->w * inlink->h; + const AVPixFmtDescriptor *pix_desc; + + pix_desc = av_pix_fmt_desc_get(inlink->format); + s->hsub = pix_desc->log2_chroma_w; + s->vsub = pix_desc->log2_chroma_h; + s->nb_planes = av_pix_fmt_count_planes(inlink->format); + + if (s->block_width < 1 || s->block_height < 1) { + s->block_width = inlink->w; + s->block_height = inlink->h; + } + + s->tmpbuf = av_malloc(bufsize); + s->filterbuf = av_malloc(bufsize); + s->gradients = av_calloc(bufsize, sizeof(*s->gradients)); + s->directions = av_malloc(bufsize); + s->blks = av_calloc((inlink->w / s->block_width) * (inlink->h / s->block_height), + sizeof(*s->blks)); + + if (!s->tmpbuf || !s->filterbuf || !s->gradients || !s->directions || !s->blks) + return AVERROR(ENOMEM); + + return 0; +} + +// edge width is defined as the distance between surrounding maxima of the edge pixel +static float edge_width(BLRContext *blr, int i, int j, int8_t dir, int w, int h, + int edge, const uint8_t *src, int src_linesize) +{ + float width = 0; + int dX, dY; + int sign; + int tmp; + int p1; + int p2; + int k, x, y; + int edge1; + int edge2; + float luma1 = 0.0; // average luma difference per edge pixel + float luma2 = 0.0; + int radius = blr->radius; + + switch(dir) { + case DIRECTION_HORIZONTAL: dX = 1; dY = 0; break; + case DIRECTION_VERTICAL: dX = 0; dY = 1; break; + case DIRECTION_45UP: dX = 1; dY = -1; break; + case DIRECTION_45DOWN: dX = 1; dY = 1; break; + } + + // determines if search in direction dX/dY is looking for a maximum or minimum + sign = src[j * src_linesize + i] > src[(j - dY) * src_linesize + i - dX] ? 1 : -1; + + // search in -(dX/dY) direction + for (k = 0; k < radius; k++) { + x = i - k*dX; + y = j - k*dY; + p1 = y * src_linesize + x; + x -= dX; + y -= dY; + p2 = y * src_linesize + x; + if (x < 0 || x >= w || y < 0 || y >= h) + return 0; + + tmp = (src[p1] - src[p2]) * sign; + + if (tmp <= 0) // local maximum found + break; + + luma1 += tmp; + } + if (k > 0) luma1 /= k; + edge1 = k; + width += k; + + // search in +(dX/dY) direction + for (k = 0; k < radius; k++) { + x = i + k * dX; + y = j + k * dY; + p1 = y * src_linesize + x; + x += dX; + y += dY; + p2 = y * src_linesize + x; + if (x < 0 || x >= w || y < 0 || y >= h) + return 0; + + tmp = (src[p1] - src[p2]) * sign; + + if (tmp >= 0) // local maximum found + break; + + luma2 -= tmp; + } + if (k > 0) luma2 /= k; + edge2 = k; + width += k; + + // for 45 degree directions approximate edge width in pixel units: 0.7 ~= sqrt(2)/2 + if (dir == DIRECTION_45UP || dir == DIRECTION_45DOWN) + width *= 0.7; + + return width; +} + +static float calculate_blur(BLRContext *s, int w, int h, int hsub, int vsub, + uint8_t* dir, int dir_linesize, + uint8_t* dst, int dst_linesize, + uint8_t* src, int src_linesize) +{ + float total_width = 0.0; + int block_count; + double block_total_width; + + int i, j; + int blkcnt = 0; + + float *blks = s->blks; + float block_pool_threshold = s->block_pct / 100.0; + + int block_width = AV_CEIL_RSHIFT(s->block_width, hsub); + int block_height = AV_CEIL_RSHIFT(s->block_height, vsub); + int brows = h / block_height; + int bcols = w / block_width; + + for (int blkj = 0; blkj < brows; blkj++) { + for (int blki = 0; blki < bcols; blki++) { + block_total_width = 0.0; + block_count = 0; + for (int inj = 0; inj < block_height; inj++) { + for (int ini = 0; ini < block_width; ini++) { + i = blki * block_width + ini; + j = blkj * block_height + inj; + + if (dst[j * dst_linesize + i] > 0) { + float width = edge_width(s, i, j, dir[j*dir_linesize+i], + w, h, dst[j*dst_linesize+i], + src, src_linesize); + if (width > 0.001) { // throw away zeros + block_count++; + block_total_width += width; + } + } + } + } + // if not enough edge pixels in a block, consider it smooth + if (block_total_width >= 2) { + blks[blkcnt] = block_total_width / block_count; + blkcnt++; + } + } + } + + // simple block pooling by sorting and keeping the sharper blocks + AV_QSORT(blks, blkcnt, float, comp); + blkcnt = ceil(blkcnt * block_pool_threshold); + for (int i = 0; i < blkcnt; i++) { + total_width += blks[i]; + } + + return total_width / blkcnt; +} + +static void set_meta(AVDictionary **metadata, const char *key, float d) +{ + char value[128]; + snprintf(value, sizeof(value), "%f", d); + av_dict_set(metadata, key, value, 0); +} + +static int blurdetect_filter_frame(AVFilterLink *inlink, AVFrame *in) +{ + AVFilterContext *ctx = inlink->dst; + BLRContext *s = ctx->priv; + AVFilterLink *outlink = ctx->outputs[0]; + + const int inw = inlink->w; + const int inh = inlink->h; + + uint8_t *tmpbuf = s->tmpbuf; + uint8_t *filterbuf = s->filterbuf; + uint16_t *gradients = s->gradients; + int8_t *directions = s->directions; + + float blur = 0.0f; + int nplanes = 0; + AVDictionary **metadata; + metadata = &in->metadata; + + for (int plane = 0; plane < s->nb_planes; plane++) { + int hsub = plane == 1 || plane == 2 ? s->hsub : 0; + int vsub = plane == 1 || plane == 2 ? s->vsub : 0; + int w = AV_CEIL_RSHIFT(inw, hsub); + int h = AV_CEIL_RSHIFT(inh, vsub); + + if (!((1 << plane) & s->planes)) + continue; + + nplanes++; + + // gaussian filter to reduce noise + ff_gaussian_blur(w, h, + filterbuf, w, + in->data[plane], in->linesize[plane]); + + // compute the 16-bits gradients and directions for the next step + ff_sobel(w, h, gradients, w, directions, w, filterbuf, w); + + // non_maximum_suppression() will actually keep & clip what's necessary and + // ignore the rest, so we need a clean output buffer + memset(tmpbuf, 0, inw * inh); + ff_non_maximum_suppression(w, h, tmpbuf, w, directions, w, gradients, w); + + + // keep high values, or low values surrounded by high values + ff_double_threshold(s->low_u8, s->high_u8, w, h, + tmpbuf, w, tmpbuf, w); + + blur += calculate_blur(s, w, h, hsub, vsub, directions, w, + tmpbuf, w, filterbuf, w); + } + + if (nplanes) + blur /= nplanes; + + s->blur_total += blur; + + // write stats + av_log(ctx, AV_LOG_VERBOSE, "blur: %.7f\n", blur); + + set_meta(metadata, "lavfi.blur", blur); + + s->nb_frames = inlink->frame_count_in; + + return ff_filter_frame(outlink, in); +} + +static av_cold void blurdetect_uninit(AVFilterContext *ctx) +{ + BLRContext *s = ctx->priv; + + if (s->nb_frames > 0) { + av_log(ctx, AV_LOG_INFO, "blur mean: %.7f\n", + s->blur_total / s->nb_frames); + } + + av_freep(&s->tmpbuf); + av_freep(&s->filterbuf); + av_freep(&s->gradients); + av_freep(&s->directions); + av_freep(&s->blks); +} + +static const enum AVPixelFormat pix_fmts[] = { + AV_PIX_FMT_GRAY8, + AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRAP, + AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV420P, + AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV440P, + AV_PIX_FMT_YUV411P, AV_PIX_FMT_YUV410P, + AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVJ411P, AV_PIX_FMT_YUVJ420P, + AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P, + AV_PIX_FMT_YUVA444P, AV_PIX_FMT_YUVA422P, AV_PIX_FMT_YUVA420P, + AV_PIX_FMT_NONE +}; + +static const AVFilterPad blurdetect_inputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .config_props = blurdetect_config_input, + .filter_frame = blurdetect_filter_frame, + }, +}; + +static const AVFilterPad blurdetect_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + }, +}; + +const AVFilter ff_vf_blurdetect = { + .name = "blurdetect", + .description = NULL_IF_CONFIG_SMALL("Blurdetect filter."), + .priv_size = sizeof(BLRContext), + .init = blurdetect_init, + .uninit = blurdetect_uninit, + FILTER_PIXFMTS_ARRAY(pix_fmts), + FILTER_INPUTS(blurdetect_inputs), + FILTER_OUTPUTS(blurdetect_outputs), + .priv_class = &blurdetect_class, + .flags = AVFILTER_FLAG_METADATA_ONLY, +}; diff --git a/tests/fate/filter-video.mak b/tests/fate/filter-video.mak index 6c0d8df032..68f4c084f8 100644 --- a/tests/fate/filter-video.mak +++ b/tests/fate/filter-video.mak @@ -867,6 +867,9 @@ fate-filter-meta-4560-rotate0: CMD = framecrc -auto_conversion_filters -flags +b REFCMP_DEPS = FFMPEG LAVFI_INDEV TESTSRC2_FILTER AVGBLUR_FILTER METADATA_FILTER +FATE_FILTER-$(call ALLYES, $(REFCMP_DEPS) BLURDETECT_FILTER) += fate-filter-refcmp-blurdetect-yuv +fate-filter-refcmp-blurdetect-yuv: CMD = cmp_metadata blurdetect yuv420p 0.015 + FATE_FILTER-$(call ALLYES, $(REFCMP_DEPS) PSNR_FILTER) += fate-filter-refcmp-psnr-rgb fate-filter-refcmp-psnr-rgb: CMD = refcmp_metadata psnr rgb24 0.002 diff --git a/tests/ref/fate/filter-refcmp-blurdetect-yuv b/tests/ref/fate/filter-refcmp-blurdetect-yuv new file mode 100644 index 0000000000..426321db0c --- /dev/null +++ b/tests/ref/fate/filter-refcmp-blurdetect-yuv @@ -0,0 +1,10 @@ +frame:0 pts:0 pts_time:0 +lavfi.blur=4.499666 +frame:1 pts:1 pts_time:1 +lavfi.blur=4.677492 +frame:2 pts:2 pts_time:2 +lavfi.blur=4.735711 +frame:3 pts:3 pts_time:3 +lavfi.blur=4.532343 +frame:4 pts:4 pts_time:4 +lavfi.blur=4.532660 |