/*
 *  $Id: field.c 29755 2026-04-09 10:31:00Z yeti-dn $
 *  Copyright (C) 2025-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  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, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include "tests/testlibgwy.h"

/* FIXME: The tolerances for various statistical and other functions were just estimated based on what they do, how
 * many operations they may involve and how well conditioned they may be. With GCC, we some fail occasionally (for
 * unlucky seeds). So they are probably quite tight. With CLANG and fast math, we some fail all the time. */

enum {
    SOME_NUMBER = 7121,
};

typedef gdouble (*MaskedStatFunc)(GwyField *field, GwyNield *nield, GwyMaskingType masking,
                                  gint col, gint row, gint width, gint height);
typedef gdouble (*ConstantDataFunc)(gdouble c, gsize n);

G_GNUC_UNUSED
static void
dump_field(GwyField *field, gint digits, const gchar *name)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    g_printerr("=====[ %s (%dx%d) ]=====\n", name, xres, yres);
    for (guint i = 0; i < yres; i++) {
        for (guint j = 0; j < xres; j++) {
            g_printerr("% .*f ", digits, gwy_field_get_val(field, j, i));
        }
        g_printerr("\n");
    }
    g_printerr("\n");
}

G_GNUC_UNUSED
static void
dump_nield(GwyNield *nield, const gchar *name)
{
    gint xres = gwy_nield_get_xres(nield), yres = gwy_nield_get_yres(nield);
    g_printerr("=====[ %s (%dx%d) ]=====\n", name, xres, yres);
    const gint *d = gwy_nield_get_data_const(nield);
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint v = d[i*xres + j];
            if (v < 0)
                g_printerr("-");
            else if (v == 0)
                g_printerr(".");
            else if (v < 10)
                g_printerr("%c", '0' + v);
            else
                g_printerr("%c", 'a' + (v - 10));
        }
        g_printerr("\n");
    }
}

static void
assert_field_content(GwyField *field, const gdouble *ref_data, guint ref_n, gdouble eps)
{
    g_assert_true(GWY_IS_FIELD(field));
    guint xres = gwy_field_get_xres(field);
    guint yres = gwy_field_get_yres(field);
    g_assert_cmpuint(xres*yres, ==, ref_n);
    const gdouble *data = gwy_field_get_data_const(field);
    g_assert_nonnull(data);

    if (eps > 0.0) {
        for (guint i = 0; i < ref_n; i++)
            g_assert_cmpfloat_with_epsilon(data[i], ref_data[i], eps);
    }
    else {
        for (guint i = 0; i < ref_n; i++)
            g_assert_cmpfloat(data[i], ==, ref_data[i]);
    }
}

static void
assert_field_properties(GwyField *field, GwyField *reference)
{
    g_assert_cmpint(gwy_field_get_xres(field), ==, gwy_field_get_xres(reference));
    g_assert_cmpint(gwy_field_get_yres(field), ==, gwy_field_get_yres(reference));
    g_assert_cmpfloat(gwy_field_get_xreal(field), ==, gwy_field_get_xreal(reference));
    g_assert_cmpfloat(gwy_field_get_yreal(field), ==, gwy_field_get_yreal(reference));
    g_assert_cmpfloat(gwy_field_get_xoffset(field), ==, gwy_field_get_xoffset(reference));
    g_assert_cmpfloat(gwy_field_get_yoffset(field), ==, gwy_field_get_yoffset(reference));
    g_assert_true(gwy_unit_equal(gwy_field_get_unit_xy(field), gwy_field_get_unit_xy(reference)));
    g_assert_true(gwy_unit_equal(gwy_field_get_unit_z(field), gwy_field_get_unit_z(reference)));
}

void
field_assert_equal(GObject *object, GObject *reference)
{
    g_assert_true(GWY_IS_FIELD(object));
    g_assert_true(GWY_IS_FIELD(reference));

    GwyField *field = GWY_FIELD(object), *field_ref = GWY_FIELD(reference);
    assert_field_properties(field, field_ref);
    assert_field_content(field, gwy_field_get_data(field_ref),
                         gwy_field_get_xres(field_ref)*gwy_field_get_yres(field_ref),
                         0.0);
}

void
test_field_basic(void)
{
    const gdouble zeros[6] = { 0, 0, 0, 0, 0, 0 };
    const gdouble twos[6] = { 2, 2, 2, 2, 2, 2 };

    GwyField *field = gwy_field_new(2, 3, 1.6, 7.5, TRUE);
    assert_field_content(field, zeros, 6, 0.0);
    g_assert_cmpfloat(gwy_field_get_xreal(field), ==, 1.6);
    g_assert_cmpfloat(gwy_field_get_yreal(field), ==, 7.5);
    g_assert_cmpfloat(gwy_field_get_dx(field), ==, 1.6/2);
    g_assert_cmpfloat(gwy_field_get_dy(field), ==, 7.5/3);
    g_assert_cmpfloat(gwy_field_get_xoffset(field), ==, 0);
    g_assert_cmpfloat(gwy_field_get_yoffset(field), ==, 0);

    gwy_field_set_xreal(field, 31);
    gwy_field_set_yreal(field, 39);
    gwy_field_set_xoffset(field, -11);
    gwy_field_set_yoffset(field, 77);
    g_assert_cmpfloat(gwy_field_get_xreal(field), ==, 31.0);
    g_assert_cmpfloat(gwy_field_get_yreal(field), ==, 39.0);
    g_assert_cmpfloat(gwy_field_get_dx(field), ==, 31.0/2);
    g_assert_cmpfloat(gwy_field_get_dy(field), ==, 39.0/3);
    g_assert_cmpfloat(gwy_field_get_xoffset(field), ==, -11);
    g_assert_cmpfloat(gwy_field_get_yoffset(field), ==, 77);

    gwy_field_fill(field, 2.0);
    assert_field_content(field, twos, 6, 0.0);

    gwy_field_clear(field);
    assert_field_content(field, zeros, 6, 0.0);

    g_assert_cmpfloat(gwy_field_get_xreal(field), ==, 31);
    g_assert_cmpfloat(gwy_field_get_yreal(field), ==, 39);
    g_assert_cmpfloat(gwy_field_get_dx(field), ==, 31.0/2);
    g_assert_cmpfloat(gwy_field_get_dy(field), ==, 39.0/3);
    g_assert_cmpfloat(gwy_field_get_xoffset(field), ==, -11);
    g_assert_cmpfloat(gwy_field_get_yoffset(field), ==, 77);

    g_assert_finalize_object(field);
}

void
test_field_invalidate(void)
{
    guint xres = 4, yres = 5, n = xres*yres;
    GwyField *field = gwy_field_new(xres, yres, 1.6, 7.5, TRUE);

    gdouble *data = gwy_field_get_data(field);
    for (guint i = 0; i < n; i++)
        data[i] = i;

    /* This must recompute the mean (which was zero). */
    g_assert_cmpfloat_with_epsilon(gwy_field_mean(field), 0.5*(n-1), 0.5*n*DBL_EPSILON);

    for (guint i = 0; i < n; i++)
        data[i] = -3.0*i;

    /* We cannot assert that the mean is *not* recalculated. Recalculating it always is a valid behaviour! */
    gwy_field_invalidate(field);
    g_assert_cmpfloat_with_epsilon(gwy_field_mean(field), -1.5*(n-1), 0.5*n*DBL_EPSILON);

    g_assert_finalize_object(field);
}

void
test_field_data_changed(void)
{
    GwyField *field = gwy_field_new(1, 1, 1.0, 1.0, TRUE);
    guint item_changed = 0;
    g_signal_connect_swapped(field, "data-changed", G_CALLBACK(record_signal), &item_changed);
    gwy_field_data_changed(field);
    g_assert_cmpuint(item_changed, ==, 1);
    gwy_field_data_changed(field);
    g_assert_cmpuint(item_changed, ==, 2);
    g_assert_finalize_object(field);
}

static GwyField*
create_field_for_serialisation(void)
{
    GwyField *field = gwy_field_new(13, 7, 26.0, 1.6, FALSE);
    gwy_field_set_xoffset(field, G_LN2);
    gwy_field_set_yoffset(field, GWY_SQRT3);
    gdouble *data = gwy_field_get_data(field);
    for (guint i = 0; i < 13*7; i++)
        data[i] = sqrt(i) - 2*G_PI;

    return field;
}

static void
set_field_units_for_serialisation(GwyField *field)
{
    gwy_unit_set_from_string(gwy_field_get_unit_xy(field), "m");
    gwy_unit_set_from_string(gwy_field_get_unit_z(field), "V");
}

void
test_field_serialization(void)
{
    GwyField *field = create_field_for_serialisation();
    serialize_object_and_back(G_OBJECT(field), field_assert_equal, FALSE, NULL);

    set_field_units_for_serialisation(field);
    serialize_object_and_back(G_OBJECT(field), field_assert_equal, FALSE, NULL);

    g_assert_finalize_object(field);
}

void
test_field_copy(void)
{
    GwyField *field = create_field_for_serialisation();
    serializable_test_copy(GWY_SERIALIZABLE(field), field_assert_equal);

    set_field_units_for_serialisation(field);
    serializable_test_copy(GWY_SERIALIZABLE(field), field_assert_equal);

    g_assert_finalize_object(field);
}

void
test_field_assign(void)
{
    GwyField *field = create_field_for_serialisation();
    serializable_test_assign(GWY_SERIALIZABLE(field), NULL, field_assert_equal);

    set_field_units_for_serialisation(field);
    serializable_test_assign(GWY_SERIALIZABLE(field), NULL, field_assert_equal);

    GwyField *another = gwy_field_new(4, 5, 1.3, 8.0, TRUE);
    gwy_field_set_xoffset(another, -1.0);
    gwy_field_set_yoffset(another, 10.0);
    gwy_unit_set_from_string(gwy_field_get_unit_xy(another), "s");
    gwy_unit_set_from_string(gwy_field_get_unit_z(another), "Hz");
    serializable_test_assign(GWY_SERIALIZABLE(field), GWY_SERIALIZABLE(another), field_assert_equal);
    g_assert_finalize_object(another);

    g_assert_finalize_object(field);
}

void
test_field_resize(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    GwyField *field = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
    for (guint k = 0; k < n; k++) {
        gint width = g_test_rand_int_range(1, 12), height = g_test_rand_int_range(1, 12);
        gwy_field_resize(field, width, height);
        g_assert_cmpint(gwy_field_get_xres(field), ==, width);
        g_assert_cmpint(gwy_field_get_yres(field), ==, height);
        /* Try to crash if the memory is not allocated correctly. */
        gdouble *d = gwy_field_get_data(field);
        gwy_clear(d, width*height);
    }
    g_assert_finalize_object(field);
}

/* NB: Stepping stone.
 *
 * Assuming gwy_field_plane_level() is correct, we then use it to make all kinds of planar fields with known
 * properties. */
void
test_field_plane_level(void)
{
    enum { xres1 = 3, yres1 = 4 };
    GwyField *field1 = gwy_field_new(xres1, yres1, 1.0, 1.0, TRUE);
    gwy_field_plane_level(field1, 2.0, -G_PI, -G_LN2);
    const gdouble expected1[xres1*yres1] = {
        -2.0, -2.0 + G_PI, -2.0 + 2*G_PI,
        -2.0 + G_LN2, -2.0 + G_PI + G_LN2, -2.0 + 2*G_PI + G_LN2,
        -2.0 + 2*G_LN2, -2.0 + G_PI + 2*G_LN2, -2.0 + 2*G_PI + 2*G_LN2,
        -2.0 + 3*G_LN2, -2.0 + G_PI + 3*G_LN2, -2.0 + 2*G_PI + 3*G_LN2,
    };
    /* These should probably be exact. But a few ε is OK. */
    assert_field_content(field1, expected1, xres1*yres1, 4*DBL_EPSILON);
    g_assert_finalize_object(field1);

    enum { xres2 = 2, yres2 = 5 };
    GwyField *field2 = gwy_field_new(xres2, yres2, 2.0, 5.0, TRUE);
    gwy_field_plane_level(field2, -1.0, 1.25, 6.4);
    const gdouble expected2[xres2*yres2] = {
        1.0, 1.0 - 1.25,
        1.0 - 6.4, 1.0 - 1.25 - 6.4,
        1.0 - 2*6.4, 1.0 - 1.25 - 2*6.4,
        1.0 - 3*6.4, 1.0 - 1.25 - 3*6.4,
        1.0 - 4*6.4, 1.0 - 1.25 - 4*6.4,
    };
    /* These should probably be exact. But a few ε is OK. */
    assert_field_content(field2, expected2, xres2*yres2, 20*DBL_EPSILON);
    g_assert_finalize_object(field2);

    enum { xres3 = 6, yres3 = 1 };
    GwyField *field3 = gwy_field_new(xres3, yres3, 0.5, 2.0, TRUE);
    gwy_field_plane_level(field3, 0.5, -2.0, 1e14);
    const gdouble expected3[xres3*yres3] = {
        -0.5, -0.5 + 2.0, -0.5 + 2*2.0, -0.5 + 3*2.0, -0.5 + 4*2.0, -0.5 + 5*2.0,
    };
    /* These should probably be exact. But a few ε is OK. */
    assert_field_content(field3, expected3, xres3*yres3, 8*DBL_EPSILON);
    g_assert_finalize_object(field3);

    enum { xres4 = 1, yres4 = 5 };
    GwyField *field4 = gwy_field_new(xres4, yres4, 1.0, 4.0, TRUE);
    gwy_field_plane_level(field4, -0.5, -1e14, 2.0);
    const gdouble expected4[xres4*yres4] = {
        0.5,
        0.5 - 2.0,
        0.5 - 2*2.0,
        0.5 - 3*2.0,
        0.5 - 4*2.0,
    };
    /* These should probably be exact. But a few ε is OK. */
    assert_field_content(field4, expected4, xres4*yres4, 6*DBL_EPSILON);
    g_assert_finalize_object(field4);
}

static GwyField*
make_random_planar_field(gdouble *pa, gdouble *pbx, gdouble *pby)
{
    gint xres = g_test_rand_int_range(2, 20);
    gint yres = g_test_rand_int_range(2, 20);
    gdouble dx = exp(2*g_test_rand_double() - 1);
    gdouble dy = exp(2*g_test_rand_double() - 1);
    GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
    gdouble a = g_test_rand_double_range(-5.0, 5.0);
    gdouble bx = g_test_rand_double_range(-1.0, 1.0);
    gdouble by = g_test_rand_double_range(-1.0, 1.0);
    gwy_field_plane_level(field, -a, -bx, -by);

    if (pa)
        *pa = a;
    if (pbx)
        *pbx = bx;
    if (pby)
        *pby = by;

    return field;
}

static GwyField*
make_random_bilinear_field(gdouble *pa, gdouble *pbx, gdouble *pby, gdouble *pcxy)
{
    gint xres = g_test_rand_int_range(2, 20);
    gint yres = g_test_rand_int_range(2, 20);
    gdouble dx = exp(2*g_test_rand_double() - 1);
    gdouble dy = exp(2*g_test_rand_double() - 1);
    GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
    gdouble a = g_test_rand_double_range(-5.0, 5.0);
    gdouble bx = g_test_rand_double_range(-1.0, 1.0);
    gdouble by = g_test_rand_double_range(-1.0, 1.0);
    gdouble cxy = g_test_rand_double_range(-0.1, 0.1);

    gdouble *d = gwy_field_get_data(field);
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            d[i*xres + j] = a + bx*j + by*i + cxy*i*j;
    }

    if (pa)
        *pa = a;
    if (pbx)
        *pbx = bx;
    if (pby)
        *pby = by;
    if (pcxy)
        *pcxy = cxy;

    return field;
}

static void
add_noise_to_field(GwyField *field)
{
    gint n = gwy_field_get_xres(field)*gwy_field_get_yres(field);
    gdouble *d = gwy_field_get_data(field);
    gdouble s = exp(2*g_test_rand_double() - 1);
    for (gint k = 0; k < n; k++)
        d[k] = s*(0.1*d[k] + g_test_rand_double());
}

static GwyField*
make_random_noise_field(void)
{
    GwyField *field = make_random_planar_field(NULL, NULL, NULL);
    add_noise_to_field(field);
    return field;
}

static GwyField*
cut_smaller_field(GwyField *field, gint *pcol, gint *prow)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint smallxres = (xres <= 2 ? xres : g_test_rand_int_range(2, xres + 1));
    gint smallyres = (yres <= 2 ? yres : g_test_rand_int_range(2, yres + 1));
    gint col = g_test_rand_int_range(0, xres+1 - smallxres);
    gint row = g_test_rand_int_range(0, yres+1 - smallyres);
    GwyField *small = gwy_field_area_extract(field, col, row, smallxres, smallyres);
    if (pcol)
        *pcol = col;
    if (prow)
        *prow = row;
    return small;
}

