diff --git a/validate/Makefile.am b/validate/Makefile.am index 0c52124c51..bd29f6f877 100644 --- a/validate/Makefile.am +++ b/validate/Makefile.am @@ -5,6 +5,7 @@ SUBDIRS = \ data \ gst \ plugins \ + gst-libs \ launcher \ tools \ pkgconfig \ diff --git a/validate/configure.ac b/validate/configure.ac index ee5fcc599d..e27cdbf96e 100644 --- a/validate/configure.ac +++ b/validate/configure.ac @@ -185,6 +185,14 @@ PKG_CHECK_MODULES(GDK, gdk-3.0, HAVE_GDK=yes, HAVE_GDK=no) AC_SUBST(GDK_CFLAGS) AC_SUBST(GDK_LIBS) +PKG_CHECK_MODULES(CAIRO, "cairo", HAVE_CAIRO=yes, HAVE_CAIRO=no) +AC_SUBST(CAIRO_CFLAGS) +AC_SUBST(CAIRO_LIBS) +AM_CONDITIONAL(HAVE_CAIRO, test ! "x$HAVE_CAIRO" = "xno") +if test "x$HAVE_CAIRO" != "xyes"; then + AC_MSG_NOTICE([Cairo is needed for the gst-validate-images-tool]) +fi + dnl checks for gstreamer AG_GST_CHECK_GST_CHECK($GST_API_VERSION, [$GST_REQ], no) @@ -307,6 +315,9 @@ plugins/Makefile plugins/fault_injection/Makefile plugins/gapplication/Makefile plugins/gtk/Makefile +gst-libs/Makefile +gst-libs/gst/Makefile +gst-libs/gst/video/Makefile tests/Makefile tests/check/Makefile pkgconfig/Makefile diff --git a/validate/gst-libs/Makefile.am b/validate/gst-libs/Makefile.am new file mode 100644 index 0000000000..062cb55aab --- /dev/null +++ b/validate/gst-libs/Makefile.am @@ -0,0 +1 @@ +SUBDIRS = gst diff --git a/validate/gst-libs/gst/Makefile.am b/validate/gst-libs/gst/Makefile.am new file mode 100644 index 0000000000..aeb34bfcbc --- /dev/null +++ b/validate/gst-libs/gst/Makefile.am @@ -0,0 +1,3 @@ +if HAVE_CAIRO +SUBDIRS = video +endif diff --git a/validate/gst-libs/gst/video/Makefile.am b/validate/gst-libs/gst/video/Makefile.am new file mode 100644 index 0000000000..89ec2692ab --- /dev/null +++ b/validate/gst-libs/gst/video/Makefile.am @@ -0,0 +1,12 @@ +libgstvalidatevideo_@GST_API_VERSION@_la_SOURCES = gstvalidatessim.c gssim.c +libgstvalidatevideo_@GST_API_VERSION@include_HEADERS = gstvalidatessim.h gssim.h + +libgstvalidatevideo_@GST_API_VERSION@_la_CFLAGS = $(GST_ALL_CFLAGS) $(CAIRO_CFLAGS) $(GST_VIDEO_CFLAGS) -I$(top_builddir) +libgstvalidatevideo_@GST_API_VERSION@_la_LIBADD = $(GST_ALL_LIBS) $(CAIRO_LIBS) $(GST_VIDEO_LIBS) $(top_builddir)/gst/validate/libgstvalidate-@GST_API_VERSION@.la +libgstvalidatevideo_@GST_API_VERSION@_la_LDFLAGS = $(GST_ALL_LDFLAGS) $(CAIRO_LDFLAGS) $(GST_VIDEO_LDFLAGS) + +lib_LTLIBRARIES = libgstvalidatevideo-@GST_API_VERSION@.la + +libgstvalidatevideo_@GST_API_VERSION@includedir = $(includedir)/gstreamer-@GST_API_VERSION@/lib/validate/video + +CLEANFILES = diff --git a/validate/gst-libs/gst/video/gssim.c b/validate/gst-libs/gst/video/gssim.c new file mode 100644 index 0000000000..60acce5d61 --- /dev/null +++ b/validate/gst-libs/gst/video/gssim.c @@ -0,0 +1,453 @@ +/* GStreamer + * + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2015 Raspberry Pi Foundation + * Author: Thibault Saunier + * + * This library 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. + * + * This library 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 this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +#include +#include +#include +#include + +#include "gssim.h" + +typedef gfloat (*SSimWeightFunc) (Gssim * self, gint y, gint x); + +typedef struct _SSimWindowCache +{ + gint x_window_start; + gint x_weight_start; + gint x_window_end; + gint y_window_start; + gint y_weight_start; + gint y_window_end; + gfloat element_summ; +} SSimWindowCache; + +/* *INDENT-OFF* */ +G_DEFINE_TYPE (Gssim, gssim, G_TYPE_OBJECT) +/* *INDENT-ON* */ + +enum +{ + PROP_FIRST_PROP = 1, + N_PROPS +}; + +struct _GssimPriv +{ + gint width; + gint height; + gint windowsize; + gint windowtype; + SSimWindowCache *windows; + gfloat *weights; + gfloat const1; + gfloat const2; + gfloat sigma; + + gfloat *orgmu; + + GstVideoConverter *converter; + GstVideoInfo in_info, out_info; +}; + + +static void +gssim_calculate_mu (Gssim * self, guint8 * buf) +{ + gint oy, ox, iy, ix; + + for (oy = 0; oy < self->priv->height; oy++) { + for (ox = 0; ox < self->priv->width; ox++) { + gfloat mu = 0; + gfloat elsumm; + gint weight_y_base, weight_x_base; + gint weight_offset; + gint pixel_offset; + gint winstart_y; + gint wghstart_y; + gint winend_y; + gint winstart_x; + gint wghstart_x; + gint winend_x; + gfloat weight; + gint source_offset; + + source_offset = oy * self->priv->width + ox; + + winstart_x = self->priv->windows[source_offset].x_window_start; + wghstart_x = self->priv->windows[source_offset].x_weight_start; + winend_x = self->priv->windows[source_offset].x_window_end; + winstart_y = self->priv->windows[source_offset].y_window_start; + wghstart_y = self->priv->windows[source_offset].y_weight_start; + winend_y = self->priv->windows[source_offset].y_window_end; + elsumm = self->priv->windows[source_offset].element_summ; + + switch (self->priv->windowtype) { + case 0: + for (iy = winstart_y; iy <= winend_y; iy++) { + pixel_offset = iy * self->priv->width; + for (ix = winstart_x; ix <= winend_x; ix++) + mu += buf[pixel_offset + ix]; + } + mu = mu / elsumm; + break; + case 1: + + weight_y_base = wghstart_y - winstart_y; + weight_x_base = wghstart_x - winstart_x; + + for (iy = winstart_y; iy <= winend_y; iy++) { + pixel_offset = iy * self->priv->width; + weight_offset = (weight_y_base + iy) * self->priv->windowsize + + weight_x_base; + for (ix = winstart_x; ix <= winend_x; ix++) { + weight = self->priv->weights[weight_offset + ix]; + mu += weight * buf[pixel_offset + ix]; + } + } + mu = mu / elsumm; + break; + } + self->priv->orgmu[oy * self->priv->width + ox] = mu; + } + } +} + +static gfloat +ssim_weight_func_none (Gssim * self, gint y, gint x) +{ + return 1; +} + +static gfloat +ssim_weight_func_gauss (Gssim * self, gint y, gint x) +{ + gfloat coord = sqrt (x * x + y * y); + return exp (-1 * (coord * coord) / (2 * self->priv->sigma * + self->priv->sigma)) / (self->priv->sigma * sqrt (2 * G_PI)); +} + + +static gboolean +gssim_regenerate_windows (Gssim * self) +{ + gint windowiseven; + gint y, x, y2, x2; + SSimWeightFunc func; + gfloat normal_summ = 0; + gint normal_count = 0; + + g_free (self->priv->weights); + + self->priv->weights = + g_new (gfloat, self->priv->windowsize * self->priv->windowsize); + + windowiseven = + ((gint) self->priv->windowsize / 2) * 2 == self->priv->windowsize ? 1 : 0; + + g_free (self->priv->windows); + + self->priv->windows = + g_new (SSimWindowCache, self->priv->height * self->priv->width); + + switch (self->priv->windowtype) { + case 0: + func = ssim_weight_func_none; + break; + case 1: + func = ssim_weight_func_gauss; + break; + default: + self->priv->windowtype = 1; + func = ssim_weight_func_gauss; + } + + for (y = 0; y < self->priv->windowsize; y++) { + gint yoffset = y * self->priv->windowsize; + for (x = 0; x < self->priv->windowsize; x++) { + self->priv->weights[yoffset + x] = + func (self, x - self->priv->windowsize / 2 + windowiseven, + y - self->priv->windowsize / 2 + windowiseven); + normal_summ += self->priv->weights[yoffset + x]; + normal_count++; + } + } + + for (y = 0; y < self->priv->height; y++) { + for (x = 0; x < self->priv->width; x++) { + SSimWindowCache win; + gint element_count = 0; + + win.x_window_start = x - self->priv->windowsize / 2 + windowiseven; + win.x_weight_start = 0; + if (win.x_window_start < 0) { + win.x_weight_start = -win.x_window_start; + win.x_window_start = 0; + } + + win.x_window_end = x + self->priv->windowsize / 2; + if (win.x_window_end >= self->priv->width) + win.x_window_end = self->priv->width - 1; + + win.y_window_start = y - self->priv->windowsize / 2 + windowiseven; + win.y_weight_start = 0; + if (win.y_window_start < 0) { + win.y_weight_start = -win.y_window_start; + win.y_window_start = 0; + } + + win.y_window_end = y + self->priv->windowsize / 2; + if (win.y_window_end >= self->priv->height) + win.y_window_end = self->priv->height - 1; + + win.element_summ = 0; + element_count = (win.y_window_end - win.y_window_start + 1) * + (win.x_window_end - win.x_window_start + 1); + if (element_count == normal_count) + win.element_summ = normal_summ; + else { + for (y2 = win.y_weight_start; y2 < self->priv->windowsize; y2++) { + for (x2 = win.x_weight_start; x2 < self->priv->windowsize; x2++) { + win.element_summ += + self->priv->weights[y2 * self->priv->windowsize + x2]; + } + } + } + self->priv->windows[(y * self->priv->width + x)] = win; + } + } + + /* FIXME: while 0.01 and 0.03 are pretty much static, the 255 implies that + * we're working with 8-bit-per-color-component format, which may not be true + */ + self->priv->const1 = 0.01 * 255 * 0.01 * 255; + self->priv->const2 = 0.03 * 255 * 0.03 * 255; + return TRUE; +} + +void +gssim_compare (Gssim * self, guint8 * org, guint8 * mod, + guint8 * out, gfloat * mean, gfloat * lowest, gfloat * highest) +{ + gint oy, ox, iy, ix; + gfloat cumulative_ssim = 0; + *lowest = G_MAXFLOAT; + *highest = -G_MAXFLOAT; + + if (self->priv->windows == NULL) + gssim_regenerate_windows (self); + gssim_calculate_mu (self, org); + + for (oy = 0; oy < self->priv->height; oy++) { + for (ox = 0; ox < self->priv->width; ox++) { + gfloat mu_o = 0, mu_m = 0; + gdouble sigma_o = 0, sigma_m = 0, sigma_om = 0; + gfloat tmp1, tmp2; + gfloat elsumm = 0; + gint weight_y_base, weight_x_base; + gint weight_offset; + gint pixel_offset; + gint winstart_y; + gint wghstart_y; + gint winend_y; + gint winstart_x; + gint wghstart_x; + gint winend_x; + gfloat weight; + gint source_offset; + + source_offset = oy * self->priv->width + ox; + + winstart_x = self->priv->windows[source_offset].x_window_start; + wghstart_x = self->priv->windows[source_offset].x_weight_start; + winend_x = self->priv->windows[source_offset].x_window_end; + winstart_y = self->priv->windows[source_offset].y_window_start; + wghstart_y = self->priv->windows[source_offset].y_weight_start; + winend_y = self->priv->windows[source_offset].y_window_end; + elsumm = self->priv->windows[source_offset].element_summ; + + switch (self->priv->windowtype) { + case 0: + for (iy = winstart_y; iy <= winend_y; iy++) { + pixel_offset = iy * self->priv->width; + for (ix = winstart_x; ix <= winend_x; ix++) { + mu_m += mod[pixel_offset + ix]; + } + } + mu_m = mu_m / elsumm; + mu_o = self->priv->orgmu[oy * self->priv->width + ox]; + for (iy = winstart_y; iy <= winend_y; iy++) { + pixel_offset = iy * self->priv->width; + for (ix = winstart_x; ix <= winend_x; ix++) { + tmp1 = org[pixel_offset + ix] - mu_o; + tmp2 = mod[pixel_offset + ix] - mu_m; + sigma_o += tmp1 * tmp1; + sigma_m += tmp2 * tmp2; + sigma_om += tmp1 * tmp2; + } + } + break; + case 1: + + weight_y_base = wghstart_y - winstart_y; + weight_x_base = wghstart_x - winstart_x; + + for (iy = winstart_y; iy <= winend_y; iy++) { + pixel_offset = iy * self->priv->width; + weight_offset = (weight_y_base + iy) * self->priv->windowsize + + weight_x_base; + for (ix = winstart_x; ix <= winend_x; ix++) { + weight = self->priv->weights[weight_offset + ix]; + mu_o += weight * org[pixel_offset + ix]; + mu_m += weight * mod[pixel_offset + ix]; + } + } + mu_m = mu_m / elsumm; + mu_o = self->priv->orgmu[oy * self->priv->width + ox]; + for (iy = winstart_y; iy <= winend_y; iy++) { + gfloat *weights_with_offset; + guint8 *org_with_offset, *mod_with_offset; + gfloat wt1, wt2; + pixel_offset = iy * self->priv->width; + weight_offset = (weight_y_base + iy) * self->priv->windowsize + + weight_x_base; + weights_with_offset = &self->priv->weights[weight_offset]; + org_with_offset = &org[pixel_offset]; + mod_with_offset = &mod[pixel_offset]; + for (ix = winstart_x; ix <= winend_x; ix++) { + weight = weights_with_offset[ix]; + tmp1 = org_with_offset[ix] - mu_o; + tmp2 = mod_with_offset[ix] - mu_m; + wt1 = weight * tmp1; + wt2 = weight * tmp2; + sigma_o += wt1 * tmp1; + sigma_m += wt2 * tmp2; + sigma_om += wt1 * tmp2; + } + } + break; + } + sigma_o = sqrt (sigma_o / elsumm); + sigma_m = sqrt (sigma_m / elsumm); + sigma_om = sigma_om / elsumm; + tmp1 = + (2 * mu_o * mu_m + self->priv->const1) * (2 * sigma_om + + self->priv->const2) / ((mu_o * mu_o + mu_m * mu_m + + self->priv->const1) * (sigma_o * sigma_o + sigma_m * sigma_m + + self->priv->const2)); + + /* SSIM can go negative, that's why it is + 127 + index * 128 instead of index * 255 */ + if (out) + out[oy * self->priv->width + ox] = 127 + tmp1 * 128; + *lowest = MIN (*lowest, tmp1); + *highest = MAX (*highest, tmp1); + cumulative_ssim += tmp1; + } + } + *mean = cumulative_ssim / (self->priv->width * self->priv->height); +} + +gboolean +gssim_configure (Gssim * self, gint width, gint height) +{ + if (width == self->priv->width && height == self->priv->height) + return FALSE; + + self->priv->width = width; + self->priv->height = height; + + g_free (self->priv->windows); + self->priv->windows = NULL; + if (self->priv->orgmu) + g_free (self->priv->orgmu); + + self->priv->orgmu = g_new (gfloat, width * height); + + return TRUE; +} + +static void +gssim_get_property (GObject * object, + guint property_id, GValue * value, GParamSpec * pspec) +{ + //Gssim *self = GSSIM (object); + + switch (property_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gssim_set_property (GObject * object, + guint property_id, const GValue * value, GParamSpec * pspec) +{ + //Gssim *self = GSSIM (object); + + switch (property_id) { + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +gssim_finalize (GObject * object) +{ + Gssim *self = GSSIM (object); + void (*chain_up) (GObject *) = + ((GObjectClass *) gssim_parent_class)->finalize; + + g_free (self->priv->orgmu); + g_free (self->priv->windows); + + chain_up (object); +} + +static void +gssim_class_init (GssimClass * klass) +{ + GObjectClass *oclass = G_OBJECT_CLASS (klass); + + oclass->get_property = gssim_get_property; + oclass->set_property = gssim_set_property; + oclass->finalize = gssim_finalize; + + g_type_class_add_private (klass, sizeof (GssimPriv)); +} + +static void +gssim_init (Gssim * self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GSSIM_TYPE, GssimPriv); + + self->priv->windowsize = 11; + self->priv->windowtype = 1; + self->priv->windows = NULL; + self->priv->sigma = 1.5; +} + +Gssim * +gssim_new (void) +{ + return g_object_new (GSSIM_TYPE, NULL); +} diff --git a/validate/gst-libs/gst/video/gssim.h b/validate/gst-libs/gst/video/gssim.h new file mode 100644 index 0000000000..e78313b916 --- /dev/null +++ b/validate/gst-libs/gst/video/gssim.h @@ -0,0 +1,62 @@ +/* GStreamer + * + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2015 Raspberry Pi Foundation + * Author: Thibault Saunier + * + * This library 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. + * + * This library 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 this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef _GSSIM_H +#define _GSSIM_H + +#include +#include +#include +#include + +G_BEGIN_DECLS + +typedef struct _GssimPriv GssimPriv; + +typedef struct { + GObject parent; + + GssimPriv *priv; +} Gssim; + +typedef struct { + GObjectClass parent; +} GssimClass; + +#define GSSIM_TYPE (gssim_get_type ()) +#define GSSIM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GSSIM_TYPE, Gssim)) +#define GSSIM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GSSIM_TYPE, GssimClass)) +#define IS_GSSIM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GSSIM_TYPE)) +#define IS_GSSIM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GSSIM_TYPE)) +#define GSSIM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GSSIM_TYPE, GssimClass)) + +GType gssim_get_type (void); +Gssim * gssim_new (void); + +void gssim_compare (Gssim * self, guint8 * org, guint8 * mod, + guint8 * out, gfloat * mean, gfloat * lowest, + gfloat * highest); +gboolean gssim_configure (Gssim * self, gint width, gint height); + +G_END_DECLS + +#endif diff --git a/validate/gst-libs/gst/video/gstvalidatessim.c b/validate/gst-libs/gst/video/gstvalidatessim.c new file mode 100644 index 0000000000..3fba67b721 --- /dev/null +++ b/validate/gst-libs/gst/video/gstvalidatessim.c @@ -0,0 +1,962 @@ +/* GStreamer + * + * Copyright (C) 2015 Raspberry Pi Foundation + * Author: Thibault Saunier + * + * This library 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. + * + * This library 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 this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ +#include +#include +#include +#include +#include + +#include +#include "gstvalidatessim.h" +#include "gssim.h" + +#include +#include +#include +#include + +#define GST_CAT_DEFAULT gstvalidatessim_debug +GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT); + +#define SIMILARITY_ISSUE_WITH_PREVIOUS g_quark_from_static_string ("ssim::image-not-similar-enough-with-theoretical-reference") +#define SIMILARITY_ISSUE g_quark_from_static_string ("ssim::image-not-similar-enough") +#define GENERAL_INPUT_ERROR g_quark_from_static_string ("ssim::general-file-error") +#define WRONG_FORMAT g_quark_from_static_string ("ssim::wrong-format") + +G_DEFINE_TYPE_WITH_CODE (GstValidateSsim, gst_validate_ssim, + G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GST_TYPE_VALIDATE_REPORTER, NULL)); + +enum +{ + PROP_FIRST_PROP = 1, + PROP_RUNNER, + PROP_LAST +}; + +typedef struct +{ + GstVideoConverter *converter; + GstVideoInfo in_info; + GstVideoInfo out_info; +} SSimConverterInfo; + +static void +ssim_convert_info_free (SSimConverterInfo * info) +{ + if (info->converter) + gst_video_converter_free (info->converter); + + g_slice_free (SSimConverterInfo, info); +} + +struct _GstValidateSsimPriv +{ + gint width; + gint height; + + Gssim *ssim; + + GList *converters; + GstVideoInfo out_info; + + SSimConverterInfo outconverter_info; + + gfloat min_avg_similarity; + gfloat min_lowest_similarity; + + GHashTable *ref_frames_cache; +}; + + +static gboolean +gst_validate_ssim_convert (GstValidateSsim * self, SSimConverterInfo * info, + GstVideoFrame * frame, GstVideoFrame * converted_frame) +{ + gboolean res = TRUE; + GstBuffer *outbuf = NULL; + + g_return_val_if_fail (info != NULL, FALSE); + + outbuf = gst_buffer_new_allocate (NULL, info->out_info.size, NULL); + if (!gst_video_frame_map (converted_frame, &info->out_info, outbuf, + GST_MAP_WRITE)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map output converted_frame"); + goto fail; + } + + gst_video_converter_frame (info->converter, frame, converted_frame); + +done: + if (outbuf) + gst_buffer_unref (outbuf); + + return res; + +fail: + res = FALSE; + goto done; +} + +static void +gst_validate_ssim_save_out (GstValidateSsim * self, GstBuffer * buffer, + const gchar * ref_file, const gchar * file, const gchar * outfolder) +{ + GstVideoFrame frame, converted; + + if (!g_file_test (outfolder, G_FILE_TEST_IS_DIR)) { + if (g_mkdir_with_parents (outfolder, 0755) != 0) { + + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not create output directory %s", outfolder); + return; + } + } + + if (self->priv->outconverter_info.converter == NULL || + self->priv->width != self->priv->outconverter_info.out_info.width || + self->priv->height != self->priv->outconverter_info.out_info.height) { + + if (self->priv->outconverter_info.converter) + gst_video_converter_free (self->priv->outconverter_info.converter); + + gst_video_info_init (&self->priv->outconverter_info.in_info); + gst_video_info_set_format (&self->priv->outconverter_info.in_info, + GST_VIDEO_FORMAT_GRAY8, self->priv->width, self->priv->height); + + gst_video_info_init (&self->priv->outconverter_info.out_info); + gst_video_info_set_format (&self->priv->outconverter_info.out_info, + GST_VIDEO_FORMAT_RGBx, self->priv->width, self->priv->height); + + self->priv->outconverter_info.converter = + gst_video_converter_new (&self->priv->outconverter_info.in_info, + &self->priv->outconverter_info.out_info, NULL); + } + + if (!gst_video_frame_map (&frame, &self->priv->outconverter_info.in_info, + buffer, GST_MAP_READ)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map output frame"); + + return; + } + + if (gst_validate_ssim_convert (self, &self->priv->outconverter_info, + &frame, &converted)) { + cairo_status_t status; + cairo_surface_t *surface; + gchar *bn1 = g_path_get_basename (ref_file); + gchar *bn2 = g_path_get_basename (file); + gchar *fname = g_strdup_printf ("%s.VS.%s.result.png", bn1, bn2); + gchar *outfile = g_build_path (G_DIR_SEPARATOR_S, outfolder, fname, NULL); + + surface = + cairo_image_surface_create_for_data (GST_VIDEO_FRAME_PLANE_DATA + (&converted, 0), CAIRO_FORMAT_RGB24, GST_VIDEO_FRAME_WIDTH (&converted), + GST_VIDEO_FRAME_HEIGHT (&converted), + GST_VIDEO_FRAME_PLANE_STRIDE (&converted, 0)); + + if ((status = cairo_surface_write_to_png (surface, outfile)) != + CAIRO_STATUS_SUCCESS) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not save %s" " cairo status is %s", outfile, + cairo_status_to_string (status)); + } + + cairo_surface_destroy (surface); + gst_video_frame_unmap (&frame); + gst_video_frame_unmap (&converted); + g_free (bn1); + g_free (bn2); + g_free (fname); + g_free (outfile); + } +} + +static gboolean +gst_validate_ssim_configure (GstValidateSsim * self, gint width, gint height) +{ + if (width == self->priv->width && height == self->priv->height) + return FALSE; + + gssim_configure (self->priv->ssim, width, height); + + self->priv->width = width; + self->priv->height = height; + + gst_video_info_init (&self->priv->out_info); + gst_video_info_set_format (&self->priv->out_info, GST_VIDEO_FORMAT_I420, + self->priv->width, self->priv->height); + + return TRUE; +} + +static void +gst_validate_ssim_configure_converter (GstValidateSsim * self, gint index, + gboolean force, GstVideoFormat in_format, gint width, gint height) +{ + SSimConverterInfo *info = g_list_nth_data (self->priv->converters, index); + + if (!info) { + info = g_slice_new0 (SSimConverterInfo); + + self->priv->converters = + g_list_insert (self->priv->converters, info, index); + } + + if (force || info->in_info.height != height || info->in_info.width != width || + info->in_info.finfo->format != in_format) { + gst_video_info_init (&info->in_info); + gst_video_info_set_format (&info->in_info, in_format, width, height); + + if (info->converter) + gst_video_converter_free (info->converter); + + info->out_info = self->priv->out_info; + + if (gst_video_info_is_equal (&info->in_info, &info->out_info)) + info->converter = NULL; + else + info->converter = + gst_video_converter_new (&info->in_info, &info->out_info, NULL); + } +} + +static GstVideoFormat +_get_format_from_surface (cairo_surface_t * surface) +{ +#if G_BYTE_ORDER == G_BIG_ENDIAN + if (cairo_surface_get_content (surface) == CAIRO_CONTENT_COLOR_ALPHA) + return GST_VIDEO_FORMAT_BGRA; + else + return GST_VIDEO_FORMAT_BGRx; +#else + if (cairo_surface_get_content (surface) == CAIRO_CONTENT_COLOR_ALPHA) + return GST_VIDEO_FORMAT_ARGB; + else + return GST_VIDEO_FORMAT_RGBx; +#endif +} + +void +gst_validate_ssim_compare_frames (GstValidateSsim * self, + GstVideoFrame * ref_frame, GstVideoFrame * frame, GstBuffer ** outbuf, + gfloat * mean, gfloat * lowest, gfloat * highest) +{ + gboolean reconf; + guint8 *outdata = NULL; + GstMapInfo map1, map2, outmap; + + GstVideoFrame converted_frame1, converted_frame2; + SSimConverterInfo *convinfo1, *convinfo2; + + reconf = + gst_validate_ssim_configure (self, ref_frame->info.width, + ref_frame->info.height); + + gst_validate_ssim_configure_converter (self, 0, reconf, + ref_frame->info.finfo->format, ref_frame->info.width, + ref_frame->info.height); + + gst_validate_ssim_configure_converter (self, 1, reconf, + frame->info.finfo->format, frame->info.width, frame->info.height); + + convinfo1 = (SSimConverterInfo *) g_list_nth_data (self->priv->converters, 0); + if (convinfo1->converter) + gst_validate_ssim_convert (self, convinfo1, ref_frame, &converted_frame1); + else + converted_frame1 = *ref_frame; + + convinfo2 = (SSimConverterInfo *) g_list_nth_data (self->priv->converters, 0); + if (convinfo2->converter) + gst_validate_ssim_convert (self, convinfo2, frame, &converted_frame2); + else + converted_frame2 = *frame; + + if (!gst_buffer_map (converted_frame1.buffer, &map1, GST_MAP_READ)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map reference frame"); + + return; + } + + if (!gst_buffer_map (converted_frame2.buffer, &map2, GST_MAP_READ)) { + gst_buffer_unmap (converted_frame1.buffer, &map1); + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map compared frame"); + + return; + } + + if (outbuf) { + *outbuf = gst_buffer_new_and_alloc (GST_ROUND_UP_4 (self->priv->width) * + self->priv->height); + if (!gst_buffer_map (*outbuf, &outmap, GST_MAP_WRITE)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map output frame"); + + gst_buffer_unref (*outbuf); + gst_buffer_unmap (converted_frame1.buffer, &map1); + gst_buffer_unmap (converted_frame2.buffer, &map2); + *outbuf = NULL; + + return; + } + + outdata = outmap.data; + } + + gssim_compare (self->priv->ssim, map1.data, map2.data, outdata, mean, + lowest, highest); + + gst_buffer_unmap (ref_frame->buffer, &map1); + gst_buffer_unmap (frame->buffer, &map2); + + if (convinfo1->converter) + gst_video_frame_unmap (&converted_frame1); + if (convinfo2->converter) + gst_video_frame_unmap (&converted_frame2); + + if (outbuf) + gst_buffer_unmap (*outbuf, &outmap); +} + +static gboolean +gst_validate_ssim_get_frame_from_png (GstValidateSsim * self, const char *file, + GstVideoFrame * frame) +{ + guint8 *data; + GstBuffer *buf; + GstVideoInfo info; + cairo_surface_t *surface = NULL; + + surface = cairo_image_surface_create_from_png (file); + if (surface == NULL + || (cairo_surface_status (surface) != CAIRO_STATUS_SUCCESS)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, "Could not open %s: %s", + file, cairo_status_to_string (cairo_surface_status (surface))); + + return FALSE; + } + + gst_video_info_init (&info); + gst_video_info_set_format (&info, + _get_format_from_surface (surface), + cairo_image_surface_get_width (surface), + cairo_image_surface_get_height (surface)); + + data = cairo_image_surface_get_data (surface); + buf = gst_buffer_new_wrapped_full (GST_MEMORY_FLAG_READONLY, + data, info.size, 0, info.size, surface, + (GDestroyNotify) cairo_surface_destroy); + if (!gst_video_frame_map (frame, &info, buf, GST_MAP_READ)) { + gst_buffer_unref (buf); + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map input frame"); + + return FALSE; + } + + gst_buffer_unref (buf); + + return TRUE; +} + +static gboolean +gst_validate_ssim_get_frame_from_file (GstValidateSsim * self, const char *file, + GstVideoFrame * frame) +{ + gchar *data; + gsize length; + GstBuffer *buf; + GstVideoInfo info; + GstVideoFormat format; + gint strv_length, width, height; + + gboolean res = TRUE; + gchar **splited_name = NULL, **splited_size = NULL, *strformat; + + GError *error = NULL; + + if (g_str_has_suffix (file, ".png")) { + return gst_validate_ssim_get_frame_from_png (self, file, frame); + } + + splited_name = g_strsplit (file, ".", -1); + strv_length = g_strv_length (splited_name); + + strformat = splited_name[strv_length - 1]; + format = gst_video_format_from_string (strformat); + if (format == GST_VIDEO_FORMAT_UNKNOWN) { + GST_VALIDATE_REPORT (self, WRONG_FORMAT, "Unknown format: %s", strformat); + + goto fail; + } + + splited_size = g_strsplit (splited_name[strv_length - 2], "x", -1); + if (g_strv_length (splited_size) != 2) { + GST_VALIDATE_REPORT (self, WRONG_FORMAT, + "Can not determine video size from filename: %s ", file); + + goto fail; + } + + errno = 0; + width = g_ascii_strtoull (splited_size[0], NULL, 10); + if (errno) { + GST_VALIDATE_REPORT (self, WRONG_FORMAT, + "Can not determine video size from filename: %s ", file); + + goto fail; + } + + errno = 0; + height = g_ascii_strtoull (splited_size[1], NULL, 10); + if (errno) { + GST_VALIDATE_REPORT (self, WRONG_FORMAT, + "Can not determine video size from filename: %s ", file); + + goto fail; + } + + gst_video_info_init (&info); + gst_video_info_set_format (&info, format, width, height); + + if (!g_file_get_contents (file, &data, &length, &error)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, "Could not open %s: %s", + file, error->message); + g_error_free (error); + + goto fail; + } + + buf = gst_buffer_new_wrapped (data, length); + if (!gst_video_frame_map (frame, &info, buf, GST_MAP_READ)) { + gst_buffer_unref (buf); + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could not map input frame"); + + goto fail; + } + gst_buffer_unref (buf); + +done: + g_strfreev (splited_name); + g_strfreev (splited_size); + + return res; + +fail: + res = FALSE; + + goto done; +} + +static gboolean +_filename_get_timestamp (GstValidateSsim * self, const gchar * filename, + GstClockTime * ts) +{ + guint h, m, s, ns; + gchar *other = g_strdup (filename); + + if (sscanf (filename, "%" GST_TIME_FORMAT "%s", &h, &m, &s, &ns, other) < 4) { + GST_INFO_OBJECT (self, "Can not sscanf %s", filename); + g_free (other); + + return FALSE; + } + + g_free (other); + *ts = (h * 3600 + m * 60 + s) * GST_SECOND + ns; + + return TRUE; +} + +typedef struct +{ + gchar *path; + GstClockTime ts; +} Frame; + +static void +_free_frame (Frame * frame) +{ + g_free (frame->path); +} + +static gint +_sort_frames (Frame * a, Frame * b) +{ + if (a->ts < b->ts) + return -1; + + if (a->ts == b->ts) + return 0; + + return 1; +} + +static Frame * +_find_frame (GstValidateSsim * self, GArray * frames, GstClockTime ts, + gboolean get_next) +{ + guint i; + Frame *lframe = &g_array_index (frames, Frame, 0); + + for (i = 1; i < frames->len; i++) { + Frame *iframe = &g_array_index (frames, Frame, i); + + if (ts >= lframe->ts && iframe->ts > ts) { + if (get_next) + return iframe; + + return lframe; + } else if (i + 1 == frames->len) { + return iframe; + } + + lframe = iframe; + } + + return NULL; +} + +static GArray * +_get_ref_frame_cache (GstValidateSsim * self, const gchar * ref_file) +{ + GFile *ref_dir_file = NULL; + GFileInfo *info; + GFileEnumerator *fenum; + GArray *frames = NULL; + gchar *ref_dir = NULL; + + ref_dir = g_path_get_dirname (ref_file); + + frames = g_hash_table_lookup (self->priv->ref_frames_cache, ref_file); + if (frames) + goto done; + + ref_dir_file = g_file_new_for_path (ref_dir); + if (!(fenum = g_file_enumerate_children (ref_dir_file, + "standard::*", G_FILE_QUERY_INFO_NONE, NULL, NULL))) { + GST_INFO ("%s is not a folder", ref_dir); + + goto done; + } + + for (info = g_file_enumerator_next_file (fenum, NULL, NULL); + info; info = g_file_enumerator_next_file (fenum, NULL, NULL)) { + Frame iframe; + const gchar *display_name = g_file_info_get_display_name (info); + + if (!_filename_get_timestamp (self, display_name, &iframe.ts)) { + g_object_unref (info); + continue; + } + + iframe.path = g_build_path (G_DIR_SEPARATOR_S, + ref_dir, g_file_info_get_name (info), NULL); + + g_object_unref (info); + + if (!frames) { + frames = g_array_new (TRUE, TRUE, sizeof (Frame)); + + g_array_set_clear_func (frames, (GDestroyNotify) _free_frame); + } + g_array_append_val (frames, iframe); + } + + if (frames) { + g_array_sort (frames, (GCompareFunc) _sort_frames); + + g_hash_table_insert (self->priv->ref_frames_cache, g_strdup (ref_dir), + frames); + } + +done: + g_clear_object (&ref_dir_file); + g_free (ref_dir); + + return frames; +} + +static gchar * +_get_ref_file_path (GstValidateSsim * self, const gchar * ref_file, + const gchar * file, gboolean get_next) +{ + Frame *frame; + GArray *frames; + gchar *real_ref_file = NULL, *fbname = NULL; + GstClockTime file_ts; + + if (!g_strrstr (ref_file, "*")) + return g_strdup (ref_file); + + fbname = g_path_get_basename (file); + if (!_filename_get_timestamp (self, fbname, &file_ts)) { + + goto done; + } + + frames = _get_ref_frame_cache (self, ref_file); + if (frames) { + frame = _find_frame (self, frames, file_ts, get_next); + + if (frame) + real_ref_file = g_strdup (frame->path); + } + +done: + g_free (fbname); + + return real_ref_file; +} + +static gboolean +gst_validate_ssim_compare_image_file (GstValidateSsim * self, + const gchar * ref_file, const gchar * file, gfloat * mean, gfloat * lowest, + gfloat * highest, const gchar * outfolder) +{ + GstBuffer *outbuf = NULL, **poutbuf = NULL; + gboolean res = TRUE; + GstVideoFrame ref_frame, frame; + gchar *real_ref_file = NULL; + + real_ref_file = _get_ref_file_path (self, ref_file, file, FALSE); + + if (!real_ref_file) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "Could find ref file for %s", ref_file); + goto fail; + } + + if (!gst_validate_ssim_get_frame_from_file (self, real_ref_file, &ref_frame)) + goto fail; + + + if (!gst_validate_ssim_get_frame_from_file (self, file, &frame)) { + gst_video_frame_unmap (&ref_frame); + + goto fail; + } + + if (outfolder) { + poutbuf = &outbuf; + } + + gst_validate_ssim_compare_frames (self, &ref_frame, &frame, + poutbuf, mean, lowest, highest); + + if (*mean < self->priv->min_avg_similarity) { + gst_video_frame_unmap (&ref_frame); + gst_video_frame_unmap (&frame); + + if (g_strcmp0 (ref_file, real_ref_file)) { + gchar *tmpref = real_ref_file; + + real_ref_file = _get_ref_file_path (self, ref_file, file, TRUE); + + GST_VALIDATE_REPORT (self, SIMILARITY_ISSUE_WITH_PREVIOUS, + "\nComparing %s with %s failed, (mean %f " + " min %f), checking next %s\n", tmpref, file, + *mean, *lowest, real_ref_file); + + g_free (tmpref); + + res = gst_validate_ssim_compare_image_file (self, + real_ref_file, file, mean, lowest, highest, outfolder); + goto done; + } + + GST_VALIDATE_REPORT (self, SIMILARITY_ISSUE, + "Average similarity '%f' between %s and %s inferior" + " than the minimum average: %f", *mean, + real_ref_file, file, self->priv->min_avg_similarity); + + goto fail; + } + + if (*lowest < self->priv->min_lowest_similarity) { + GST_VALIDATE_REPORT (self, SIMILARITY_ISSUE, + "Lowest similarity '%f' between %s and %s inferior" + " than the minimum lowest similarity: %f", *lowest, + real_ref_file, file, self->priv->min_lowest_similarity); + + gst_video_frame_unmap (&ref_frame); + gst_video_frame_unmap (&frame); + + goto fail; + } + + gst_video_frame_unmap (&ref_frame); + gst_video_frame_unmap (&frame); + +done: + + g_free (real_ref_file); + if (outbuf) + gst_buffer_unref (outbuf); + + return res; + +fail: + res = FALSE; + + if (outbuf) + gst_validate_ssim_save_out (self, outbuf, real_ref_file, file, outfolder); + + goto done; +} + +static gboolean +_check_directory (GstValidateSsim * self, const gchar * ref_dir, + const gchar * compared_dir, gfloat * mean, gfloat * lowest, + gfloat * highest, const gchar * outfolder) +{ + gint nfiles = 0, nnotfound = 0, nfailures = 0; + gboolean res = TRUE; + GFileInfo *info; + GFileEnumerator *fenum; + GFile *file = g_file_new_for_path (ref_dir); + + if (!(fenum = g_file_enumerate_children (file, + "standard::*", G_FILE_QUERY_INFO_NONE, NULL, NULL))) { + GST_INFO ("%s is not a folder", ref_dir); + res = FALSE; + + goto done; + } + + for (info = g_file_enumerator_next_file (fenum, NULL, NULL); + info; info = g_file_enumerator_next_file (fenum, NULL, NULL)) { + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_REGULAR || + g_file_info_get_file_type (info) == G_FILE_TYPE_SYMBOLIC_LINK) { + gchar *compared_file = g_build_path (G_DIR_SEPARATOR_S, + compared_dir, g_file_info_get_name (info), NULL); + gchar *ref_file = NULL; + + if (!g_file_test (compared_file, G_FILE_TEST_IS_REGULAR)) { + GST_INFO_OBJECT (self, "Could not find file %s", compared_file); + nnotfound++; + res = FALSE; + } else { + + ref_file = + g_build_path (G_DIR_SEPARATOR_S, ref_dir, + g_file_info_get_name (info), NULL); + if (!gst_validate_ssim_compare_image_files (self, ref_file, + compared_file, mean, lowest, highest, outfolder)) { + nfailures++; + res = FALSE; + } else { + nfiles++; + } + } + + gst_validate_printf (NULL, + "\r", + g_file_info_get_display_name (info), *mean, *lowest, + nfiles, nfailures, nnotfound); + + g_free (compared_file); + g_free (ref_file); + } + + g_object_unref (info); + } + +done: + gst_object_unref (file); + if (fenum) + gst_object_unref (fenum); + + return res; +} + +gboolean +gst_validate_ssim_compare_image_files (GstValidateSsim * self, + const gchar * ref_file, const gchar * file, gfloat * mean, gfloat * lowest, + gfloat * highest, const gchar * outfolder) +{ + if (g_file_test (ref_file, G_FILE_TEST_IS_DIR)) { + if (!g_file_test (file, G_FILE_TEST_IS_DIR)) { + GST_VALIDATE_REPORT (self, GENERAL_INPUT_ERROR, + "%s is a directory but %s is not", ref_file, file); + + return FALSE; + } + + return _check_directory (self, ref_file, file, mean, lowest, highest, + outfolder); + } else { + return gst_validate_ssim_compare_image_file (self, ref_file, file, mean, + lowest, highest, outfolder); + } +} + +static void +gst_validate_ssim_get_property (GObject * object, + guint property_id, GValue * value, GParamSpec * pspec) +{ + switch (property_id) { + case PROP_RUNNER: + /* we assume the runner is valid as long as this scenario is, + * no ref taken */ + g_value_set_object (value, + gst_validate_reporter_get_runner (GST_VALIDATE_REPORTER (object))); + break; + default: + break; + } +} + +static void +gst_validate_ssim_set_property (GObject * object, + guint property_id, const GValue * value, GParamSpec * pspec) +{ + switch (property_id) { + case PROP_RUNNER: + /* we assume the runner is valid as long as this scenario is, + * no ref taken */ + gst_validate_reporter_set_runner (GST_VALIDATE_REPORTER (object), + g_value_get_object (value)); + break; + default: + break; + } +} + +static void +gst_validate_ssim_dispose (GObject * object) +{ + GstValidateSsim *self = GST_VALIDATE_SSIM (object); + void (*chain_up) (GObject *) = + ((GObjectClass *) gst_validate_ssim_parent_class)->dispose; + + gst_object_unref (self->priv->ssim); + + chain_up (object); +} + +static void +gst_validate_ssim_finalize (GObject * object) +{ + GstValidateSsim *self = GST_VALIDATE_SSIM (object); + void (*chain_up) (GObject *) = + ((GObjectClass *) gst_validate_ssim_parent_class)->finalize; + + g_list_free_full (self->priv->converters, + (GDestroyNotify) ssim_convert_info_free); + + if (self->priv->outconverter_info.converter) + gst_video_converter_free (self->priv->outconverter_info.converter); + g_hash_table_unref (self->priv->ref_frames_cache); + + chain_up (object); +} + +static gpointer +_register_issues (gpointer data) +{ + gst_validate_issue_register (gst_validate_issue_new (SIMILARITY_ISSUE, + "Compared images where not similar enough", + "The images checker detected that the images" + " it is comparing did not have the similarity" + " level as defined with min-avg-similarity or" + " min-lowest-similarity", GST_VALIDATE_REPORT_LEVEL_CRITICAL)); + + gst_validate_issue_register (gst_validate_issue_new + (SIMILARITY_ISSUE_WITH_PREVIOUS, + "Comparison with theoretically reference image failed", + " In a case were we have reference frames with the following" + " timestamps: [0.00, 0.10, 0.20, 0.30] comparing a frame with" + " 0.05 as a timestamp will be done with the first frame. " + " If that fails, it will report a ssim::image-not-similar-enough-with-theoretical-reference" + " warning and try with the second reference frame.", + GST_VALIDATE_REPORT_LEVEL_WARNING)); + + gst_validate_issue_register (gst_validate_issue_new (GENERAL_INPUT_ERROR, + "Something went wrong handling image files", + "An error accured when working with input files", + GST_VALIDATE_REPORT_LEVEL_CRITICAL)); + + gst_validate_issue_register (gst_validate_issue_new (WRONG_FORMAT, + "The format or dimensions of the compared images do not match ", + "The format or dimensions of the compared images do not match ", + GST_VALIDATE_REPORT_LEVEL_CRITICAL)); + + return NULL; +} + +static void +gst_validate_ssim_class_init (GstValidateSsimClass * klass) +{ + GObjectClass *oclass = G_OBJECT_CLASS (klass); + static GOnce _once = G_ONCE_INIT; + + GST_DEBUG_CATEGORY_INIT (gstvalidatessim_debug, "validatessim", 0, + "Validate ssim plugin"); + + oclass->get_property = gst_validate_ssim_get_property; + oclass->set_property = gst_validate_ssim_set_property; + oclass->dispose = gst_validate_ssim_dispose; + oclass->finalize = gst_validate_ssim_finalize; + + g_type_class_add_private (klass, sizeof (GstValidateSsimPriv)); + + g_once (&_once, _register_issues, NULL); + + g_object_class_install_property (oclass, PROP_RUNNER, + g_param_spec_object ("validate-runner", "VALIDATE Runner", + "The Validate runner to " "report errors to", + GST_TYPE_VALIDATE_RUNNER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE)); +} + +static void +gst_validate_ssim_init (GstValidateSsim * self) +{ + self->priv = + G_TYPE_INSTANCE_GET_PRIVATE (self, GST_VALIDATE_SSIM_TYPE, + GstValidateSsimPriv); + + self->priv->ssim = gssim_new (); + self->priv->ref_frames_cache = g_hash_table_new_full (g_str_hash, + g_str_equal, g_free, (GDestroyNotify) g_array_unref); +} + +GstValidateSsim * +gst_validate_ssim_new (GstValidateRunner * runner, + gfloat min_avg_similarity, gfloat min_lowest_similarity) +{ + GstValidateSsim *self = + g_object_new (GST_VALIDATE_SSIM_TYPE, "validate-runner", runner, NULL); + + self->priv->min_avg_similarity = min_avg_similarity; + self->priv->min_lowest_similarity = min_lowest_similarity; + + gst_validate_reporter_set_name (GST_VALIDATE_REPORTER (self), + g_strdup ("gst-validate-images-checker")); + + return self; +} diff --git a/validate/gst-libs/gst/video/gstvalidatessim.h b/validate/gst-libs/gst/video/gstvalidatessim.h new file mode 100644 index 0000000000..484a3d10d6 --- /dev/null +++ b/validate/gst-libs/gst/video/gstvalidatessim.h @@ -0,0 +1,73 @@ +/* GStreamer + * + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2015 Raspberry Pi Foundation + * Author: Thibault Saunier + * + * This library 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. + * + * This library 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 this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#ifndef _GST_VALIDATE_SSIM_H +#define _GST_VALIDATE_SSIM_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include +#include +#include + +G_BEGIN_DECLS + +typedef struct _GstValidateSsimPriv GstValidateSsimPriv; + +typedef struct { + GObject parent; + + GstValidateSsimPriv *priv; +} GstValidateSsim; + +typedef struct { + GObjectClass parent; +} GstValidateSsimClass; + +#define GST_VALIDATE_SSIM_TYPE (gst_validate_ssim_get_type ()) +#define GST_VALIDATE_SSIM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GST_VALIDATE_SSIM_TYPE, GstValidateSsim)) +#define GST_VALIDATE_SSIM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GST_VALIDATE_SSIM_TYPE, GstValidateSsimClass)) +#define IS_GST_VALIDATE_SSIM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GST_VALIDATE_SSIM_TYPE)) +#define IS_GST_VALIDATE_SSIM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), GST_VALIDATE_SSIM_TYPE)) +#define GST_VALIDATE_SSIM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GST_VALIDATE_SSIM_TYPE, GstValidateSsimClass)) + +GType gst_validate_ssim_get_type (void); + +GstValidateSsim * gst_validate_ssim_new (GstValidateRunner *runner, + gfloat min_avg_similarity, + gfloat min_lowest_similarity); + +gboolean gst_validate_ssim_compare_image_files (GstValidateSsim *self, const gchar *ref_file, + const gchar * file, gfloat * mean, gfloat * lowest, + gfloat * highest, const gchar *outfolder); + +void gst_validate_ssim_compare_frames (GstValidateSsim * self, GstVideoFrame *ref_frame, + GstVideoFrame *frame, GstBuffer **outbuf, + gfloat * mean, gfloat * lowest, gfloat * highest); + +G_END_DECLS + +#endif diff --git a/validate/tools/.gitignore b/validate/tools/.gitignore index 9f05fa8d8e..6c0414b505 100644 --- a/validate/tools/.gitignore +++ b/validate/tools/.gitignore @@ -1,6 +1,8 @@ gst-validate-1.0 gst-validate-transcoding-1.0 gst-validate-media-check-1.0 +gst-validate-images-check-1.0 gst-validate-1.0-debug gst-validate-media-check-1.0-debug gst-validate-transcoding-1.0-debug +gst-validate-images-check-1.0-debug diff --git a/validate/tools/Makefile.am b/validate/tools/Makefile.am index db5ed4b8d5..a650700fc1 100644 --- a/validate/tools/Makefile.am +++ b/validate/tools/Makefile.am @@ -34,4 +34,18 @@ gst_validate_media_check_@GST_API_VERSION@_SOURCES = gst-validate-media-check.c gst_validate_media_check_@GST_API_VERSION@_debug_SOURCES = gst-validate-media-check.c gst_validate_media_check_@GST_API_VERSION@_debug_LDFLAGS = -no-install +if HAVE_CAIRO +bin_PROGRAMS += gst-validate-images-check-@GST_API_VERSION@ +noinst_PROGRAMS += gst-validate-images-check-@GST_API_VERSION@-debug + +gst_validate_images_check_@GST_API_VERSION@_SOURCES = gst-validate-images-check.c +gst_validate_images_check_@GST_API_VERSION@_CFLAGS = $(AM_CFLAGS) -I$(top_builddir)/gst-libs/gst/video/ +gst_validate_images_check_@GST_API_VERSION@_LDADD = $(LDADD) $(top_builddir)/gst-libs/gst/video/libgstvalidatevideo-@GST_API_VERSION@.la + +gst_validate_images_check_@GST_API_VERSION@_debug_SOURCES = gst-validate-images-check.c +gst_validate_images_check_@GST_API_VERSION@_debug_CFLAGS = $(AM_CFLAGS) -I$(top_builddir)/gst-libs/gst/video/ +gst_validate_images_check_@GST_API_VERSION@_debug_LDADD = $(LDADD) $(top_builddir)/gst-libs/gst/video/libgstvalidatevideo-@GST_API_VERSION@.la +gst_validate_images_check_@GST_API_VERSION@_debug_LDFLAGS = -no-install +endif + CLEANFILES = $(bin_SCRIPTS) diff --git a/validate/tools/gst-validate-images-check.c b/validate/tools/gst-validate-images-check.c new file mode 100644 index 0000000000..4f45a242f2 --- /dev/null +++ b/validate/tools/gst-validate-images-check.c @@ -0,0 +1,109 @@ +/* GStreamer + * + * Copyright (C) 2014 Mathieu Duponchelle + * Copyright (C) 2015 Raspberry Pi Foundation + * Author: Thibault Saunier + * + * This library 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. + * + * This library 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 this library; if not, write to the + * Free Software Foundation, Inc., 59 Temple Place - Suite 330, + * Boston, MA 02111-1307, USA. + */ + +#include + +#include +#include +#include + +int +main (int argc, char **argv) +{ + GstValidateSsim *ssim; + gint rep_err, ret = 0; + GError *err = NULL; + GstValidateRunner *runner = NULL; + GOptionContext *ctx; + const gchar *outfolder = NULL; + gfloat mssim = 0, lowest = 1, highest = -1; + gdouble min_avg_similarity = 0.95, min_lowest_similarity = -1.0; + + GOptionEntry options[] = { + {"min-avg-similarity", 'a', 0, G_OPTION_ARG_DOUBLE, + &min_avg_similarity, + "The minimum average similarity under which we consider" + "the test as failing", + NULL}, + {"min-lowest-similarity", 'l', 0, G_OPTION_ARG_DOUBLE, + &min_lowest_similarity, + "The minimum 'lowest' similarity under which we consider" + "the test as failing", + NULL}, + {"result-output-folder", 'r', 0, G_OPTION_ARG_STRING, + &outfolder, + "The folder in which to store resulting grey scale images" + " when the test failed. In that folder you will find" + " images with the structural difference between" + " the reference frame and the failed one", + NULL}, + {NULL} + }; + + g_set_prgname ("gst-validate-mages-check-" GST_API_VERSION); + ctx = g_option_context_new ("/reference/file/path /compared/file/path"); + + g_option_context_set_summary (ctx, + "The gst-validate-images-check calculates SSIM (Structural SIMilarity) " + " index for the images. And according to min-lowest-similarity and" + " min-avg-similarity, it will concider the images similare enough" + " or report critical issues in the GstValidate reporting system"); + g_option_context_add_main_entries (ctx, options, NULL); + + if (!g_option_context_parse (ctx, &argc, &argv, &err)) { + g_printerr ("Error initializing: %s\n", err->message); + g_option_context_free (ctx); + return -1; + } + + if (argc != 3) { + gchar *help = g_option_context_get_help (ctx, FALSE, NULL); + g_printerr ("%s", help); + g_free (help); + g_option_context_free (ctx); + + return -1; + } + + gst_init (&argc, &argv); + gst_validate_init (); + + runner = gst_validate_runner_new (); + ssim = + gst_validate_ssim_new (runner, min_avg_similarity, min_lowest_similarity); + + gst_validate_ssim_compare_image_files (ssim, argv[1], argv[2], &mssim, + &lowest, &highest, outfolder); + + rep_err = gst_validate_runner_exit (runner, TRUE); + if (ret == 0) { + ret = rep_err; + if (rep_err != 0) + g_print ("Returning %d as error where found", rep_err); + } + + g_object_unref (ssim); + g_object_unref (runner); + gst_validate_deinit (); + + return ret; +}