[gdk-pixbuf] tests: Add two-step scaler test



commit eee598c062f213413384465d0a3d881e5f99a00e
Author: Martin Guy <martinwguy gmail com>
Date:   Fri Jan 6 15:17:02 2017 +0100

    tests: Add two-step scaler test
    
    Test the output of scaling functions to see whether
    optimisation/simplification to avoid huge memory and CPU usage is
    effective and does not regress.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=80925

 tests/Makefile.am             |    7 +
 tests/pixbuf-scale-two-step.c |  335 +++++++++++++++++++++++++++++++++++++++++
 2 files changed, 342 insertions(+), 0 deletions(-)
---
diff --git a/tests/Makefile.am b/tests/Makefile.am
index c03f3bb..f346217 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -38,6 +38,7 @@ test_programs =                       \
        pixbuf-reftest                  \
        pixbuf-resource                 \
        pixbuf-scale                    \
+       pixbuf-scale-two-step           \
        pixbuf-short-gif-write          \
        pixbuf-save                     \
        pixbuf-readonly-to-mutable      \
@@ -117,6 +118,12 @@ pixbuf_scale_SOURCES =                     \
        test-common.h                   \
        $(NULL)
 
+pixbuf_scale_two_step_SOURCES =                \
+       pixbuf-scale-two-step.c         \
+       test-common.c                   \
+       test-common.h                   \
+       $(NULL)
+
 pixbuf_stream_SOURCES =                        \
        pixbuf-stream.c                 \
        test-common.c                   \