void
test_field_stats_avg_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble expected = a + bx*(xres - 1.0)/2.0 + by*(yres - 1.0)/2.0;
        g_assert_cmpfloat_with_epsilon(gwy_field_mean(field), expected, 4*DBL_EPSILON*xres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_avg_bilinear(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by, cxy;
        GwyField *field = make_random_bilinear_field(&a, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint xm1 = xres - 1, ym1 = yres - 1;
        gdouble expected = a + bx*xm1/2.0 + by*ym1/2.0 + cxy*xm1*ym1/4.0;
        g_assert_cmpfloat_with_epsilon(gwy_field_mean(field), expected, 8*DBL_EPSILON*yres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_avg_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_mean(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                                       gwy_field_mean(small),
                                       2*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_min_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble expected = fmin(fmin(a, a + bx*(xres - 1)), fmin(a + by*(yres - 1), a + bx*(xres - 1) + by*(yres - 1)));
        g_assert_cmpfloat_with_epsilon(gwy_field_min(field), expected, 2*DBL_EPSILON*xres*yres);

        gwy_field_invalidate(field);
        gdouble min, max;
        gwy_field_min_max(field, &min, &max);
        g_assert_cmpfloat_with_epsilon(min, expected, 2*DBL_EPSILON*xres*yres);

        g_assert_finalize_object(field);
    }
}

void
test_field_stats_min_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_min(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                                       gwy_field_min(small),
                                       2*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_max_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble expected = fmax(fmax(a, a + bx*(xres - 1)), fmax(a + by*(yres - 1), a + bx*(xres - 1) + by*(yres - 1)));
        g_assert_cmpfloat_with_epsilon(gwy_field_max(field), expected, 2*DBL_EPSILON*xres*yres);

        gwy_field_invalidate(field);
        gdouble min, max;
        gwy_field_min_max(field, &min, &max);
        g_assert_cmpfloat_with_epsilon(max, expected, 2*DBL_EPSILON*xres*yres);

        g_assert_finalize_object(field);
    }
}

void
test_field_stats_max_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_max(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                                       gwy_field_max(small),
                                       2*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_min_max_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gdouble min = gwy_field_area_min(field, NULL, GWY_MASK_IGNORE, col, row, width, height);
        gwy_field_invalidate(field);
        gdouble max = gwy_field_area_max(field, NULL, GWY_MASK_IGNORE, col, row, width, height);
        gwy_field_invalidate(field);
        gdouble mmin, mmax;
        gwy_field_area_min_max(field, NULL, GWY_MASK_IGNORE, col, row, width, height, &mmin, &mmax);
        g_assert_cmpfloat_with_epsilon(min, mmin, 2*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, mmax, 2*DBL_EPSILON);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_min_max_masked(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyNield *mask = make_random_mask(xres, yres, TRUE, FALSE, 3);
        GwyMaskingType masking;
        if (g_test_rand_int_range(0, 2))
            masking = GWY_MASK_INCLUDE;
        else
            masking = GWY_MASK_EXCLUDE;
        gdouble min = gwy_field_area_min(field, mask, masking, col, row, width, height);
        gwy_field_invalidate(field);
        gdouble max = gwy_field_area_max(field, mask, masking, col, row, width, height);
        gwy_field_invalidate(field);
        gdouble mmin, mmax;
        gwy_field_area_min_max(field, mask, masking, col, row, width, height, &mmin, &mmax);
        g_assert_cmpfloat_with_epsilon(min, mmin, 2*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, mmax, 2*DBL_EPSILON);
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_median_full(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint npts = gwy_field_get_xres(field)*gwy_field_get_yres(field);
        gdouble *d = g_new(gdouble, npts);
        gwy_assign(d, gwy_field_get_data_const(field), npts);
        qsort(d, npts, sizeof(gdouble), gwy_compare_double);
        gdouble expected = d[npts/2];
        g_assert_cmpfloat(gwy_field_median(field), ==, expected);
        g_free(d);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_median_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat(gwy_field_area_median(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                          ==,
                          gwy_field_median(small));
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_sum_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble expected = xres*yres*(a + bx*(xres - 1.0)/2.0 + by*(yres - 1.0)/2.0);
        g_assert_cmpfloat_with_epsilon(gwy_field_sum(field), expected, 2*DBL_EPSILON*xres*xres*yres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_sum_bilinear(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by, cxy;
        GwyField *field = make_random_bilinear_field(&a, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint xm1 = xres - 1, ym1 = yres - 1;
        gdouble expected = xres*yres*(a + bx*xm1/2.0 + by*ym1/2.0 + cxy*xm1*ym1/4.0);
        g_assert_cmpfloat_with_epsilon(gwy_field_sum(field), expected, 4*DBL_EPSILON*xres*xres*yres*yres);
        g_assert_finalize_object(field);
    }
}

/* FIXME: With CLANG fast math, even a simple summation like here can give differences of the order 100ε, not a few ε.
 * Is it too much? It behaves more like accumulating bad roundings, instead of them cancelling out on average and the
 * error growing only with √N. */
void
test_field_stats_sum_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_sum(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                                       gwy_field_sum(small),
                                       4*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_mean_square_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint xm1 = xres - 1, ym1 = yres - 1;
        gdouble x6 = (2*xres - 1.0)/6.0, y6 = (2*yres - 1.0)/6.0;
        gdouble expected = (a*a + bx*bx*x6*xm1 + by*by*y6*ym1 + a*bx*xm1 + a*by*ym1 + bx*by*0.5*xm1*ym1);
        g_assert_cmpfloat_with_epsilon(gwy_field_mean_square(field), expected,
                                       4*DBL_EPSILON*xres*xres*yres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_mean_square_bilinear(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by, cxy;
        GwyField *field = make_random_bilinear_field(&a, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint xm1 = xres - 1, ym1 = yres - 1;
        gdouble x6 = (2*xres - 1.0)/6.0, y6 = (2*yres - 1.0)/6.0;
        gdouble expected = (a*a + bx*bx*xm1*x6 + by*by*ym1*y6 + cxy*cxy*xm1*x6*ym1*y6
                            + a*bx*xm1 + a*by*ym1 + (bx*by + a*cxy)*0.5*xm1*ym1
                            + bx*cxy*xm1*x6*ym1 + by*cxy*xm1*ym1*y6);
        g_assert_cmpfloat_with_epsilon(gwy_field_mean_square(field), expected, 4*DBL_EPSILON*xres*xres*yres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_mean_square_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_mean_square(field, NULL, GWY_MASK_IGNORE,
                                                                  col, row, width, height),
                                       gwy_field_mean_square(small),
                                       2*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_rms_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble bx, by;
        GwyField *field = make_random_planar_field(NULL, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint npix = xres*yres;
        gint x2m1 = xres*xres - 1, y2m1 = yres*yres - 1;
        gdouble expected = sqrt(bx*bx*x2m1/12.0 + by*by*y2m1/12.0);
        expected *= sqrt(npix/(npix - 1.0));   // Degrees of freedom
        g_assert_cmpfloat_with_epsilon(gwy_field_rms(field), expected, 2*DBL_EPSILON*npix);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_rms_bilinear(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble bx, by, cxy;
        GwyField *field = make_random_bilinear_field(NULL, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint npix = xres*yres;
        gint xm1 = xres - 1, ym1 = yres - 1;
        gint x2m1 = xres*xres - 1, y2m1 = yres*yres - 1;
        gdouble expected = sqrt(bx*bx*x2m1/12.0 + by*by*y2m1/12.0
                                + bx*cxy*x2m1*ym1/12.0 + by*cxy*xm1*y2m1/12.0
                                + cxy*cxy*xm1*ym1*(7.0*xres*yres + xres + yres - 5.0)/144.0);
        expected *= sqrt(npix/(npix - 1.0));   // Degrees of freedom
        g_assert_cmpfloat_with_epsilon(gwy_field_rms(field), expected, 2*DBL_EPSILON*npix);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_rms_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        g_assert_cmpfloat_with_epsilon(gwy_field_area_rms(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                                       gwy_field_rms(small),
                                       2*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

// There is no (sane) explicit formula for Ra because z - mean(z) changes sign along an arbitary straight line.
// So we are again doing a summation, except using an explicit formula for values, not reading any pixel data.
static gdouble
expected_ra_for_planar_field(gint xres, gint yres, gdouble bx, gdouble by)
{
    gdouble s = 0.0, cj = 0.5*(xres - 1), ci = 0.5*(yres - 1);
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            s += fabs((i - ci)*by + (j - cj)*bx);
    }
    gint n = xres*yres;
    gdouble corr = pow(n/(n - 1.0), 0.62);
    return n > 1 ? s/n*corr : 0;
}

void
test_field_stats_stats_planar(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint npix = xres*yres;
        gint xm1 = xres - 1, ym1 = yres - 1;
        gint x2m1 = xres*xres - 1, y2m1 = yres*yres - 1;
        gdouble tx = bx*bx*x2m1, ty = by*by*y2m1;
        gdouble expected_avg = a + bx*xm1/2.0 + by*ym1/2.0;
        gdouble m2 = (tx + ty)/12.0;
        gdouble expected_rms = sqrt(m2) * sqrt(npix/(npix - 1.0)); // Degrees of freedom
        gdouble expected_ra = expected_ra_for_planar_field(xres, yres, bx, by);
        gdouble m4 = (bx*bx*tx*(xres*xres - 7.0/3.0) + by*by*ty*(yres*yres - 7.0/3.0))/80.0 + tx*ty/24.0;
        gdouble expected_skew = 0.0;
        gdouble expected_kurtosis = m4/(m2*m2) - 3.0;
        gdouble avg, rms, ra, skew, kurtosis;
        gwy_field_stats(field, &avg, &ra, &rms, &skew, &kurtosis);
        g_assert_cmpfloat_with_epsilon(avg, expected_avg, 4*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(rms, expected_rms, 4*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(ra, expected_ra, 4*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(skew, expected_skew, 40*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(kurtosis, expected_kurtosis, 50*DBL_EPSILON*xres*yres);

        // This should sucessfully do nothing.
        gwy_field_stats(field, NULL, NULL, NULL, NULL, NULL);

        gwy_field_invalidate(field);
        gwy_field_stats(field, &avg, NULL, NULL, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(avg, expected_avg, 4*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, &ra, NULL, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(ra, expected_ra, 4*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, NULL, &rms, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(rms, expected_rms, 4*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, NULL, NULL, &skew, NULL);
        g_assert_cmpfloat_with_epsilon(skew, expected_skew, 40*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, NULL, NULL, NULL, &kurtosis);
        g_assert_cmpfloat_with_epsilon(kurtosis, expected_kurtosis, 50*DBL_EPSILON*xres*yres);

        g_assert_finalize_object(field);
    }
}

// There is no (sane) explicit formula for Ra because z - mean(z) changes sign along an arbitary straight line.
// So we are again doing a summation, except using an explicit formula for values, not reading any pixel data.
static gdouble
expected_ra_for_bilinear_field(gint xres, gint yres, gdouble bx, gdouble by, gdouble cxy)
{
    gdouble s = 0.0, cj = 0.5*(xres - 1), ci = 0.5*(yres - 1);
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            s += fabs((i - ci)*by + (j - cj)*bx + (i*j - ci*cj)*cxy);
    }
    gint n = xres*yres;
    gdouble corr = pow(n/(n - 1.0), 0.62);
    return n > 1 ? s/n*corr : 0;
}

void
test_field_stats_stats_bilinear(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by, cxy;
        GwyField *field = make_random_bilinear_field(&a, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint npix = xres*yres;
        gint xm1 = xres - 1, ym1 = yres - 1;
        gint x2m1 = xres*xres - 1, y2m1 = yres*yres - 1;
        gdouble expected_avg = a + bx*xm1/2.0 + by*ym1/2.0 + cxy*xm1*ym1/4.0;
        gdouble m2 = (bx*bx*x2m1/12.0 + by*by*y2m1/12.0
                      + bx*cxy*x2m1*ym1/12.0 + by*cxy*xm1*y2m1/12.0
                      + cxy*cxy*xm1*ym1*(7.0*xres*yres + xres + yres - 5.0)/144.0);
        gdouble m3 = cxy*x2m1*y2m1*(bx*cxy*xm1/2.0 + by*cxy*ym1/2.0 + bx*by + cxy*cxy*xm1*ym1/4.0)/24.0;
        /*
        gdouble m4b = (bx*bx*bx*bx*x2m1*(3.0*xres*xres - 7.0)/240.0
                       + bx*bx*by*by*x2m1*y2m1/24.0
                       + by*by*by*by*y2m1*(3.0*yres*yres - 7.0)/240.0);
        gdouble m4x = bx*cxy*(bx*bx*x2m1*(3.0*xres*xres - 7.0)*ym1
                              + bx*cxy*x2m1*ym1*(17.0*xres*xres*yres - 10.0*xres*yres - xres*xres
                                                 - 23.0*yres - 10.0*xres + 19.0)/4.0
                              + cxy*cxy*x2m1*ym1*ym1*(11.0*xres*xres*yres - 10.0*xres*yres + 5*xres*xres
                                                      - 9.0*yres - 10.0*xres + 5.0)/4.0)/120.0;
        gdouble m4y = by*cxy*(by*by*y2m1*(3.0*yres*yres - 7.0)*ym1
                              + by*cxy*y2m1*xm1*(17.0*yres*yres*xres - 10.0*yres*xres - yres*yres
                                                 - 23.0*xres - 10.0*yres + 19.0)/4.0
                              + cxy*cxy*y2m1*xm1*xm1*(11.0*yres*yres*xres - 10.0*yres*xres + 5*yres*yres
                                                      - 9.0*xres - 10.0*yres + 5.0)/4.0)/120.0;
        gdouble m4c = cxy*cxy*cxy*cxy*xm1*ym1*(429.0*xres*xres*xres*yres*yres*yres
                                               - 231.0*xres*xres*xres*yres*yres - 231.0*xres*xres*yres*yres*yres
                                               - 441.0*xres*xres*xres*yres
                                               - 291.0*xres*xres*yres*yres
                                               - 441.0*xres*yres*yres*yres
                                               - 141.0*xres*xres*xres + 699.0*xres*xres*yres
                                               + 699.0*xres*yres*yres - 141.0*yres*yres*yres
                                               + 399.0*xres*xres - 11.0*xres*yres + 399.0*yres*yres
                                               - 311.0*yres - 311.0*xres - 11.0)/57600.0;
        gdouble m4 = m4b + m4x + m4y + m4c;
        */
        gdouble expected_rms = sqrt(m2) * sqrt(npix/(npix - 1.0)); // Degrees of freedom
        gdouble expected_skew = m3/sqrt(m2*m2*m2);
        //gdouble expected_kurtosis = m4/(m2*m2);
        gdouble expected_ra = expected_ra_for_bilinear_field(xres, yres, bx, by, cxy);
        gdouble avg, rms, ra, skew, kurtosis;
        gwy_field_stats(field, &avg, &ra, &rms, &skew, &kurtosis);
        g_assert_cmpfloat_with_epsilon(avg, expected_avg, 6*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(rms, expected_rms, 6*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(ra, expected_ra, 4*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat_with_epsilon(skew, expected_skew, 60*DBL_EPSILON*xres*yres);
        //g_assert_cmpfloat_with_epsilon(kurtosis, expected_kurtosis, 50*DBL_EPSILON*xres*yres);

        // This should sucessfully do nothing.
        gwy_field_stats(field, NULL, NULL, NULL, NULL, NULL);

        gwy_field_invalidate(field);
        gwy_field_stats(field, &avg, NULL, NULL, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(avg, expected_avg, 8*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, &ra, NULL, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(ra, expected_ra, 8*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, NULL, &rms, NULL, NULL);
        g_assert_cmpfloat_with_epsilon(rms, expected_rms, 8*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        gwy_field_stats(field, NULL, NULL, NULL, &skew, NULL);
        g_assert_cmpfloat_with_epsilon(skew, expected_skew, 60*DBL_EPSILON*xres*yres);
        gwy_field_invalidate(field);
        //gwy_field_stats(field, NULL, NULL, NULL, NULL, &kurtosis);
        //g_assert_cmpfloat_with_epsilon(kurtosis, expected_kurtosis, 50*DBL_EPSILON*xres*yres);

        g_assert_finalize_object(field);
    }
}

void
test_field_stats_stats_area(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint col, row;
        GwyField *small = cut_smaller_field(field, &col, &row);
        gint width = gwy_field_get_xres(small), height = gwy_field_get_yres(small);
        gdouble avg, rms, ra, skew, kurtosis;
        gwy_field_area_stats(field, NULL, GWY_MASK_IGNORE, col, row, width, height,
                             &avg, &ra, &rms, &skew, &kurtosis);
        gdouble small_avg, small_rms, small_ra, small_skew, small_kurtosis;
        gwy_field_stats(small, &small_avg, &small_ra, &small_rms, &small_skew, &small_kurtosis);
        g_assert_cmpfloat_with_epsilon(avg, small_avg, 2*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(ra, small_ra, 2*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(rms, small_rms, 2*DBL_EPSILON);
        /* XXX: These tolerances were eyeballed and may be too small. */
        g_assert_cmpfloat_with_epsilon(skew, small_skew, 6*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(kurtosis, small_kurtosis, 10*DBL_EPSILON);
        g_assert_finalize_object(small);
        g_assert_finalize_object(field);
    }
}

static void
test_field_stats_area_common(gboolean make_pixels_square)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
        gdouble dA = dx*dy;
        if (make_pixels_square) {
            dx = dy = sqrt(dA);
            gwy_field_set_xreal(field, dx*xres);
            gwy_field_set_yreal(field, dy*yres);
        }
        gdouble rbx = bx/dx, rby = by/dy; // plane level uses per-pixels slopes!
        // The calculation uses mirror boundary extrapolation, meaning image edges are flatter than inside.
        // It is one of possible definitions, but check that it does what we expect.
        gdouble expected_inner = (xres - 1)*(yres - 1)*dA*sqrt(1 + rbx*rbx + rby*rby);
        gdouble expected_hedges = (xres - 1)*dA*sqrt(1 + rbx*rbx);
        gdouble expected_vedges = (yres - 1)*dA*sqrt(1 + rby*rby);
        gdouble expected_corners = dA;
        gdouble expected = expected_inner + expected_hedges + expected_vedges + expected_corners;
        g_assert_cmpfloat_with_epsilon(gwy_field_surface_area(field), expected,
                                       DBL_EPSILON*xres*yres*expected);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_area_square(void)
{
    test_field_stats_area_common(TRUE);
}

void
test_field_stats_area_nonsquare(void)
{
    test_field_stats_area_common(FALSE);
}

void
test_field_stats_variation_common(gboolean make_pixels_square)
{
    guint n = g_test_slow() ? 10000 : 1000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
        gdouble dA = dx*dy;
        if (make_pixels_square) {
            dx = dy = sqrt(dA);
            gwy_field_set_xreal(field, dx*xres);
            gwy_field_set_yreal(field, dy*yres);
        }
        gdouble rbx = bx/dx, rby = by/dy; // plane level uses per-pixels slopes!
        // The calculation uses mirror boundary extrapolation, meaning image edges are flatter than inside.
        // It is one of possible definitions, but check that it does what we expect.
        gdouble expected_inner = (xres - 1)*(yres - 1)*dA*sqrt(rbx*rbx + rby*rby);
        gdouble expected_hedges = (xres - 1)*dA*fabs(rbx);
        gdouble expected_vedges = (yres - 1)*dA*fabs(rby);
        gdouble expected_corners = 0.0;
        gdouble expected = expected_inner + expected_hedges + expected_vedges + expected_corners;
        g_assert_cmpfloat_with_epsilon(gwy_field_variation(field), expected,
                                       4*DBL_EPSILON*xres*yres*expected);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_variation_square(void)
{
    test_field_stats_variation_common(TRUE);
}

void
test_field_stats_variation_nonsquare(void)
{
    test_field_stats_variation_common(FALSE);
}

void
test_field_stats_volume_constant(void)
{
    static GwyFieldVolumeMethod methods[] = {
        GWY_FIELD_VOLUME_DEFAULT,
        GWY_FIELD_VOLUME_GWYDDION2,
        GWY_FIELD_VOLUME_TRIANGULAR,
        GWY_FIELD_VOLUME_BILINEAR,
        GWY_FIELD_VOLUME_BIQUADRATIC,
    };
    guint n = 100;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(3, 20);
        gint yres = g_test_rand_int_range(3, 20);
        gdouble dx = 0.5 + g_test_rand_double();
        gdouble dy = 0.5 + g_test_rand_double();
        gdouble c = 2*g_test_rand_double() - 1.0;
        GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, FALSE);
        gwy_field_fill(field, c);
        GwyNield *mask = make_random_mask(xres, yres, FALSE, FALSE, 1);
        guint npix = gwy_nield_count(mask);

        for (guint k = 0; k < G_N_ELEMENTS(methods); k++) {
            gdouble expected = dx*dy*c*npix;
            gdouble volume = gwy_field_area_volume(field, mask, GWY_MASK_INCLUDE, methods[k], 0, 0, xres, yres);
            // The error seems to get pretty large. Not sure why as there should be no cancellation whatsoever.
            g_assert_cmpfloat_with_epsilon(volume, expected, 60*xres*yres*DBL_EPSILON);

            expected = dx*dy*c*(xres*yres - npix);
            volume = gwy_field_area_volume(field, mask, GWY_MASK_EXCLUDE, methods[k], 0, 0, xres, yres);
            g_assert_cmpfloat_with_epsilon(volume, expected, 60*xres*yres*DBL_EPSILON);
        }
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_volume_planar(void)
{
    static GwyFieldVolumeMethod methods[] = {
        GWY_FIELD_VOLUME_DEFAULT,
        GWY_FIELD_VOLUME_GWYDDION2,
        GWY_FIELD_VOLUME_TRIANGULAR,
        GWY_FIELD_VOLUME_BILINEAR,
        GWY_FIELD_VOLUME_BIQUADRATIC,
    };
    guint n = g_test_slow() ? 1000 : 10000;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        if (xres < 3 || yres < 3) {
            g_assert_finalize_object(field);
            continue;
        }

        gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
        gdouble zavg = a + bx*(xres - 1.0)/2.0 + by*(yres - 1.0)/2.0;
        gdouble expected = (xres - 2)*dx * (yres - 2)*dy * zavg;
        for (guint k = 0; k < G_N_ELEMENTS(methods); k++) {
            gdouble volume = gwy_field_area_volume(field, NULL, GWY_MASK_IGNORE, methods[k], 1, 1, xres-2, yres-2);
            g_assert_cmpfloat_with_epsilon(volume, expected, 5e3*(fabs(expected) + 1)*DBL_EPSILON);
        }
        g_assert_finalize_object(field);
    }
}

static void
test_field_stats_masked_common(MaskedStatFunc calc_stat, ConstantDataFunc const_value)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyNield *nield = make_random_mask(xres, yres, TRUE, FALSE, 3);

        gdouble ref_incl = calc_stat(field, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres);
        gdouble ref_excl = calc_stat(field, nield, GWY_MASK_EXCLUDE, 0, 0, xres, yres);
        gdouble ref_area_incl = calc_stat(field, nield, GWY_MASK_INCLUDE, col, row, width, height);
        gdouble ref_area_excl = calc_stat(field, nield, GWY_MASK_EXCLUDE, col, row, width, height);

        gdouble a = g_test_rand_double(), b = g_test_rand_double();

        /* Test that we get the known value from the constant-filled regions and the complement is unaffected.
         * This does not test whether the reported value is actually right; only that it is calculated consistently
         * with respect to masking.
         *
         * g_assert_cmpfloat_with_epsilon() has a strict inequality so we have to fudge it to get 0 == 0 succeed.
         * Also some of the expected values can be G_MAXDOUBLE (when n = 0), so be careful with multiplications. */
        gint a_nfull = gwy_nield_area_count(nield, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres);
        gint a_narea = gwy_nield_area_count(nield, NULL, GWY_MASK_IGNORE, col, row, width, height);
        GwyField *repl_a = gwy_field_copy(field);
        gwy_field_area_fill(repl_a, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres, a);
        gdouble full_a = const_value(a, a_nfull), area_a = const_value(a, a_narea);
        gdouble a_incl = calc_stat(repl_a, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres);
        gdouble a_excl = calc_stat(repl_a, nield, GWY_MASK_EXCLUDE, 0, 0, xres, yres);
        gdouble a_area_incl = calc_stat(repl_a, nield, GWY_MASK_INCLUDE, col, row, width, height);
        gdouble a_area_excl = calc_stat(repl_a, nield, GWY_MASK_EXCLUDE, col, row, width, height);
        g_assert_cmpfloat_with_epsilon(a_incl, full_a, fabs(full_a)*DBL_EPSILON*50 + 30*DBL_EPSILON);
        g_assert_cmpfloat(a_excl, ==, ref_excl);
        g_assert_cmpfloat_with_epsilon(a_area_incl, area_a, fabs(area_a)*DBL_EPSILON*50 + 30*DBL_EPSILON);
        g_assert_cmpfloat(a_area_excl, ==, ref_area_excl);
        g_assert_finalize_object(repl_a);

        gint b_nfull = xres*yres - a_nfull, b_narea = width*height - a_narea;
        GwyField *repl_b = gwy_field_copy(field);
        gwy_field_area_fill(repl_b, nield, GWY_MASK_EXCLUDE, 0, 0, xres, yres, b);
        gdouble full_b = const_value(b, b_nfull), area_b = const_value(b, b_narea);
        gdouble b_incl = calc_stat(repl_b, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres);
        gdouble b_excl = calc_stat(repl_b, nield, GWY_MASK_EXCLUDE, 0, 0, xres, yres);
        gdouble b_area_incl = calc_stat(repl_b, nield, GWY_MASK_INCLUDE, col, row, width, height);
        gdouble b_area_excl = calc_stat(repl_b, nield, GWY_MASK_EXCLUDE, col, row, width, height);
        g_assert_cmpfloat(b_incl, ==, ref_incl);
        g_assert_cmpfloat_with_epsilon(b_excl, full_b, fabs(full_b)*DBL_EPSILON*50 + 30*DBL_EPSILON);
        g_assert_cmpfloat(b_area_incl, ==, ref_area_incl);
        g_assert_cmpfloat_with_epsilon(b_area_excl, area_b, fabs(area_b)*DBL_EPSILON*50 + 30*DBL_EPSILON);
        g_assert_finalize_object(repl_b);

        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

static gdouble
min_for_const(gdouble c, gsize n)
{
    return n ? c : G_MAXDOUBLE;
}

void
test_field_stats_min_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_min, min_for_const);
}

static gdouble
max_for_const(gdouble c, gsize n)
{
    return n ? c : -G_MAXDOUBLE;
}

void
test_field_stats_max_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_max, max_for_const);
}

static gdouble
avg_for_const(gdouble c, gsize n)
{
    return n ? c : 0.0;
}

void
test_field_stats_avg_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_mean, avg_for_const);
}

static gdouble
sum_for_const(gdouble c, gsize n)
{
    return n*c;
}

void
test_field_stats_sum_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_sum, sum_for_const);
}

static gdouble
rms_for_const(G_GNUC_UNUSED gdouble c, G_GNUC_UNUSED gsize n)
{
    return 0.0;
}

void
test_field_stats_rms_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_rms, rms_for_const);
}

static gdouble
mean_square_for_const(gdouble c, gsize n)
{
    return n ? c*c : 0.0;
}

void
test_field_stats_mean_square_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_mean_square, mean_square_for_const);
}

static gdouble
median_for_const(gdouble c, gsize n)
{
    return n ? c : 0.0;
}

void
test_field_stats_median_masked(void)
{
    test_field_stats_masked_common(gwy_field_area_median, median_for_const);
}

static void
test_field_stats_minkowski_black_white_auto(GwyMinkowskiFunctional functional)
{
    guint n = g_test_slow() ? 200 : 20;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        add_noise_to_field(field);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        GwyNield *mask = make_random_mask(xres, yres, TRUE, FALSE, 3);
        GwyMaskingType masking;
        if (g_test_rand_int_range(0, 3) == 0)
            masking = GWY_MASK_INCLUDE;
        else if (g_test_rand_int_range(0, 2) == 0)
            masking = GWY_MASK_EXCLUDE;
        else
            masking = GWY_MASK_IGNORE;

        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyLine *line = gwy_field_area_minkowski(field, mask, masking, col, row, width, height,
                                                 functional, -1, G_MAXDOUBLE, -G_MAXDOUBLE);

        guint npixels = width*height;
        if (masking == GWY_MASK_INCLUDE)
            npixels = gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);
        else if (masking == GWY_MASK_EXCLUDE)
            npixels = width*height - gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);

        if (!npixels) {
            // The no-data result should be a single-pixel and cleared line. Do not check anything beyond that.
            g_assert_cmpint(gwy_line_get_res(line), ==, 1);
            g_assert_cmpfloat(gwy_line_min(line), ==, 0.0);
            g_assert_cmpfloat(gwy_line_max(line), ==, 0.0);
        }
        else {
            GwyField *farea = gwy_field_area_extract(field, col, row, width, height);
            GwyNield *marea = gwy_nield_area_extract(mask, col, row, width, height);
            GwyNield *grains = gwy_field_new_nield_alike(farea);

            gint nlevels = gwy_line_get_res(line);
            gdouble off = gwy_line_get_offset(line), step = gwy_line_get_dx(line);

            for (gint k = 0; k < nlevels; k++) {
                gdouble z = off + (k + 0.5)*step;
                if (functional == GWY_MINKOWSKI_CONNECTIVITY) {
                    gwy_nield_mark_by_threshold(grains, farea, z, TRUE);
                    if (masking == GWY_MASK_INCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_INTERSECTION);
                    else if (masking == GWY_MASK_EXCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_SUBTRACT);
                    gint ngrains_white = gwy_nield_number_contiguous(grains);

                    gwy_nield_mark_by_threshold(grains, farea, z, FALSE);
                    if (masking == GWY_MASK_INCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_INTERSECTION);
                    else if (masking == GWY_MASK_EXCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_SUBTRACT);
                    gint ngrains_black = gwy_nield_number_contiguous(grains);

                    g_assert_cmpfloat_with_epsilon(gwy_line_get_val(line, k),
                                                   (ngrains_white - ngrains_black)/(gdouble)npixels,
                                                   2*DBL_EPSILON);
                }
                else {
                    gwy_nield_mark_by_threshold(grains, farea, z, functional == GWY_MINKOWSKI_WHITE);
                    if (masking == GWY_MASK_INCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_INTERSECTION);
                    else if (masking == GWY_MASK_EXCLUDE)
                        gwy_nield_merge(grains, marea, GWY_MERGE_SUBTRACT);
                    gint ngrains = gwy_nield_number_contiguous(grains);
                    g_assert_cmpfloat_with_epsilon(gwy_line_get_val(line, k), ngrains/(gdouble)npixels, 2*DBL_EPSILON);
                }
            }

            g_assert_finalize_object(grains);
            g_assert_finalize_object(marea);
            g_assert_finalize_object(farea);
        }

        g_assert_finalize_object(line);
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_minkowski_black_auto(void)
{
    test_field_stats_minkowski_black_white_auto(GWY_MINKOWSKI_BLACK);
}

void
test_field_stats_minkowski_white_auto(void)
{
    test_field_stats_minkowski_black_white_auto(GWY_MINKOWSKI_WHITE);
}

void
test_field_stats_minkowski_connectivity_auto(void)
{
    test_field_stats_minkowski_black_white_auto(GWY_MINKOWSKI_CONNECTIVITY);
}

void
test_field_fit_plane_random(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble fita, fitbx, fitby;
        gwy_field_fit_plane(field, &fita, &fitbx, &fitby);
        gdouble s = fmax(fabs(a), fmax(fabs(bx), fabs(by)));
        g_assert_cmpfloat_with_epsilon(fita, a, 4*DBL_EPSILON*xres*yres*s);
        g_assert_cmpfloat_with_epsilon(fitbx, bx, DBL_EPSILON*xres*yres*s);
        g_assert_cmpfloat_with_epsilon(fitby, by, DBL_EPSILON*xres*yres*s);
        gwy_field_plane_level(field, a, bx, by);
        g_assert_cmpfloat(fabs(gwy_field_min(field)), <, DBL_EPSILON*xres*yres);
        g_assert_cmpfloat(fabs(gwy_field_max(field)), <, DBL_EPSILON*xres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_fit_plane_degenerate(void)
{
    gdouble fita, fitbx, fitby;

    const double data1[3] = { 3.0, 2.0, 1.0 };
    GwyField *field1 = gwy_field_new(3, 1, 1.0, 1.0, FALSE);
    gwy_assign(gwy_field_get_data(field1), data1, 3);
    gwy_field_fit_plane(field1, &fita, &fitbx, &fitby);
    g_assert_cmpfloat_with_epsilon(fita, 3.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, -1.0, 6*DBL_EPSILON);
    g_assert_cmpfloat(fitby, ==, 0.0);
    g_assert_finalize_object(field1);

    const double data2[2] = { -1.0, 1.0 };
    GwyField *field2 = gwy_field_new(1, 2, 1.0, 1.0, FALSE);
    gwy_assign(gwy_field_get_data(field2), data2, 2);
    gwy_field_fit_plane(field2, &fita, &fitbx, &fitby);
    g_assert_cmpfloat_with_epsilon(fita, -1.0, 4*DBL_EPSILON);
    g_assert_cmpfloat(fitbx, ==, 0.0);
    g_assert_cmpfloat_with_epsilon(fitby, 2.0, 4*DBL_EPSILON);
    g_assert_finalize_object(field2);
}

void
test_field_fit_plane_area_basic(void)
{
    gdouble fita, fitbx, fitby;
    gint rank;
    const gdouble data[12] = {
        7.5, 7.5, 7.5, 7.5,
        7.5, 7.5, 3.1, 4.1,
        7.5, 7.5, 1.1, 4.1,
    };

    GwyField *field = gwy_field_new(4, 3, 1.0, 1.0, FALSE);
    gwy_assign(gwy_field_get_data(field), data, 12);

    /* Here we mainly check that the constant coefficient is with respect to the area's top left corner. */
    rank = gwy_field_area_fit_plane(field, NULL, GWY_MASK_IGNORE, 2, 1, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 3);
    g_assert_cmpfloat_with_epsilon(fita, 2.6, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 2.0, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -1.0, 10*DBL_EPSILON);

    GwyNield *mask = gwy_nield_new(4, 3);
    gwy_nield_fill(mask, 1);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 2, 1, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 3);
    g_assert_cmpfloat_with_epsilon(fita, 2.6, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 2.0, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -1.0, 10*DBL_EPSILON);

    gwy_nield_set_val(mask, 3, 2, 0);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 2, 1, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 3);
    g_assert_cmpfloat_with_epsilon(fita, 3.1, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 1.0, 10*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -2.0, 10*DBL_EPSILON);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);
}

void
test_field_fit_plane_area_degenerate_2x2(void)
{
    gdouble fita, fitbx, fitby;
    gint rank;
    const gdouble data[4] = {
        3.0,  1.0,
        -1.0, 2.0,
    };

    GwyField *field = gwy_field_new(2, 2, 1.0, 1.0, FALSE);
    gwy_assign(gwy_field_get_data(field), data, 4);

    GwyNield *mask = gwy_nield_new(2, 2);

    /* Column 0 */
    gwy_nield_clear(mask);
    gwy_nield_set_val(mask, 0, 0, 1);
    gwy_nield_set_val(mask, 0, 1, 1);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, 3.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 0.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -4.0, 6*DBL_EPSILON);

    /* Column 1 */
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_EXCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, 1.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 0.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, 1.0, 6*DBL_EPSILON);

    /* Row 0 */
    gwy_nield_clear(mask);
    gwy_nield_set_val(mask, 0, 1, 1);
    gwy_nield_set_val(mask, 1, 1, 1);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_EXCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, 3.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, -2.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, 0.0, 6*DBL_EPSILON);

    /* Row 1 */
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, -1.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 3.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, 0.0, 6*DBL_EPSILON);

    /* Major diagonal */
    gwy_nield_clear(mask);
    gwy_nield_set_val(mask, 0, 0, 1);
    gwy_nield_set_val(mask, 1, 1, 1);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, 3.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, -0.5, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -0.5, 6*DBL_EPSILON);

    /* Minor diagonal (the constant is for area's top left corner!) */
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_EXCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 2);
    g_assert_cmpfloat_with_epsilon(fita, 0.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitbx, 1.0, 6*DBL_EPSILON);
    g_assert_cmpfloat_with_epsilon(fitby, -1.0, 6*DBL_EPSILON);

    /* Single pixel */
    gwy_nield_clear(mask);
    gwy_nield_set_val(mask, 0, 1, 1);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 1);
    g_assert_cmpfloat_with_epsilon(fita, -1.0, 6*DBL_EPSILON);
    g_assert_cmpfloat(fitbx, ==, 0.0);
    g_assert_cmpfloat(fitby, ==, 0.0);

    /* Nothing */
    gwy_nield_clear(mask);
    rank = gwy_field_area_fit_plane(field, mask, GWY_MASK_INCLUDE, 0, 0, 2, 2, &fita, &fitbx, &fitby);
    g_assert_cmpint(rank, ==, 0);
    g_assert_cmpfloat(fita, ==, 0.0);
    g_assert_cmpfloat(fitbx, ==, 0.0);
    g_assert_cmpfloat(fitby, ==, 0.0);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);
}

void
test_field_fit_poly_linear(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint term_powers[6] = { 0, 0, 1, 0, 0, 1 };
        gdouble coeffs[3];
        gwy_field_fit_poly(field, NULL, GWY_MASK_IGNORE, 3, term_powers, coeffs);
        gwy_field_subtract_poly(field, 3, term_powers, coeffs);
        g_assert_cmpfloat(fabs(gwy_field_min(field)), <, 4*DBL_EPSILON*xres*yres);
        g_assert_cmpfloat(fabs(gwy_field_max(field)), <, 4*DBL_EPSILON*xres*yres);
        g_assert_finalize_object(field);
    }
}

void
test_field_fit_poly_masked(void)
{
    guint n = g_test_slow() ? 10000 : 1000;

    guint count = 0;
    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by, cxy;
        GwyField *field = make_random_bilinear_field(&a, &bx, &by, &cxy);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyNield *nield = make_random_mask(xres, yres, TRUE, FALSE, 1);
        /* Ignore too small pixel sets. */
        if (gwy_nield_area_count(nield, NULL, GWY_MASK_IGNORE, col, row, width, height) < 20) {
            g_assert_finalize_object(nield);
            g_assert_finalize_object(field);
            continue;
        }

        gint term_powers[8] = { 0, 0, 1, 0, 0, 1, 1, 1 };
        gdouble coeffs[4];
        gwy_field_area_fit_poly(field, nield, GWY_MASK_INCLUDE, col, row, width, height, 4, term_powers, coeffs);
        /* The coefficients are meant for the area, not the entire field. This is just how the function is defined. */
        GwyField *part = gwy_field_area_extract(field, col, row, width, height);
        gwy_field_subtract_poly(part, 4, term_powers, coeffs);
        /* The errors can be pretty large because we are using only a bit of the data and extrapolate. */
        g_assert_cmpfloat(fabs(gwy_field_min(part)), <, 1e-12*width*height);
        g_assert_cmpfloat(fabs(gwy_field_max(part)), <, 1e-12*width*height);
        g_assert_finalize_object(part);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
        count++;
    }

    g_assert_cmpuint(count, >=, n/5);
}

GwyField*
make_ij_field(void)
{
    gint xres = g_test_rand_int_range(2, 20);
    gint yres = g_test_rand_int_range(2, 20);
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gdouble *d = gwy_field_get_data(field);

    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            d[i*xres + j] = SOME_NUMBER*i + j;
    }

    return field;
}

void
test_field_area_extract(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        GwyField *extracted = gwy_field_area_extract(field, col, row, width, height);
        const gdouble *d = gwy_field_get_data_const(extracted);
        for (gint i = 0; i < height; i++) {
            for (gint j = 0; j < width; j++) {
                g_assert_cmpfloat(d[i*width + j], ==, SOME_NUMBER*(i + row) + (j + col));
            }
        }
        g_assert_finalize_object(extracted);
        g_assert_finalize_object(field);
    }
}

void
test_field_crop(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        gwy_field_crop(field, col, row, width, height);
        g_assert_cmpint(gwy_field_get_xres(field), ==, width);
        g_assert_cmpint(gwy_field_get_yres(field), ==, height);
        const gdouble *d = gwy_field_get_data_const(field);
        for (gint i = 0; i < height; i++) {
            for (gint j = 0; j < width; j++) {
                g_assert_cmpfloat(d[i*width + j], ==, SOME_NUMBER*(i + row) + (j + col));
            }
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_area_copy(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint xres2 = g_test_rand_int_range(width, 25), yres2 = g_test_rand_int_range(height, 25);
        gint destcol = g_test_rand_int_range(0, xres2-width+1), destrow = g_test_rand_int_range(0, yres2-height+1);
        GwyField *destfield = gwy_field_new(xres2, yres2, xres2, yres2, FALSE);
        gwy_field_fill(destfield, G_PI);
        gwy_field_area_copy(field, destfield, col, row, width, height, destcol, destrow);
        const gdouble *d = gwy_field_get_data_const(destfield);
        for (gint i = 0; i < yres2; i++) {
            for (gint j = 0; j < xres2; j++) {
                if (i >= destrow && i-destrow < height && j >= destcol && j-destcol < width)
                    g_assert_cmpfloat(d[i*xres2 + j], ==, SOME_NUMBER*(i + row - destrow) + (j + col - destcol));
                else
                    g_assert_cmpfloat(d[i*xres2 + j], ==, G_PI);
            }
        }
        g_assert_finalize_object(destfield);
        g_assert_finalize_object(field);
    }
}

void
test_field_flip_x(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_flip(field, TRUE, FALSE);
        const gdouble *d = gwy_field_get_data_const(field);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint flipped_j = xres-1 - j;
                g_assert_cmpfloat(d[i*xres + j], ==, SOME_NUMBER*i + flipped_j);
            }
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_flip_y(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_flip(field, FALSE, TRUE);
        const gdouble *d = gwy_field_get_data_const(field);
        for (gint i = 0; i < yres; i++) {
            gint flipped_i = yres-1 - i;
            for (gint j = 0; j < xres; j++) {
                g_assert_cmpfloat(d[i*xres + j], ==, SOME_NUMBER*flipped_i + j);
            }
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_flip_xy(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_flip(field, TRUE, TRUE);
        const gdouble *d = gwy_field_get_data_const(field);
        for (gint i = 0; i < yres; i++) {
            gint flipped_i = yres-1 - i;
            for (gint j = 0; j < xres; j++) {
                gint flipped_j = xres-1 - j;
                g_assert_cmpfloat(d[i*xres + j], ==, SOME_NUMBER*flipped_i + flipped_j);
            }
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_setval(void)
{
    gint xres = g_test_rand_int_range(2, 20);
    gint yres = g_test_rand_int_range(2, 20);
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_field_fill(field, G_PI);

    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            gwy_field_set_val(field, j, i, i*SOME_NUMBER + j);
    }
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++)
            g_assert_cmpfloat(gwy_field_get_val(field, j, i), ==, i*SOME_NUMBER + j);
    }
    g_assert_finalize_object(field);
}

void
test_field_transpose_major(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble xreal = gwy_field_get_xreal(field), yreal = gwy_field_get_yreal(field);
        GwyField *transposed = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
        gwy_field_transpose(field, transposed, FALSE);
        g_assert_cmpint(gwy_field_get_xres(transposed), ==, yres);
        g_assert_cmpint(gwy_field_get_yres(transposed), ==, xres);
        g_assert_cmpint(gwy_field_get_xreal(transposed), ==, yreal);
        g_assert_cmpint(gwy_field_get_yreal(transposed), ==, xreal);
        const gdouble *d = gwy_field_get_data_const(transposed);
        for (gint i = 0; i < xres; i++) {
            for (gint j = 0; j < yres; j++)
                g_assert_cmpfloat(d[i*yres + j], ==, SOME_NUMBER*j + i);
        }
        GwyField *transposed2 = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
        gwy_field_transpose(transposed, transposed2, FALSE);
        field_assert_equal(G_OBJECT(field), G_OBJECT(transposed2));
        g_assert_finalize_object(transposed2);
        g_assert_finalize_object(transposed);
        g_assert_finalize_object(field);
    }
}

void
test_field_transpose_minor(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        GwyField *field = make_ij_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gdouble xreal = gwy_field_get_xreal(field), yreal = gwy_field_get_yreal(field);
        GwyField *transposed = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
        gwy_field_transpose(field, transposed, TRUE);
        g_assert_cmpint(gwy_field_get_xres(transposed), ==, yres);
        g_assert_cmpint(gwy_field_get_yres(transposed), ==, xres);
        g_assert_cmpint(gwy_field_get_xreal(transposed), ==, yreal);
        g_assert_cmpint(gwy_field_get_yreal(transposed), ==, xreal);
        const gdouble *d = gwy_field_get_data_const(transposed);
        for (gint i = 0; i < xres; i++) {
            for (gint j = 0; j < yres; j++)
                g_assert_cmpfloat(d[i*yres + j], ==, SOME_NUMBER*(yres-1 - j) + (xres-1 - i));
        }
        GwyField *transposed2 = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
        gwy_field_transpose(transposed, transposed2, TRUE);
        field_assert_equal(G_OBJECT(field), G_OBJECT(transposed2));
        g_assert_finalize_object(transposed2);
        g_assert_finalize_object(transposed);
        g_assert_finalize_object(field);
    }
}

static void
assert_field_compatibility(gint xres1, gint yres1,
                           gdouble xreal1, gdouble yreal1,
                           const gchar *xyunit1, const gchar *zunit1,
                           gint xres2, gint yres2,
                           gdouble xreal2, gdouble yreal2,
                           const gchar *xyunit2, const gchar *zunit2,
                           GwyDataMismatchFlags flags_to_test,
                           GwyDataMismatchFlags expected_result)
{
    GwyField *field1 = gwy_field_new(xres1, yres1, xreal1, yreal1, TRUE);
    if (xyunit1)
        gwy_unit_set_from_string(gwy_field_get_unit_xy(field1), xyunit1);
    if (zunit1)
        gwy_unit_set_from_string(gwy_field_get_unit_z(field1), zunit1);

    GwyField *field2 = gwy_field_new(xres2, yres2, xreal2, yreal2, TRUE);
    if (xyunit2)
        gwy_unit_set_from_string(gwy_field_get_unit_xy(field2), xyunit2);
    if (zunit2)
        gwy_unit_set_from_string(gwy_field_get_unit_z(field2), zunit2);

    g_assert_cmphex(gwy_field_is_incompatible(field1, field2, flags_to_test), ==, expected_result);
    g_assert_finalize_object(field1);
    g_assert_finalize_object(field2);
}

void
test_field_compatibility_res(void)
{
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               1, 2, 0.3, 1e40, NULL, "A",
                               GWY_DATA_MISMATCH_RES, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               1, 1, 0.3, 1e40, NULL, "A",
                               GWY_DATA_MISMATCH_RES, GWY_DATA_MISMATCH_RES);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               2, 2, 0.3, 1e40, NULL, "A",
                               GWY_DATA_MISMATCH_RES, GWY_DATA_MISMATCH_RES);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               2, 1, 0.3, 1e40, NULL, "A",
                               GWY_DATA_MISMATCH_RES, GWY_DATA_MISMATCH_RES);
}

