Commit | Line | Data |
---|---|---|
2ba45a60 DM |
1 | /* |
2 | * Copyright (c) 2012-2014 Clément Bœsch <u pkh me> | |
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 | * Edge detection filter | |
24 | * | |
25 | * @see https://en.wikipedia.org/wiki/Canny_edge_detector | |
26 | */ | |
27 | ||
28 | #include "libavutil/avassert.h" | |
29 | #include "libavutil/opt.h" | |
30 | #include "avfilter.h" | |
31 | #include "formats.h" | |
32 | #include "internal.h" | |
33 | #include "video.h" | |
34 | ||
35 | enum FilterMode { | |
36 | MODE_WIRES, | |
37 | MODE_COLORMIX, | |
38 | NB_MODE | |
39 | }; | |
40 | ||
41 | struct plane_info { | |
42 | uint8_t *tmpbuf; | |
43 | uint16_t *gradients; | |
44 | char *directions; | |
45 | }; | |
46 | ||
47 | typedef struct { | |
48 | const AVClass *class; | |
49 | struct plane_info planes[3]; | |
50 | int nb_planes; | |
51 | double low, high; | |
52 | uint8_t low_u8, high_u8; | |
53 | enum FilterMode mode; | |
54 | } EdgeDetectContext; | |
55 | ||
56 | #define OFFSET(x) offsetof(EdgeDetectContext, x) | |
57 | #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM | |
58 | static const AVOption edgedetect_options[] = { | |
59 | { "high", "set high threshold", OFFSET(high), AV_OPT_TYPE_DOUBLE, {.dbl=50/255.}, 0, 1, FLAGS }, | |
60 | { "low", "set low threshold", OFFSET(low), AV_OPT_TYPE_DOUBLE, {.dbl=20/255.}, 0, 1, FLAGS }, | |
61 | { "mode", "set mode", OFFSET(mode), AV_OPT_TYPE_INT, {.i64=MODE_WIRES}, 0, NB_MODE-1, FLAGS, "mode" }, | |
62 | { "wires", "white/gray wires on black", 0, AV_OPT_TYPE_CONST, {.i64=MODE_WIRES}, INT_MIN, INT_MAX, FLAGS, "mode" }, | |
63 | { "colormix", "mix colors", 0, AV_OPT_TYPE_CONST, {.i64=MODE_COLORMIX}, INT_MIN, INT_MAX, FLAGS, "mode" }, | |
64 | { NULL } | |
65 | }; | |
66 | ||
67 | AVFILTER_DEFINE_CLASS(edgedetect); | |
68 | ||
69 | static av_cold int init(AVFilterContext *ctx) | |
70 | { | |
71 | EdgeDetectContext *edgedetect = ctx->priv; | |
72 | ||
73 | edgedetect->low_u8 = edgedetect->low * 255. + .5; | |
74 | edgedetect->high_u8 = edgedetect->high * 255. + .5; | |
75 | return 0; | |
76 | } | |
77 | ||
78 | static int query_formats(AVFilterContext *ctx) | |
79 | { | |
80 | const EdgeDetectContext *edgedetect = ctx->priv; | |
81 | ||
82 | if (edgedetect->mode == MODE_WIRES) { | |
83 | static const enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_GRAY8, AV_PIX_FMT_NONE}; | |
84 | ff_set_common_formats(ctx, ff_make_format_list(pix_fmts)); | |
85 | } else if (edgedetect->mode == MODE_COLORMIX) { | |
86 | static const enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_GBRP, AV_PIX_FMT_GRAY8, AV_PIX_FMT_NONE}; | |
87 | ff_set_common_formats(ctx, ff_make_format_list(pix_fmts)); | |
88 | } else { | |
89 | av_assert0(0); | |
90 | } | |
91 | return 0; | |
92 | } | |
93 | ||
94 | static int config_props(AVFilterLink *inlink) | |
95 | { | |
96 | int p; | |
97 | AVFilterContext *ctx = inlink->dst; | |
98 | EdgeDetectContext *edgedetect = ctx->priv; | |
99 | ||
100 | edgedetect->nb_planes = inlink->format == AV_PIX_FMT_GRAY8 ? 1 : 3; | |
101 | for (p = 0; p < edgedetect->nb_planes; p++) { | |
102 | struct plane_info *plane = &edgedetect->planes[p]; | |
103 | ||
104 | plane->tmpbuf = av_malloc(inlink->w * inlink->h); | |
105 | plane->gradients = av_calloc(inlink->w * inlink->h, sizeof(*plane->gradients)); | |
106 | plane->directions = av_malloc(inlink->w * inlink->h); | |
107 | if (!plane->tmpbuf || !plane->gradients || !plane->directions) | |
108 | return AVERROR(ENOMEM); | |
109 | } | |
110 | return 0; | |
111 | } | |
112 | ||
113 | static void gaussian_blur(AVFilterContext *ctx, int w, int h, | |
114 | uint8_t *dst, int dst_linesize, | |
115 | const uint8_t *src, int src_linesize) | |
116 | { | |
117 | int i, j; | |
118 | ||
119 | memcpy(dst, src, w); dst += dst_linesize; src += src_linesize; | |
120 | memcpy(dst, src, w); dst += dst_linesize; src += src_linesize; | |
121 | for (j = 2; j < h - 2; j++) { | |
122 | dst[0] = src[0]; | |
123 | dst[1] = src[1]; | |
124 | for (i = 2; i < w - 2; i++) { | |
125 | /* Gaussian mask of size 5x5 with sigma = 1.4 */ | |
126 | dst[i] = ((src[-2*src_linesize + i-2] + src[2*src_linesize + i-2]) * 2 | |
127 | + (src[-2*src_linesize + i-1] + src[2*src_linesize + i-1]) * 4 | |
128 | + (src[-2*src_linesize + i ] + src[2*src_linesize + i ]) * 5 | |
129 | + (src[-2*src_linesize + i+1] + src[2*src_linesize + i+1]) * 4 | |
130 | + (src[-2*src_linesize + i+2] + src[2*src_linesize + i+2]) * 2 | |
131 | ||
132 | + (src[ -src_linesize + i-2] + src[ src_linesize + i-2]) * 4 | |
133 | + (src[ -src_linesize + i-1] + src[ src_linesize + i-1]) * 9 | |
134 | + (src[ -src_linesize + i ] + src[ src_linesize + i ]) * 12 | |
135 | + (src[ -src_linesize + i+1] + src[ src_linesize + i+1]) * 9 | |
136 | + (src[ -src_linesize + i+2] + src[ src_linesize + i+2]) * 4 | |
137 | ||
138 | + src[i-2] * 5 | |
139 | + src[i-1] * 12 | |
140 | + src[i ] * 15 | |
141 | + src[i+1] * 12 | |
142 | + src[i+2] * 5) / 159; | |
143 | } | |
144 | dst[i ] = src[i ]; | |
145 | dst[i + 1] = src[i + 1]; | |
146 | ||
147 | dst += dst_linesize; | |
148 | src += src_linesize; | |
149 | } | |
150 | memcpy(dst, src, w); dst += dst_linesize; src += src_linesize; | |
151 | memcpy(dst, src, w); | |
152 | } | |
153 | ||
154 | enum { | |
155 | DIRECTION_45UP, | |
156 | DIRECTION_45DOWN, | |
157 | DIRECTION_HORIZONTAL, | |
158 | DIRECTION_VERTICAL, | |
159 | }; | |
160 | ||
161 | static int get_rounded_direction(int gx, int gy) | |
162 | { | |
163 | /* reference angles: | |
164 | * tan( pi/8) = sqrt(2)-1 | |
165 | * tan(3pi/8) = sqrt(2)+1 | |
166 | * Gy/Gx is the tangent of the angle (theta), so Gy/Gx is compared against | |
167 | * <ref-angle>, or more simply Gy against <ref-angle>*Gx | |
168 | * | |
169 | * Gx and Gy bounds = [-1020;1020], using 16-bit arithmetic: | |
170 | * round((sqrt(2)-1) * (1<<16)) = 27146 | |
171 | * round((sqrt(2)+1) * (1<<16)) = 158218 | |
172 | */ | |
173 | if (gx) { | |
174 | int tanpi8gx, tan3pi8gx; | |
175 | ||
176 | if (gx < 0) | |
177 | gx = -gx, gy = -gy; | |
178 | gy <<= 16; | |
179 | tanpi8gx = 27146 * gx; | |
180 | tan3pi8gx = 158218 * gx; | |
181 | if (gy > -tan3pi8gx && gy < -tanpi8gx) return DIRECTION_45UP; | |
182 | if (gy > -tanpi8gx && gy < tanpi8gx) return DIRECTION_HORIZONTAL; | |
183 | if (gy > tanpi8gx && gy < tan3pi8gx) return DIRECTION_45DOWN; | |
184 | } | |
185 | return DIRECTION_VERTICAL; | |
186 | } | |
187 | ||
188 | static void sobel(int w, int h, | |
189 | uint16_t *dst, int dst_linesize, | |
190 | int8_t *dir, int dir_linesize, | |
191 | const uint8_t *src, int src_linesize) | |
192 | { | |
193 | int i, j; | |
194 | ||
195 | for (j = 1; j < h - 1; j++) { | |
196 | dst += dst_linesize; | |
197 | dir += dir_linesize; | |
198 | src += src_linesize; | |
199 | for (i = 1; i < w - 1; i++) { | |
200 | const int gx = | |
201 | -1*src[-src_linesize + i-1] + 1*src[-src_linesize + i+1] | |
202 | -2*src[ i-1] + 2*src[ i+1] | |
203 | -1*src[ src_linesize + i-1] + 1*src[ src_linesize + i+1]; | |
204 | const int gy = | |
205 | -1*src[-src_linesize + i-1] + 1*src[ src_linesize + i-1] | |
206 | -2*src[-src_linesize + i ] + 2*src[ src_linesize + i ] | |
207 | -1*src[-src_linesize + i+1] + 1*src[ src_linesize + i+1]; | |
208 | ||
209 | dst[i] = FFABS(gx) + FFABS(gy); | |
210 | dir[i] = get_rounded_direction(gx, gy); | |
211 | } | |
212 | } | |
213 | } | |
214 | ||
215 | static void non_maximum_suppression(int w, int h, | |
216 | uint8_t *dst, int dst_linesize, | |
217 | const int8_t *dir, int dir_linesize, | |
218 | const uint16_t *src, int src_linesize) | |
219 | { | |
220 | int i, j; | |
221 | ||
222 | #define COPY_MAXIMA(ay, ax, by, bx) do { \ | |
223 | if (src[i] > src[(ay)*src_linesize + i+(ax)] && \ | |
224 | src[i] > src[(by)*src_linesize + i+(bx)]) \ | |
225 | dst[i] = av_clip_uint8(src[i]); \ | |
226 | } while (0) | |
227 | ||
228 | for (j = 1; j < h - 1; j++) { | |
229 | dst += dst_linesize; | |
230 | dir += dir_linesize; | |
231 | src += src_linesize; | |
232 | for (i = 1; i < w - 1; i++) { | |
233 | switch (dir[i]) { | |
234 | case DIRECTION_45UP: COPY_MAXIMA( 1, -1, -1, 1); break; | |
235 | case DIRECTION_45DOWN: COPY_MAXIMA(-1, -1, 1, 1); break; | |
236 | case DIRECTION_HORIZONTAL: COPY_MAXIMA( 0, -1, 0, 1); break; | |
237 | case DIRECTION_VERTICAL: COPY_MAXIMA(-1, 0, 1, 0); break; | |
238 | } | |
239 | } | |
240 | } | |
241 | } | |
242 | ||
243 | static void double_threshold(int low, int high, int w, int h, | |
244 | uint8_t *dst, int dst_linesize, | |
245 | const uint8_t *src, int src_linesize) | |
246 | { | |
247 | int i, j; | |
248 | ||
249 | for (j = 0; j < h; j++) { | |
250 | for (i = 0; i < w; i++) { | |
251 | if (src[i] > high) { | |
252 | dst[i] = src[i]; | |
253 | continue; | |
254 | } | |
255 | ||
256 | if ((!i || i == w - 1 || !j || j == h - 1) && | |
257 | src[i] > low && | |
258 | (src[-src_linesize + i-1] > high || | |
259 | src[-src_linesize + i ] > high || | |
260 | src[-src_linesize + i+1] > high || | |
261 | src[ i-1] > high || | |
262 | src[ i+1] > high || | |
263 | src[ src_linesize + i-1] > high || | |
264 | src[ src_linesize + i ] > high || | |
265 | src[ src_linesize + i+1] > high)) | |
266 | dst[i] = src[i]; | |
267 | else | |
268 | dst[i] = 0; | |
269 | } | |
270 | dst += dst_linesize; | |
271 | src += src_linesize; | |
272 | } | |
273 | } | |
274 | ||
275 | static void color_mix(int w, int h, | |
276 | uint8_t *dst, int dst_linesize, | |
277 | const uint8_t *src, int src_linesize) | |
278 | { | |
279 | int i, j; | |
280 | ||
281 | for (j = 0; j < h; j++) { | |
282 | for (i = 0; i < w; i++) | |
283 | dst[i] = (dst[i] + src[i]) >> 1; | |
284 | dst += dst_linesize; | |
285 | src += src_linesize; | |
286 | } | |
287 | } | |
288 | ||
289 | static int filter_frame(AVFilterLink *inlink, AVFrame *in) | |
290 | { | |
291 | AVFilterContext *ctx = inlink->dst; | |
292 | EdgeDetectContext *edgedetect = ctx->priv; | |
293 | AVFilterLink *outlink = ctx->outputs[0]; | |
294 | int p, direct = 0; | |
295 | AVFrame *out; | |
296 | ||
297 | if (edgedetect->mode != MODE_COLORMIX && av_frame_is_writable(in)) { | |
298 | direct = 1; | |
299 | out = in; | |
300 | } else { | |
301 | out = ff_get_video_buffer(outlink, outlink->w, outlink->h); | |
302 | if (!out) { | |
303 | av_frame_free(&in); | |
304 | return AVERROR(ENOMEM); | |
305 | } | |
306 | av_frame_copy_props(out, in); | |
307 | } | |
308 | ||
309 | for (p = 0; p < edgedetect->nb_planes; p++) { | |
310 | struct plane_info *plane = &edgedetect->planes[p]; | |
311 | uint8_t *tmpbuf = plane->tmpbuf; | |
312 | uint16_t *gradients = plane->gradients; | |
313 | int8_t *directions = plane->directions; | |
314 | ||
315 | /* gaussian filter to reduce noise */ | |
316 | gaussian_blur(ctx, inlink->w, inlink->h, | |
317 | tmpbuf, inlink->w, | |
318 | in->data[p], in->linesize[p]); | |
319 | ||
320 | /* compute the 16-bits gradients and directions for the next step */ | |
321 | sobel(inlink->w, inlink->h, | |
322 | gradients, inlink->w, | |
323 | directions,inlink->w, | |
324 | tmpbuf, inlink->w); | |
325 | ||
326 | /* non_maximum_suppression() will actually keep & clip what's necessary and | |
327 | * ignore the rest, so we need a clean output buffer */ | |
328 | memset(tmpbuf, 0, inlink->w * inlink->h); | |
329 | non_maximum_suppression(inlink->w, inlink->h, | |
330 | tmpbuf, inlink->w, | |
331 | directions,inlink->w, | |
332 | gradients, inlink->w); | |
333 | ||
334 | /* keep high values, or low values surrounded by high values */ | |
335 | double_threshold(edgedetect->low_u8, edgedetect->high_u8, | |
336 | inlink->w, inlink->h, | |
337 | out->data[p], out->linesize[p], | |
338 | tmpbuf, inlink->w); | |
339 | ||
340 | if (edgedetect->mode == MODE_COLORMIX) { | |
341 | color_mix(inlink->w, inlink->h, | |
342 | out->data[p], out->linesize[p], | |
343 | in->data[p], in->linesize[p]); | |
344 | } | |
345 | } | |
346 | ||
347 | if (!direct) | |
348 | av_frame_free(&in); | |
349 | return ff_filter_frame(outlink, out); | |
350 | } | |
351 | ||
352 | static av_cold void uninit(AVFilterContext *ctx) | |
353 | { | |
354 | int p; | |
355 | EdgeDetectContext *edgedetect = ctx->priv; | |
356 | ||
357 | for (p = 0; p < edgedetect->nb_planes; p++) { | |
358 | struct plane_info *plane = &edgedetect->planes[p]; | |
359 | av_freep(&plane->tmpbuf); | |
360 | av_freep(&plane->gradients); | |
361 | av_freep(&plane->directions); | |
362 | } | |
363 | } | |
364 | ||
365 | static const AVFilterPad edgedetect_inputs[] = { | |
366 | { | |
367 | .name = "default", | |
368 | .type = AVMEDIA_TYPE_VIDEO, | |
369 | .config_props = config_props, | |
370 | .filter_frame = filter_frame, | |
371 | }, | |
372 | { NULL } | |
373 | }; | |
374 | ||
375 | static const AVFilterPad edgedetect_outputs[] = { | |
376 | { | |
377 | .name = "default", | |
378 | .type = AVMEDIA_TYPE_VIDEO, | |
379 | }, | |
380 | { NULL } | |
381 | }; | |
382 | ||
383 | AVFilter ff_vf_edgedetect = { | |
384 | .name = "edgedetect", | |
385 | .description = NULL_IF_CONFIG_SMALL("Detect and draw edge."), | |
386 | .priv_size = sizeof(EdgeDetectContext), | |
387 | .init = init, | |
388 | .uninit = uninit, | |
389 | .query_formats = query_formats, | |
390 | .inputs = edgedetect_inputs, | |
391 | .outputs = edgedetect_outputs, | |
392 | .priv_class = &edgedetect_class, | |
393 | .flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC, | |
394 | }; |