The diff method shows the per pixel difference while the dssim method also outputs the dssim value. --- libavfilter/Makefile | 1 + libavfilter/allfilters.c | 1 + libavfilter/dssim.c | 318 +++++++++++++++++++++++++++++++++++++++++++++++ libavfilter/vf_compare.c | 274 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 594 insertions(+) create mode 100644 libavfilter/dssim.c create mode 100644 libavfilter/vf_compare.c
diff --git a/libavfilter/Makefile b/libavfilter/Makefile index 530aa57..06c9766 100644 --- a/libavfilter/Makefile +++ b/libavfilter/Makefile @@ -41,6 +41,7 @@ OBJS-$(CONFIG_ANULLSINK_FILTER) += asink_anullsink.o OBJS-$(CONFIG_BLACKFRAME_FILTER) += vf_blackframe.o OBJS-$(CONFIG_BOXBLUR_FILTER) += vf_boxblur.o +OBJS-$(CONFIG_COMPARE_FILTER) += vf_compare.o dssim.o OBJS-$(CONFIG_COPY_FILTER) += vf_copy.o OBJS-$(CONFIG_CROP_FILTER) += vf_crop.o OBJS-$(CONFIG_CROPDETECT_FILTER) += vf_cropdetect.o diff --git a/libavfilter/allfilters.c b/libavfilter/allfilters.c index 94b3115..c6fd312 100644 --- a/libavfilter/allfilters.c +++ b/libavfilter/allfilters.c @@ -52,6 +52,7 @@ void avfilter_register_all(void) REGISTER_FILTER (BLACKFRAME, blackframe, vf); REGISTER_FILTER (BOXBLUR, boxblur, vf); + REGISTER_FILTER (COMPARE, compare, vf); REGISTER_FILTER (COPY, copy, vf); REGISTER_FILTER (CROP, crop, vf); REGISTER_FILTER (CROPDETECT, cropdetect, vf); diff --git a/libavfilter/dssim.c b/libavfilter/dssim.c new file mode 100644 index 0000000..65c8ed6 --- /dev/null +++ b/libavfilter/dssim.c @@ -0,0 +1,318 @@ +/* + * Copyright (c) 2011 porneL. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials + * provided with the distribution. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include "dssim.h" +#include "avfilter.h" +#include "libavutil/common.h" + +typedef struct { + float l, A, b, a; +} LABA; + +static const float D65x = 0.9505f, D65y = 1.0f, D65z = 1.089f; + +static inline LABA rgba_to_laba(const float gamma, const uint8_t *pix) +{ + float b = powf(pix[0] / 255.0f, 1.0f / gamma), + g = powf(pix[1] / 255.0f, 1.0f / gamma), + r = powf(pix[2] / 255.0f, 1.0f / gamma), + a = pix[3] / 255.0f; + + float fx = (r * 0.4124f + g * 0.3576f + b * 0.1805f) / D65x; + float fy = (r * 0.2126f + g * 0.7152f + b * 0.0722f) / D65y; + float fz = (r * 0.0193f + g * 0.1192f + b * 0.9505f) / D65z; + const float epsilon = 216.0 / 24389.0; + + fx = + ((fx > epsilon) ? powf(fx, + (1.0f / 3.0f)) : (7.787f * fx + 16.0f / 116.0f)); + fy = + ((fy > epsilon) ? powf(fy, + (1.0f / 3.0f)) : (7.787f * fy + 16.0f / 116.0f)); + fz = + ((fz > epsilon) ? powf(fz, + (1.0f / 3.0f)) : (7.787f * fz + 16.0f / 116.0f)); + return (LABA) { + (116.0f * fy - 16.0f) / 100.0 * a, + (86.2f + 500.0f * (fx - fy)) / 220.0 * a, /* 86 is a fudge to make the value positive */ + (107.9f + 200.0f * (fy - fz)) / 220.0 * a, /* 107 is a fudge to make the value positive */ + a + }; +} + +/* Macros to avoid repeating every line 4 times */ + +#define LABA_OP(dst, X, op, Y) dst = (LABA) { \ + (X).l op(Y).l, \ + (X).A op(Y).A, \ + (X).b op(Y).b, \ + (X).a op(Y).a } \ + +#define LABA_OPC(dst, X, op, Y) dst = (LABA) { \ + (X).l op(Y), \ + (X).A op(Y), \ + (X).b op(Y), \ + (X).a op(Y) } \ + +#define LABA_OP1(dst, op, Y) dst = (LABA) { \ + dst.l op(Y).l, \ + dst.A op(Y).A, \ + dst.b op(Y).b, \ + dst.a op(Y).a } \ + + +typedef void rowcallback (LABA *, int width); + +static void square_row(LABA *row, int width) +{ + for (int i = 0; i < width; i++) + LABA_OP(row[i], row[i], *, row[i]); +} + +/* + * Blurs image horizontally (width 2*size+1) and writes it transposed to dst + * (called twice gives 2d blur) + * Callback is executed on every row before blurring + */ +static void transposing_1d_blur(LABA *restrict src, + LABA *restrict dst, + int width, + int height, + const int size, + rowcallback *const callback) +{ + const float sizef = size; + int i, j; + + for (j = 0; j < height; j++) { + LABA *restrict row = src + j * width; + LABA sum; + + // preprocess line + if (callback) + callback(row, width); + + // accumulate sum for pixels outside line + LABA_OPC(sum, row[0], *, sizef); + for (i = 0; i < size; i++) + LABA_OP1(sum, +=, row[i]); + + // blur with left side outside line + for (i = 0; i < size; i++) { + LABA_OP1(sum, -=, row[0]); + LABA_OP1(sum, +=, row[i + size]); + + LABA_OPC(dst[i * height + j], sum, /, sizef * 2.0f); + } + + for (i = size; i < width - size; i++) { + LABA_OP1(sum, -=, row[i - size]); + LABA_OP1(sum, +=, row[i + size]); + + LABA_OPC(dst[i * height + j], sum, /, sizef * 2.0f); + } + + // blur with right side outside line + for (i = width - size; i < width; i++) { + LABA_OP1(sum, -=, row[i - size]); + LABA_OP1(sum, +=, row[width - 1]); + + LABA_OPC(dst[i * height + j], sum, /, sizef * 2.0f); + } + } +} + +/* + * Filters image with callback and blurs (lousy approximate of gaussian) + * it proportionally to + */ +static void blur(LABA *restrict src, LABA *restrict tmp, LABA *restrict dst, + int width, int height, rowcallback *const callback) +{ + int small = 1, big = 1; + if (FFMIN(height, width) > 100) + big++; + if (FFMIN(height, width) > 200) + big++; + if (FFMIN(height, width) > 500) + small++; + if (FFMIN(height, width) > 800) + big++; + + transposing_1d_blur(src, tmp, width, height, 1, callback); + transposing_1d_blur(tmp, dst, height, width, 1, NULL); + transposing_1d_blur(src, tmp, width, height, small, NULL); + transposing_1d_blur(tmp, dst, height, width, small, NULL); + transposing_1d_blur(dst, tmp, width, height, big, NULL); + transposing_1d_blur(tmp, dst, height, width, big, NULL); +} + +/* + * Conversion is not reversible + */ +static inline LABA convert_pixel(const uint8_t *pix, float gamma, int i, int j) +{ + LABA f1 = rgba_to_laba(gamma, pix); + + // Compose image on coloured background to better judge dissimilarity with various backgrounds + int n = i ^ j; + if (n & 4) { + f1.l += 1.0 - f1.a; // using premultiplied alpha + } + if (n & 8) { + f1.A += 1.0 - f1.a; + } + if (n & 16) { + f1.b += 1.0 - f1.a; + } + + // Since alpha is already blended with other channels, + // lower amplitude of alpha to lower score for alpha difference + f1.a *= 0.75; + + // SSIM is supposed to be applied only to luma, + // lower amplitude of chroma to lower score for chroma difference + // (chroma is not ignored completely, because IMHO it also matters) + f1.A *= 0.75; + f1.b *= 0.75; + + return f1; +} + +/* + * Algorithm based on Rabah Mehdi's C++ implementation + * + * frees memory in read_info structs. + * saves dissimilarity visualisation as ssimfilename (pass NULL if not needed) + */ +void ff_dssim_frame(AVFilterContext *ctx, + AVFilterBufferRef *dst, AVFilterBufferRef *src) +{ + CmpContext *s = ctx->priv; + + uint8_t *dp = dst->data[0]; + uint8_t *sp = src->data[0]; + + float gamma1, gamma2; + + const double c1 = 0.01 * 0.01, c2 = 0.03 * 0.03; + const float gamma = 1.0 / 2.2; + double avgminssim = 0; + + int width = FFMIN(dst->video->w, src->video->w), + height = FFMIN(dst->video->h, src->video->h); + + LABA *restrict img1 = av_malloc(width * height * sizeof(LABA)); + LABA *restrict img2 = av_malloc(width * height * sizeof(LABA)); + LABA *restrict img1_img2 = av_malloc(width * height * sizeof(LABA)); + LABA *tmp = av_malloc(width * height * sizeof(LABA)); + LABA *restrict sigma12 = av_malloc(width * height * sizeof(LABA)); + LABA *restrict sigma1_sq = av_malloc(width * height * sizeof(LABA)); + LABA *restrict sigma2_sq = av_malloc(width * height * sizeof(LABA)); + LABA *restrict mu1 = img1_img2; + LABA *restrict mu2 = img1; + + int i, j, k = 0; + + gamma1 = gamma2 = 0.45455; + + for (j = 0; j < height; j++) { + uint8_t *d = dp, *s = sp; + for (i = 0; i < width; i++, k++) { + LABA f1 = convert_pixel(d, gamma1, i, j); + LABA f2 = convert_pixel(s, gamma2, i, j); + + img1[k] = f1; + img2[k] = f2; + + // part of computation + LABA_OP(img1_img2[k], f1, *, f2); + d += 4; + s += 4; + } + dp += dst->linesize[0]; + sp += src->linesize[0]; + } + + blur(img1_img2, tmp, sigma12, width, height, NULL); + + blur(img1, tmp, mu1, width, height, NULL); + + blur(img1, tmp, sigma1_sq, width, height, square_row); + + blur(img2, tmp, mu2, width, height, NULL); + + blur(img2, tmp, sigma2_sq, width, height, square_row); + av_free(img2); + av_freep(&tmp); + +#define SSIM(r) \ + ((2.0 * (mu1[k].r * mu2[k].r) + c1) * \ + (2.0 * (sigma12[k].r - (mu1[k].r * mu2[k].r)) + c2)) / \ + (((mu1[k].r * mu1[k].r) + (mu2[k].r * mu2[k].r) + c1) * \ + ((sigma1_sq[k].r - (mu1[k].r * mu1[k].r)) + \ + (sigma2_sq[k].r - (mu2[k].r * mu2[k].r)) + c2)) + + k = 0; + dp = dst->data[0]; + + for (j = 0; j < height; j++) { + uint8_t *d = dp; + for (i = 0; i < width; i++, k++) { + LABA ssim = (LABA) {SSIM(l), SSIM(A), SSIM(b), SSIM(a)}; + + double minssim = FFMIN(FFMIN(ssim.l, ssim.A), + FFMIN(ssim.b, ssim.a)); + float max = 1.0 - FFMIN(FFMIN(ssim.l, ssim.A), ssim.b); + float maxsq = max * max; + float r, g, b; + + avgminssim += minssim; + + r = (1.0 - ssim.a) + maxsq; + g = max + maxsq; + b = max * 0.5f + (1.0 - ssim.a) * 0.5f + maxsq; + + d[0] = av_clip_uint8(powf(r, gamma) * 256.0f); + d[1] = av_clip_uint8(powf(g, gamma) * 256.0f); + d[2] = av_clip_uint8(powf(b, gamma) * 256.0f); + d[3] = 255; + d += 4; + } + dp += dst->linesize[0]; + } + + av_free(mu1); + av_free(mu2); + av_free(sigma12); + av_free(sigma1_sq); + av_free(sigma2_sq); + + avgminssim /= (double)width * height; + + s->avg += 1.0 / (avgminssim) - 1.0; + s->n++; + + av_log(NULL, AV_LOG_INFO, "pts %"PRId64" dssim value %.4f\n", + dst->pts, 1.0 / (avgminssim) - 1.0); +} diff --git a/libavfilter/vf_compare.c b/libavfilter/vf_compare.c new file mode 100644 index 0000000..9095bf6 --- /dev/null +++ b/libavfilter/vf_compare.c @@ -0,0 +1,274 @@ +/* + * Copyright (c) 2012 Luca Barbato + * + * This file is part of Libav. + * + * Libav is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * Libav is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with Libav; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +/** + * @file + * compare two videos. + */ + +#include "libavutil/opt.h" +#include "libavutil/common.h" +#include "libavutil/pixdesc.h" +#include "libavutil/avstring.h" +#include "libavutil/avassert.h" +#include "libavutil/parseutils.h" +#include "avfilter.h" +#include "internal.h" +#include "formats.h" +#include "video.h" +#include "dssim.h" + +#define MAIN 0 +#define REF 1 + +enum { + CMP_DIFF, + CMP_DSSIM, + NB_CMP +}; + +#define OFFSET(x) offsetof(CmpContext, x) +#define V AV_OPT_FLAG_VIDEO_PARAM +static const AVOption options[] = { + { "method", "", OFFSET(comparator), AV_OPT_TYPE_INT,{ .i64 = CMP_DIFF }, 0, NB_CMP, V, "compare" }, + { "diff", "Output the different pixels", 0, AV_OPT_TYPE_CONST, {.i64 = CMP_DIFF}, INT_MIN, INT_MAX, V, "compare" }, + { "dssim","Calculate the dssim and highlight it", 0, AV_OPT_TYPE_CONST, {.i64 = CMP_DSSIM}, INT_MIN, INT_MAX, V, "compare" }, + { NULL }, +}; + +static const AVClass class = { + .class_name = "compare filter", + .item_name = av_default_item_name, + .option = options, + .version = LIBAVUTIL_VERSION_INT, +}; + +static void diff_frame(AVFilterContext *ctx, + AVFilterBufferRef *dst, AVFilterBufferRef *src) +{ + uint8_t *dp = dst->data[0]; + uint8_t *sp = src->data[0]; + int i, j; + int width, height; + + width = FFMIN(dst->video->w, src->video->w); + height = FFMIN(dst->video->h, src->video->h); + for (i = 0; i < height; i++) { + uint8_t *d = dp, *s = sp; + for (j = 0; j < width; j++) { + int dr, dg, db; + dr = d[0]-s[0]; + dg = d[1]-s[1]; + db = d[2]-s[2]; + d[0] = 128 - dr; + d[1] = 128 - dg; + d[2] = 128 - db; + d[3] = 255; + d += 4; + s += 4; + } + dp += dst->linesize[0]; + sp += src->linesize[0]; + } +} + +static av_cold int init(AVFilterContext *ctx, const char *args) +{ + CmpContext *s = ctx->priv; + int ret; + + s->class = &class; + av_opt_set_defaults(s); + + if ((ret = av_set_options_string(s, args, "=", ":")) < 0) { + av_log(ctx, AV_LOG_ERROR, "Error parsing options string %s.\n", args); + return ret; + } + + switch (s->comparator) { + case CMP_DIFF: + s->compare = diff_frame; + break; + case CMP_DSSIM: + s->compare = ff_dssim_frame; + break; + default: + return AVERROR_INVALIDDATA; + } + + return 0; +} + +static av_cold void uninit(AVFilterContext *ctx) +{ + CmpContext *s = ctx->priv; + + if (s->n) + av_log(NULL, AV_LOG_INFO, "Average metric: %.4f\n", s->avg/s->n); + + avfilter_unref_bufferp(&s->main); + avfilter_unref_bufferp(&s->ref); +} + +static int query_formats(AVFilterContext *ctx) +{ + const enum AVPixelFormat pix_fmts[] = { AV_PIX_FMT_RGB32, AV_PIX_FMT_NONE }; + + AVFilterFormats *formats = ff_make_format_list(pix_fmts); + + ff_formats_ref(formats, &ctx->inputs [MAIN ]->out_formats); + ff_formats_ref(formats, &ctx->inputs [REF ]->out_formats); + ff_formats_ref(formats, &ctx->outputs[MAIN ]->in_formats); + + return 0; +} + +static int config_output(AVFilterLink *outlink) +{ + AVFilterContext *ctx = outlink->src; + + outlink->w = ctx->inputs[MAIN]->w; + outlink->h = ctx->inputs[MAIN]->h; + outlink->time_base = ctx->inputs[MAIN]->time_base; + + return 0; +} + +static int null_start_frame(AVFilterLink *inlink, AVFilterBufferRef *buf) +{ + return 0; +} + +static int null_draw_slice(AVFilterLink *inlink, int y, int h, int slice_dir) +{ + return 0; +} + +static int end_frame_main(AVFilterLink *inlink) +{ + CmpContext *s = inlink->dst->priv; + + av_assert0(!s->main); + s->main = inlink->cur_buf; + inlink->cur_buf = NULL; + + return 0; +} + +static int end_frame_ref(AVFilterLink *inlink) +{ + CmpContext *s = inlink->dst->priv; + + av_assert0(!s->ref); + s->ref = inlink->cur_buf; + inlink->cur_buf = NULL; + + return 0; +} + +static int output_frame(AVFilterContext *ctx) +{ + CmpContext *s = ctx->priv; + AVFilterLink *outlink = ctx->outputs[0]; + int ret = ff_start_frame(outlink, s->main); + if (ret >= 0) + ret = ff_draw_slice(outlink, 0, outlink->h, 1); + if (ret >= 0) + ret = ff_end_frame(outlink); + s->main = NULL; + + return ret; +} + +static int request_frame(AVFilterLink *outlink) +{ + AVFilterContext *ctx = outlink->src; + CmpContext *s = ctx->priv; + int ret = 0; + + /* get a frame on the main input */ + if (!s->main) { + ret = ff_request_frame(ctx->inputs[MAIN]); + if (ret < 0) + return ret; + } + + /* get a new frame on the reference input */ + if (!s->ref) { + ret = ff_request_frame(ctx->inputs[REF]); + if (ret < 0) + return ret; + } + + s->compare(ctx, s->main, s->ref); + + avfilter_unref_bufferp(&s->ref); + + return output_frame(ctx); +} + +static const AVFilterPad vf_compare_inputs[] = { + { + .name = "main", + .type = AVMEDIA_TYPE_VIDEO, + .start_frame = null_start_frame, + .draw_slice = null_draw_slice, + .end_frame = end_frame_main, + .min_perms = AV_PERM_READ, + .rej_perms = AV_PERM_REUSE2 | AV_PERM_PRESERVE, + .needs_fifo = 1, + }, + { + .name = "ref", + .type = AVMEDIA_TYPE_VIDEO, + .start_frame = null_start_frame, + .draw_slice = null_draw_slice, + .end_frame = end_frame_ref, + .min_perms = AV_PERM_READ, + .rej_perms = AV_PERM_REUSE2, + .needs_fifo = 1, + }, + { NULL } +}; + +static const AVFilterPad vf_compare_outputs[] = { + { + .name = "default", + .type = AVMEDIA_TYPE_VIDEO, + .config_props = config_output, + .request_frame = request_frame, + }, + { NULL } +}; + +AVFilter avfilter_vf_compare = { + .name = "compare", + .description = NULL_IF_CONFIG_SMALL("Compare two video sources"), + + .init = init, + .uninit = uninit, + + .priv_size = sizeof(CmpContext), + + .query_formats = query_formats, + + .inputs = vf_compare_inputs, + .outputs = vf_compare_outputs, +}; -- 1.7.12 _______________________________________________ libav-devel mailing list libav-devel@libav.org https://lists.libav.org/mailman/listinfo/libav-devel