void
test_field_compatibility_real(void)
{
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, G_PI, 7.0, NULL, "A",
                               GWY_DATA_MISMATCH_REAL, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, 0.99*G_PI, 7.0, NULL, "A",
                               GWY_DATA_MISMATCH_REAL, GWY_DATA_MISMATCH_REAL);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, G_PI, 7.01, NULL, "A",
                               GWY_DATA_MISMATCH_REAL, GWY_DATA_MISMATCH_REAL);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, 7.0, G_PI, NULL, "A",
                               GWY_DATA_MISMATCH_REAL, GWY_DATA_MISMATCH_REAL);
    /* Tiny differences do not break compatibility. */
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, G_PI*(1.0 - 1e-14), 7.0*(1.0 + 1e-14), NULL, "A",
                               GWY_DATA_MISMATCH_REAL, 0);
}

void
test_field_compatibility_lateral(void)
{
    assert_field_compatibility(1, 2, G_PI, 7.0, NULL, NULL,
                               3, 4, 1.0, 1e30, NULL, NULL,
                               GWY_DATA_MISMATCH_LATERAL, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, NULL, "m",
                               3, 4, 1.0, 1e30, "", "A",
                               GWY_DATA_MISMATCH_LATERAL, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "mV", "m",
                               3, 4, 1.0, 1e30, "kilovolts", "A",
                               GWY_DATA_MISMATCH_LATERAL, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "V", "m",
                               3, 4, 1.0, 1e30, "A", "V",
                               GWY_DATA_MISMATCH_LATERAL, GWY_DATA_MISMATCH_LATERAL);
    assert_field_compatibility(1, 2, G_PI, 7.0, "N", "",
                               3, 4, 1.0, 1e30, "", "N",
                               GWY_DATA_MISMATCH_LATERAL, GWY_DATA_MISMATCH_LATERAL);
}

