Line | Branch | Exec | Source |
---|---|---|---|
1 | /* | ||
2 | * Copyright (c) 2021 Thilo Borgmann <thilo.borgmann _at_ mail.de> | ||
3 | * | ||
4 | * This file is part of FFmpeg. | ||
5 | * | ||
6 | * FFmpeg is free software; you can redistribute it and/or | ||
7 | * modify it under the terms of the GNU Lesser General Public | ||
8 | * License as published by the Free Software Foundation; either | ||
9 | * version 2.1 of the License, or (at your option) any later version. | ||
10 | * | ||
11 | * FFmpeg is distributed in the hope that it will be useful, | ||
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
14 | * Lesser General Public License for more details. | ||
15 | * | ||
16 | * You should have received a copy of the GNU Lesser General Public | ||
17 | * License along with FFmpeg; if not, write to the Free Software | ||
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
19 | */ | ||
20 | |||
21 | /** | ||
22 | * @file | ||
23 | * No-reference blurdetect filter | ||
24 | * | ||
25 | * Implementing: | ||
26 | * Marziliano, Pina, et al. "A no-reference perceptual blur metric." Proceedings. | ||
27 | * International conference on image processing. Vol. 3. IEEE, 2002. | ||
28 | * https://infoscience.epfl.ch/record/111802/files/14%20A%20no-reference%20perceptual%20blur%20metric.pdf | ||
29 | * | ||
30 | * @author Thilo Borgmann <thilo.borgmann _at_ mail.de> | ||
31 | */ | ||
32 | |||
33 | #include "libavutil/imgutils.h" | ||
34 | #include "libavutil/opt.h" | ||
35 | #include "libavutil/qsort.h" | ||
36 | #include "internal.h" | ||
37 | #include "edge_common.h" | ||
38 | #include "video.h" | ||
39 | |||
40 | ✗ | static int comp(const float *a,const float *b) | |
41 | { | ||
42 | ✗ | return FFDIFFSIGN(*a, *b); | |
43 | } | ||
44 | |||
45 | typedef struct BLRContext { | ||
46 | const AVClass *class; | ||
47 | |||
48 | int hsub, vsub; | ||
49 | int nb_planes; | ||
50 | |||
51 | float low, high; | ||
52 | uint8_t low_u8, high_u8; | ||
53 | int radius; // radius during local maxima detection | ||
54 | int block_pct; // percentage of "sharpest" blocks in the image to use for bluriness calculation | ||
55 | int block_width; // width for block abbreviation | ||
56 | int block_height; // height for block abbreviation | ||
57 | int planes; // number of planes to filter | ||
58 | |||
59 | double blur_total; | ||
60 | uint64_t nb_frames; | ||
61 | |||
62 | float *blks; | ||
63 | uint8_t *filterbuf; | ||
64 | uint8_t *tmpbuf; | ||
65 | uint16_t *gradients; | ||
66 | int8_t *directions; | ||
67 | } BLRContext; | ||
68 | |||
69 | #define OFFSET(x) offsetof(BLRContext, x) | ||
70 | #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM | ||
71 | static const AVOption blurdetect_options[] = { | ||
72 | { "high", "set high threshold", OFFSET(high), AV_OPT_TYPE_FLOAT, {.dbl=30/255.}, 0, 1, FLAGS }, | ||
73 | { "low", "set low threshold", OFFSET(low), AV_OPT_TYPE_FLOAT, {.dbl=15/255.}, 0, 1, FLAGS }, | ||
74 | { "radius", "search radius for maxima detection", OFFSET(radius), AV_OPT_TYPE_INT, {.i64=50}, 1, 100, FLAGS }, | ||
75 | { "block_pct", "block pooling threshold when calculating blurriness", OFFSET(block_pct), AV_OPT_TYPE_INT, {.i64=80}, 1, 100, FLAGS }, | ||
76 | { "block_width", "block size for block-based abbreviation of blurriness", OFFSET(block_width), AV_OPT_TYPE_INT, {.i64=-1}, -1, INT_MAX, FLAGS }, | ||
77 | { "block_height", "block size for block-based abbreviation of blurriness", OFFSET(block_height), AV_OPT_TYPE_INT, {.i64=-1}, -1, INT_MAX, FLAGS }, | ||
78 | { "planes", "set planes to filter", OFFSET(planes), AV_OPT_TYPE_INT, {.i64=1}, 0, 15, FLAGS }, | ||
79 | { NULL } | ||
80 | }; | ||
81 | |||
82 | AVFILTER_DEFINE_CLASS(blurdetect); | ||
83 | |||
84 | 2 | static av_cold int blurdetect_init(AVFilterContext *ctx) | |
85 | { | ||
86 | 2 | BLRContext *s = ctx->priv; | |
87 | |||
88 | 2 | s->low_u8 = s->low * 255. + .5; | |
89 | 2 | s->high_u8 = s->high * 255. + .5; | |
90 | |||
91 | 2 | return 0; | |
92 | } | ||
93 | |||
94 | 1 | static int blurdetect_config_input(AVFilterLink *inlink) | |
95 | { | ||
96 | 1 | AVFilterContext *ctx = inlink->dst; | |
97 | 1 | BLRContext *s = ctx->priv; | |
98 | 1 | const int bufsize = inlink->w * inlink->h; | |
99 | const AVPixFmtDescriptor *pix_desc; | ||
100 | |||
101 | 1 | pix_desc = av_pix_fmt_desc_get(inlink->format); | |
102 | 1 | s->hsub = pix_desc->log2_chroma_w; | |
103 | 1 | s->vsub = pix_desc->log2_chroma_h; | |
104 | 1 | s->nb_planes = av_pix_fmt_count_planes(inlink->format); | |
105 | |||
106 |
1/4✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
|
1 | if (s->block_width < 1 || s->block_height < 1) { |
107 | 1 | s->block_width = inlink->w; | |
108 | 1 | s->block_height = inlink->h; | |
109 | } | ||
110 | |||
111 | 1 | s->tmpbuf = av_malloc(bufsize); | |
112 | 1 | s->filterbuf = av_malloc(bufsize); | |
113 | 1 | s->gradients = av_calloc(bufsize, sizeof(*s->gradients)); | |
114 | 1 | s->directions = av_malloc(bufsize); | |
115 | 1 | s->blks = av_calloc((inlink->w / s->block_width) * (inlink->h / s->block_height), | |
116 | sizeof(*s->blks)); | ||
117 | |||
118 |
5/10✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 1 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 1 times.
✗ Branch 7 not taken.
✗ Branch 8 not taken.
✓ Branch 9 taken 1 times.
|
1 | if (!s->tmpbuf || !s->filterbuf || !s->gradients || !s->directions || !s->blks) |
119 | ✗ | return AVERROR(ENOMEM); | |
120 | |||
121 | 1 | return 0; | |
122 | } | ||
123 | |||
124 | // edge width is defined as the distance between surrounding maxima of the edge pixel | ||
125 | 9475 | static float edge_width(BLRContext *blr, int i, int j, int8_t dir, int w, int h, | |
126 | int edge, const uint8_t *src, int src_linesize) | ||
127 | { | ||
128 | 9475 | float width = 0; | |
129 | int dX, dY; | ||
130 | int sign; | ||
131 | int tmp; | ||
132 | int p1; | ||
133 | int p2; | ||
134 | int k, x, y; | ||
135 | 9475 | int radius = blr->radius; | |
136 | |||
137 |
4/5✓ Branch 0 taken 2344 times.
✓ Branch 1 taken 3412 times.
✓ Branch 2 taken 1970 times.
✓ Branch 3 taken 1749 times.
✗ Branch 4 not taken.
|
9475 | switch(dir) { |
138 | 2344 | case DIRECTION_HORIZONTAL: dX = 1; dY = 0; break; | |
139 | 3412 | case DIRECTION_VERTICAL: dX = 0; dY = 1; break; | |
140 | 1970 | case DIRECTION_45UP: dX = 1; dY = -1; break; | |
141 | 1749 | case DIRECTION_45DOWN: dX = 1; dY = 1; break; | |
142 | ✗ | default: dX = 1; dY = 1; break; | |
143 | } | ||
144 | |||
145 | // determines if search in direction dX/dY is looking for a maximum or minimum | ||
146 |
2/2✓ Branch 0 taken 4985 times.
✓ Branch 1 taken 4490 times.
|
9475 | sign = src[j * src_linesize + i] > src[(j - dY) * src_linesize + i - dX] ? 1 : -1; |
147 | |||
148 | // search in -(dX/dY) direction | ||
149 |
1/2✓ Branch 0 taken 34427 times.
✗ Branch 1 not taken.
|
34427 | for (k = 0; k < radius; k++) { |
150 | 34427 | x = i - k*dX; | |
151 | 34427 | y = j - k*dY; | |
152 | 34427 | p1 = y * src_linesize + x; | |
153 | 34427 | x -= dX; | |
154 | 34427 | y -= dY; | |
155 | 34427 | p2 = y * src_linesize + x; | |
156 |
5/8✓ Branch 0 taken 34426 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 34426 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34426 times.
✗ Branch 5 not taken.
✗ Branch 6 not taken.
✓ Branch 7 taken 34426 times.
|
34427 | if (x < 0 || x >= w || y < 0 || y >= h) |
157 | 1 | return 0; | |
158 | |||
159 | 34426 | tmp = (src[p1] - src[p2]) * sign; | |
160 | |||
161 |
2/2✓ Branch 0 taken 9474 times.
✓ Branch 1 taken 24952 times.
|
34426 | if (tmp <= 0) // local maximum found |
162 | 9474 | break; | |
163 | } | ||
164 | 9474 | width += k; | |
165 | |||
166 | // search in +(dX/dY) direction | ||
167 |
1/2✓ Branch 0 taken 34177 times.
✗ Branch 1 not taken.
|
34177 | for (k = 0; k < radius; k++) { |
168 | 34177 | x = i + k * dX; | |
169 | 34177 | y = j + k * dY; | |
170 | 34177 | p1 = y * src_linesize + x; | |
171 | 34177 | x += dX; | |
172 | 34177 | y += dY; | |
173 | 34177 | p2 = y * src_linesize + x; | |
174 |
5/8✓ Branch 0 taken 34177 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 34177 times.
✗ Branch 3 not taken.
✓ Branch 4 taken 34167 times.
✓ Branch 5 taken 10 times.
✗ Branch 6 not taken.
✓ Branch 7 taken 34167 times.
|
34177 | if (x < 0 || x >= w || y < 0 || y >= h) |
175 | 10 | return 0; | |
176 | |||
177 | 34167 | tmp = (src[p1] - src[p2]) * sign; | |
178 | |||
179 |
2/2✓ Branch 0 taken 9464 times.
✓ Branch 1 taken 24703 times.
|
34167 | if (tmp >= 0) // local maximum found |
180 | 9464 | break; | |
181 | } | ||
182 | 9464 | width += k; | |
183 | |||
184 | // for 45 degree directions approximate edge width in pixel units: 0.7 ~= sqrt(2)/2 | ||
185 |
4/4✓ Branch 0 taken 7504 times.
✓ Branch 1 taken 1960 times.
✓ Branch 2 taken 1748 times.
✓ Branch 3 taken 5756 times.
|
9464 | if (dir == DIRECTION_45UP || dir == DIRECTION_45DOWN) |
186 | 3708 | width *= 0.7; | |
187 | |||
188 | 9464 | return width; | |
189 | } | ||
190 | |||
191 | 5 | static float calculate_blur(BLRContext *s, int w, int h, int hsub, int vsub, | |
192 | int8_t* dir, int dir_linesize, | ||
193 | uint8_t* dst, int dst_linesize, | ||
194 | uint8_t* src, int src_linesize) | ||
195 | { | ||
196 | 5 | float total_width = 0.0; | |
197 | int block_count; | ||
198 | double block_total_width; | ||
199 | |||
200 | int i, j; | ||
201 | 5 | int blkcnt = 0; | |
202 | |||
203 | 5 | float *blks = s->blks; | |
204 | 5 | float block_pool_threshold = s->block_pct / 100.0; | |
205 | |||
206 | 5 | int block_width = AV_CEIL_RSHIFT(s->block_width, hsub); | |
207 | 5 | int block_height = AV_CEIL_RSHIFT(s->block_height, vsub); | |
208 | 5 | int brows = h / block_height; | |
209 | 5 | int bcols = w / block_width; | |
210 | |||
211 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
|
10 | for (int blkj = 0; blkj < brows; blkj++) { |
212 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
|
10 | for (int blki = 0; blki < bcols; blki++) { |
213 | 5 | block_total_width = 0.0; | |
214 | 5 | block_count = 0; | |
215 |
2/2✓ Branch 0 taken 1000 times.
✓ Branch 1 taken 5 times.
|
1005 | for (int inj = 0; inj < block_height; inj++) { |
216 |
2/2✓ Branch 0 taken 300000 times.
✓ Branch 1 taken 1000 times.
|
301000 | for (int ini = 0; ini < block_width; ini++) { |
217 | 300000 | i = blki * block_width + ini; | |
218 | 300000 | j = blkj * block_height + inj; | |
219 | |||
220 |
2/2✓ Branch 0 taken 9475 times.
✓ Branch 1 taken 290525 times.
|
300000 | if (dst[j * dst_linesize + i] > 0) { |
221 | 9475 | float width = edge_width(s, i, j, dir[j*dir_linesize+i], | |
222 | 9475 | w, h, dst[j*dst_linesize+i], | |
223 | src, src_linesize); | ||
224 |
2/2✓ Branch 0 taken 9455 times.
✓ Branch 1 taken 20 times.
|
9475 | if (width > 0.001) { // throw away zeros |
225 | 9455 | block_count++; | |
226 | 9455 | block_total_width += width; | |
227 | } | ||
228 | } | ||
229 | } | ||
230 | } | ||
231 | // if not enough edge pixels in a block, consider it smooth | ||
232 |
2/4✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 5 times.
✗ Branch 3 not taken.
|
5 | if (block_total_width >= 2 && block_count) { |
233 | 5 | blks[blkcnt] = block_total_width / block_count; | |
234 | 5 | blkcnt++; | |
235 | } | ||
236 | } | ||
237 | } | ||
238 | |||
239 | // simple block pooling by sorting and keeping the sharper blocks | ||
240 |
3/44✗ Branch 0 not taken.
✗ Branch 1 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
✗ Branch 9 not taken.
✗ Branch 10 not taken.
✗ Branch 12 not taken.
✗ Branch 13 not taken.
✗ Branch 14 not taken.
✗ Branch 15 not taken.
✗ Branch 16 not taken.
✗ Branch 17 not taken.
✗ Branch 19 not taken.
✗ Branch 20 not taken.
✗ Branch 21 not taken.
✗ Branch 22 not taken.
✗ Branch 24 not taken.
✗ Branch 25 not taken.
✗ Branch 26 not taken.
✗ Branch 27 not taken.
✗ Branch 28 not taken.
✗ Branch 29 not taken.
✗ Branch 30 not taken.
✗ Branch 31 not taken.
✗ Branch 32 not taken.
✗ Branch 33 not taken.
✗ Branch 34 not taken.
✗ Branch 35 not taken.
✗ Branch 36 not taken.
✗ Branch 37 not taken.
✗ Branch 39 not taken.
✗ Branch 40 not taken.
✗ Branch 41 not taken.
✗ Branch 42 not taken.
✗ Branch 43 not taken.
✗ Branch 44 not taken.
✗ Branch 46 not taken.
✗ Branch 47 not taken.
✗ Branch 48 not taken.
✓ Branch 49 taken 5 times.
✓ Branch 50 taken 5 times.
✓ Branch 51 taken 5 times.
|
10 | AV_QSORT(blks, blkcnt, float, comp); |
241 | 5 | blkcnt = ceil(blkcnt * block_pool_threshold); | |
242 |
2/2✓ Branch 0 taken 5 times.
✓ Branch 1 taken 5 times.
|
10 | for (int i = 0; i < blkcnt; i++) { |
243 | 5 | total_width += blks[i]; | |
244 | } | ||
245 | |||
246 | 5 | return total_width / blkcnt; | |
247 | } | ||
248 | |||
249 | 5 | static void set_meta(AVDictionary **metadata, const char *key, float d) | |
250 | { | ||
251 | char value[128]; | ||
252 | 5 | snprintf(value, sizeof(value), "%f", d); | |
253 | 5 | av_dict_set(metadata, key, value, 0); | |
254 | 5 | } | |
255 | |||
256 | 5 | static int blurdetect_filter_frame(AVFilterLink *inlink, AVFrame *in) | |
257 | { | ||
258 | 5 | AVFilterContext *ctx = inlink->dst; | |
259 | 5 | BLRContext *s = ctx->priv; | |
260 | 5 | AVFilterLink *outlink = ctx->outputs[0]; | |
261 | |||
262 | 5 | const int inw = inlink->w; | |
263 | 5 | const int inh = inlink->h; | |
264 | |||
265 | 5 | uint8_t *tmpbuf = s->tmpbuf; | |
266 | 5 | uint8_t *filterbuf = s->filterbuf; | |
267 | 5 | uint16_t *gradients = s->gradients; | |
268 | 5 | int8_t *directions = s->directions; | |
269 | |||
270 | 5 | float blur = 0.0f; | |
271 | 5 | int nplanes = 0; | |
272 | AVDictionary **metadata; | ||
273 | 5 | metadata = &in->metadata; | |
274 | |||
275 |
2/2✓ Branch 0 taken 15 times.
✓ Branch 1 taken 5 times.
|
20 | for (int plane = 0; plane < s->nb_planes; plane++) { |
276 |
4/4✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 5 times.
|
15 | int hsub = plane == 1 || plane == 2 ? s->hsub : 0; |
277 |
4/4✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
✓ Branch 2 taken 5 times.
✓ Branch 3 taken 5 times.
|
15 | int vsub = plane == 1 || plane == 2 ? s->vsub : 0; |
278 | 15 | int w = AV_CEIL_RSHIFT(inw, hsub); | |
279 | 15 | int h = AV_CEIL_RSHIFT(inh, vsub); | |
280 | |||
281 |
2/2✓ Branch 0 taken 10 times.
✓ Branch 1 taken 5 times.
|
15 | if (!((1 << plane) & s->planes)) |
282 | 10 | continue; | |
283 | |||
284 | 5 | nplanes++; | |
285 | |||
286 | // gaussian filter to reduce noise | ||
287 | 5 | ff_gaussian_blur_8(w, h, | |
288 | filterbuf, w, | ||
289 | 5 | in->data[plane], in->linesize[plane], 1); | |
290 | |||
291 | // compute the 16-bits gradients and directions for the next step | ||
292 | 5 | ff_sobel_8(w, h, gradients, w, directions, w, filterbuf, w, 1); | |
293 | |||
294 | // non_maximum_suppression() will actually keep & clip what's necessary and | ||
295 | // ignore the rest, so we need a clean output buffer | ||
296 | 5 | memset(tmpbuf, 0, inw * inh); | |
297 | 5 | ff_non_maximum_suppression(w, h, tmpbuf, w, directions, w, gradients, w); | |
298 | |||
299 | |||
300 | // keep high values, or low values surrounded by high values | ||
301 | 5 | ff_double_threshold(s->low_u8, s->high_u8, w, h, | |
302 | tmpbuf, w, tmpbuf, w); | ||
303 | |||
304 | 5 | blur += calculate_blur(s, w, h, hsub, vsub, directions, w, | |
305 | tmpbuf, w, filterbuf, w); | ||
306 | } | ||
307 | |||
308 |
1/2✓ Branch 0 taken 5 times.
✗ Branch 1 not taken.
|
5 | if (nplanes) |
309 | 5 | blur /= nplanes; | |
310 | |||
311 | 5 | s->blur_total += blur; | |
312 | |||
313 | // write stats | ||
314 | 5 | av_log(ctx, AV_LOG_VERBOSE, "blur: %.7f\n", blur); | |
315 | |||
316 | 5 | set_meta(metadata, "lavfi.blur", blur); | |
317 | |||
318 | 5 | s->nb_frames = inlink->frame_count_in; | |
319 | |||
320 | 5 | return ff_filter_frame(outlink, in); | |
321 | } | ||
322 | |||
323 | 2 | static av_cold void blurdetect_uninit(AVFilterContext *ctx) | |
324 | { | ||
325 | 2 | BLRContext *s = ctx->priv; | |
326 | |||
327 |
2/2✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
|
2 | if (s->nb_frames > 0) { |
328 | 1 | av_log(ctx, AV_LOG_INFO, "blur mean: %.7f\n", | |
329 | 1 | s->blur_total / s->nb_frames); | |
330 | } | ||
331 | |||
332 | 2 | av_freep(&s->tmpbuf); | |
333 | 2 | av_freep(&s->filterbuf); | |
334 | 2 | av_freep(&s->gradients); | |
335 | 2 | av_freep(&s->directions); | |
336 | 2 | av_freep(&s->blks); | |
337 | 2 | } | |
338 | |||
339 | static const enum AVPixelFormat pix_fmts[] = { | ||
340 | AV_PIX_FMT_GRAY8, | ||
341 | AV_PIX_FMT_GBRP, AV_PIX_FMT_GBRAP, | ||
342 | AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV420P, | ||
343 | AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUV440P, | ||
344 | AV_PIX_FMT_YUV411P, AV_PIX_FMT_YUV410P, | ||
345 | AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVJ411P, AV_PIX_FMT_YUVJ420P, | ||
346 | AV_PIX_FMT_YUVJ422P, AV_PIX_FMT_YUVJ444P, | ||
347 | AV_PIX_FMT_YUVA444P, AV_PIX_FMT_YUVA422P, AV_PIX_FMT_YUVA420P, | ||
348 | AV_PIX_FMT_NONE | ||
349 | }; | ||
350 | |||
351 | static const AVFilterPad blurdetect_inputs[] = { | ||
352 | { | ||
353 | .name = "default", | ||
354 | .type = AVMEDIA_TYPE_VIDEO, | ||
355 | .config_props = blurdetect_config_input, | ||
356 | .filter_frame = blurdetect_filter_frame, | ||
357 | }, | ||
358 | }; | ||
359 | |||
360 | const AVFilter ff_vf_blurdetect = { | ||
361 | .name = "blurdetect", | ||
362 | .description = NULL_IF_CONFIG_SMALL("Blurdetect filter."), | ||
363 | .priv_size = sizeof(BLRContext), | ||
364 | .init = blurdetect_init, | ||
365 | .uninit = blurdetect_uninit, | ||
366 | FILTER_PIXFMTS_ARRAY(pix_fmts), | ||
367 | FILTER_INPUTS(blurdetect_inputs), | ||
368 | FILTER_OUTPUTS(ff_video_default_filterpad), | ||
369 | .priv_class = &blurdetect_class, | ||
370 | .flags = AVFILTER_FLAG_METADATA_ONLY, | ||
371 | }; | ||
372 |