Line | Branch | Exec | Source |
---|---|---|---|
1 | /* | ||
2 | * This file is part of FFmpeg. | ||
3 | * | ||
4 | * FFmpeg is free software; you can redistribute it and/or | ||
5 | * modify it under the terms of the GNU Lesser General Public | ||
6 | * License as published by the Free Software Foundation; either | ||
7 | * version 2.1 of the License, or (at your option) any later version. | ||
8 | * | ||
9 | * FFmpeg is distributed in the hope that it will be useful, | ||
10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | ||
12 | * Lesser General Public License for more details. | ||
13 | * | ||
14 | * You should have received a copy of the GNU Lesser General Public | ||
15 | * License along with FFmpeg; if not, write to the Free Software | ||
16 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | ||
17 | */ | ||
18 | |||
19 | /** | ||
20 | * @file | ||
21 | * video scene change detection filter | ||
22 | */ | ||
23 | |||
24 | #include "libavutil/imgutils.h" | ||
25 | #include "libavutil/opt.h" | ||
26 | #include "libavutil/pixdesc.h" | ||
27 | #include "libavutil/timestamp.h" | ||
28 | |||
29 | #include "avfilter.h" | ||
30 | #include "filters.h" | ||
31 | #include "scene_sad.h" | ||
32 | #include "video.h" | ||
33 | |||
34 | typedef struct SCDetContext { | ||
35 | const AVClass *class; | ||
36 | |||
37 | ptrdiff_t width[4]; | ||
38 | ptrdiff_t height[4]; | ||
39 | int nb_planes; | ||
40 | int bitdepth; | ||
41 | ff_scene_sad_fn sad; | ||
42 | double prev_mafd; | ||
43 | double scene_score; | ||
44 | AVFrame *prev_picref; | ||
45 | double threshold; | ||
46 | int sc_pass; | ||
47 | } SCDetContext; | ||
48 | |||
49 | #define OFFSET(x) offsetof(SCDetContext, x) | ||
50 | #define V AV_OPT_FLAG_VIDEO_PARAM | ||
51 | #define F AV_OPT_FLAG_FILTERING_PARAM | ||
52 | |||
53 | static const AVOption scdet_options[] = { | ||
54 | { "threshold", "set scene change detect threshold", OFFSET(threshold), AV_OPT_TYPE_DOUBLE, {.dbl = 10.}, 0, 100., V|F }, | ||
55 | { "t", "set scene change detect threshold", OFFSET(threshold), AV_OPT_TYPE_DOUBLE, {.dbl = 10.}, 0, 100., V|F }, | ||
56 | { "sc_pass", "Set the flag to pass scene change frames", OFFSET(sc_pass), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, V|F }, | ||
57 | { "s", "Set the flag to pass scene change frames", OFFSET(sc_pass), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, V|F }, | ||
58 | {NULL} | ||
59 | }; | ||
60 | |||
61 | AVFILTER_DEFINE_CLASS(scdet); | ||
62 | |||
63 | static const enum AVPixelFormat pix_fmts[] = { | ||
64 | AV_PIX_FMT_RGB24, AV_PIX_FMT_BGR24, AV_PIX_FMT_RGBA, | ||
65 | AV_PIX_FMT_ABGR, AV_PIX_FMT_BGRA, AV_PIX_FMT_GRAY8, | ||
66 | AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUVJ420P, | ||
67 | AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUVJ422P, | ||
68 | AV_PIX_FMT_YUV440P, AV_PIX_FMT_YUVJ440P, | ||
69 | AV_PIX_FMT_YUV444P, AV_PIX_FMT_YUVJ444P, | ||
70 | AV_PIX_FMT_YUV420P9, AV_PIX_FMT_YUV420P10, AV_PIX_FMT_YUV420P12, | ||
71 | AV_PIX_FMT_YUV422P9, AV_PIX_FMT_YUV422P10, AV_PIX_FMT_YUV422P12, | ||
72 | AV_PIX_FMT_YUV444P9, AV_PIX_FMT_YUV444P10, AV_PIX_FMT_YUV444P12, | ||
73 | AV_PIX_FMT_NONE | ||
74 | }; | ||
75 | |||
76 | 1 | static int config_input(AVFilterLink *inlink) | |
77 | { | ||
78 | 1 | AVFilterContext *ctx = inlink->dst; | |
79 | 1 | SCDetContext *s = ctx->priv; | |
80 | 1 | const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format); | |
81 | 3 | int is_yuv = !(desc->flags & AV_PIX_FMT_FLAG_RGB) && | |
82 |
2/4✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
|
2 | (desc->flags & AV_PIX_FMT_FLAG_PLANAR) && |
83 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | desc->nb_components >= 3; |
84 | |||
85 | 1 | s->bitdepth = desc->comp[0].depth; | |
86 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | s->nb_planes = is_yuv ? 1 : av_pix_fmt_count_planes(inlink->format); |
87 | |||
88 |
2/2✓ Branch 0 taken 4 times.
✓ Branch 1 taken 1 times.
|
5 | for (int plane = 0; plane < 4; plane++) { |
89 | 4 | ptrdiff_t line_size = av_image_get_linesize(inlink->format, inlink->w, plane); | |
90 | 4 | s->width[plane] = line_size >> (s->bitdepth > 8); | |
91 |
4/4✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 2 times.
|
4 | s->height[plane] = inlink->h >> ((plane == 1 || plane == 2) ? desc->log2_chroma_h : 0); |
92 | } | ||
93 | |||
94 |
1/2✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
|
1 | s->sad = ff_scene_sad_get_fn(s->bitdepth == 8 ? 8 : 16); |
95 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
|
1 | if (!s->sad) |
96 | ✗ | return AVERROR(EINVAL); | |
97 | |||
98 | 1 | return 0; | |
99 | } | ||
100 | |||
101 | 1 | static av_cold void uninit(AVFilterContext *ctx) | |
102 | { | ||
103 | 1 | SCDetContext *s = ctx->priv; | |
104 | |||
105 | 1 | av_frame_free(&s->prev_picref); | |
106 | 1 | } | |
107 | |||
108 | 1308 | static double get_scene_score(AVFilterContext *ctx, AVFrame *frame) | |
109 | { | ||
110 | 1308 | double ret = 0; | |
111 | 1308 | SCDetContext *s = ctx->priv; | |
112 | 1308 | AVFrame *prev_picref = s->prev_picref; | |
113 | |||
114 |
3/4✓ Branch 0 taken 1307 times.
✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1307 times.
✗ Branch 3 not taken.
|
1308 | if (prev_picref && frame->height == prev_picref->height |
115 |
1/2✓ Branch 0 taken 1307 times.
✗ Branch 1 not taken.
|
1307 | && frame->width == prev_picref->width) { |
116 | 1307 | uint64_t sad = 0; | |
117 | double mafd, diff; | ||
118 | 1307 | uint64_t count = 0; | |
119 | |||
120 |
2/2✓ Branch 0 taken 1307 times.
✓ Branch 1 taken 1307 times.
|
2614 | for (int plane = 0; plane < s->nb_planes; plane++) { |
121 | uint64_t plane_sad; | ||
122 | 1307 | s->sad(prev_picref->data[plane], prev_picref->linesize[plane], | |
123 | 1307 | frame->data[plane], frame->linesize[plane], | |
124 | s->width[plane], s->height[plane], &plane_sad); | ||
125 | 1307 | sad += plane_sad; | |
126 | 1307 | count += s->width[plane] * s->height[plane]; | |
127 | } | ||
128 | |||
129 | 1307 | mafd = (double)sad * 100. / count / (1ULL << s->bitdepth); | |
130 | 1307 | diff = fabs(mafd - s->prev_mafd); | |
131 |
2/2✓ Branch 0 taken 1060 times.
✓ Branch 1 taken 247 times.
|
1307 | ret = av_clipf(FFMIN(mafd, diff), 0, 100.); |
132 | 1307 | s->prev_mafd = mafd; | |
133 | 1307 | av_frame_free(&prev_picref); | |
134 | } | ||
135 | 1308 | s->prev_picref = av_frame_clone(frame); | |
136 | 1308 | return ret; | |
137 | } | ||
138 | |||
139 | 2627 | static int set_meta(SCDetContext *s, AVFrame *frame, const char *key, const char *value) | |
140 | { | ||
141 | 2627 | return av_dict_set(&frame->metadata, key, value, 0); | |
142 | } | ||
143 | |||
144 | 1321 | static int activate(AVFilterContext *ctx) | |
145 | { | ||
146 | int ret; | ||
147 | 1321 | AVFilterLink *inlink = ctx->inputs[0]; | |
148 | 1321 | AVFilterLink *outlink = ctx->outputs[0]; | |
149 | 1321 | SCDetContext *s = ctx->priv; | |
150 | AVFrame *frame; | ||
151 | |||
152 |
1/2✗ Branch 1 not taken.
✓ Branch 2 taken 1321 times.
|
1321 | FF_FILTER_FORWARD_STATUS_BACK(outlink, inlink); |
153 | |||
154 | 1321 | ret = ff_inlink_consume_frame(inlink, &frame); | |
155 |
1/2✗ Branch 0 not taken.
✓ Branch 1 taken 1321 times.
|
1321 | if (ret < 0) |
156 | ✗ | return ret; | |
157 | |||
158 |
2/2✓ Branch 0 taken 1308 times.
✓ Branch 1 taken 13 times.
|
1321 | if (frame) { |
159 | char buf[64]; | ||
160 | 1308 | s->scene_score = get_scene_score(ctx, frame); | |
161 | 1308 | snprintf(buf, sizeof(buf), "%0.3f", s->prev_mafd); | |
162 | 1308 | set_meta(s, frame, "lavfi.scd.mafd", buf); | |
163 | 1308 | snprintf(buf, sizeof(buf), "%0.3f", s->scene_score); | |
164 | 1308 | set_meta(s, frame, "lavfi.scd.score", buf); | |
165 | |||
166 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1297 times.
|
1308 | if (s->scene_score >= s->threshold) { |
167 | 11 | av_log(s, AV_LOG_INFO, "lavfi.scd.score: %.3f, lavfi.scd.time: %s\n", | |
168 | 11 | s->scene_score, av_ts2timestr(frame->pts, &inlink->time_base)); | |
169 | 11 | set_meta(s, frame, "lavfi.scd.time", | |
170 | 11 | av_ts2timestr(frame->pts, &inlink->time_base)); | |
171 | } | ||
172 |
1/2✓ Branch 0 taken 1308 times.
✗ Branch 1 not taken.
|
1308 | if (s->sc_pass) { |
173 |
2/2✓ Branch 0 taken 11 times.
✓ Branch 1 taken 1297 times.
|
1308 | if (s->scene_score >= s->threshold) |
174 | 11 | return ff_filter_frame(outlink, frame); | |
175 | else { | ||
176 | 1297 | av_frame_free(&frame); | |
177 | } | ||
178 | } else | ||
179 | ✗ | return ff_filter_frame(outlink, frame); | |
180 | } | ||
181 | |||
182 |
2/2✓ Branch 1 taken 1 times.
✓ Branch 2 taken 1309 times.
|
1310 | FF_FILTER_FORWARD_STATUS(inlink, outlink); |
183 |
1/2✓ Branch 1 taken 1309 times.
✗ Branch 2 not taken.
|
1309 | FF_FILTER_FORWARD_WANTED(outlink, inlink); |
184 | |||
185 | ✗ | return FFERROR_NOT_READY; | |
186 | } | ||
187 | |||
188 | static const AVFilterPad scdet_inputs[] = { | ||
189 | { | ||
190 | .name = "default", | ||
191 | .type = AVMEDIA_TYPE_VIDEO, | ||
192 | .config_props = config_input, | ||
193 | }, | ||
194 | }; | ||
195 | |||
196 | const AVFilter ff_vf_scdet = { | ||
197 | .name = "scdet", | ||
198 | .description = NULL_IF_CONFIG_SMALL("Detect video scene change"), | ||
199 | .priv_size = sizeof(SCDetContext), | ||
200 | .priv_class = &scdet_class, | ||
201 | .uninit = uninit, | ||
202 | .flags = AVFILTER_FLAG_METADATA_ONLY, | ||
203 | FILTER_INPUTS(scdet_inputs), | ||
204 | FILTER_OUTPUTS(ff_video_default_filterpad), | ||
205 | FILTER_PIXFMTS_ARRAY(pix_fmts), | ||
206 | .activate = activate, | ||
207 | }; | ||
208 |