void
test_field_compatibility_value(void)
{
    assert_field_compatibility(1, 2, G_PI, 7.0, NULL, NULL,
                               3, 4, 1.0, 1e30, NULL, NULL,
                               GWY_DATA_MISMATCH_VALUE, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", NULL,
                               3, 4, 1.0, 1e30, "A", "",
                               GWY_DATA_MISMATCH_VALUE, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", "micrometres",
                               3, 4, 1.0, 1e30, "A", "pm",
                               GWY_DATA_MISMATCH_VALUE, 0);
    assert_field_compatibility(1, 2, G_PI, 7.0, "m", "A",
                               3, 4, 1.0, 1e30, "A", "V",
                               GWY_DATA_MISMATCH_VALUE, GWY_DATA_MISMATCH_VALUE);
    assert_field_compatibility(1, 2, G_PI, 7.0, "", "N",
                               3, 4, 1.0, 1e30, "N", "",
                               GWY_DATA_MISMATCH_VALUE, GWY_DATA_MISMATCH_VALUE);
}

void
test_field_compatibility_measure(void)
{
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 6e-9, 2e-9, "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, 0);
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 6.01e-9, 2e-9, "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 6e-9, 1.99e-9, "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 1.5e-9, 1e-9, "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 2e-9, 1e-9, "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, GWY_DATA_MISMATCH_MEASURE);
    /* Tiny differences do not break compatibility. */
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "g",
                               3, 4, 6e-9*(1.0 - 1e-14), 2e-9*(1.0 + 1e-14), "A", "N",
                               GWY_DATA_MISMATCH_MEASURE, 0);
}

void
test_field_compatibility_mixed(void)
{
    GwyDataMismatchFlags all_flags = (GWY_DATA_MISMATCH_RES
                                      | GWY_DATA_MISMATCH_REAL
                                      | GWY_DATA_MISMATCH_MEASURE
                                      | GWY_DATA_MISMATCH_LATERAL
                                      | GWY_DATA_MISMATCH_VALUE);
    assert_field_compatibility(1, 2, 2e-9, 1e-9, "m", "V",
                               3, 4, 2e-9, 1e-9, "A", "V",
                               all_flags,
                               GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_MEASURE | GWY_DATA_MISMATCH_LATERAL);
    assert_field_compatibility(3, 4, 3e-9, 4e-9, "m", "V",
                               3, 4, 4e-9, 3e-9, "A", "N",
                               all_flags,
                               GWY_DATA_MISMATCH_REAL | GWY_DATA_MISMATCH_MEASURE
                               | GWY_DATA_MISMATCH_LATERAL | GWY_DATA_MISMATCH_VALUE);
    assert_field_compatibility(3, 4, 3e-9, 40e-9, NULL, NULL,
                               6, 8, 6e-9, 80e-9, NULL, NULL,
                               all_flags,
                               GWY_DATA_MISMATCH_RES | GWY_DATA_MISMATCH_REAL);
}

void
test_field_clear_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(3, 15);
        gint yres = g_test_rand_int_range(3, 15);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);

        gdouble a = 10.0*g_test_rand_double() - 5.0;
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gdouble avg, expected;

        gwy_field_fill(field, a);
        gwy_field_invalidate(field);
        g_assert_cmpfloat(gwy_field_min(field), ==, a);
        g_assert_cmpfloat(gwy_field_max(field), ==, a);
        gwy_field_invalidate(field);

        gwy_field_area_clear(field, col, row, width, height);
        avg = gwy_field_mean(field);
        expected = (xres*yres - width*height)*a/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fabs(a)*DBL_EPSILON);

        GwyField *detail1 = gwy_field_area_extract(field, col, row, width, height);
        avg = gwy_field_mean(detail1);
        expected = 0.0;
        g_assert_cmpfloat(avg, ==, expected);
        g_assert_finalize_object(detail1);

        g_assert_finalize_object(field);
    }
}

void
test_field_fill_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(3, 15);
        gint yres = g_test_rand_int_range(3, 15);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);

        gdouble a = g_test_rand_double(), b = g_test_rand_double();
        gwy_field_fill(field, a);
        gwy_field_invalidate(field);
        g_assert_cmpfloat(gwy_field_min(field), ==, a);
        g_assert_cmpfloat(gwy_field_max(field), ==, a);
        gwy_field_invalidate(field);

        GwyNield *nield = gwy_nield_new(xres, yres);
        gint width = g_test_rand_int_range(2, xres), height = g_test_rand_int_range(2, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        guint m = 0, npix = g_test_rand_int_range(1, width*height);
        gdouble avg, expected;

        while (m < npix) {
            gint i = row + g_test_rand_int_range(0, height);
            gint j = col + g_test_rand_int_range(0, width);
            if (!gwy_nield_get_val(nield, j, i)) {
                gwy_nield_set_val(nield, j, i, 1);
                m++;
            }
        }

        gwy_field_area_fill(field, nield, GWY_MASK_INCLUDE, col, row, width, height, b);
        g_assert_cmpfloat(gwy_field_min(field), ==, fmin(a, b));
        g_assert_cmpfloat(gwy_field_max(field), ==, fmax(a, b));

        avg = gwy_field_mean(field);
        expected = (npix*b + (xres*yres - npix)*a)/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fmax(a, b)*DBL_EPSILON);

        GwyField *detail1 = gwy_field_area_extract(field, col, row, width, height);
        avg = gwy_field_mean(detail1);
        expected = (npix*b + (width*height - npix)*a)/(width*height);
        g_assert_cmpfloat_with_epsilon(avg, expected, width*height*fmax(a, b)*DBL_EPSILON);
        g_assert_finalize_object(detail1);

        gwy_field_area_fill(field, nield, GWY_MASK_IGNORE, col, row, width, height, b);
        avg = gwy_field_mean(field);
        expected = (width*height*b + (xres*yres - width*height)*a)/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fmax(a, b)*DBL_EPSILON);

        gwy_field_area_fill(field, nield, GWY_MASK_IGNORE, col, row, width, height, a);
        avg = gwy_field_mean(field);
        expected = a;
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*a*DBL_EPSILON);

        gwy_field_area_fill(field, nield, GWY_MASK_EXCLUDE, col, row, width, height, b);
        avg = gwy_field_mean(field);
        expected = ((width*height - npix)*b + (xres*yres - (width*height - npix))*a)/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fmax(a, b)*DBL_EPSILON);

        GwyField *detail2 = gwy_field_area_extract(field, col, row, width, height);
        avg = gwy_field_mean(detail2);
        expected = (npix*a + (width*height - npix)*b)/(width*height);
        g_assert_cmpfloat_with_epsilon(avg, expected, width*height*fmax(a, b)*DBL_EPSILON);
        g_assert_finalize_object(detail2);

        gwy_field_area_fill(field, nield, GWY_MASK_INCLUDE, col, row, width, height, b);
        avg = gwy_field_mean(field);
        expected = (width*height*b + (xres*yres - width*height)*a)/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fmax(a, b)*DBL_EPSILON);

        gwy_field_fill(field, a);
        gwy_nield_fill(nield, 4);
        gwy_field_area_fill(field, nield, GWY_MASK_INCLUDE, col, row, width, height, b);
        avg = gwy_field_mean(field);
        expected = (width*height*b + (xres*yres - width*height)*a)/(xres*yres);
        g_assert_cmpfloat_with_epsilon(avg, expected, xres*yres*fmax(a, b)*DBL_EPSILON);

        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_fill_area_mask(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(3, 15);
        gint yres = g_test_rand_int_range(3, 15);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
        gint width = g_test_rand_int_range(2, xres), height = g_test_rand_int_range(2, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);

        gdouble a = g_test_rand_double(), b = g_test_rand_double(), c = g_test_rand_double();
        gwy_field_fill(field, c);
        gwy_field_invalidate(field);

        GwyNield *nield = make_random_mask(xres, yres, TRUE, FALSE, 3);
        gwy_field_area_fill_mask(field, nield, col, row, width, height, b, a);
        gint count = gwy_nield_area_count(nield, NULL, GWY_MASK_IGNORE, col, row, width, height);

        if (count) {
            g_assert_cmpfloat(gwy_field_area_min(field, nield, GWY_MASK_INCLUDE, col, row, width, height), ==, a);
            g_assert_cmpfloat(gwy_field_area_max(field, nield, GWY_MASK_INCLUDE, col, row, width, height), ==, a);
        }
        if (width*height - count) {
            g_assert_cmpfloat(gwy_field_area_min(field, nield, GWY_MASK_EXCLUDE, col, row, width, height), ==, b);
            g_assert_cmpfloat(gwy_field_area_max(field, nield, GWY_MASK_EXCLUDE, col, row, width, height), ==, b);
        }

        if (width*height < xres*yres) {
            gwy_nield_fill(nield, 1);
            gwy_nield_area_clear(nield, col, row, width, height);
            g_assert_cmpfloat(gwy_field_area_min(field, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres), ==, c);
            g_assert_cmpfloat(gwy_field_area_max(field, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres), ==, c);
        }

        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

static void
test_field_extend_simple(const gdouble *bigdata, guint bigxres, guint bigyres,
                         guint xres, guint yres, guint col, guint row,
                         GwyExteriorType exterior, gdouble fill_value,
                         gdouble eps)
{
    GwyField *bigfield = gwy_field_new(bigxres, bigyres, bigxres, bigyres, FALSE);
    gwy_assign(gwy_field_get_data(bigfield), bigdata, bigxres*bigyres);

    GwyField *field = gwy_field_area_extract(bigfield, col, row, xres, yres);
    for (gint left = 0; left <= col; left++) {
        for (gint right = 0; right <= bigxres - (col + xres); right++) {
            for (gint up = 0; up <= row; up++) {
                for (gint down = 0; down <= bigyres - (row + yres); down++) {
                    //g_printerr("ext [%d,%d]x[%d,%d]\n", left, right, up, down);
                    GwyField *expected = gwy_field_area_extract(bigfield,
                                                                col - left, row - up,
                                                                xres + left + right, yres + up + down);
                    gwy_field_set_xoffset(expected, -left);
                    gwy_field_set_yoffset(expected, -up);

                    GwyField *extended = gwy_field_extend(field, left, right, up, down,
                                                          exterior, fill_value, TRUE);

                    if (eps == 0.0)
                        field_assert_equal(G_OBJECT(extended), G_OBJECT(expected));
                    else {
                        assert_field_properties(extended, expected);
                        assert_field_content(extended, gwy_field_get_data(expected),
                                             gwy_field_get_xres(expected)*gwy_field_get_yres(expected),
                                             1e-14);
                    }

                    g_assert_finalize_object(extended);
                    g_assert_finalize_object(expected);
                }
            }
        }
    }

    g_assert_finalize_object(field);
    g_assert_finalize_object(bigfield);
}

void
test_field_extend_border(void)
{
    enum { bigxres = 9, bigyres = 9 };
    enum { xres = 3, yres = 2 };
    enum { col = 3, row = 4 };
    const gdouble bigdata[bigxres*bigyres] = {
        4.0,  4.0,  4.0,    4.0,  -1.0, 0.0,   0.0, 0.0, 0.0,
        4.0,  4.0,  4.0,    4.0,  -1.0, 0.0,   0.0, 0.0, 0.0,
        4.0,  4.0,  4.0,    4.0,  -1.0, 0.0,   0.0, 0.0, 0.0,
        4.0,  4.0,  4.0,    4.0,  -1.0, 0.0,   0.0, 0.0, 0.0,

        4.0,  4.0,  4.0,    4.0,  -1.0, 0.0,   0.0, 0.0, 0.0,
        G_PI, G_PI, G_PI,   G_PI, 5.5,  1e5,   1e5, 1e5, 1e5,

        G_PI, G_PI, G_PI,   G_PI, 5.5,  1e5,   1e5, 1e5, 1e5,
        G_PI, G_PI, G_PI,   G_PI, 5.5,  1e5,   1e5, 1e5, 1e5,
        G_PI, G_PI, G_PI,   G_PI, 5.5,  1e5,   1e5, 1e5, 1e5,
    };

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_BORDER, G_MAXDOUBLE, 0.0);
}

void
test_field_extend_mirror(void)
{
    enum { bigxres = 11, bigyres = 8 };
    enum { xres = 3, yres = 2 };
    enum { col = 4, row = 3 };
    const gdouble bigdata[bigxres*bigyres] = {
        1e5, 1e5, 5.5,  G_PI,   G_PI, 5.5,  1e5,   1e5, 5.5,  G_PI, G_PI,
        1e5, 1e5, 5.5,  G_PI,   G_PI, 5.5,  1e5,   1e5, 5.5,  G_PI, G_PI,
        0.0, 0.0, -1.0, 4.0,    4.0,  -1.0, 0.0,   0.0, -1.0, 4.0,  4.0,

        0.0, 0.0, -1.0, 4.0,    4.0,  -1.0, 0.0,   0.0, -1.0, 4.0,  4.0,
        1e5, 1e5, 5.5,  G_PI,   G_PI, 5.5,  1e5,   1e5, 5.5,  G_PI, G_PI,

        1e5, 1e5, 5.5,  G_PI,   G_PI, 5.5,  1e5,   1e5, 5.5,  G_PI, G_PI,
        0.0, 0.0, -1.0, 4.0,    4.0,  -1.0, 0.0,   0.0, -1.0, 4.0,  4.0,
        0.0, 0.0, -1.0, 4.0,    4.0,  -1.0, 0.0,   0.0, -1.0, 4.0,  4.0,
    };

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_MIRROR, G_MAXDOUBLE, 0.0);
}

void
test_field_extend_periodic(void)
{
    enum { bigxres = 11, bigyres = 8 };
    enum { xres = 3, yres = 2 };
    enum { col = 4, row = 3 };
    const gdouble bigdata[bigxres*bigyres] = {
        1e5, G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5, G_PI,
        0.0, 4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0, 4.0,
        1e5, G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5, G_PI,

        0.0, 4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0, 4.0,
        1e5, G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5, G_PI,

        0.0, 4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0, 4.0,
        1e5, G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5,   G_PI, 5.5,  1e5, G_PI,
        0.0, 4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0,   4.0,  -1.0, 0.0, 4.0,
    };

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_PERIODIC, G_MAXDOUBLE, 0.0);
}

void
test_field_extend_fixed(void)
{
    enum { bigxres = 11, bigyres = 8 };
    enum { xres = 3, yres = 2 };
    enum { col = 4, row = 3 };
    const gdouble bigdata[bigxres*bigyres] = {
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,

        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   4.0,     -1.0,    0.0,       G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_PI,    5.5,     1e5,       G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,

        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
        G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2,   G_SQRT2, G_SQRT2, G_SQRT2, G_SQRT2,
    };

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_FIXED_VALUE, G_SQRT2, 0.0);
}

/* Verify reproduction of constant with some tolerance. */
void
test_field_extend_laplace(void)
{
    enum { bigxres = 11, bigyres = 8 };
    enum { xres = 3, yres = 2 };
    enum { col = 4, row = 3 };
    gdouble bigdata[bigxres*bigyres];

    for (guint i = 0; i < bigxres*bigyres; i++)
        bigdata[i] = GWY_SQRT_PI;

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_LAPLACE, G_MAXDOUBLE, 1e-14);
}

/* Verify reproduction of constant with some tolerance. */
void
test_field_extend_connect(void)
{
    enum { bigxres = 11, bigyres = 8 };
    enum { xres = 3, yres = 2 };
    enum { col = 4, row = 3 };
    gdouble bigdata[bigxres*bigyres];

    for (guint i = 0; i < bigxres*bigyres; i++)
        bigdata[i] = GWY_SQRT_PI;

    test_field_extend_simple(bigdata, bigxres, bigyres, xres, yres, col, row, GWY_EXTERIOR_CONNECT, G_MAXDOUBLE, 1e-14);
}

void
test_field_extend_undefined(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_planar_field(NULL, NULL, NULL);
        gint left = g_test_rand_int_range(0, 10);
        gint right = g_test_rand_int_range(0, 10);
        gint up = g_test_rand_int_range(0, 10);
        gint down = g_test_rand_int_range(0, 10);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        GwyField *extended = gwy_field_extend(field, left, right, up, down,
                                              GWY_EXTERIOR_UNDEFINED, G_MAXDOUBLE, FALSE);
        GwyField *extracted = gwy_field_area_extract(extended, left, up, xres, yres);
        /* There may be rounding errors in the real size, unfortunately. Test it with an ε and then just make the two
         * dimensions exactly equal. */
        g_assert_cmpfloat_with_epsilon(gwy_field_get_xreal(extracted), gwy_field_get_xreal(field),
                                       2*(xres + left + right)*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(gwy_field_get_yreal(extracted), gwy_field_get_yreal(field),
                                       2*(yres + up + down)*DBL_EPSILON);
        gwy_field_set_xreal(extracted, gwy_field_get_xreal(field));
        gwy_field_set_yreal(extracted, gwy_field_get_yreal(field));
        field_assert_equal(G_OBJECT(extracted), G_OBJECT(field));

        g_assert_finalize_object(field);
        g_assert_finalize_object(extracted);
        g_assert_finalize_object(extended);
    }
}

