blob: 748b67b07ab74568fc8c6b88fdfce7b0f077d57f
1 | /* |
2 | * Copyright (c) 2015-2016 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 | * @see http://blog.pkh.me/p/22-understanding-selective-coloring-in-adobe-photoshop.html |
23 | * @todo |
24 | * - use integers so it can be made bitexact and a FATE test can be added |
25 | */ |
26 | |
27 | #include "libavutil/avassert.h" |
28 | #include "libavutil/file.h" |
29 | #include "libavutil/intreadwrite.h" |
30 | #include "libavutil/opt.h" |
31 | #include "libavutil/pixdesc.h" |
32 | #include "libavcodec/mathops.h" // for mid_pred(), which is a macro so no link dependency |
33 | #include "avfilter.h" |
34 | #include "drawutils.h" |
35 | #include "formats.h" |
36 | #include "internal.h" |
37 | #include "video.h" |
38 | |
39 | #define R 0 |
40 | #define G 1 |
41 | #define B 2 |
42 | #define A 3 |
43 | |
44 | enum color_range { |
45 | // WARNING: do NOT reorder (see parse_psfile()) |
46 | RANGE_REDS, |
47 | RANGE_YELLOWS, |
48 | RANGE_GREENS, |
49 | RANGE_CYANS, |
50 | RANGE_BLUES, |
51 | RANGE_MAGENTAS, |
52 | RANGE_WHITES, |
53 | RANGE_NEUTRALS, |
54 | RANGE_BLACKS, |
55 | NB_RANGES |
56 | }; |
57 | |
58 | enum correction_method { |
59 | CORRECTION_METHOD_ABSOLUTE, |
60 | CORRECTION_METHOD_RELATIVE, |
61 | NB_CORRECTION_METHODS, |
62 | }; |
63 | |
64 | static const char *color_names[NB_RANGES] = { |
65 | "red", "yellow", "green", "cyan", "blue", "magenta", "white", "neutral", "black" |
66 | }; |
67 | |
68 | typedef int (*get_range_scale_func)(int r, int g, int b, int min_val, int max_val); |
69 | |
70 | struct process_range { |
71 | int range_id; |
72 | uint32_t mask; |
73 | get_range_scale_func get_scale; |
74 | }; |
75 | |
76 | typedef struct ThreadData { |
77 | AVFrame *in, *out; |
78 | } ThreadData; |
79 | |
80 | typedef struct { |
81 | const AVClass *class; |
82 | int correction_method; |
83 | char *opt_cmyk_adjust[NB_RANGES]; |
84 | float cmyk_adjust[NB_RANGES][4]; |
85 | struct process_range process_ranges[NB_RANGES]; // color ranges to process |
86 | int nb_process_ranges; |
87 | char *psfile; |
88 | uint8_t rgba_map[4]; |
89 | int is_16bit; |
90 | int step; |
91 | } SelectiveColorContext; |
92 | |
93 | #define OFFSET(x) offsetof(SelectiveColorContext, x) |
94 | #define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM |
95 | #define RANGE_OPTION(color_name, range) \ |
96 | { color_name"s", "adjust "color_name" regions", OFFSET(opt_cmyk_adjust[range]), AV_OPT_TYPE_STRING, {.str=NULL}, CHAR_MIN, CHAR_MAX, FLAGS } |
97 | |
98 | static const AVOption selectivecolor_options[] = { |
99 | { "correction_method", "select correction method", OFFSET(correction_method), AV_OPT_TYPE_INT, {.i64 = CORRECTION_METHOD_ABSOLUTE}, 0, NB_CORRECTION_METHODS-1, FLAGS, "correction_method" }, |
100 | { "absolute", NULL, 0, AV_OPT_TYPE_CONST, {.i64=CORRECTION_METHOD_ABSOLUTE}, INT_MIN, INT_MAX, FLAGS, "correction_method" }, |
101 | { "relative", NULL, 0, AV_OPT_TYPE_CONST, {.i64=CORRECTION_METHOD_RELATIVE}, INT_MIN, INT_MAX, FLAGS, "correction_method" }, |
102 | RANGE_OPTION("red", RANGE_REDS), |
103 | RANGE_OPTION("yellow", RANGE_YELLOWS), |
104 | RANGE_OPTION("green", RANGE_GREENS), |
105 | RANGE_OPTION("cyan", RANGE_CYANS), |
106 | RANGE_OPTION("blue", RANGE_BLUES), |
107 | RANGE_OPTION("magenta", RANGE_MAGENTAS), |
108 | RANGE_OPTION("white", RANGE_WHITES), |
109 | RANGE_OPTION("neutral", RANGE_NEUTRALS), |
110 | RANGE_OPTION("black", RANGE_BLACKS), |
111 | { "psfile", "set Photoshop selectivecolor file name", OFFSET(psfile), AV_OPT_TYPE_STRING, {.str=NULL}, .flags = FLAGS }, |
112 | { NULL } |
113 | }; |
114 | |
115 | AVFILTER_DEFINE_CLASS(selectivecolor); |
116 | |
117 | static int get_rgb_scale(int r, int g, int b, int min_val, int max_val) |
118 | { |
119 | return max_val - mid_pred(r, g, b); |
120 | } |
121 | |
122 | static int get_cmy_scale(int r, int g, int b, int min_val, int max_val) |
123 | { |
124 | return mid_pred(r, g, b) - min_val; |
125 | } |
126 | |
127 | #define DECLARE_RANGE_SCALE_FUNCS(nbits) \ |
128 | static int get_neutrals_scale##nbits(int r, int g, int b, int min_val, int max_val) \ |
129 | { \ |
130 | /* 1 - (|max-0.5| + |min-0.5|) */ \ |
131 | return (((1<<nbits)-1)*2 - ( abs((max_val<<1) - ((1<<nbits)-1)) \ |
132 | + abs((min_val<<1) - ((1<<nbits)-1))) + 1) >> 1; \ |
133 | } \ |
134 | \ |
135 | static int get_whites_scale##nbits(int r, int g, int b, int min_val, int max_val) \ |
136 | { \ |
137 | /* (min - 0.5) * 2 */ \ |
138 | return (min_val<<1) - ((1<<nbits)-1); \ |
139 | } \ |
140 | \ |
141 | static int get_blacks_scale##nbits(int r, int g, int b, int min_val, int max_val) \ |
142 | { \ |
143 | /* (0.5 - max) * 2 */ \ |
144 | return ((1<<nbits)-1) - (max_val<<1); \ |
145 | } \ |
146 | |
147 | DECLARE_RANGE_SCALE_FUNCS(8) |
148 | DECLARE_RANGE_SCALE_FUNCS(16) |
149 | |
150 | static int register_range(SelectiveColorContext *s, int range_id) |
151 | { |
152 | const float *cmyk = s->cmyk_adjust[range_id]; |
153 | |
154 | /* If the color range has user settings, register the color range |
155 | * as "to be processed" */ |
156 | if (cmyk[0] || cmyk[1] || cmyk[2] || cmyk[3]) { |
157 | struct process_range *pr = &s->process_ranges[s->nb_process_ranges++]; |
158 | |
159 | if (cmyk[0] < -1.0 || cmyk[0] > 1.0 || |
160 | cmyk[1] < -1.0 || cmyk[1] > 1.0 || |
161 | cmyk[2] < -1.0 || cmyk[2] > 1.0 || |
162 | cmyk[3] < -1.0 || cmyk[3] > 1.0) { |
163 | av_log(s, AV_LOG_ERROR, "Invalid %s adjustments (%g %g %g %g). " |
164 | "Settings must be set in [-1;1] range\n", |
165 | color_names[range_id], cmyk[0], cmyk[1], cmyk[2], cmyk[3]); |
166 | return AVERROR(EINVAL); |
167 | } |
168 | |
169 | pr->range_id = range_id; |
170 | pr->mask = 1 << range_id; |
171 | if (pr->mask & (1<<RANGE_REDS | 1<<RANGE_GREENS | 1<<RANGE_BLUES)) pr->get_scale = get_rgb_scale; |
172 | else if (pr->mask & (1<<RANGE_CYANS | 1<<RANGE_MAGENTAS | 1<<RANGE_YELLOWS)) pr->get_scale = get_cmy_scale; |
173 | else if (!s->is_16bit && (pr->mask & 1<<RANGE_WHITES)) pr->get_scale = get_whites_scale8; |
174 | else if (!s->is_16bit && (pr->mask & 1<<RANGE_NEUTRALS)) pr->get_scale = get_neutrals_scale8; |
175 | else if (!s->is_16bit && (pr->mask & 1<<RANGE_BLACKS)) pr->get_scale = get_blacks_scale8; |
176 | else if ( s->is_16bit && (pr->mask & 1<<RANGE_WHITES)) pr->get_scale = get_whites_scale16; |
177 | else if ( s->is_16bit && (pr->mask & 1<<RANGE_NEUTRALS)) pr->get_scale = get_neutrals_scale16; |
178 | else if ( s->is_16bit && (pr->mask & 1<<RANGE_BLACKS)) pr->get_scale = get_blacks_scale16; |
179 | else |
180 | av_assert0(0); |
181 | } |
182 | return 0; |
183 | } |
184 | |
185 | static int parse_psfile(AVFilterContext *ctx, const char *fname) |
186 | { |
187 | int16_t val; |
188 | int ret, i, version; |
189 | uint8_t *buf; |
190 | size_t size; |
191 | SelectiveColorContext *s = ctx->priv; |
192 | |
193 | ret = av_file_map(fname, &buf, &size, 0, NULL); |
194 | if (ret < 0) |
195 | return ret; |
196 | |
197 | #define READ16(dst) do { \ |
198 | if (size < 2) { \ |
199 | ret = AVERROR_INVALIDDATA; \ |
200 | goto end; \ |
201 | } \ |
202 | dst = AV_RB16(buf); \ |
203 | buf += 2; \ |
204 | size -= 2; \ |
205 | } while (0) |
206 | |
207 | READ16(version); |
208 | if (version != 1) |
209 | av_log(s, AV_LOG_WARNING, "Unsupported selective color file version %d, " |
210 | "the settings might not be loaded properly\n", version); |
211 | |
212 | READ16(s->correction_method); |
213 | |
214 | // 1st CMYK entry is reserved/unused |
215 | for (i = 0; i < FF_ARRAY_ELEMS(s->cmyk_adjust[0]); i++) { |
216 | READ16(val); |
217 | if (val) |
218 | av_log(s, AV_LOG_WARNING, "%c value of first CMYK entry is not 0 " |
219 | "but %d\n", "CMYK"[i], val); |
220 | } |
221 | |
222 | for (i = 0; i < FF_ARRAY_ELEMS(s->cmyk_adjust); i++) { |
223 | int k; |
224 | for (k = 0; k < FF_ARRAY_ELEMS(s->cmyk_adjust[0]); k++) { |
225 | READ16(val); |
226 | s->cmyk_adjust[i][k] = val / 100.; |
227 | } |
228 | ret = register_range(s, i); |
229 | if (ret < 0) |
230 | goto end; |
231 | } |
232 | |
233 | end: |
234 | av_file_unmap(buf, size); |
235 | return ret; |
236 | } |
237 | |
238 | static int config_input(AVFilterLink *inlink) |
239 | { |
240 | int i, ret; |
241 | AVFilterContext *ctx = inlink->dst; |
242 | SelectiveColorContext *s = ctx->priv; |
243 | const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format); |
244 | |
245 | s->is_16bit = desc->comp[0].depth > 8; |
246 | s->step = av_get_padded_bits_per_pixel(desc) >> (3 + s->is_16bit); |
247 | |
248 | ret = ff_fill_rgba_map(s->rgba_map, inlink->format); |
249 | if (ret < 0) |
250 | return ret; |
251 | |
252 | /* If the following conditions are not met, it will cause trouble while |
253 | * parsing the PS file */ |
254 | av_assert0(FF_ARRAY_ELEMS(s->cmyk_adjust) == 10 - 1); |
255 | av_assert0(FF_ARRAY_ELEMS(s->cmyk_adjust[0]) == 4); |
256 | |
257 | if (s->psfile) { |
258 | ret = parse_psfile(ctx, s->psfile); |
259 | if (ret < 0) |
260 | return ret; |
261 | } else { |
262 | for (i = 0; i < FF_ARRAY_ELEMS(s->opt_cmyk_adjust); i++) { |
263 | const char *opt_cmyk_adjust = s->opt_cmyk_adjust[i]; |
264 | |
265 | if (opt_cmyk_adjust) { |
266 | float *cmyk = s->cmyk_adjust[i]; |
267 | |
268 | sscanf(s->opt_cmyk_adjust[i], "%f %f %f %f", cmyk, cmyk+1, cmyk+2, cmyk+3); |
269 | ret = register_range(s, i); |
270 | if (ret < 0) |
271 | return ret; |
272 | } |
273 | } |
274 | } |
275 | |
276 | av_log(s, AV_LOG_VERBOSE, "Adjustments:%s\n", s->nb_process_ranges ? "" : " none"); |
277 | for (i = 0; i < s->nb_process_ranges; i++) { |
278 | const struct process_range *pr = &s->process_ranges[i]; |
279 | const float *cmyk = s->cmyk_adjust[pr->range_id]; |
280 | |
281 | av_log(s, AV_LOG_VERBOSE, "%8ss: C=%6g M=%6g Y=%6g K=%6g\n", |
282 | color_names[pr->range_id], cmyk[0], cmyk[1], cmyk[2], cmyk[3]); |
283 | } |
284 | |
285 | return 0; |
286 | } |
287 | |
288 | static int query_formats(AVFilterContext *ctx) |
289 | { |
290 | static const enum AVPixelFormat pix_fmts[] = { |
291 | AV_PIX_FMT_RGB24, AV_PIX_FMT_BGR24, |
292 | AV_PIX_FMT_RGBA, AV_PIX_FMT_BGRA, |
293 | AV_PIX_FMT_ARGB, AV_PIX_FMT_ABGR, |
294 | AV_PIX_FMT_0RGB, AV_PIX_FMT_0BGR, |
295 | AV_PIX_FMT_RGB0, AV_PIX_FMT_BGR0, |
296 | AV_PIX_FMT_RGB48, AV_PIX_FMT_BGR48, |
297 | AV_PIX_FMT_RGBA64, AV_PIX_FMT_BGRA64, |
298 | AV_PIX_FMT_NONE |
299 | }; |
300 | AVFilterFormats *fmts_list = ff_make_format_list(pix_fmts); |
301 | if (!fmts_list) |
302 | return AVERROR(ENOMEM); |
303 | return ff_set_common_formats(ctx, fmts_list); |
304 | } |
305 | |
306 | static inline int comp_adjust(int scale, float value, float adjust, float k, int correction_method) |
307 | { |
308 | const float min = -value; |
309 | const float max = 1. - value; |
310 | float res = (-1. - adjust) * k - adjust; |
311 | if (correction_method == CORRECTION_METHOD_RELATIVE) |
312 | res *= max; |
313 | return lrint(av_clipf(res, min, max) * scale); |
314 | } |
315 | |
316 | #define DECLARE_SELECTIVE_COLOR_FUNC(nbits) \ |
317 | static inline int selective_color_##nbits(AVFilterContext *ctx, ThreadData *td, \ |
318 | int jobnr, int nb_jobs, int direct, int correction_method) \ |
319 | { \ |
320 | int i, x, y; \ |
321 | const AVFrame *in = td->in; \ |
322 | AVFrame *out = td->out; \ |
323 | const SelectiveColorContext *s = ctx->priv; \ |
324 | const int height = in->height; \ |
325 | const int width = in->width; \ |
326 | const int slice_start = (height * jobnr ) / nb_jobs; \ |
327 | const int slice_end = (height * (jobnr+1)) / nb_jobs; \ |
328 | const int dst_linesize = out->linesize[0]; \ |
329 | const int src_linesize = in->linesize[0]; \ |
330 | const uint8_t roffset = s->rgba_map[R]; \ |
331 | const uint8_t goffset = s->rgba_map[G]; \ |
332 | const uint8_t boffset = s->rgba_map[B]; \ |
333 | const uint8_t aoffset = s->rgba_map[A]; \ |
334 | \ |
335 | for (y = slice_start; y < slice_end; y++) { \ |
336 | uint##nbits##_t *dst = ( uint##nbits##_t *)(out->data[0] + y * dst_linesize); \ |
337 | const uint##nbits##_t *src = (const uint##nbits##_t *)( in->data[0] + y * src_linesize); \ |
338 | \ |
339 | for (x = 0; x < width * s->step; x += s->step) { \ |
340 | const int r = src[x + roffset]; \ |
341 | const int g = src[x + goffset]; \ |
342 | const int b = src[x + boffset]; \ |
343 | const int min_color = FFMIN3(r, g, b); \ |
344 | const int max_color = FFMAX3(r, g, b); \ |
345 | const int is_white = (r > 1<<(nbits-1) && g > 1<<(nbits-1) && b > 1<<(nbits-1)); \ |
346 | const int is_neutral = (r || g || b) && \ |
347 | r != (1<<nbits)-1 && g != (1<<nbits)-1 && b != (1<<nbits)-1; \ |
348 | const int is_black = (r < 1<<(nbits-1) && g < 1<<(nbits-1) && b < 1<<(nbits-1)); \ |
349 | const uint32_t range_flag = (r == max_color) << RANGE_REDS \ |
350 | | (r == min_color) << RANGE_CYANS \ |
351 | | (g == max_color) << RANGE_GREENS \ |
352 | | (g == min_color) << RANGE_MAGENTAS \ |
353 | | (b == max_color) << RANGE_BLUES \ |
354 | | (b == min_color) << RANGE_YELLOWS \ |
355 | | is_white << RANGE_WHITES \ |
356 | | is_neutral << RANGE_NEUTRALS \ |
357 | | is_black << RANGE_BLACKS; \ |
358 | \ |
359 | const float rnorm = r * (1.f / ((1<<nbits)-1)); \ |
360 | const float gnorm = g * (1.f / ((1<<nbits)-1)); \ |
361 | const float bnorm = b * (1.f / ((1<<nbits)-1)); \ |
362 | int adjust_r = 0, adjust_g = 0, adjust_b = 0; \ |
363 | \ |
364 | for (i = 0; i < s->nb_process_ranges; i++) { \ |
365 | const struct process_range *pr = &s->process_ranges[i]; \ |
366 | \ |
367 | if (range_flag & pr->mask) { \ |
368 | const int scale = pr->get_scale(r, g, b, min_color, max_color); \ |
369 | \ |
370 | if (scale > 0) { \ |
371 | const float *cmyk_adjust = s->cmyk_adjust[pr->range_id]; \ |
372 | const float adj_c = cmyk_adjust[0]; \ |
373 | const float adj_m = cmyk_adjust[1]; \ |
374 | const float adj_y = cmyk_adjust[2]; \ |
375 | const float k = cmyk_adjust[3]; \ |
376 | \ |
377 | adjust_r += comp_adjust(scale, rnorm, adj_c, k, correction_method); \ |
378 | adjust_g += comp_adjust(scale, gnorm, adj_m, k, correction_method); \ |
379 | adjust_b += comp_adjust(scale, bnorm, adj_y, k, correction_method); \ |
380 | } \ |
381 | } \ |
382 | } \ |
383 | \ |
384 | if (!direct || adjust_r || adjust_g || adjust_b) { \ |
385 | dst[x + roffset] = av_clip_uint##nbits(r + adjust_r); \ |
386 | dst[x + goffset] = av_clip_uint##nbits(g + adjust_g); \ |
387 | dst[x + boffset] = av_clip_uint##nbits(b + adjust_b); \ |
388 | if (!direct && s->step == 4) \ |
389 | dst[x + aoffset] = src[x + aoffset]; \ |
390 | } \ |
391 | } \ |
392 | } \ |
393 | return 0; \ |
394 | } |
395 | |
396 | #define DEF_SELECTIVE_COLOR_FUNC(name, direct, correction_method, nbits) \ |
397 | static int selective_color_##name##_##nbits(AVFilterContext *ctx, void *arg, int jobnr, int nb_jobs) \ |
398 | { \ |
399 | return selective_color_##nbits(ctx, arg, jobnr, nb_jobs, direct, correction_method); \ |
400 | } |
401 | |
402 | #define DEF_SELECTIVE_COLOR_FUNCS(nbits) \ |
403 | DECLARE_SELECTIVE_COLOR_FUNC(nbits) \ |
404 | DEF_SELECTIVE_COLOR_FUNC(indirect_absolute, 0, CORRECTION_METHOD_ABSOLUTE, nbits) \ |
405 | DEF_SELECTIVE_COLOR_FUNC(indirect_relative, 0, CORRECTION_METHOD_RELATIVE, nbits) \ |
406 | DEF_SELECTIVE_COLOR_FUNC( direct_absolute, 1, CORRECTION_METHOD_ABSOLUTE, nbits) \ |
407 | DEF_SELECTIVE_COLOR_FUNC( direct_relative, 1, CORRECTION_METHOD_RELATIVE, nbits) |
408 | |
409 | DEF_SELECTIVE_COLOR_FUNCS(8) |
410 | DEF_SELECTIVE_COLOR_FUNCS(16) |
411 | |
412 | typedef int (*selective_color_func_type)(AVFilterContext *ctx, void *td, int jobnr, int nb_jobs); |
413 | |
414 | static int filter_frame(AVFilterLink *inlink, AVFrame *in) |
415 | { |
416 | AVFilterContext *ctx = inlink->dst; |
417 | AVFilterLink *outlink = ctx->outputs[0]; |
418 | int direct; |
419 | AVFrame *out; |
420 | ThreadData td; |
421 | const SelectiveColorContext *s = ctx->priv; |
422 | static const selective_color_func_type funcs[2][2][2] = { |
423 | { |
424 | {selective_color_indirect_absolute_8, selective_color_indirect_relative_8}, |
425 | {selective_color_direct_absolute_8, selective_color_direct_relative_8}, |
426 | },{ |
427 | {selective_color_indirect_absolute_16, selective_color_indirect_relative_16}, |
428 | {selective_color_direct_absolute_16, selective_color_direct_relative_16}, |
429 | } |
430 | }; |
431 | |
432 | if (av_frame_is_writable(in)) { |
433 | direct = 1; |
434 | out = in; |
435 | } else { |
436 | direct = 0; |
437 | out = ff_get_video_buffer(outlink, outlink->w, outlink->h); |
438 | if (!out) { |
439 | av_frame_free(&in); |
440 | return AVERROR(ENOMEM); |
441 | } |
442 | av_frame_copy_props(out, in); |
443 | } |
444 | |
445 | td.in = in; |
446 | td.out = out; |
447 | ctx->internal->execute(ctx, funcs[s->is_16bit][direct][s->correction_method], |
448 | &td, NULL, FFMIN(inlink->h, ff_filter_get_nb_threads(ctx))); |
449 | |
450 | if (!direct) |
451 | av_frame_free(&in); |
452 | return ff_filter_frame(outlink, out); |
453 | } |
454 | |
455 | static const AVFilterPad selectivecolor_inputs[] = { |
456 | { |
457 | .name = "default", |
458 | .type = AVMEDIA_TYPE_VIDEO, |
459 | .filter_frame = filter_frame, |
460 | .config_props = config_input, |
461 | }, |
462 | { NULL } |
463 | }; |
464 | |
465 | static const AVFilterPad selectivecolor_outputs[] = { |
466 | { |
467 | .name = "default", |
468 | .type = AVMEDIA_TYPE_VIDEO, |
469 | }, |
470 | { NULL } |
471 | }; |
472 | |
473 | AVFilter ff_vf_selectivecolor = { |
474 | .name = "selectivecolor", |
475 | .description = NULL_IF_CONFIG_SMALL("Apply CMYK adjustments to specific color ranges."), |
476 | .priv_size = sizeof(SelectiveColorContext), |
477 | .query_formats = query_formats, |
478 | .inputs = selectivecolor_inputs, |
479 | .outputs = selectivecolor_outputs, |
480 | .priv_class = &selectivecolor_class, |
481 | .flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC | AVFILTER_FLAG_SLICE_THREADS, |
482 | }; |
483 |