diff --git a/tests/pixbuf-scale-two-step.c b/tests/pixbuf-scale-two-step.c
new file mode 100644
index 0000000..d80f155
--- /dev/null
+++ b/tests/pixbuf-scale-two-step.c
@@ -0,0 +1,335 @@
+/* -*- Mode: C; c-basic-offset: 2; -*- */
+/* GdkPixbuf library - test loaders
+ *
+ *  Copyright (C) 2016 Martin Guy <martinwguy gmail com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/*
+ * Test the two-step scaler speed optimization in gdk-pixbuf/pixops.c
+ * for result correctness. See https://bugzilla.gnome.org/show_bug.cgi?id=80925
+ *
+ * The two-step scaler kicks in when ceil(1/scale_x + 1) * ceil(1/scale_y + 1)
+ * (the number of 2KB filters created by make_filter_table()) exceeds 1000 so
+ * test cases wanting it have to do drastic image reductions, such as reducing
+ * both dimensions by a factor of more that sqrt(1000) - say by 40 both ways.
+ *
+ * There is a global boolean in the two-step-scaler allowing us to disable the
+ * two-step optimization to be able to compare the results with and without it.
+ */
+
+#include "config.h"
+#include "gdk-pixbuf/gdk-pixbuf.h"
+#include "test-common.h"
+
+/* Size of images to be scaled */
+#define SOURCE_WIDTH 400
+#define SOURCE_HEIGHT 400
+
+/* To trigger the two-step scaler, we need to reduce the total area by
+ * more than 1000.  40x40 is 1600. */
+#define SCALE_FACTOR (1.0/40.0)
+
+/* First, some functions to make test images */
+
+/*
+ * pixdata_almost_equal(): A helper function to check whether the pixels in
+ * two images are almost the same: pixel color values are allowed to differ
+ * by at most one.
+ */
+
+/* Are two values the same or differ by one? */
+#define within_one(a, b) \
+       ((a) == (b) || (a) == (b) + 1 || (a) + 1 == (b))
+
+static gboolean
+pixdata_almost_equal (GdkPixbuf *one, GdkPixbuf *two)
+{
+  guchar *one_row;      /* Pointer to start of row of pixels in one */
+  guchar *one_pixel;    /* Pointer to current pixel data in one */
+  guchar *two_row;      /* Pointer to start of row of pixels in two */
+  guchar *two_pixel;    /* Pointer to current pixel data in two */
+  guint x, y;
+
+  if (gdk_pixbuf_get_width (one) != gdk_pixbuf_get_width (two) ||
+      gdk_pixbuf_get_height (one) != gdk_pixbuf_get_height (two))
+    {
+      g_test_fail();
+    }
+
+  for (y = 0, one_row = gdk_pixbuf_get_pixels (one),
+              two_row = gdk_pixbuf_get_pixels (two);
+       y < gdk_pixbuf_get_height (one);
+       y++, one_row += gdk_pixbuf_get_rowstride (one),
+            two_row += gdk_pixbuf_get_rowstride (two))
+    {
+      for (x = 0, one_pixel = one_row, two_pixel = two_row;
+          x < gdk_pixbuf_get_width(one);
+          x++, one_pixel += gdk_pixbuf_get_n_channels (one),
+               two_pixel += gdk_pixbuf_get_n_channels (two))
+        {
+         if (!(within_one(one_pixel[0], two_pixel[0]) &&
+               within_one(one_pixel[1], two_pixel[1]) &&
+               within_one(one_pixel[2], two_pixel[2])))
+           {
+             return FALSE;
+           }
+        }
+    }
+
+  return TRUE;
+}
+
+/* Create a checkerboard of (1,1,1) and (255,255,255) pixels and
+ * scale it to one fortieth of the image dimensions.
+ * See if the results are similar enough with and without the two-step
+ * optimization. */
+static void
+test_old_and_new (gconstpointer data)
+{
+  GdkInterpType interp_type = *(GdkInterpType *) data;
+  const GdkPixbuf *source;              /* Source image */
+  gint width = SOURCE_WIDTH;            /* Size of source image */
+  gint height = SOURCE_HEIGHT;
+  GdkPixbuf *one;                       /* Version scaled by the old code */
+  GdkPixbuf *two;                       /* Version scaled in two steps */
+
+  /* Use an extreme source image, checkerboard, to exacerbate any artifacts */
+  source = make_checkerboard (width, height);
+
+  /* Scale it without and with the two-step optimization */
+  g_setenv ("GDK_PIXBUF_DISABLE_TWO_STEP_SCALER", "1", TRUE);
+  one = gdk_pixbuf_scale_simple (source,
+                                (int) (width * SCALE_FACTOR + 0.5),
+                                (int) (height * SCALE_FACTOR + 0.5),
+                                interp_type);
+  g_unsetenv("GDK_PIXBUF_DISABLE_TWO_STEP_SCALER");
+  two = gdk_pixbuf_scale_simple (source,
+                                (int) (width * SCALE_FACTOR + 0.5),
+                                (int) (height * SCALE_FACTOR + 0.5),
+                                interp_type);
+
+  /* Check that the results are almost the same. An error of one color value
+   * is admissible because the intermediate image's color values are stored
+   * in 8-bit color values. In practice, with the checkerboard pattern all
+   * pixels are (129,129,129) with the two-step scaler instead of (128,128,128)
+   * while the rg pattern gives identical results.
+   */
+  if (!pixdata_almost_equal(one, two))
+    g_test_fail();
+
+  g_object_unref (G_OBJECT (one));
+  g_object_unref (G_OBJECT (two));
+  g_object_unref (G_OBJECT (source));
+}
+
+/* Crop a region out of a scaled image using gdk_pixbuf_new_subpixbuf() on a
+ * scaled image and by cropping it as part of the scaling, and check that the
+ * results are identical. */
+static void
+crop_n_compare(const GdkPixbuf *source,        /* The source image */
+              double           scale_factor,  /* is scaled by this amount */
+              gint             offset_x,      /* and from this offset in the scaled image */
+              gint             offset_y,
+              gint             width,         /* a region of this size is cropped out */
+              gint             height,
+              GdkInterpType    interp_type)
+{
+  GdkPixbuf *whole_scaled;     /* The whole image scaled but not cropped */
+  GdkPixbuf *cropped;          /* The scaled-then-cropped result */
+  GdkPixbuf *scaled;           /* The cropped-while-scaled result */
+  guint new_width, new_height; /* Size of whole scaled image */
+
+  /* First, scale the whole image and crop it */
+  new_width = (int) (gdk_pixbuf_get_width (source) * scale_factor + 0.5);
+  new_height = (int) (gdk_pixbuf_get_height (source) * scale_factor + 0.5);
+  g_assert_cmpint (new_width, ==, 10);
+  g_assert_cmpint (new_height, ==, 10);
+
+  whole_scaled = gdk_pixbuf_scale_simple (source, new_width, new_height, interp_type);
+  g_assert_nonnull (whole_scaled);
+  cropped = gdk_pixbuf_new_subpixbuf (whole_scaled, offset_x, offset_y, width, height);
+  g_assert_nonnull (cropped);
+
+  /* Now crop and scale it in one go */
+  scaled = gdk_pixbuf_new (GDK_COLORSPACE_RGB, 0, 8, width, height);
+  g_assert_nonnull (scaled);
+  gdk_pixbuf_scale (source, scaled,
+                   0, 0,                              /* dest_[xy] */
+                   width, height,                     /* dest_width/height */
+                   -1.0 * offset_x, -1.0 * offset_y,
+                   scale_factor, scale_factor,
+                   interp_type);
+
+  /* and check that the results are the same.
+   * We can't use pixdata_equal() because it fails if rowstride is different
+   * and gdk_pixbuf_new_subpixbuf() reuses whole_scaled's image data with its
+   * larger rowstride. */
+  {
+    guchar *scaled_row;     /* Pointer to start of row of pixels in scaled */
+    guchar *scaled_pixel;   /* Pointer to current pixel data in scaled */
+    guchar *cropped_row;    /* Pointer to start of row of pixels in cropped */
+    guchar *cropped_pixel;  /* Pointer to current pixel data in cropped */
+    guint x, y;
+
+    if (gdk_pixbuf_get_width (scaled) != gdk_pixbuf_get_width (cropped) ||
+       gdk_pixbuf_get_height (scaled) != gdk_pixbuf_get_height (cropped))
+      {
+        g_test_fail();
+      }
+
+    for (y = 0, scaled_row = gdk_pixbuf_get_pixels (scaled),
+              cropped_row = gdk_pixbuf_get_pixels (cropped);
+        y < gdk_pixbuf_get_height (scaled);
+        y++, scaled_row += gdk_pixbuf_get_rowstride (scaled),
+             cropped_row += gdk_pixbuf_get_rowstride (cropped))
+      {
+        for (x = 0, scaled_pixel = scaled_row, cropped_pixel = cropped_row;
+           x < gdk_pixbuf_get_width(scaled);
+           x++, scaled_pixel += gdk_pixbuf_get_n_channels (scaled),
+                cropped_pixel += gdk_pixbuf_get_n_channels (cropped))
+         {
+           if (!(scaled_pixel[0] == cropped_pixel[0] &&
+                 scaled_pixel[1] == cropped_pixel[1] &&
+                 scaled_pixel[2] == cropped_pixel[2]))
+           {
+             g_test_fail();
+           }
+         }
+      }
+  }
+
+  g_object_unref (G_OBJECT (whole_scaled));
+  g_object_unref (G_OBJECT (cropped));
+  g_object_unref (G_OBJECT (scaled));
+}
+
+/* Check that offsets work.
+ * We should be able to crop a region of an image using the scaler using
+ * negative offsets. */
+static void
+test_offset (gconstpointer data)
+{
+  GdkInterpType interp_type = *(GdkInterpType *) data;
+  const GdkPixbuf *source;                     /* Source image */
+  gint swidth = SOURCE_WIDTH;                  /* Size of source image */
+  gint sheight = SOURCE_HEIGHT;
+  gint dwidth = (swidth * SCALE_FACTOR + 0.5); /* Size of scaled image */
+  gint dheight = (sheight * SCALE_FACTOR + 0.5);
+
+  source = make_rg (swidth, sheight);
+
+  /* Check that the scaler correctly crops out an image half the size of the
+   * original from each corner and from the middle */
+  crop_n_compare (source, SCALE_FACTOR, 0,          0,           dwidth / 2, dheight / 2, interp_type);
+  crop_n_compare (source, SCALE_FACTOR, 0,          dheight / 2, dwidth / 2, dheight / 2, interp_type);
+  crop_n_compare (source, SCALE_FACTOR, dwidth / 2, 0,           dwidth / 2, dheight / 2, interp_type);
+  crop_n_compare (source, SCALE_FACTOR, dwidth / 2, dheight / 2, dwidth / 2, dheight / 2, interp_type);
+  crop_n_compare (source, SCALE_FACTOR, dwidth / 4, dheight / 4, dwidth / 2, dheight / 2, interp_type);
+
+  g_object_unref (G_OBJECT (source));
+}
+
+/* Test the dest_x and dest_y fields by making a copy of an image by
+ * scaling 1:1 and using dest to copy the four quadrants separately.
+ *
+ * When scaled, images are:
+ * 1) scaled by the scale factors with respect to the top-left corner
+ * 2) translated by the offsets (negative to shift the image left/up in its
+ *    frame)
+ * 3) a region of size dest_width x dest-height starting at (dest_x,dest_y)
+ *    in the scaled-and-offset image is copied to (dest_x,dest_y) in the
+ *    destination image. See the illustration at
+ *    https://developer.gnome.org/gdk-pixbuf/2.22/gdk-pixbuf-scaling.html#gdk-pixbuf-composite */
+static void
+test_dest (gconstpointer data)
+{
+  GdkInterpType interp_type = *(GdkInterpType *) data;
+  const GdkPixbuf *source;             /* Source image */
+  GdkPixbuf *scaled;                   /* Scaled whole image */
+  GdkPixbuf *copy;                     /* Destination image */
+  gint swidth = SOURCE_WIDTH;          /* Size of source image */
+  gint sheight = SOURCE_HEIGHT;
+  gint dwidth = swidth * SCALE_FACTOR; /* Size of scaled image */
+  gint dheight = sheight * SCALE_FACTOR;
+
+  source = make_checkerboard (swidth, sheight);
+
+  copy = gdk_pixbuf_new (GDK_COLORSPACE_RGB, 0, 8, dwidth, dheight);
+  g_assert_nonnull (copy);
+
+  /* Copy the four quadrants separately */
+  gdk_pixbuf_scale ((const GdkPixbuf *) source, copy,
+                   0, 0,                    /* dest_[xy] */
+                   dwidth / 2, dheight / 2, /* dest_width/height */
+                   0.0, 0.0,                /* offset_[xy] */
+                   SCALE_FACTOR,  SCALE_FACTOR,
+                   interp_type);
+  gdk_pixbuf_scale ((const GdkPixbuf *) source, copy,
+                   dwidth / 2, 0,           /* dest_[xy] */
+                   dwidth / 2, dheight / 2, /* dest_width/height */
+                   0.0, 0.0,                /* offset_[xy] */
+                   SCALE_FACTOR,  SCALE_FACTOR,
+                   interp_type);
+  gdk_pixbuf_scale ((const GdkPixbuf *)source, copy,
+                   0, dheight / 2,          /* dest_[xy] */
+                   dwidth / 2, dheight / 2, /* dest_width/height */
+                   0.0, 0.0,                /* offset_[xy] */
+                   SCALE_FACTOR,  SCALE_FACTOR,
+                   interp_type);
+  gdk_pixbuf_scale ((const GdkPixbuf *)source, copy,
+                   dwidth / 2, dheight / 2, /* dest_[xy] */
+                   dwidth / 2, dheight / 2, /* dest_width/height */
+                   0.0, 0.0,                /* offset_[xy] */
+                   SCALE_FACTOR,  SCALE_FACTOR,
+                   interp_type);
+
+  scaled = gdk_pixbuf_scale_simple (source, dwidth, dheight, interp_type);
+  g_assert_nonnull (scaled);
+
+  /* Compare the all-at-once and the composite images */
+  {
+    GError *error = NULL;
+    if (!pixdata_equal(scaled, copy, &error))
+      g_test_fail();
+  }
+
+  g_object_unref (G_OBJECT (source));
+  g_object_unref (G_OBJECT (copy));
+  g_object_unref (G_OBJECT (scaled));
+}
+
+int
+main (int argc, char **argv)
+{
+  GdkInterpType tiles = GDK_INTERP_TILES;
+  GdkInterpType bilinear = GDK_INTERP_BILINEAR;
+  GdkInterpType hyper = GDK_INTERP_HYPER;
+  g_test_init (&argc, &argv, NULL);
+
+  g_test_add_data_func ("/pixbuf/scale/two-step/tiles", &tiles, test_old_and_new);
+  g_test_add_data_func ("/pixbuf/scale/two-step/bilinear", &bilinear, test_old_and_new);
+  g_test_add_data_func ("/pixbuf/scale/two-step/hyper", &hyper, test_old_and_new);
+
+  g_test_add_data_func ("/pixbuf/scale/two-step/offset/tiles", &tiles, test_offset);
+  g_test_add_data_func ("/pixbuf/scale/two-step/offset/bilinear", &bilinear, test_offset);
+  g_test_add_data_func ("/pixbuf/scale/two-step/offset/hyper", &hyper, test_offset);
+
+  g_test_add_data_func ("/pixbuf/scale/two-step/dest/tiles", &tiles, test_dest);
+  g_test_add_data_func ("/pixbuf/scale/two-step/dest/bilinear", &bilinear, test_dest);
+  g_test_add_data_func ("/pixbuf/scale/two-step/dest/hyper", &hyper, test_dest);
+
+  return g_test_run ();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]