static void
assert_dist_moments(GwyLine *dist, gdouble avg, gdouble rms, gdouble eps)
{
    /* Compare the first and second moment computed from the distribution to the real values. Of course, they will not
     * be the same, but it should not be too far. */
    gint nstat = gwy_line_get_res(dist);
    gdouble dx = gwy_line_get_dx(dist), xoff = gwy_line_get_offset(dist);
    const gdouble *distdata = gwy_line_get_data_const(dist);

    gdouble s1 = 0.0;
    for (gint i = 0; i < nstat; i++) {
        gdouble z = ((i + 0.5)*dx + xoff);
        s1 += distdata[i]*z*dx;
    }

    g_assert_cmpfloat_with_epsilon(s1, avg, eps);

    gdouble s2 = 0.0;
    for (gint i = 0; i < nstat; i++) {
        gdouble z = ((i + 0.5)*dx + xoff);
        s2 += distdata[i]*(z - avg)*(z - avg)*dx;
    }
    s2 = sqrt(s2);
    g_assert_cmpfloat_with_epsilon(s2, rms, eps);
}

void
test_field_stats_height_dist_full(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(10, 30), yres = g_test_rand_int_range(10, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
        gdouble a = 5.0*g_test_rand_double() + 3.0, b = 1.5*g_test_rand_double();

        for (gint i = 0; i < yres; i++) {
            gdouble y = (i + 0.5)/yres;
            for (gint j = 0; j < xres; j++) {
                gdouble x = (j + 0.5)/xres;
                gwy_field_set_val(field, j, i, a + exp(b*(x + y)) + g_test_rand_double());
            }
        }

        GwyLine *dist = gwy_line_new(1, 1.0, FALSE);
        gwy_field_area_height_dist(field, NULL, GWY_MASK_IGNORE, dist, 0, 0, xres, yres, -1);

        assert_dist_moments(dist, gwy_field_mean(field), gwy_field_rms(field), 0.1);

        g_assert_finalize_object(dist);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_height_dist_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(15, 30), yres = g_test_rand_int_range(15, 30);
        gint width = g_test_rand_int_range(10, xres), height = g_test_rand_int_range(10, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
        gdouble a = 5.0*g_test_rand_double() + 3.0, b = 1.5*g_test_rand_double();
        gwy_field_fill(field, G_MAXDOUBLE);

        for (gint i = 0; i < height; i++) {
            gdouble y = (i + 0.5)/height;
            for (gint j = 0; j < width; j++) {
                gdouble x = (j + 0.5)/width;
                gwy_field_set_val(field, j + col, i + row, a + exp(b*(x + y)) + g_test_rand_double());
            }
        }

        GwyLine *dist = gwy_line_new(1, 1.0, FALSE);
        gwy_field_area_height_dist(field, NULL, GWY_MASK_IGNORE, dist, col, row, width, height, -1);

        assert_dist_moments(dist,
                            gwy_field_area_mean(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                            gwy_field_area_rms(field, NULL, GWY_MASK_IGNORE, col, row, width, height),
                            0.1);

        g_assert_finalize_object(dist);
        g_assert_finalize_object(field);
    }
}

void
test_field_stats_height_dist_masked(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint k = 0; k < n; k++) {
        gint xres = g_test_rand_int_range(17, 40), yres = g_test_rand_int_range(17, 40);
        gint width = g_test_rand_int_range(15, xres), height = g_test_rand_int_range(15, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
        gdouble a = 5.0*g_test_rand_double() + 3.0, b = 1.5*g_test_rand_double();
        gwy_field_fill(field, G_MAXDOUBLE);

        GwyNield *nield = make_random_mask(xres, yres, TRUE, FALSE, 3);

        for (gint i = 0; i < height; i++) {
            gdouble y = (i + 0.5)/height;
            for (gint j = 0; j < width; j++) {
                gdouble x = (j + 0.5)/width;
                if (gwy_nield_get_val(nield, j + col, i + row) > 0)
                    gwy_field_set_val(field, j + col, i + row, a + exp(b*(x + y)) + g_test_rand_double());
            }
        }

        GwyLine *dist = gwy_line_new(1, 1.0, FALSE);
        gwy_field_area_height_dist(field, nield, GWY_MASK_INCLUDE, dist, col, row, width, height, -1);

        assert_dist_moments(dist,
                            gwy_field_area_mean(field, nield, GWY_MASK_INCLUDE, col, row, width, height),
                            gwy_field_area_rms(field, nield, GWY_MASK_INCLUDE, col, row, width, height),
                            0.1);

        g_assert_finalize_object(nield);
        g_assert_finalize_object(dist);
        g_assert_finalize_object(field);
    }
}

static void
test_field_line_stats_common(GwyField *field, GwyNield *mask, GwyMaskingType masking,
                             gint col, gint row, gint width, gint height,
                             GwyLineStatQuantity quantity, GwyOrientation orientation,
                             const gdouble *expected_data, const gdouble *expected_weight,
                             gint lineres, gdouble linereal, gdouble lineoff, gdouble eps)
{
    for (gint ii = 0; ii < 2; ii++) {
        GwyLine *weights = ii ? gwy_line_new(1, 1.0, FALSE) : NULL;
        GwyLine *line = gwy_field_area_line_stats(field, mask, masking, col, row, width, height,
                                                  quantity, orientation, weights);
        g_assert_true(GWY_IS_LINE(line));
        g_assert_cmpint(gwy_line_get_res(line), ==, lineres);
        g_assert_cmpfloat(gwy_line_get_real(line), ==, linereal);
        g_assert_cmpfloat(gwy_line_get_offset(line), ==, lineoff);
        const gdouble *data = gwy_line_get_data_const(line);

        if (eps > 0.0) {
            for (guint i = 0; i < lineres; i++)
                g_assert_cmpfloat_with_epsilon(data[i], expected_data[i], eps);
        }
        else {
            for (guint i = 0; i < lineres; i++)
                g_assert_cmpfloat(data[i], ==, expected_data[i]);
        }

        if (weights) {
            g_assert_true(GWY_IS_LINE(weights));
            g_assert_cmpint(gwy_line_get_res(weights), ==, lineres);
            g_assert_cmpfloat(gwy_line_get_real(weights), ==, linereal);
            g_assert_cmpfloat(gwy_line_get_offset(weights), ==, lineoff);
            const gdouble *wdata = gwy_line_get_data_const(weights);

            if (eps > 0.0) {
                for (guint i = 0; i < lineres; i++)
                    g_assert_cmpfloat_with_epsilon(wdata[i], expected_weight[i], eps);
            }
            else {
                for (guint i = 0; i < lineres; i++)
                    g_assert_cmpfloat(wdata[i], ==, expected_weight[i]);
            }
            g_assert_finalize_object(weights);
        }

        g_assert_finalize_object(line);
    }
}

void
test_field_line_stats_rows_mean(void)
{
    enum { xres1 = 5, yres1 = 3 };
    static const gdouble data1[xres1*yres1] = {
        -2.6, G_PI, 0.0, 1e7, G_PI,
        0.3, -1.5, G_E, -200, G_E,
        G_LN2, 7.7, 1000, -4, 0.0,
    };
    static const gchar msrc1[] =
        " 1  1\n"
        "  1 1\n"
        "1    ";
    const gdouble expected_data1[yres1] = { G_PI, G_E, G_LN2 };
    const gdouble expected_wdata1[yres1] = { 2.0, 2.0, 1.0 };

    GwyField *field;
    GwyNield *mask;

    field = gwy_field_new(xres1, yres1, xres1, yres1, FALSE);
    gwy_field_set_xoffset(field, -1.5);
    gwy_field_set_yoffset(field, 2.0);
    gwy_assign(gwy_field_get_data(field), data1, xres1*yres1);
    mask = parse_nield(msrc1);

    test_field_line_stats_common(field, mask, GWY_MASK_INCLUDE, 0, 0, xres1, yres1,
                                 GWY_LINE_STAT_MEAN, GWY_ORIENTATION_HORIZONTAL,
                                 expected_data1, expected_wdata1, yres1, yres1, 2.0,
                                 4*DBL_EPSILON);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);

    enum { xres2 = 2, yres2 = 4 };
    static const gdouble data2[xres1*yres1] = {
        1e6, G_PI,
        -0.3, 1.5,
        G_E, G_E,
        G_SQRT2, -38.0,
    };
    static const gchar msrc2[] =
        " 1\n"
        "  \n"
        "11\n"
        "1 ";
    const gdouble expected_data2[yres2] = { G_PI, 0.0, G_E, G_SQRT2 };
    const gdouble expected_wdata2[yres2] = { 1.0, 0.0, 2.0, 1.0 };

    field = gwy_field_new(xres2, yres2, xres2, yres2, FALSE);
    gwy_field_set_xoffset(field, 0.5);
    gwy_field_set_yoffset(field, -6.0);
    gwy_assign(gwy_field_get_data(field), data2, xres2*yres2);
    mask = parse_nield(msrc2);

    test_field_line_stats_common(field, mask, GWY_MASK_INCLUDE, 0, 0, xres2, yres2,
                                 GWY_LINE_STAT_MEAN, GWY_ORIENTATION_HORIZONTAL,
                                 expected_data2, expected_wdata2, yres2, yres2, -6.0,
                                 4*DBL_EPSILON);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);
}

void
test_field_line_stats_columns_mean(void)
{
    enum { xres1 = 6, yres1 = 2 };
    static const gdouble data1[xres1*yres1] = {
        G_PI, 1e6, 0.0, G_E, G_LN2, -1e6,
        0.0, 1e6, G_SQRT2, G_E, -3.3, -2e6,
    };
    static const gchar msrc1[] =
        "1  11 \n"
        "  11  ";
    const gdouble expected_data1[xres1] = { G_PI, 0.0, G_SQRT2, G_E, G_LN2, 0.0 };
    const gdouble expected_wdata1[xres1] = { 1.0, 0.0, 1.0, 2.0, 1.0, 0.0 };

    GwyField *field;
    GwyNield *mask;

    field = gwy_field_new(xres1, yres1, xres1, yres1, FALSE);
    gwy_field_set_xoffset(field, -2.5);
    gwy_field_set_yoffset(field, 0.75);
    gwy_assign(gwy_field_get_data(field), data1, xres1*yres1);
    mask = parse_nield(msrc1);

    test_field_line_stats_common(field, mask, GWY_MASK_INCLUDE, 0, 0, xres1, yres1,
                                 GWY_LINE_STAT_MEAN, GWY_ORIENTATION_VERTICAL,
                                 expected_data1, expected_wdata1, xres1, xres1, -2.5,
                                 4*DBL_EPSILON);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);

    enum { xres2 = 3, yres2 = 5 };
    static const gdouble data2[xres2*yres2] = {
        G_SQRT2, 1e6, -1.0,
        0.5, -3.0, G_LN2,
        7.0, 7.1, 7.2,
        G_SQRT2, G_E, G_LN2,
        G_SQRT2, G_E, 0.3,
    };
    static const gchar msrc2[] =
        "1  \n"
        "  1\n"
        "   \n"
        "111\n"
        "11 ";
    const gdouble expected_data2[xres2] = { G_SQRT2, G_E, G_LN2 };
    const gdouble expected_wdata2[xres2] = { 3.0, 2.0, 2.0 };

    field = gwy_field_new(xres2, yres2, xres2, yres2, FALSE);
    gwy_field_set_xoffset(field, -1.0);
    gwy_field_set_yoffset(field, 1.5);
    gwy_assign(gwy_field_get_data(field), data2, xres2*yres2);
    mask = parse_nield(msrc2);

    test_field_line_stats_common(field, mask, GWY_MASK_INCLUDE, 0, 0, xres2, yres2,
                                 GWY_LINE_STAT_MEAN, GWY_ORIENTATION_VERTICAL,
                                 expected_data2, expected_wdata2, xres2, xres2, -1.0,
                                 4*DBL_EPSILON);

    g_assert_finalize_object(mask);
    g_assert_finalize_object(field);
}

void
test_field_filter_square_minimum(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(5, 12), yres = g_test_rand_int_range(5, 12);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint size = g_test_rand_int_range(1, MIN(width, height)+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_filter_minimum(filtered, size, col, row, width, height);

        GwyField *reference = gwy_field_copy(field);
        for (gint i = 0; i < height; i++) {
            /* The function documentation says it completely ignores anything outside the rectangle.
             * Check that it is what happens. */
            gint top = row + i - (size - 1)/2, bot = row + i + size/2;
            top = MAX(top, row);
            bot = MIN(bot, row+height-1);
            for (gint j = 0; j < width; j++) {
                gint left = col + j - (size - 1)/2, right = col + j + size/2;
                left = MAX(left, col);
                right = MIN(right, col+width-1);
                gdouble v = gwy_field_area_min(field, NULL, GWY_MASK_IGNORE,
                                               left, top, right+1 - left, bot+1 - top);
                gwy_field_set_val(reference, col + j, row + i, v);
            }
        }

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_square_maximum(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(5, 12), yres = g_test_rand_int_range(5, 12);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint size = g_test_rand_int_range(1, MIN(width, height)+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_filter_maximum(filtered, size, col, row, width, height);

        GwyField *reference = gwy_field_copy(field);
        for (gint i = 0; i < height; i++) {
            /* The function documentation says it completely ignores anything outside the rectangle.
             * Check that it is what happens. */
            gint top = row + i - (size - 1)/2, bot = row + i + size/2;
            top = MAX(top, row);
            bot = MIN(bot, row+height-1);
            for (gint j = 0; j < width; j++) {
                gint left = col + j - (size - 1)/2, right = col + j + size/2;
                left = MAX(left, col);
                right = MIN(right, col+width-1);
                gdouble v = gwy_field_area_max(field, NULL, GWY_MASK_IGNORE,
                                               left, top, right+1 - left, bot+1 - top);
                gwy_field_set_val(reference, col + j, row + i, v);
            }
        }

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

static gdouble
minmax_the_hard_way(GwyField *field, GwyNield *kernel, gint col, gint row,
                    GwyMinMaxFilterType filtertype)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint kxres = gwy_nield_get_xres(kernel), kyres = gwy_nield_get_yres(kernel);

    gint koffx = (kxres - 1)/2, koffy = (kyres - 1)/2;

    const gdouble *data = gwy_field_get_data_const(field);
    const gint *kdata = gwy_nield_get_data(kernel);
    gdouble m = G_MAXDOUBLE, M = -G_MAXDOUBLE;
    gdouble v0 = data[row*xres + col];
    gboolean do_flip = (filtertype == GWY_MIN_MAX_FILTER_FROSION || filtertype == GWY_MIN_MAX_FILTER_DILATION);
    for (gint ki = 0; ki < kyres; ki++) {
        gint i = (do_flip ? row - ki + koffy : row + ki - koffy);
        /* The function documentation specifies border-extend behaviour. */
        i = CLAMP(i, 0, yres-1);
        for (gint kj = 0; kj < kxres; kj++) {
            gint j = (do_flip ? col - kj + koffx : col + kj - koffx);
            j = CLAMP(j, 0, xres-1);
            if (kdata[ki*kxres + kj] <= 0)
                continue;

            gdouble v = data[i*xres + j];
            m = fmin(m, v);
            M = fmax(M, v);
        }
    }

    if (filtertype == GWY_MIN_MAX_FILTER_MINIMUM || filtertype == GWY_MIN_MAX_FILTER_FROSION)
        return (M >= m ? m : v0);
    if (filtertype == GWY_MIN_MAX_FILTER_MAXIMUM || filtertype == GWY_MIN_MAX_FILTER_DILATION)
        return (M >= m ? M : v0);
    if (filtertype == GWY_MIN_MAX_FILTER_RANGE)
        return (M >= m ? M - m : 0.0);
    if (filtertype == GWY_MIN_MAX_FILTER_NORMALIZATION)
        return (M > m ? (v0 - m)/(M - m) : 0.5);

    g_return_val_if_reached(0.0);
}

static void
test_field_filter_minmax(GwyMinMaxFilterType filtertype)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(5, 12), yres = g_test_rand_int_range(5, 12);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint kxres = g_test_rand_int_range(1, width+1), kyres = g_test_rand_int_range(1, height+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *kernel = make_random_mask(kxres, kyres, TRUE, TRUE, 2);
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_filter_min_max(filtered, kernel, filtertype, col, row, width, height);

        GwyField *reference = gwy_field_copy(field);
        for (gint i = 0; i < height; i++) {
            for (gint j = 0; j < width; j++) {
                gdouble v = minmax_the_hard_way(field, kernel, col + j, row + i, filtertype);
                gwy_field_set_val(reference, col + j, row + i, v);
            }
        }

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(kernel);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_minmax_minimum(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_MINIMUM);
}

void
test_field_filter_minmax_maximum(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_MAXIMUM);
}

void
test_field_filter_minmax_range(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_RANGE);
}

void
test_field_filter_minmax_normalization(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_NORMALIZATION);
}

void
test_field_filter_minmax_dilation(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_DILATION);
}

void
test_field_filter_minmax_frosion(void)
{
    test_field_filter_minmax(GWY_MIN_MAX_FILTER_FROSION);
}

static gdouble
rank_the_hard_way(GwyField *field, GwyNield *kernel, gint col, gint row,
                  gint k)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint kxres = gwy_nield_get_xres(kernel), kyres = gwy_nield_get_yres(kernel);

    gint koffx = (kxres - 1)/2, koffy = (kyres - 1)/2;

    const gdouble *data = gwy_field_get_data_const(field);
    const gint *kdata = gwy_nield_get_data(kernel);
    guint n = gwy_nield_count(kernel);
    gdouble v[n];
    guint nn = 0;
    for (gint ki = 0; ki < kyres; ki++) {
        gint i = row + ki - koffy;
        /* The function documentation specifies border-extend behaviour. */
        i = CLAMP(i, 0, yres-1);
        for (gint kj = 0; kj < kxres; kj++) {
            gint j = col + kj - koffx;
            j = CLAMP(j, 0, xres-1);
            if (kdata[ki*kxres + kj] <= 0)
                continue;

            g_assert_cmpuint(nn, <, n);
            v[nn++] = data[i*xres + j];
        }
    }
    g_assert_cmpuint(nn, ==, n);

    return gwy_math_kth_rank(v, n, k);
}

