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