The diff method shows the per pixel difference while the dssim method
also outputs the dssim value.
---
Now with proper attribution.
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..b0980ba
--- /dev/null
+++ b/libavfilter/dssim.c
@@ -0,0 +1,318 @@
+/*
+ * Copyright (c) 2011 Kornel Lesiński. 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
[email protected]
https://lists.libav.org/mailman/listinfo/libav-devel