void
test_field_filter_rank(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        // Make the image large enough to also excersise the big-kernel implementation with some consistency.
        // It still reduces to a simple miniumum or maximum filter quite often.
        gint xres = g_test_rand_int_range(5, 25), yres = g_test_rand_int_range(5, 25);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint kxres = g_test_rand_int_range(1, width+1), kyres = g_test_rand_int_range(1, height+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *kernel = make_random_mask(kxres, kyres, TRUE, TRUE, 3);
        guint nk = gwy_nield_count(kernel);
        guint k = g_test_rand_int_range(0, nk);
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_filter_kth_rank(filtered, kernel, col, row, width, height, k, NULL);

        GwyField *reference = gwy_field_copy(field);
        for (gint i = 0; i < height; i++) {
            for (gint j = 0; j < width; j++) {
                gdouble v = rank_the_hard_way(field, kernel, col + j, row + i, k);
                gwy_field_set_val(reference, col + j, row + i, v);
            }
        }

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(kernel);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

static gdouble
trimmed_mean_the_hard_way(GwyField *field, GwyNield *kernel, gint col, gint row,
                          gint nlowest, gint nhighest)
{
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
    gint kxres = gwy_nield_get_xres(kernel), kyres = gwy_nield_get_yres(kernel);

    gint koffx = (kxres - 1)/2, koffy = (kyres - 1)/2;

    const gdouble *data = gwy_field_get_data_const(field);
    const gint *kdata = gwy_nield_get_data(kernel);
    guint n = gwy_nield_count(kernel);
    gdouble v[n];
    guint nn = 0;
    for (gint ki = 0; ki < kyres; ki++) {
        gint i = row + ki - koffy;
        /* The function documentation specifies border-extend behaviour. */
        i = CLAMP(i, 0, yres-1);
        for (gint kj = 0; kj < kxres; kj++) {
            gint j = col + kj - koffx;
            j = CLAMP(j, 0, xres-1);
            if (kdata[ki*kxres + kj] <= 0)
                continue;

            g_assert_cmpuint(nn, <, n);
            v[nn++] = data[i*xres + j];
        }
    }
    g_assert_cmpuint(nn, ==, n);

    return gwy_math_trimmed_mean(v, n, nlowest, nhighest);
}

void
test_field_filter_trimmed_mean(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        // Make the image large enough to also excersise the big-kernel implementation with some consistency.
        // It still reduces to simpler filters quite often.
        gint xres = g_test_rand_int_range(5, 30), yres = g_test_rand_int_range(5, 30);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width+1), row = g_test_rand_int_range(0, yres-height+1);
        gint kxres = g_test_rand_int_range(1, width+1), kyres = g_test_rand_int_range(1, height+1);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *kernel = make_random_mask(kxres, kyres, TRUE, TRUE, 3);
        guint nk = gwy_nield_count(kernel);
        guint ntotal = g_test_rand_int_range(0, nk);
        guint nlower = 0, nupper = 0;
        if (ntotal) {
            nlower = g_test_rand_int_range(0, ntotal);
            nupper = ntotal - nlower;
        }
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_filter_trimmed_mean(filtered, kernel, col, row, width, height, nlower, nupper, NULL);

        GwyField *reference = gwy_field_copy(field);
        for (gint i = 0; i < height; i++) {
            for (gint j = 0; j < width; j++) {
                gdouble v = trimmed_mean_the_hard_way(field, kernel, col + j, row + i, nlower, nupper);
                gwy_field_set_val(reference, col + j, row + i, v);
            }
        }

#if 0
        dump_nield(kernel, "kernel");
        dump_field(field, 1, "field");
        dump_field(filtered, 1, "filtered");
        dump_field(reference, 1, "reference");
        gwy_field_subtract_fields(field, filtered, reference);
        dump_field(field, 1, "diff");
#endif

        // Check the result with tolerance. There may be errors in calculating the mean, and especially when there
        // is no trimming and the function reduces to a convoluion.
        assert_field_properties(filtered, reference);
        assert_field_content(filtered, gwy_field_get_data_const(reference),
                             gwy_field_get_xres(reference)*gwy_field_get_yres(reference),
                             2*width*height*DBL_EPSILON);

        g_assert_finalize_object(reference);
        g_assert_finalize_object(kernel);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

static void
make_bump(GwyField *field,
          gint col, gint row,
          gdouble x, gdouble y,
          gdouble h)
{
    GwyField *cut = gwy_field_area_extract(field, col-1, row-1, 3, 3);
    gdouble *d = gwy_field_get_data(cut);
    for (gint i = -1; i <= 1; i++) {
        gdouble yy = i - y;
        for (gint j = -1; j <= 1; j++) {
            gdouble xx = j - x;
            d[(i + 1)*3 + (j + 1)] += h*(3.0 - xx*xx - yy*yy);
        }
    }
    gwy_field_area_copy(cut, field, 0, 0, 3, 3, col-1, row-1);
    g_assert_finalize_object(cut);
}

void
test_field_local_maximum_subpixel(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(5, 30), yres = g_test_rand_int_range(5, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        gint col = g_test_rand_int_range(1, xres-1), row = g_test_rand_int_range(1, yres-1);
        gdouble x = 0.9*g_test_rand_double() - 0.45, y = 0.9*g_test_rand_double() - 0.45;
        gdouble h = 2*g_test_rand_double() + 3;

        make_bump(field, col, row, x, y, h);

        gdouble rcol = col, rrow = row;
        gboolean ok = gwy_field_local_maximum(field, &rcol, &rrow, 0, 0);
        g_assert_true(ok);
        g_assert_cmpfloat_with_epsilon(rcol - col, x, 1e-9);
        g_assert_cmpfloat_with_epsilon(rrow - row, y, 1e-9);

        g_assert_finalize_object(field);
    }
}

void
test_field_local_maximum_search(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(8, 30), yres = g_test_rand_int_range(8, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        gint col = g_test_rand_int_range(3, xres-3), row = g_test_rand_int_range(3, yres-3);
        gdouble h = 2*g_test_rand_double() + 3;

        make_bump(field, col, row, 0.0, 0.0, h);

        gdouble deltacol = g_test_rand_int_range(0, 7) - 3, deltarow = g_test_rand_int_range(0, 7) - 3;
        gdouble rcol = col + deltacol, rrow = row + deltarow;
        gboolean ok = gwy_field_local_maximum(field, &rcol, &rrow, ABS(deltacol) + 1, ABS(deltarow) + 1);
        g_assert_true(ok);
        g_assert_cmpfloat(fabs(rcol - col), <, 1e-9);
        g_assert_cmpfloat(fabs(rrow - row), <, 1e-9);

        g_assert_finalize_object(field);
    }
}

void
test_field_local_maximum_stripe(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(5, 30), yres = g_test_rand_int_range(5, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        gint col = g_test_rand_int_range(1, xres-1), row = g_test_rand_int_range(1, yres-1);
        gdouble h = 2*g_test_rand_double() + 3;

        make_bump(field, col, row, 0.0, 0.0, h);

        gdouble rcol, rrow;
        gboolean ok;

        rcol = col;
        rrow = (row <= yres/2 ? yres-1 : 0);
        ok = gwy_field_local_maximum(field, &rcol, &rrow, -1, 0);
        g_assert_false(ok);

        rcol = col;
        rrow = (row <= yres/2 ? yres-1 : 0);
        ok = gwy_field_local_maximum(field, &rcol, &rrow, 0, -1);
        g_assert_true(ok);

        rcol = (col <= xres/2 ? xres-1 : 0);
        rrow =  row;
        ok = gwy_field_local_maximum(field, &rcol, &rrow, 0, -1);
        g_assert_false(ok);

        rcol = (col <= xres/2 ? xres-1 : 0);
        rrow =  row;
        ok = gwy_field_local_maximum(field, &rcol, &rrow, -1, 0);
        g_assert_true(ok);

        g_assert_finalize_object(field);
    }
}

void
test_field_local_maximum_edge(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(5, 30), yres = g_test_rand_int_range(5, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);

        add_noise_to_field(field);

        // Move one of them to the edge. The function should fail then.
        gint col = g_test_rand_int_range(0, xres), row = g_test_rand_int_range(0, yres);
        if (g_test_rand_int() % 2)
            col = (col >= xres/2 ? xres-1 : 0);
        else
            row = (row >= yres/2 ? yres-1 : 0);
        gwy_field_set_val(field, col, row, 10.0 + 5.0*g_test_rand_double());

        gdouble rcol = col, rrow = row;
        gboolean ok;

        ok = gwy_field_local_maximum(field, &rcol, &rrow, g_test_rand_int_range(0, 10), g_test_rand_int_range(0, 10));
        g_assert_false(ok);
        g_assert_cmpfloat(rcol, ==, col);
        g_assert_cmpfloat(rrow, ==, row);

        ok = gwy_field_local_maximum(field, &rcol, &rrow, -1, g_test_rand_int_range(0, 10));
        g_assert_false(ok);
        g_assert_cmpfloat(rcol, ==, col);
        g_assert_cmpfloat(rrow, ==, row);

        ok = gwy_field_local_maximum(field, &rcol, &rrow, g_test_rand_int_range(0, 10), -1);
        g_assert_false(ok);
        g_assert_cmpfloat(rcol, ==, col);
        g_assert_cmpfloat(rrow, ==, row);

        ok = gwy_field_local_maximum(field, &rcol, &rrow, -1, -1);
        g_assert_false(ok);
        g_assert_cmpfloat(rcol, ==, col);
        g_assert_cmpfloat(rrow, ==, row);

        g_assert_finalize_object(field);
    }
}

static inline gdouble
make_wide_scale_number(void)
{
    gint sign = g_test_rand_int() % 2 ? 1 : -1;
    gdouble mag = exp(60*(g_test_rand_double() - 0.5));
    return sign*mag;
}

static gdouble
field_correlation_coeff(GwyField *fielda, GwyField *fieldb,
                        GwyNield *mask, GwyMaskingType masking,
                        gint col, gint row, gint width, gint height)
{
    // We should not execute this assertion at all if the region is empty.
    g_return_val_if_fail(!mask || gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height), 1.0);

    GwyField *adiff = gwy_field_copy(fielda), *bdiff = gwy_field_copy(fieldb);
    gwy_field_add(adiff, -gwy_field_area_mean(fielda, mask, masking, col, row, width, height));
    gwy_field_add(bdiff, -gwy_field_area_mean(fieldb, mask, masking, col, row, width, height));
    GwyField *adotb = gwy_field_new_alike(fielda, FALSE);
    gwy_field_multiply_fields(adotb, adiff, bdiff);
    gdouble s2a = gwy_field_area_mean_square(adiff, mask, masking, col, row, width, height);
    gdouble s2b = gwy_field_area_mean_square(bdiff, mask, masking, col, row, width, height);
    gdouble sab = gwy_field_area_mean(adotb, mask, masking, col, row, width, height);
    g_assert_finalize_object(adotb);
    g_assert_finalize_object(bdiff);
    g_assert_finalize_object(adiff);
    return sab/sqrt(s2a*s2b + G_MINDOUBLE);
}

static void
assert_unchanged_outside(GwyField *field, GwyField *original,
                         GwyNield *mask, GwyMaskingType masking,
                         gint col, gint row, gint width, gint height)
{
    gint xres = gwy_field_get_xres(original), yres = gwy_field_get_yres(original);
    g_assert_cmpint(gwy_field_get_xres(field), ==, xres);
    g_assert_cmpint(gwy_field_get_yres(field), ==, yres);
    GwyField *diff = gwy_field_new_alike(original, FALSE);
    gwy_field_subtract_fields(diff, field, original);
    gwy_field_area_fill(diff, mask, masking, col, row, width, height, 0.0);
    gwy_field_invalidate(field);
    gdouble min, max;
    gwy_field_min_max(diff, &min, &max);
    g_assert_cmpfloat(min, ==, 0.0);
    g_assert_cmpfloat(max, ==, 0.0);
    g_assert_finalize_object(diff);
}

void
test_field_normalize_random(void)
{
    guint n = g_test_slow() ? 50 : 10;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_multiply(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_normalize(filtered);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_min_max(filtered, &min, &max);
        g_assert_cmpfloat_with_epsilon(min, 0.0, DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, 1.0, DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(field_correlation_coeff(field, filtered, NULL, GWY_MASK_IGNORE,
                                                               0, 0, xres, yres), 1.0, xres*yres*DBL_EPSILON);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_normalize_flat(void)
{
    guint n = g_test_slow() ? 20 : 5;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(5, 30), yres = g_test_rand_int_range(5, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
        gwy_field_fill(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_normalize(filtered);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_min_max(filtered, &min, &max);
        // That's what the docs specify.
        g_assert_cmpfloat(min, ==, 0.0);
        g_assert_cmpfloat(max, ==, 0.0);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_renormalize_positive(void)
{
    guint n = g_test_slow() ? 50 : 10;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_multiply(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        // Be reasonable. Obivously offset of 1e50 and range 1e-50 will not work because doubles cannot represent such
        // thing.
        gdouble offset = make_wide_scale_number(), range = fabs(offset)*exp(5*(g_test_rand_double() - 0.5));
        gwy_field_renormalize(filtered, range, offset);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_min_max(filtered, &min, &max);
        g_assert_cmpfloat_with_epsilon(min, offset, DBL_EPSILON*fabs(offset));
        g_assert_cmpfloat_with_epsilon(max, offset + range, DBL_EPSILON*(fabs(offset + range)));
        g_assert_cmpfloat_with_epsilon(field_correlation_coeff(field, filtered, NULL, GWY_MASK_IGNORE,
                                                               0, 0, xres, yres), 1.0, xres*yres*DBL_EPSILON);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_renormalize_negative(void)
{
    guint n = g_test_slow() ? 50 : 10;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_multiply(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        // Be reasonable. Obivously offset of 1e50 and range 1e-50 will not work because doubles cannot represent such
        // thing.
        gdouble offset = make_wide_scale_number(), range = fabs(offset)*exp(5*(g_test_rand_double() - 0.5));
        gwy_field_renormalize(filtered, -range, offset);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_min_max(filtered, &min, &max);
        g_assert_cmpfloat_with_epsilon(min, offset - range, DBL_EPSILON*fabs(offset - range));
        g_assert_cmpfloat_with_epsilon(max, offset, DBL_EPSILON*fabs(offset));
        g_assert_cmpfloat_with_epsilon(field_correlation_coeff(field, filtered, NULL, GWY_MASK_IGNORE,
                                                               0, 0, xres, yres), 1.0, xres*yres*DBL_EPSILON);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_renormalize_flat(void)
{
    guint n = g_test_slow() ? 50 : 10;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gwy_field_fill(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        // Be reasonable. Obivously offset of 1e50 and range 1e-50 will not work because doubles cannot represent such
        // thing.
        gdouble offset = make_wide_scale_number(), range = fabs(offset)*exp(5*(g_test_rand_double() - 0.5));
        // Check that the result is filled with offset regardless of range sign for flat fields.
        if (g_test_rand_int() % 2)
            range = -range;
        gwy_field_renormalize(filtered, range, offset);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_min_max(filtered, &min, &max);
        g_assert_cmpfloat_with_epsilon(min, offset, DBL_EPSILON*fabs(offset));
        g_assert_cmpfloat_with_epsilon(max, offset, DBL_EPSILON*fabs(offset));
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_renormalize_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        gwy_field_multiply(field, make_wide_scale_number());
        GwyField *filtered = gwy_field_copy(field);
        // Be reasonable. Obivously offset of 1e50 and range 1e-50 will not work because doubles cannot represent such
        // thing.
        gdouble offset = make_wide_scale_number(), range = fabs(offset)*exp(5*(g_test_rand_double() - 0.5));
        if (g_test_rand_int() % 2)
            range = -range;
        gwy_field_area_renormalize(filtered, col, row, width, height, range, offset);
        gwy_field_invalidate(filtered);
        gdouble min, max;
        gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, col, row, width, height, &min, &max);
        if (width*height == 1) {
            g_assert_cmpfloat(min, ==, offset);
            g_assert_cmpfloat(max, ==, offset);
        }
        else {
            if (range > 0) {
                g_assert_cmpfloat_with_epsilon(min, offset, DBL_EPSILON*fabs(offset));
                g_assert_cmpfloat_with_epsilon(max, offset + range, DBL_EPSILON*fabs(offset + range));
            }
            else {
                g_assert_cmpfloat_with_epsilon(min, offset + range, DBL_EPSILON*fabs(offset + range));
                g_assert_cmpfloat_with_epsilon(max, offset, DBL_EPSILON*fabs(offset));
            }
            g_assert_cmpfloat_with_epsilon(field_correlation_coeff(field, filtered, NULL, GWY_MASK_IGNORE,
                                                                   col, row, width, height), 1.0,
                                           xres*yres*DBL_EPSILON);
        }
        assert_unchanged_outside(filtered, field, NULL, GWY_MASK_IGNORE, col, row, width, height);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_threshold_full(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_multiply(field, make_wide_scale_number());
        gdouble threshold = (g_test_rand_double() - 0.5)*5*gwy_field_rms(field) + gwy_field_mean(field);
        GwyField *filtered = gwy_field_copy(field);
        gdouble abv = g_test_rand_double(), bel = g_test_rand_double();
        gwy_field_threshold(filtered, threshold, bel, abv);
        gwy_field_invalidate(filtered);

        GwyNield *nield = gwy_field_new_nield_alike(field);
        gwy_nield_mark_by_threshold(nield, field, threshold, TRUE);
        GwyField *reference = gwy_field_new_alike(field, FALSE);
        gwy_field_area_fill_mask(reference, nield, 0, 0, xres, yres, bel, abv);

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_threshold_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        gwy_field_multiply(field, make_wide_scale_number());
        gdouble threshold = (g_test_rand_double() - 0.5)*5*gwy_field_rms(field) + gwy_field_mean(field);
        GwyField *filtered = gwy_field_copy(field);
        gdouble abv = g_test_rand_double(), bel = g_test_rand_double();
        gwy_field_area_threshold(filtered, col, row, width, height, threshold, bel, abv);

        GwyNield *nield = gwy_field_new_nield_alike(field);
        gwy_nield_mark_by_threshold(nield, field, threshold, TRUE);
        GwyField *reference = gwy_field_copy(field);
        gwy_field_area_fill_mask(reference, nield, col, row, width, height, bel, abv);

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_clamp_full(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gwy_field_multiply(field, make_wide_scale_number());
        gdouble lower = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        gdouble upper = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        GWY_ORDER(gdouble, lower, upper);
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_clamp(filtered, lower, upper);

        GwyNield *nield = gwy_field_new_nield_alike(field);
        GwyField *reference = gwy_field_copy(field);
        gwy_nield_mark_by_threshold(nield, field, upper, TRUE);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres, upper);
        gwy_nield_mark_by_threshold(nield, field, lower, FALSE);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, 0, 0, xres, yres, lower);

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_clamp_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        gwy_field_multiply(field, make_wide_scale_number());
        gdouble lower = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        gdouble upper = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        GWY_ORDER(gdouble, lower, upper);
        GwyField *filtered = gwy_field_copy(field);
        gwy_field_area_clamp(filtered, NULL, GWY_MASK_IGNORE, col, row, width, height, lower, upper);

        GwyNield *nield = gwy_field_new_nield_alike(field);
        GwyField *reference = gwy_field_copy(field);
        gwy_nield_mark_by_threshold(nield, field, upper, TRUE);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, col, row, width, height, upper);
        gwy_nield_mark_by_threshold(nield, field, lower, FALSE);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, col, row, width, height, lower);

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_clamp_masked(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        GwyNield *mask = make_random_mask(xres, yres, FALSE, TRUE, 3);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        gwy_field_multiply(field, make_wide_scale_number());
        gdouble lower = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        gdouble upper = (g_test_rand_double() - 0.5)*2*gwy_field_rms(field) + gwy_field_mean(field);
        GWY_ORDER(gdouble, lower, upper);
        GwyField *filtered = gwy_field_copy(field);
        GwyMaskingType masking = g_test_rand_int() % 2 ? GWY_MASK_INCLUDE : GWY_MASK_EXCLUDE;
        gwy_field_area_clamp(filtered, mask, masking, col, row, width, height, lower, upper);

        GwyNield *nield = gwy_field_new_nield_alike(field);
        GwyField *reference = gwy_field_copy(field);
        gwy_nield_mark_by_threshold(nield, field, upper, TRUE);
        gwy_nield_merge(nield, mask, masking == GWY_MASK_INCLUDE ? GWY_MERGE_INTERSECTION : GWY_MERGE_SUBTRACT);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, col, row, width, height, upper);
        gwy_nield_mark_by_threshold(nield, field, lower, FALSE);
        gwy_nield_merge(nield, mask, masking == GWY_MASK_INCLUDE ? GWY_MERGE_INTERSECTION : GWY_MERGE_SUBTRACT);
        gwy_field_area_fill(reference, nield, GWY_MASK_INCLUDE, col, row, width, height, lower);

        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

static void
filter_filter_3x3_single_pixels(GwyField *field)
{
    GwyField *extended = gwy_field_extend(field, 1, 1, 1, 1, GWY_EXTERIOR_BORDER, 0.0, FALSE);
    gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);

    // Check convolutions with all 9 possible single-pixel kernels. This does not check linearity, but the main issue
    // is various image border related nonsense.
    for (guint ki = 0; ki < 9; ki++) {
        gdouble kernel[9];
        gwy_clear(kernel, 9);
        kernel[ki] = G_PI;

        GwyField *filtered = gwy_field_new_alike(field, FALSE);
        gwy_field_filter_3x3(field, filtered, kernel);

        GwyField *reference = gwy_field_area_extract(extended, 2 - ki % 3, 2 - ki/3, xres, yres);
        gwy_field_multiply(reference, G_PI);
        field_assert_equal(G_OBJECT(filtered), G_OBJECT(reference));

        g_assert_finalize_object(reference);
        g_assert_finalize_object(filtered);
    }
    g_assert_finalize_object(extended);
}

void
test_field_filter_3x3_full_fat(void)
{
    enum { xres = 4, yres = 3 };
    const gdouble srcdata[xres*yres] = {
        1.0, 0.0, -5.0, 3.0,
        3.0, 2.0, 4.0, -1.0,
        0.0, 6.0, -5.0, 2.0,
    };
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_assign(gwy_field_get_data(field), srcdata, xres*yres);
    filter_filter_3x3_single_pixels(field);
    g_assert_finalize_object(field);
}

void
test_field_filter_3x3_full_tall1(void)
{
    enum { xres = 1, yres = 5 };
    const gdouble srcdata[xres*yres] = {
        1.0, 2.0, -4.0, 5.0, -3.0
    };
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_assign(gwy_field_get_data(field), srcdata, xres*yres);
    filter_filter_3x3_single_pixels(field);
    g_assert_finalize_object(field);
}

void
test_field_filter_3x3_full_tall2(void)
{
    enum { xres = 2, yres = 3 };
    const gdouble srcdata[xres*yres] = {
        1.0, 2.0,
        -4.0, 5.0,
        -3.0, -1.0,
    };
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_assign(gwy_field_get_data(field), srcdata, xres*yres);
    filter_filter_3x3_single_pixels(field);
    g_assert_finalize_object(field);
}

void
test_field_filter_3x3_full_squat1(void)
{
    enum { xres = 5, yres = 1 };
    const gdouble srcdata[xres*yres] = {
        1.0, 2.0, -4.0, 5.0, -3.0
    };
    GwyField *field = gwy_field_new(xres, yres, xres, yres, FALSE);
    gwy_assign(gwy_field_get_data(field), srcdata, xres*yres);
    filter_filter_3x3_single_pixels(field);
    g_assert_finalize_object(field);
}

void
test_field_filter_3x3_area(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        GwyField *extended = gwy_field_extend(field, 1, 1, 1, 1, GWY_EXTERIOR_BORDER, 0.0, FALSE);

        // Check convolutions with all 9 possible single-pixel kernels. This does not check linearity, but the main
        // issue is various image border related nonsense.
        for (guint ki = 0; ki < 9; ki++) {
            gdouble kernel[9];
            gwy_clear(kernel, 9);
            kernel[ki] = 1.0;

            GwyField *filtered = gwy_field_new_alike(field, FALSE);
            gwy_field_fill(filtered, G_PI);
            add_noise_to_field(filtered);
            GwyField *original = gwy_field_copy(filtered); // Do not trust the filter to not modify the input.
            gwy_field_area_filter_3x3(field, filtered, NULL, GWY_MASK_IGNORE, col, row, width, height, kernel);

            assert_unchanged_outside(filtered, original, NULL, GWY_MASK_IGNORE, col, row, width, height);

            GwyField *reference = gwy_field_area_extract(extended, col + 2 - ki % 3, row + 2 - ki/3, width, height);
            GwyField *area = gwy_field_area_extract(filtered, col, row, width, height);
            // Bloody rounding errors. We get to the same size through different intermediate fields, so the real
            // dimensions may not match exactly.
            g_assert_cmpfloat_with_epsilon(gwy_field_get_xreal(area), gwy_field_get_xreal(reference),
                                           xres*gwy_field_get_xreal(field)*DBL_EPSILON);
            g_assert_cmpfloat_with_epsilon(gwy_field_get_yreal(area), gwy_field_get_yreal(reference),
                                           yres*gwy_field_get_yreal(field)*DBL_EPSILON);
            gwy_field_set_xreal(area, gwy_field_get_xreal(reference));
            gwy_field_set_yreal(area, gwy_field_get_yreal(reference));
            field_assert_equal(G_OBJECT(area), G_OBJECT(reference));

            g_assert_finalize_object(area);
            g_assert_finalize_object(reference);
            g_assert_finalize_object(original);
            g_assert_finalize_object(filtered);
        }
        g_assert_finalize_object(extended);
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_3x3_masked(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint i = 0; i < n; i++) {
        GwyField *field = make_random_noise_field();
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        GwyNield *mask = make_random_mask(xres, yres, FALSE, TRUE, 3);
        GwyField *extended = gwy_field_extend(field, 1, 1, 1, 1, GWY_EXTERIOR_BORDER, 0.0, FALSE);

        // Check convolutions with all 9 possible single-pixel kernels. This does not check linearity, but the main
        // issue is various image border related nonsense.
        for (guint ki = 0; ki < 9; ki++) {
            gdouble kernel[9];
            gwy_clear(kernel, 9);
            kernel[ki] = 1.0;

            GwyField *filtered = gwy_field_new_alike(field, FALSE);
            gwy_field_fill(filtered, G_PI);
            add_noise_to_field(filtered);
            GwyField *original = gwy_field_copy(filtered); // Do not trust the filter to not modify the input.
            gwy_field_area_filter_3x3(field, filtered, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres, kernel);

            assert_unchanged_outside(filtered, original, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres);

            GwyField *reference = gwy_field_area_extract(extended, 2 - ki % 3, 2 - ki/3, xres, yres);
            gwy_field_subtract_fields(filtered, filtered, reference);
            gwy_field_area_fill(filtered, mask, GWY_MASK_EXCLUDE, 0, 0, xres, yres, 0.0);

            gdouble min, max;
            gwy_field_min_max(filtered, &min, &max);
            // That's what the docs specify.
            g_assert_cmpfloat(min, ==, 0.0);
            g_assert_cmpfloat(max, ==, 0.0);

            g_assert_finalize_object(reference);
            g_assert_finalize_object(original);
            g_assert_finalize_object(filtered);
        }
        g_assert_finalize_object(extended);
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

static void
test_derivative_filter_planar(GwyDerivativeFilterType filter_type, gint kind, gboolean as_derivative, gboolean in_place)
{
    guint n = g_test_slow() ? 100 : 10;

    for (guint i = 0; i < n; i++) {
        gdouble a, bx, by;
        GwyField *field = make_random_planar_field(&a, &bx, &by);
        GwyField *original = in_place ? NULL : gwy_field_copy(field);
        gint xres = gwy_field_get_xres(field), yres = gwy_field_get_yres(field);
        if (as_derivative) {
            bx /= gwy_field_get_dx(field);
            by /= gwy_field_get_dy(field);
        }
        gdouble expected = 0.0;
        if (ABS(kind) == 1)
            expected = bx;
        else if (ABS(kind) == 2)
            expected = by;
        else if (ABS(kind) == 3)
            expected = hypot(bx, by);

        GwyField *filtered;
        if (in_place)
            filtered = field;
        else
            filtered = gwy_field_new(1, 1, 1.0, 1.0, FALSE);

        gwy_field_derivative_filter(field, filtered, filter_type, as_derivative);

        if (xres > 2 && yres > 2) {
            // Boundary pixels are, unfortunately, weird. One thing we can always require: that the derivatives at
            // boundaries have the same sign and are at most of the same magnitude.
            //
            // The only filters for which we require to give the constant derivative even at boundaries are the
            // simplest xder and yder.
            //
            // For Laplace, we do not require anything for the boundary pixels.
            gdouble min, max;
            if (kind < 0)
                gwy_field_min_max(filtered, &min, &max);
            else if (kind == 1) {
                gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 1, 0, xres-2, yres, &min, &max);
                gwy_field_area_fill(filtered, NULL, GWY_MASK_IGNORE, 1, 0, xres-2, yres, 0.0);
            }
            else if (kind == 2) {
                gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 0, 1, xres, yres-2, &min, &max);
                gwy_field_area_fill(filtered, NULL, GWY_MASK_IGNORE, 0, 1, xres, yres-2, 0.0);
            }
            else {
                gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 1, 1, xres-2, yres-2, &min, &max);
                gwy_field_area_fill(filtered, NULL, GWY_MASK_IGNORE, 1, 1, xres-2, yres-2, 0.0);
            }

            gdouble eps = (kind == 0 ? 500 : 50);
            g_assert_cmpfloat(max, >=, min);
            g_assert_cmpfloat_with_epsilon(min, expected, eps*DBL_EPSILON);
            g_assert_cmpfloat_with_epsilon(max, expected, eps*DBL_EPSILON);

            if (kind > 0) {
                gwy_field_min_max(filtered, &min, &max);
                g_assert_cmpfloat(min*expected, >=, 0);
                g_assert_cmpfloat(max*expected, >=, 0);
                g_assert_cmpfloat(fabs(max), <=, fabs(expected)*(1.0 + 100*DBL_EPSILON));
            }
        }

        if (!in_place) {
            // Check if the source field is untouched.
            field_assert_equal(G_OBJECT(field), G_OBJECT(original));
            g_assert_finalize_object(filtered);
            g_assert_finalize_object(original);
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_derivative_xder_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER, -1, TRUE, TRUE);
}

void
test_field_filter_derivative_xder_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER, -1, TRUE, FALSE);
}

void
test_field_filter_derivative_xder_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER, -1, FALSE, TRUE);
}

void
test_field_filter_derivative_xder_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER, -1, FALSE, FALSE);
}

void
test_field_filter_derivative_yder_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER, -2, TRUE, TRUE);
}

void
test_field_filter_derivative_yder_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER, -2, TRUE, FALSE);
}

void
test_field_filter_derivative_yder_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER, -2, FALSE, TRUE);
}

void
test_field_filter_derivative_yder_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER, -2, FALSE, FALSE);
}

void
test_field_filter_derivative_der_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DER, -3, TRUE, TRUE);
}

void
test_field_filter_derivative_der_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DER, -3, TRUE, FALSE);
}

void
test_field_filter_derivative_der_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DER, -3, FALSE, TRUE);
}

void
test_field_filter_derivative_der_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DER, -3, FALSE, FALSE);
}

void
test_field_filter_derivative_hsobel_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSOBEL, 1, TRUE, TRUE);
}

void
test_field_filter_derivative_hsobel_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSOBEL, 1, TRUE, FALSE);
}

void
test_field_filter_derivative_hsobel_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSOBEL, 1, FALSE, TRUE);
}

void
test_field_filter_derivative_hsobel_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSOBEL, 1, FALSE, FALSE);
}

void
test_field_filter_derivative_vsobel_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSOBEL, 2, TRUE, TRUE);
}

void
test_field_filter_derivative_vsobel_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSOBEL, 2, TRUE, FALSE);
}

void
test_field_filter_derivative_vsobel_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSOBEL, 2, FALSE, TRUE);
}

void
test_field_filter_derivative_vsobel_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSOBEL, 2, FALSE, FALSE);
}

void
test_field_filter_derivative_sobel_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SOBEL, 3, TRUE, TRUE);
}

void
test_field_filter_derivative_sobel_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SOBEL, 3, TRUE, FALSE);
}

void
test_field_filter_derivative_sobel_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SOBEL, 3, FALSE, TRUE);
}

void
test_field_filter_derivative_sobel_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SOBEL, 3, FALSE, FALSE);
}

void
test_field_filter_derivative_hprewitt_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HPREWITT, 1, TRUE, TRUE);
}

void
test_field_filter_derivative_hprewitt_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HPREWITT, 1, TRUE, FALSE);
}

void
test_field_filter_derivative_hprewitt_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HPREWITT, 1, FALSE, TRUE);
}

void
test_field_filter_derivative_hprewitt_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HPREWITT, 1, FALSE, FALSE);
}

void
test_field_filter_derivative_vprewitt_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VPREWITT, 2, TRUE, TRUE);
}

void
test_field_filter_derivative_vprewitt_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VPREWITT, 2, TRUE, FALSE);
}

void
test_field_filter_derivative_vprewitt_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VPREWITT, 2, FALSE, TRUE);
}

void
test_field_filter_derivative_vprewitt_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VPREWITT, 2, FALSE, FALSE);
}

void
test_field_filter_derivative_prewitt_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_PREWITT, 3, TRUE, TRUE);
}

void
test_field_filter_derivative_prewitt_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_PREWITT, 3, TRUE, FALSE);
}

void
test_field_filter_derivative_prewitt_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_PREWITT, 3, FALSE, TRUE);
}

void
test_field_filter_derivative_prewitt_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_PREWITT, 3, FALSE, FALSE);
}

void
test_field_filter_derivative_hscharr_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSCHARR, 1, TRUE, TRUE);
}

void
test_field_filter_derivative_hscharr_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSCHARR, 1, TRUE, FALSE);
}

void
test_field_filter_derivative_hscharr_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSCHARR, 1, FALSE, TRUE);
}

void
test_field_filter_derivative_hscharr_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_HSCHARR, 1, FALSE, FALSE);
}

void
test_field_filter_derivative_vscharr_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSCHARR, 2, TRUE, TRUE);
}

void
test_field_filter_derivative_vscharr_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSCHARR, 2, TRUE, FALSE);
}

void
test_field_filter_derivative_vscharr_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSCHARR, 2, FALSE, TRUE);
}

void
test_field_filter_derivative_vscharr_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_VSCHARR, 2, FALSE, FALSE);
}

void
test_field_filter_derivative_scharr_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SCHARR, 3, TRUE, TRUE);
}

void
test_field_filter_derivative_scharr_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SCHARR, 3, TRUE, FALSE);
}

void
test_field_filter_derivative_scharr_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SCHARR, 3, FALSE, TRUE);
}

void
test_field_filter_derivative_scharr_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SCHARR, 3, FALSE, FALSE);
}

void
test_field_filter_derivative_laplace_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_LAPLACE, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_laplace_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_LAPLACE, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_laplace_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_LAPLACE, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_laplace_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_LAPLACE, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_xder2_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER2, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_xder2_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER2, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_xder2_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER2, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_xder2_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XDER2, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_yder2_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER2, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_yder2_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER2, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_yder2_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER2, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_yder2_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_YDER2, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_sxder2_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SXDER2, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_sxder2_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SXDER2, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_sxder2_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SXDER2, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_sxder2_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SXDER2, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_syder2_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SYDER2, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_syder2_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SYDER2, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_syder2_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SYDER2, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_syder2_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_SYDER2, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_xyder_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XYDER, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_xyder_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XYDER, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_xyder_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XYDER, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_xyder_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_XYDER, 0, FALSE, FALSE);
}

void
test_field_filter_derivative_det2der_planar_as_derivative_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DET2DER, 0, TRUE, TRUE);
}

void
test_field_filter_derivative_det2der_planar_as_derivative_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DET2DER, 0, TRUE, FALSE);
}

void
test_field_filter_derivative_det2der_planar_discrete_inplace(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DET2DER, 0, FALSE, TRUE);
}

void
test_field_filter_derivative_det2der_planar_discrete_other(void)
{
    test_derivative_filter_planar(GWY_DERIVATIVE_FILTER_DET2DER, 0, FALSE, FALSE);
}

static void
test_derivative_filter_quadratic(gboolean as_derivative, gboolean in_place,
                                 GwyDerivativeFilterType filter_type, gint kind)
{
    guint n = g_test_slow() ? 100 : 10;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(3, 20), yres = g_test_rand_int_range(3, 20);
        gdouble dx = exp(2*g_test_rand_double() - 1);
        gdouble dy = exp(2*g_test_rand_double() - 1);
        GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
        gdouble a = g_test_rand_double_range(-5.0, 5.0);
        gdouble bx = g_test_rand_double_range(-1.0, 1.0);
        gdouble by = g_test_rand_double_range(-1.0, 1.0);
        gdouble cxx = g_test_rand_double_range(-0.2, 0.2);
        gdouble cxy = g_test_rand_double_range(-0.2, 0.2);
        gdouble cyy = g_test_rand_double_range(-0.2, 0.2);
        gdouble *data = gwy_field_get_data(field);

        for (gint row = 0; row < yres; row++) {
            gdouble y = as_derivative ? dy*row : row;
            for (gint col = 0; col < xres; col++) {
                gdouble x = as_derivative ? dx*col : col;
                data[row*xres + col] = a + bx*x + by*y + cxx*x*x + cxy*x*y + cyy*y*y;
            }
        }
        GwyField *original = in_place ? NULL : gwy_field_copy(field);

        GwyField *filtered;
        if (in_place)
            filtered = field;
        else
            filtered = gwy_field_new(1, 1, 1.0, 1.0, FALSE);

        gwy_field_derivative_filter(field, filtered, filter_type, as_derivative);

        gdouble min, max;
        gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 1, 1, xres-2, yres-2, &min, &max);

        // The mixed derivative is just cxy, but the second unidirectional derivatives are 2*cxy and 2*cyy.
        gdouble expected = 0.0;
        if (kind == 0)
            expected = 2*(cxx + cyy);
        else if (kind == 1)
            expected = 2*cxx;
        else if (kind == 2)
            expected = 2*cyy;
        else if (kind == 3)
            expected = cxy;
        else if (kind == 4)
            expected = 4*cxx*cyy - cxy*cxy;

        g_assert_cmpfloat(max, >=, min);
        g_assert_cmpfloat_with_epsilon(min, expected, 8000*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, expected, 8000*DBL_EPSILON);

        if (!in_place) {
            // Check if the source field is untouched.
            field_assert_equal(G_OBJECT(field), G_OBJECT(original));
            g_assert_finalize_object(filtered);
            g_assert_finalize_object(original);
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_derivative_laplace_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_LAPLACE, 0);
}

void
test_field_filter_derivative_laplace_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_LAPLACE, 0);
}

void
test_field_filter_derivative_laplace_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_LAPLACE, 0);
}

void
test_field_filter_derivative_laplace_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_LAPLACE, 0);
}

void
test_field_filter_derivative_xder2_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_XDER2, 1);
}

void
test_field_filter_derivative_xder2_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_XDER2, 1);
}

void
test_field_filter_derivative_xder2_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_XDER2, 1);
}

void
test_field_filter_derivative_xder2_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_XDER2, 1);
}

void
test_field_filter_derivative_yder2_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_YDER2, 2);
}

void
test_field_filter_derivative_yder2_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_YDER2, 2);
}

void
test_field_filter_derivative_yder2_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_YDER2, 2);
}

void
test_field_filter_derivative_yder2_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_YDER2, 2);
}

void
test_field_filter_derivative_xyder_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_XYDER, 3);
}

void
test_field_filter_derivative_xyder_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_XYDER, 3);
}

void
test_field_filter_derivative_xyder_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_XYDER, 3);
}

void
test_field_filter_derivative_xyder_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_XYDER, 3);
}

void
test_field_filter_derivative_sxder2_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_SXDER2, 1);
}

void
test_field_filter_derivative_sxder2_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_SXDER2, 1);
}

void
test_field_filter_derivative_sxder2_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_SXDER2, 1);
}

void
test_field_filter_derivative_sxder2_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_SXDER2, 1);
}

void
test_field_filter_derivative_syder2_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_SYDER2, 2);
}

void
test_field_filter_derivative_syder2_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_SYDER2, 2);
}

void
test_field_filter_derivative_syder2_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_SYDER2, 2);
}

void
test_field_filter_derivative_syder2_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_SYDER2, 2);
}

void
test_field_filter_derivative_det2der_quadratic_as_derivative_inplace(void)
{
    test_derivative_filter_quadratic(TRUE, TRUE, GWY_DERIVATIVE_FILTER_DET2DER, 4);
}

void
test_field_filter_derivative_det2der_quadratic_as_derivative_other(void)
{
    test_derivative_filter_quadratic(TRUE, FALSE, GWY_DERIVATIVE_FILTER_DET2DER, 4);
}

void
test_field_filter_derivative_det2der_quadratic_discrete_inplace(void)
{
    test_derivative_filter_quadratic(FALSE, TRUE, GWY_DERIVATIVE_FILTER_DET2DER, 4);
}

void
test_field_filter_derivative_det2der_quadratic_discrete_other(void)
{
    test_derivative_filter_quadratic(FALSE, FALSE, GWY_DERIVATIVE_FILTER_DET2DER, 4);
}

static void
test_enhance_filter_const(gboolean in_place, GwyEnhanceFilterType filter_type)
{
    guint n = g_test_slow() ? 100 : 10;

    for (guint i = 0; i < n; i++) {
        gint xres = g_test_rand_int_range(3, 20), yres = g_test_rand_int_range(3, 20);
        gdouble dx = exp(2*g_test_rand_double() - 1);
        gdouble dy = exp(2*g_test_rand_double() - 1);
        GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
        gdouble a = g_test_rand_double_range(-5.0, 5.0);
        gwy_field_fill(field, a);
        GwyField *original = in_place ? NULL : gwy_field_copy(field);

        GwyField *filtered;
        if (in_place)
            filtered = field;
        else
            filtered = gwy_field_new(1, 1, 1.0, 1.0, FALSE);

        gwy_field_enhance_filter(field, filtered, filter_type);

        gdouble min, max;
        gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, &min, &max);

        g_assert_cmpfloat(max, >=, min);
        g_assert_cmpfloat_with_epsilon(min, a, 1000*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, a, 1000*DBL_EPSILON);

        if (!in_place) {
            // Check if the source field is untouched.
            field_assert_equal(G_OBJECT(field), G_OBJECT(original));
            g_assert_finalize_object(filtered);
            g_assert_finalize_object(original);
        }
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_enhance_dechecker_const_inplace(void)
{
    test_enhance_filter_const(TRUE, GWY_ENHANCE_FILTER_DECHECKER);
}

void
test_field_filter_enhance_dechecker_const_other(void)
{
    test_enhance_filter_const(FALSE, GWY_ENHANCE_FILTER_DECHECKER);
}

void
test_field_filter_enhance_kuwahara_const_inplace(void)
{
    test_enhance_filter_const(TRUE, GWY_ENHANCE_FILTER_KUWAHARA);
}

void
test_field_filter_enhance_kuwahara_const_other(void)
{
    test_enhance_filter_const(FALSE, GWY_ENHANCE_FILTER_KUWAHARA);
}

void
test_field_filter_enhance_mlv_const_inplace(void)
{
    test_enhance_filter_const(TRUE, GWY_ENHANCE_FILTER_MLV);
}

void
test_field_filter_enhance_mlv_const_other(void)
{
    test_enhance_filter_const(FALSE, GWY_ENHANCE_FILTER_MLV);
}

static void
test_filter_dechecker_full(gboolean in_place)
{
    guint n = g_test_slow() ? 100 : 10;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(5, 20), yres = g_test_rand_int_range(5, 20);
        gdouble dx = exp(2*g_test_rand_double() - 1);
        gdouble dy = exp(2*g_test_rand_double() - 1);
        GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
        gdouble a = g_test_rand_double_range(-5.0, 5.0), b = g_test_rand_double_range(-5.0, 5.0);
        gdouble *d = gwy_field_get_data(field);

        // Make a checkerboard pattern to suppress.
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++)
                d[i*xres + j] = ((i + j) % 2 ? a : b);
        }

        GwyField *filtered;
        if (in_place)
            filtered = field;
        else
            filtered = gwy_field_new(1, 1, 1.0, 1.0, FALSE);

        gwy_field_enhance_filter(field, filtered, GWY_ENHANCE_FILTER_DECHECKER);

        gdouble min, max;
        gwy_field_area_min_max(filtered, NULL, GWY_MASK_IGNORE, 2, 2, xres-4, yres-4, &min, &max);

        gdouble expected = 0.5*(a + b);
        g_assert_cmpfloat(max, >=, min);
        g_assert_cmpfloat_with_epsilon(min, expected, 1000*DBL_EPSILON);
        g_assert_cmpfloat_with_epsilon(max, expected, 1000*DBL_EPSILON);

        if (!in_place)
            g_assert_finalize_object(filtered);
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_enhance_dechecker_suppression_inplace_full(void)
{
    test_filter_dechecker_full(TRUE);
}

void
test_field_filter_enhance_dechecker_suppression_other_full(void)
{
    test_filter_dechecker_full(FALSE);
}

static void
test_filter_dechecker_masked(gboolean in_place)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(5, 20), yres = g_test_rand_int_range(5, 20);
        gdouble dx = exp(2*g_test_rand_double() - 1);
        gdouble dy = exp(2*g_test_rand_double() - 1);
        GwyField *field = gwy_field_new(xres, yres, xres*dx, yres*dy, TRUE);
        gdouble a = g_test_rand_double_range(-5.0, 5.0), b = g_test_rand_double_range(-5.0, 5.0);
        gdouble *d = gwy_field_get_data(field);
        GwyNield *mask = make_random_mask(xres, yres, FALSE, TRUE, 3);
        gint width = g_test_rand_int_range(1, xres), height = g_test_rand_int_range(1, yres);
        gint col = g_test_rand_int_range(0, xres-width), row = g_test_rand_int_range(0, yres-height);
        GwyMaskingType masking = g_test_rand_int() % 2 ? GWY_MASK_INCLUDE : GWY_MASK_EXCLUDE;

        // Make a checkerboard pattern to suppress.
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++)
                d[i*xres + j] = ((i + j) % 2 ? a : b);
        }
        GwyField *original = gwy_field_copy(field);

        GwyField *filtered;
        if (in_place)
            filtered = field;
        else
            filtered = gwy_field_copy(field);

        gwy_field_area_enhance_filter(field, filtered, mask, masking, col, row, width, height,
                                      GWY_ENHANCE_FILTER_DECHECKER);

        // We distinguish three distinct regions:
        // - untouched data outside the area and masking
        // - corrected data in the area, masked and farther from edge than 2 pixels
        // - processed data close to the edges, where we do not assert anything
        gint count = 0;
        gint col1 = MAX(col, 2), row1 = MAX(row, 2);
        gint col2 = MIN(col + width, xres-2), row2 = MIN(row + height, yres-2);
        if (col2 > col1 && row2 > row1) {
            GwyNield *fullmask = gwy_nield_new_alike(mask);
            gwy_nield_fill(fullmask, 1);
            count = gwy_nield_area_count(fullmask, mask, masking, col1, row1, col2 - col1, row2 - row1);
            g_assert_finalize_object(fullmask);
        }

        if (count) {
            gdouble min, max;
            gwy_field_area_min_max(filtered, mask, masking, col1, row1, col2 - col1, row2 - row1, &min, &max);

            gdouble expected = 0.5*(a + b);
            g_assert_cmpfloat(max, >=, min);
            g_assert_cmpfloat_with_epsilon(min, expected, 1000*DBL_EPSILON);
            g_assert_cmpfloat_with_epsilon(max, expected, 1000*DBL_EPSILON);
        }
        assert_unchanged_outside(filtered, original, mask, masking, col, row, width, height);

        if (!in_place) {
            field_assert_equal(G_OBJECT(field), G_OBJECT(original));
            g_assert_finalize_object(filtered);
        }
        g_assert_finalize_object(original);
        g_assert_finalize_object(mask);
        g_assert_finalize_object(field);
    }
}

void
test_field_filter_enhance_dechecker_suppression_inplace_masked(void)
{
    test_filter_dechecker_masked(TRUE);
}

void
test_field_filter_enhance_dechecker_suppression_other_masked(void)
{
    test_filter_dechecker_masked(FALSE);
}

static void
check_laplace_condition(const gdouble *data, gint xres, gint yres, gint col, gint row,
                        gdouble epsilon)
{
    gint k = row*xres + col;
    gint nn = 0;
    gdouble sn = 0.0;
    if (row) {
        sn += data[k - xres];
        nn++;
    }
    if (col) {
        sn += data[k-1];
        nn++;
    }
    if (col < xres-1) {
        sn += data[k+1];
        nn++;
    }
    if (row < yres-1) {
        sn += data[k + xres];
        nn++;
    }
    gdouble nnavg = sn/nn;
    g_assert_cmpfloat_with_epsilon(data[k], nnavg, epsilon);
}

static void
test_field_laplace_solve_single(gboolean number_it)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(2, 30), yres = g_test_rand_int_range(2, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *nield = make_random_mask(xres, yres, TRUE, TRUE, 3);
        if (number_it)
            gwy_nield_number_contiguous(nield);

        gint maxgno = gwy_nield_max(nield);

        // Try to interpolate each grain.
        const gdouble *refdata = gwy_field_get_data_const(field);
        const gint *mdata = gwy_nield_get_data_const(nield);
        for (gint gno = 1; gno <= maxgno; gno++) {
            GwyField *interpolated = gwy_field_copy(field);
            gwy_field_laplace_solve(interpolated, nield, gno, 1.0);

            const gdouble *data = gwy_field_get_data_const(interpolated);
            for (gint i = 0; i < yres; i++) {
                for (gint j = 0; j < xres; j++) {
                    gint k = i*xres + j;
                    if (mdata[k] == gno) {
                        // Check ∇²z = 0 (approximately).
                        check_laplace_condition(data, xres, yres, j, i, 1e-10);
                    }
                    else {
                        // Check other data are unchanged.
                        g_assert_cmpfloat(data[k], ==, refdata[k]);
                    }
                }
            }

            g_assert_finalize_object(interpolated);
        }

        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_laplace_solve_scattered(void)
{
    test_field_laplace_solve_single(FALSE);
}

void
test_field_laplace_solve_numbered(void)
{
    test_field_laplace_solve_single(TRUE);
}

static void
test_field_laplace_solve_merged(gboolean outside)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(2, 30), yres = g_test_rand_int_range(2, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *nield = make_random_mask(xres, yres, TRUE, TRUE, 3);

        const gdouble *refdata = gwy_field_get_data_const(field);
        const gint *mdata = gwy_nield_get_data_const(nield);
        GwyField *interpolated = gwy_field_copy(field);

        gwy_field_laplace_solve(interpolated, nield, outside ? GWY_LAPLACE_UNMASKED : GWY_LAPLACE_MASKED, 1.0);

        const gdouble *data = gwy_field_get_data_const(interpolated);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j;
                if ((outside && mdata[k] <= 0) || (!outside && mdata[k] > 0)) {
                    // Check ∇²z = 0 (approximately).
                    check_laplace_condition(data, xres, yres, j, i, 1e-10);
                }
                else {
                    // Check other data are unchanged.
                    g_assert_cmpfloat(data[k], ==, refdata[k]);
                }
            }
        }

        g_assert_finalize_object(interpolated);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_laplace_solve_masked(void)
{
    test_field_laplace_solve_merged(FALSE);
}

void
test_field_laplace_solve_unmasked(void)
{
    test_field_laplace_solve_merged(TRUE);
}

void
test_field_laplace_solve_all_grains(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(2, 30), yres = g_test_rand_int_range(2, 30);
        GwyField *field = gwy_field_new(xres, yres, xres, yres, TRUE);
        add_noise_to_field(field);

        GwyNield *nield = make_random_mask(xres, yres, TRUE, TRUE, 3);
        const gint *mdata = gwy_nield_get_data_const(nield);
        GwyField *interpolated = gwy_field_copy(field);

        gwy_field_laplace_solve(interpolated, nield, GWY_LAPLACE_ALL_GRAINS, 1.0);

        // The result does not satisfy any nice local condition where grains were touching because the interpolation
        // is from the original masked data there. Assume single-grain interpolation works because it has been tested
        // independently. Construct what we should get by running the interpolation grain by grain, always using
        // a fresh copy of the original data.
        GwyField *reference = gwy_field_copy(field);
        GwyField *workspace = gwy_field_new_alike(field, FALSE);
        gint m = gwy_nield_max(nield);
        GwyNield *grain = gwy_nield_new_alike(nield);

        for (gint i = 1; i <= m; i++) {
            gwy_nield_copy_data(nield, grain);
            if (!gwy_nield_isolate(grain, i, NULL))
                continue;
            gwy_field_copy_data(field, workspace);
            gwy_field_laplace_solve(workspace, grain, 1, 1.0);
            gwy_field_copy_data_mask(workspace, reference, grain, GWY_MASK_INCLUDE);
        }

        const gdouble *intdata = gwy_field_get_data_const(interpolated);
        const gdouble *refdata = gwy_field_get_data_const(reference);

        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j;
                if (mdata[k] > 0)
                    g_assert_cmpfloat_with_epsilon(intdata[k], refdata[k], 1e-9);
                else {
                    // Unmasked data must match exactly.
                    g_assert_cmpfloat(intdata[k], ==, refdata[k]);
                }
            }
        }

        g_assert_finalize_object(workspace);
        g_assert_finalize_object(grain);
        g_assert_finalize_object(reference);
        g_assert_finalize_object(interpolated);
        g_assert_finalize_object(nield);
        g_assert_finalize_object(field);
    }
}

void
test_field_tip_dilation(void)
{
    enum { txres = 3, tyres = 5 };
    static const gdouble tip_data[txres*tyres] = {
        1.0, 3.0, 5.0,
        0.0, 1.0, 4.0,
        0.0, 0.0, 3.0,
        0.0, 0.0, 2.0,
        0.0, 0.0, 1.0,
    };

    GwyField *tip = gwy_field_new(txres, tyres, txres, tyres, FALSE);
    gwy_assign(gwy_field_get_data(tip), tip_data, txres*tyres);

    GwyField *surface = gwy_field_new(40, 50, 40, 50, TRUE);
    gwy_field_set_val(surface, 10, 11, 1.0);
    gwy_field_set_val(surface, 30, 10, 2.0);
    gwy_field_set_val(surface, 11, 25, 3.0);
    gwy_field_set_val(surface, 33, 24, 4.0);
    gwy_field_set_val(surface, 9, 40, 5.0);
    gwy_field_set_val(surface, 31, 41, 6.0);
    // Shift the flat base plane to check what happens to the overall height.
    gwy_field_add(surface, -2.0);

    GwyField *result = gwy_field_new_alike(surface, FALSE);
    gwy_field_tip_dilation(surface, tip, result, TRUE, NULL, NULL);

    // Check that by scanning δ-functions of different heights, the expected tip images are formed.
    GwyField *reference = gwy_field_new_alike(surface, TRUE);

    gwy_field_add(tip, 1.0);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 31 - txres/2, 41 - tyres/2);

    gwy_field_add(tip, -1.0);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 9 - txres/2, 40 - tyres/2);

    gwy_field_add(tip, -1.0);
    gwy_field_clamp(tip, 0.0, G_MAXDOUBLE);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 33 - txres/2, 24 - tyres/2);

    gwy_field_add(tip, -1.0);
    gwy_field_clamp(tip, 0.0, G_MAXDOUBLE);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 11 - txres/2, 25 - tyres/2);

    gwy_field_add(tip, -1.0);
    gwy_field_clamp(tip, 0.0, G_MAXDOUBLE);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 30 - txres/2, 10 - tyres/2);

    gwy_field_add(tip, -1.0);
    gwy_field_clamp(tip, 0.0, G_MAXDOUBLE);
    gwy_field_area_copy(tip, reference, 0, 0, txres, tyres, 10 - txres/2, 11 - tyres/2);

    gwy_field_add(reference, -2.0);

    field_assert_equal(G_OBJECT(result), G_OBJECT(reference));

    g_assert_finalize_object(reference);
    g_assert_finalize_object(result);
    g_assert_finalize_object(surface);
    g_assert_finalize_object(tip);
}

void
test_field_tip_erosion_spike(void)
{
    guint n = g_test_slow() ? 1000 : 100;

    gulong ok_count = 0, weird_count = 0, bad_count = 0;
    for (guint itr = 0; itr < n; itr++) {
        gint txres = g_test_rand_int_range(2, 7), tyres = g_test_rand_int_range(2, 7);
        gint cx = g_test_rand_double_range(0.5, txres-0.5), cy = g_test_rand_double_range(0.5, tyres-0.5);
        gdouble q = g_test_rand_double_range(0.5, 1.5);
        GwyField *tip = gwy_field_new(txres, tyres, txres, tyres, FALSE);
        for (gint i = 0; i < tyres; i++) {
            gdouble y = i - cy;
            for (gint j = 0; j < txres; j++) {
                gdouble x = j - cx;
                // A parabola is symmetrical, but its discterisation is not, so this should be enough for asymmetry.
                gwy_field_set_val(tip, j, i, -q*(x*x + y*y + g_test_rand_double_range(0.0, 0.1)));
            }
        }
        gwy_field_add(tip, -gwy_field_min(tip));

        // We can have sharp features with negative curvature, but we cannot have non-unique contact with the surface if
        // we want to test that erosion reconstructs the original surface. We cannot have inner corners and large
        // positive curvature. Apart from trivial cases, it actually not easy to achieve a reconstruction with no
        // occasional weird pixels slightly wrong. So accept a small number of differences.
        gint xres = g_test_rand_int_range(12, 30), yres = g_test_rand_int_range(12, 30);
        gdouble sx = g_test_rand_double_range(5.0, xres-5.0), sy = g_test_rand_double_range(5.0, yres-5.0);
        gdouble R = g_test_rand_double_range(8.0, 30.0), delta = g_test_rand_double_range(1.0, 0.5*R);
        GwyField *surface = gwy_field_new(xres, yres, xres, yres, TRUE);
        for (gint i = 0; i < yres; i++) {
            gdouble y = i - sy;
            for (gint j = 0; j < xres; j++) {
                gdouble x = j - sx;
                gdouble r = hypot(x, y);
                if (r + delta < R)
                    gwy_field_set_val(surface, j, i, R - sqrt(R*R - (R - r - delta)*(R - r - delta)));
            }
        }
        GwyField *dilated = gwy_field_new_alike(surface, FALSE);
        gwy_field_tip_dilation(surface, tip, dilated, TRUE, NULL, NULL);

        GwyField *eroded = gwy_field_new_alike(dilated, FALSE);
        gwy_field_tip_erosion(dilated, tip, eroded, TRUE, NULL, NULL);

        gwy_field_subtract_fields(dilated, eroded, surface);
        const gdouble *diff = gwy_field_get_data_const(dilated);
        for (guint i = tyres/2; i < yres - tyres/2; i++) {
            for (guint j = txres/2; j < xres - txres/2; j++) {
                g_assert_cmpfloat(fabs(diff[i*xres + j]), <, 0.2);
                if (fabs(diff[i*xres + j]) < 1e-9)
                    ok_count++;
                else if (fabs(diff[i*xres + j]) < 0.05)
                    weird_count++;
                else
                    bad_count++;
            }
        }

        g_assert_finalize_object(eroded);
        g_assert_finalize_object(dilated);
        g_assert_finalize_object(surface);
        g_assert_finalize_object(tip);
    }

    g_assert_cmpuint(bad_count, <=, ok_count/100);
    g_assert_cmpuint(weird_count, <=, ok_count/12);
}

// This is a slow test. Blind tip reconstruction is not fast.
void
test_field_tip_blind_estimate(void)
{
    guint n = g_test_slow() ? 100 : 10;

    gwy_app_init(FALSE, "enable-threads", TRUE, NULL);
    GwyTipModel *model = gwy_inventory_get_item(gwy_tip_models(), "Pyramid");
    g_assert_cmpuint(gwy_tip_model_get_n_params(model), ==, 4);
    gdouble params[4];

    guint total_diff_ok = 0, total_plausible = 0;
    for (guint itr = 0; itr < n; itr++) {
        gint xres = g_test_rand_int_range(120, 300), yres = g_test_rand_int_range(120, 300);
        GwyField *surface = gwy_field_new(xres, yres, xres, yres, TRUE);
        gdouble *s = gwy_field_get_data(surface);
        for (gint i = 0; i < xres*yres; i++) {
            gint k = g_test_rand_int_range(0, xres*yres);
            s[k] += g_test_rand_double_range(0.0, 20.0);
        }
        gdouble min, max;
        gwy_field_min_max(surface, &min, &max);

        gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_NSIDES, g_test_rand_int_range(3, 6));
        gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_RADIUS, g_test_rand_double_range(0.5, 3.0));
        gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_SLOPE, g_test_rand_double_range(0.1, 0.2));
        gwy_tip_model_fill_param(model, params, GWY_TIP_PARAM_ROTATION, g_test_rand_double_range(0.0, G_PI));
        GwyField *tip = gwy_field_new(1, 1, 1.0, 1.0, FALSE);
        gwy_tip_model_create_for_zrange(model, tip, max - min, TRUE, params);

        // The tip model function may decide to create a pretty big tip image.
        gint txres = gwy_field_get_xres(tip), tyres = gwy_field_get_yres(tip);
        if (txres > xres/3 || tyres > yres/3) {
            xres = MAX(xres, 3*txres);
            yres = MAX(yres, 3*tyres);
            gwy_field_resize(surface, xres, yres);
            gwy_field_set_xreal(surface, xres);
            gwy_field_set_yreal(surface, yres);
            gwy_field_clear(surface);
            s = gwy_field_get_data(surface);
            for (gint i = 0; i < xres*yres; i++) {
                gint k = g_test_rand_int_range(0, xres*yres);
                s[k] += g_test_rand_double_range(0.0, 20.0);
            }
        }

        GwyField *dilated = gwy_field_new_alike(surface, FALSE);
        gwy_field_tip_dilation(surface, tip, dilated, TRUE, NULL, NULL);

        // This is usually way too big because gwy_tip_model_create_for_zrange() adds a safety margin.
        GwyField *estimated_tip = gwy_field_new_alike(tip, TRUE);
        gwy_field_tip_estimate_partial(dilated, estimated_tip, 1.0, FALSE, NULL, NULL);
        gwy_field_tip_estimate_full(dilated, estimated_tip, 1.0, FALSE, NULL, NULL);

        GwyField *diff = gwy_field_new_alike(tip, FALSE);
        gwy_field_subtract_fields(diff, estimated_tip, tip);
        gwy_field_add(diff, gwy_field_max(tip) - gwy_field_max(estimated_tip));

        GwyNield *plausible_mask = gwy_field_new_nield_alike(diff);
        // Tip range is not relevant. Mark parts of the tip that could be in contact with the surface.
        gdouble surfrange = gwy_field_max(dilated) - gwy_field_min(dilated);
        gwy_nield_mark_by_threshold(plausible_mask, tip, gwy_field_max(tip) - surfrange, TRUE);

        GwyNield *diff_mask = gwy_field_new_nield_alike(tip);
        gwy_nield_mark_range(diff_mask, diff, -0.1*surfrange, 0.1*surfrange);

        g_assert_cmpint(gwy_nield_get_val(diff_mask, txres/2, tyres/2), >, 0);
        g_assert_cmpint(gwy_nield_count(diff_mask), >=, gwy_nield_count(plausible_mask)/6);
        total_diff_ok += gwy_nield_count(diff_mask);
        total_plausible += gwy_nield_count(plausible_mask);

        g_assert_finalize_object(diff_mask);
        g_assert_finalize_object(plausible_mask);
        g_assert_finalize_object(diff);
        g_assert_finalize_object(estimated_tip);
        g_assert_finalize_object(dilated);
        g_assert_finalize_object(surface);
        g_assert_finalize_object(tip);
    }

    g_assert_cmpint(total_diff_ok, >=, total_plausible/4);
}

/* vim: set cin columns=120 tw=118 et ts=4 sw=4 cino=>1s,e0,n0,f0,{0,}0,^0,\:1s,=0,g1s,h0,t0,+1s,c3,(0,u0 : */
