/*
 *  $Id: stats.c 29741 2026-04-06 08:37:30Z yeti-dn $
 *  Copyright (C) 2003-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@gwyddion.net.
 *
 *  Copyright (C) 2013 Brazilian Nanotechnology National Laboratory
 *  E-mail: Vinicius Barboza <vinicius.barboza@lnnano.cnpem.br>
 *
 *  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 "config.h"
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/field.h"
#include "libgwyddion/level.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/linestats.h"
#include "libgwyddion/watershed.h"
#include "libgwyddion/filters.h"

#include "libgwyddion/omp.h"
#include "libgwyddion/internal.h"

enum {
    FOREGROUND_FLAG = 1,
    BACKGROUND_FLAG = 0,
};

typedef gdouble (*LineStatFunc)(GwyLine *dline);

/**
 * gwy_field_max:
 * @field: A data field.
 *
 * Finds the maximum value of a data field.
 *
 * This quantity is cached.
 *
 * Returns: The maximum value.
 **/
gdouble
gwy_field_max(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), -G_MAXDOUBLE);
    gwy_debug("%s", FCTEST(field, MAX) ? "cache" : "lame");

    if (!FCTEST(field, MAX)) {
        const gdouble *d = field->priv->data;
        gdouble max = d[0];
        gint i, n = field->xres * field->yres;

        /* Too trivial to parallelise. */
        for (i = 1; i < n; i++)
            max = fmax(max, d[i]);
        FCVAL(field, MAX) = max;
        field->priv->cached |= FCBIT(MAX);
    }

    return FCVAL(field, MAX);
}


/**
 * gwy_field_area_max:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Finds the maximum value in a rectangular part of a data field.
 *
 * Returns: The maximum value.  When the number of samples to calculate maximum of is zero, -%G_MAXDOUBLE is returned.
 **/
gdouble
gwy_field_area_max(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   gint col, gint row,
                   gint width, gint height)
{
    gdouble max = -G_MAXDOUBLE;

    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return max;

    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    if (!mask) {
        if (col == 0 && width == xres && row == 0 && height == field->yres)
            return gwy_field_max(field);

        /* Too trivial to parallelise? */
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;

            for (gint j = 0; j < width; j++)
                max = fmax(max, drow[j]);
        }
    }
    else {
        const gint *mpos = mask->priv->data + row*xres + col;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(max:max) \
            shared(datapos,mpos,masking,xres,width,height)
#endif
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            const gint *mrow = mpos + i*xres;

            for (gint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, masking))
                    max = fmax(max, drow[j]);
            }
        }
    }

    return max;
}

/**
 * gwy_field_min:
 * @field: A data field.
 *
 * Finds the minimum value of a data field.
 *
 * This quantity is cached.
 *
 * Returns: The minimum value.
 **/
gdouble
gwy_field_min(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), -G_MAXDOUBLE);
    gwy_debug("%s", FCTEST(field, MIN) ? "cache" : "lame");

    if (!FCTEST(field, MIN)) {
        const gdouble *d = field->priv->data;
        gdouble min = d[0];
        gint i, n = field->xres * field->yres;

        /* Too trivial to parallelise. */
        for (i = 1; i < n; i++)
            min = fmin(min, d[i]);
        FCVAL(field, MIN) = min;
        field->priv->cached |= FCBIT(MIN);
    }

    return FCVAL(field, MIN);
}

/**
 * gwy_field_area_min:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Finds the minimum value in a rectangular part of a data field.
 *
 * Returns: The minimum value.  When the number of samples to calculate minimum of is zero, %G_MAXDOUBLE is returned.
 **/
gdouble
gwy_field_area_min(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   gint col, gint row,
                   gint width, gint height)
{
    gdouble min = G_MAXDOUBLE;

    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return min;

    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    if (!mask) {
        if (col == 0 && width == xres && row == 0 && height == field->yres)
            return gwy_field_min(field);

        /* Too trivial to parallelise? */
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;

            for (gint j = 0; j < width; j++)
                min = fmin(min, drow[j]);
        }
    }
    else {
        const gint *mpos = mask->priv->data + row*xres + col;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(min:min) \
            shared(datapos,mpos,masking,xres,width,height)
#endif
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            const gint *mrow = mpos + i*xres;

            for (gint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, masking))
                    min = fmin(min, drow[j]);
            }
        }
    }

    return min;
}

/**
 * gwy_field_min_max:
 * @field: A data field.
 * @min: (out): Location to store minimum to.
 * @max: (out): Location to store maximum to.
 *
 * Finds minimum and maximum values of a data field.
 **/
void
gwy_field_min_max(GwyField *field,
                  gdouble *min,
                  gdouble *max)
{
    gboolean need_min = FALSE, need_max = FALSE;
    gdouble min1, max1;
    const gdouble *d;
    gint i, n;

    g_return_if_fail(GWY_IS_FIELD(field));

    if (min) {
        if (FCTEST(field, MIN))
            *min = FCVAL(field, MIN);
        else
            need_min = TRUE;
    }
    if (max) {
        if (FCTEST(field, MAX))
            *max = FCVAL(field, MAX);
        else
            need_max = TRUE;
    }

    if (!need_min && !need_max)
        return;
    else if (!need_min) {
        *max = gwy_field_max(field);
        return;
    }
    else if (!need_max) {
        *min = gwy_field_min(field);
        return;
    }

    d = field->priv->data;
    min1 = d[0];
    max1 = d[0];
    n = field->xres*field->yres;
    /* Too trivial to parallelise. */
    for (i = 1; i < n; i++) {
        min1 = fmin(min1, d[i]);
        max1 = fmax(max1, d[i]);
    }

    *min = min1;
    *max = max1;
    FCVAL(field, MIN) = min1;
    FCVAL(field, MAX) = max1;
    field->priv->cached |= FCBIT(MIN) | FCBIT(MAX);
}

/**
 * gwy_field_area_min_max:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @min: (out): Location to store minimum to.
 * @max: (out): Location to store maximum to.
 *
 * Finds minimum and maximum values in a rectangular part of a data field.
 *
 * Both @min and @max must be non-%NULL. Use gwy_field_area_get_min() or gwy_field_area_get_max() if you only need
 * one of the values.
 **/
void
gwy_field_area_min_max(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       gint col, gint row,
                       gint width, gint height,
                       gdouble *min,
                       gdouble *max)
{
    g_return_if_fail(min && max);
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;

    gdouble min1 = G_MAXDOUBLE, max1 = -G_MAXDOUBLE;
    gint xres = field->xres, yres = field->yres;
    const gdouble *datapos = field->priv->data + row*xres + col;

    if (!mask) {
        if (col == 0 && width == xres && row == 0 && height == yres) {
            gwy_field_min_max(field, min, max);
            return;
        }

        /* Too trivial to parallelise? */
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;

            for (gint j = 0; j < width; j++) {
                min1 = fmin(min1, drow[j]);
                max1 = fmax(max1, drow[j]);
            }
        }
    }
    else {
        const gint *mpos = mask->priv->data + row*xres + col;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(min:min1) reduction(max:max1) \
            shared(datapos,mpos,xres,width,height,masking)
#endif
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            const gint *mrow = mpos + i*xres;

            for (gint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, masking)) {
                    min1 = fmin(min1, drow[j]);
                    max1 = fmax(max1, drow[j]);
                }
            }
        }
    }

    *min = min1;
    *max = max1;
}

static gdouble
calc_sum_nomask(GwyField *field,
                gint col, gint row, gint width, gint height)
{
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;

    /* Too trivial to paralelise. */
    gdouble s = 0.0;
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        for (gint j = 0; j < width; j++)
            s += drow[j];
    }
    return s;
}

static gdouble
calc_sqdsum_nomask(GwyField *field,
                   gint col, gint row, gint width, gint height,
                   gdouble avg)
{
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;

    /* Too trivial to paralelise. */
    gdouble s = 0.0;
    if (avg != 0) {
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            for (gint j = 0; j < width; j++) {
                gdouble t = drow[j] - avg;
                s += t*t;
            }
        }
    }
    else {
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            for (gint j = 0; j < width; j++)
                s += drow[j]*drow[j];
        }
    }
    return s;
}

static gdouble
calc_median_nomask(GwyField *field,
                   gint col, gint row, gint width, gint height)
{
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    gdouble *buffer = g_new(gdouble, width*height);

    if (height == 1 || (col == 0 && width == xres))
        gwy_assign(buffer, datapos, width*height);
    else {
        for (gint i = 0; i < height; i++)
            gwy_assign(buffer + i*width, datapos + i*xres, width);
    }
    gdouble median = gwy_math_median(buffer, width*height);
    g_free(buffer);

    return median;
}

/**
 * gwy_field_sum:
 * @field: A data field.
 *
 * Sums all values in a data field.
 *
 * This quantity is cached.
 *
 * Returns: The sum of all values.
 **/
gdouble
gwy_field_sum(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    gwy_debug("%s", FCTEST(field, SUM) ? "cache" : "lame");

    if (!FCTEST(field, SUM)) {
        FCVAL(field, SUM) = calc_sum_nomask(field, 0, 0, field->xres, field->yres);
        field->priv->cached |= FCBIT(SUM);
    }

    return FCVAL(field, SUM);
}

/**
 * gwy_field_area_sum:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Sums values of a rectangular part of a data field.
 *
 * Returns: The sum of all values inside area.
 **/
gdouble
gwy_field_area_sum(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   gint col, gint row,
                   gint width, gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    if (!mask) {
        if (col == 0 && row == 0 && width == xres && height == yres)
            return gwy_field_sum(field);
        return calc_sum_nomask(field, col, row, width, height);
    }

    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *mpos = mask->priv->data + row*xres + col;
    gdouble sum = 0.0;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
    reduction(+:sum) \
    shared(datapos,mpos,xres,width,height,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = mpos + i*xres;
        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking))
                sum += drow[j];
        }
    }

    return sum;
}

/**
 * gwy_field_mean:
 * @field: A data field
 *
 * Computes average value of a data field.
 *
 * This quantity is cached.
 *
 * Returns: The average value.
 **/
gdouble
gwy_field_mean(GwyField *field)
{
    return gwy_field_sum(field)/((field->xres * field->yres));
}

/**
 * gwy_field_area_mean:
 * @field: A data field
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes average value of a rectangular part of a data field.
 *
 * Returns: The average value.
 **/
gdouble
gwy_field_area_mean(GwyField *field,
                    GwyNield *mask,
                    GwyMaskingType masking,
                    gint col, gint row,
                    gint width, gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    if (!mask)
        return calc_sum_nomask(field, col, row, width, height)/(width*height);

    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *mpos = mask->priv->data + row*xres + col;
    gdouble avg = 0.0;
    gsize n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:avg,n) \
            shared(datapos,mpos,xres,width,height,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = mpos + i*xres;

        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                avg += drow[j];
                n++;
            }
        }
    }

    return n ? avg/n : 0.0;
}

/**
 * gwy_field_rms:
 * @field: A data field.
 *
 * Computes root mean square value of a data field.
 *
 * The root mean square value is calculated with respect to the mean value. See gwy_field_mean_square() for
 * a similar function which does not subtract the mean value.
 *
 * This quantity is cached.
 *
 * Returns: The root mean square value.
 **/
gdouble
gwy_field_rms(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    gwy_debug("%s", FCTEST(field, RMS) ? "cache" : "lame");

    if (!FCTEST(field, RMS)) {
        gint xres = field->xres, yres = field->yres;
        gsize n = (gsize)xres * (gsize)yres;
        gdouble sqdsum = calc_sqdsum_nomask(field, 0, 0, xres, yres, gwy_field_mean(field));
        FCVAL(field, RMS) = (n > 1 ? sqrt(sqdsum/(n - 1)) : 0.0);
        field->priv->cached |= FCBIT(RMS);
    }

    return FCVAL(field, RMS);
}

/**
 * gwy_field_area_rms:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes root mean square value of deviations of a rectangular part of a data field.
 *
 * The root mean square value is calculated with respect to the mean value.
 *
 * Returns: The root mean square value of deviations from the mean value.
 **/
gdouble
gwy_field_area_rms(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   gint col, gint row,
                   gint width, gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    if (!mask) {
        if (col == 0 && row == 0 && width == xres && height == yres)
            return gwy_field_rms(field);
        gsize n = width*height;
        if (n == 1)
            return 0.0;
        gdouble avg = calc_sum_nomask(field, col, row, width, height)/n;
        return sqrt(calc_sqdsum_nomask(field, col, row, width, height, avg)/(n - 1));
    }

    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *mpos = mask->priv->data + row*xres + col;
    /* NB: Do not use Var[x] = E[x²] - E[x]². Bad, bad rounding errors. */
    gdouble avg = gwy_field_area_mean(field, mask, masking, col, row, width, height);
    gdouble rms = 0.0;
    gsize n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:rms,n) \
            shared(datapos,mpos,xres,width,height,avg,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = mpos + i*xres;

        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                gdouble t = drow[j] - avg;
                rms += t*t;
                n++;
            }
        }
    }

    return n > 1 ? sqrt(rms/(n - 1)) : 0.0;
}

/**
 * gwy_field_area_grainwise_rms:
 * @field: A data field.
 * @nield: (nullable): Number field specifying grain numbers.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes grain-wise root mean square value of deviations of a rectangular part of a data field.
 *
 * Grain-wise means that the mean value is determined for each grain separately. Grains are regions in @mask,
 * not necessarily cotinguous, which have the same number. Avoid huge numbers in @mask; the function allocates
 * memory proportional to gwy_nield_max().
 *
 * If all @mask is just a single grain, the function is equivalent to normal masked rms in %GWY_MASK_INCLUDE mode.
 * If you want grains to be defined as contiguous regions, use gwy_nield_number_contiguous() beforehand.
 *
 * It is possible, albeit probably ill-advised, to pass a %NULL @nield. The function then returns zero as it does for
 * empty @nield.
 *
 * Returns: The root mean square value of deviations from the mean value.
 **/
gdouble
gwy_field_area_grainwise_rms(GwyField *field,
                             GwyNield *nield,
                             gint col,
                             gint row,
                             gint width,
                             gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &nield, NULL))
        return 0.0;

    gint xres = field->xres;
    if (!nield)
        return 0.0;

    gint maxgno = gwy_nield_max(nield);
    if (maxgno <= 0)
        return 0.0;

    gdouble *avg = g_new0(gdouble, maxgno+1);
    gint *size = g_new0(gint, maxgno+1);
    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = nield->priv->data + row*xres + col;
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;
        for (gint j = 0; j < width; j++) {
            gint gno = mrow[j];
            if (gno > 0) {
                avg[gno] += drow[j];
                size[gno]++;
            }
        }
    }

    gsize n = 0;
    for (gint i = 1; i <= maxgno; i++) {
        if (size[i] > 1) {
            avg[i] /= size[i];
            /* Losing one degree of freedom per grain. */
            n += size[i] - 1;
        }
    }
    g_free(size);

    if (!n) {
        g_free(avg);
        return 0.0;
    }

    gdouble rms = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:rms) \
            shared(datapos,maskpos,avg,xres,width,height)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;
        for (gint j = 0; j < width; j++) {
            gint gno = mrow[j];
            if (gno > 0) {
                gdouble t = drow[j] - avg[gno];
                rms += t*t;
            }
        }
    }
    g_free(avg);

    return sqrt(rms/n);
}

/**
 * gwy_field_mean_square:
 * @field: A two-dimensional data field.
 *
 * Computes mean square value of a data field.
 *
 * See gwy_field_area_get_mean_square() for remarks.
 *
 * Returns: The mean square value.
 **/
gdouble
gwy_field_mean_square(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    gwy_debug("%s", FCTEST(field, MSQ) ? "cache" : "lame");

    if (!FCTEST(field, MSQ)) {
        gint xres = field->xres, yres = field->yres;
        FCVAL(field, MSQ) = calc_sqdsum_nomask(field, 0, 0, xres, yres, 0.0)/(xres*yres);
        field->priv->cached |= FCBIT(MSQ);
    }

    return FCVAL(field, MSQ);
}

/**
 * gwy_field_area_mean_square:
 * @field: A two-dimensional data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes mean square value of a rectangular part of a data field.
 *
 * Unlike gwy_field_rms(), this function does <emphasis>not</emphasis> subtract the mean value beforehand.
 * Therefore, it is useful to sum the squared values of data fields which can have the zero level set differently, for
 * instance when the field contains a distribution.
 *
 * Returns: The mean square value.
 **/
gdouble
gwy_field_area_mean_square(GwyField *field,
                           GwyNield *mask,
                           GwyMaskingType masking,
                           gint col,
                           gint row,
                           gint width,
                           gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    if (!mask) {
        if (col == 0 && row == 0 && width == xres && height == yres)
            return gwy_field_mean_square(field);
        return calc_sqdsum_nomask(field, col, row, width, height, 0.0)/(width*height);
    }

    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask->priv->data + row*xres + col;
    gsize n = 0;
    gdouble msq = 0.0;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:msq,n) \
            shared(datapos,maskpos,xres,width,height,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;

        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                msq += drow[j]*drow[j];
                n++;
            }
        }
    }

    return n ? msq/n : 0.0;
}

static void
compute_autorange(const gdouble *values, guint n,
                  gdouble min, gdouble max,
                  gdouble *rmin, gdouble *rmax)
{
    enum { AR_NDH = 512 };

    guint *dh;
    guint i, j;
    gdouble q;

    if (n < 4 || min >= max) {
        *rmin = MIN(min, max);
        *rmax = MAX(min, max);
        return;
    }

    q = AR_NDH/(max - min);
    dh = g_new(guint, AR_NDH);
    n = gwy_math_histogram(values, n, min, max, AR_NDH, dh);

    j = 0;
    for (i = j = 0; i < AR_NDH-1 && dh[i] < 5e-2*n/AR_NDH && j < 2e-2*n; i++)
        j += dh[i];
    *rmin = min + i/q;

    j = 0;
    for (i = AR_NDH-1, j = 0; i > 0 && dh[i] < 5e-2*n/AR_NDH && j < 2e-2*n; i--)
        j += dh[i];
    *rmax = min + (i + 1)/q;

    g_free(dh);

    /* Someone passed us NaNs in field? */
    if (!(*rmin < *rmax)) {
        *rmin = min;
        *rmax = max;
    }
}

/**
 * gwy_field_autorange:
 * @field: A data field.
 * @from: (out): Location to store range start.
 * @to: (out): Location to store range end.
 *
 * Computes data field value range with outliers cut-off.
 *
 * The purpose of this function is to find a range is suitable for false color mapping.  The precise method how it is
 * calculated is unspecified and may be subject to changes.
 *
 * However, it is guaranteed minimum <= @from <= @to <= maximum.
 *
 * The range is cached.
 **/
void
gwy_field_autorange(GwyField *field,
                    gdouble *from,
                    gdouble *to)
{
    g_return_if_fail(GWY_IS_FIELD(field));

    if ((from && !FCTEST(field, ARF)) || (to && !FCTEST(field, ART))) {
        gdouble min, max, rmin, rmax;

        gwy_field_min_max(field, &min, &max);
        compute_autorange(field->priv->data, field->xres*field->yres, min, max, &rmin, &rmax);
        FCVAL(field, ARF) = rmin;
        FCVAL(field, ART) = rmax;
        field->priv->cached |= FCBIT(ARF) | FCBIT(ART);
    }

    if (from)
        *from = FCVAL(field, ARF);
    if (to)
        *to = FCVAL(field, ART);
}

/**
 * gwy_field_area_autorange:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @from: (out): Location to store range start.
 * @to: (out): Location to store range end.
 *
 * Computes data field area value range with outliers cut-off.
 *
 * See gwy_field_autorange() for discussion.
 **/
void
gwy_field_area_autorange(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col, gint row,
                         gint width, gint height,
                         gdouble *from, gdouble *to)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;

    gint xres = field->xres, yres = field->yres;
    if (!mask && col == 0 && row == 0 && width == xres && height == yres) {
        gwy_field_autorange(field, from, to);
        return;
    }

    gdouble min, max, rmin, rmax;
    gwy_field_area_min_max(field, mask, masking, col, row, width, height, &min, &max);
    if (!(min < max)) {
        rmin = min;
        rmax = max;
    }
    else {
        const gdouble *datapos = field->priv->data + row*xres + col;
        const gint *maskpos = (mask ? mask->priv->data + row*xres + col : NULL);
        gdouble *values = g_new(gdouble, width*height);
        gint n = 0;

        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            if (maskpos) {
                const gint *mrow = maskpos + i*xres;
                for (gint j = 0; j < width; j++) {
                    if (nielded_included(mrow + j, masking))
                        values[n++] = drow[j];
                }
            }
            else {
                gwy_assign(values + n, drow, width);
                n += width;
            }
        }

        compute_autorange(values, n, min, max, &rmin, &rmax);
        g_free(values);
    }

    if (from)
        *from = rmin;
    if (to)
        *to = rmax;
}

static void
calc_stats_nomask(GwyField *field,
                  gint col, gint row, gint width, gint height,
                  gdouble *pavg, gdouble *psa, gdouble *ps2, gdouble *ps3, gdouble *ps4)
{
    gsize n = width*height;
    gdouble avg = calc_sum_nomask(field, col, row, width, height)/n;
    gdouble sa = 0.0, s2 = 0.0, s3 = 0.0, s4 = 0.0;
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sa,s2,s3,s4) \
            shared(datapos,width,height,xres,avg)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *d = datapos + i*xres;
        for (gint j = 0; j < width; j++) {
            gdouble dz = d[j] - avg, dz2 = dz*dz;
            sa += fabs(dz);
            s2 += dz2;
            s3 += dz*dz2;
            s4 += dz2*dz2;
        }
    }

    *pavg = avg;
    *psa = sa;
    *ps2 = s2;
    *ps3 = s3;
    *ps4 = s4;
}

static void
sums_to_stats(gdouble sa, gdouble s2, gdouble s3, gdouble s4, gsize n,
              gdouble *ra, gdouble *rms, gdouble *skew, gdouble *kurtosis)
{
    gdouble r = (n > 1 ? s2/(n - 1) : 0.0);

    if (rms)
        *rms = sqrt(r);
    if (ra)
        *ra = (n > 1 ? sa/n * pow(n/(n - 1.0), 0.62) : 0.0);

    /* Use the biased 1/n for the denominators. The idea is that having the denominators bias-corrected but keeping
     * 1/n in the numerators would only increase the bias. Correcting nothing is thus better. The real bias comes from
     * the data being highly correlated anyway. So just do not spend extra effort on making it worse. */
    if (skew) {
        if (r > 0.0)
            *skew = s3/sqrt(s2*s2*s2/n);
        else
            *skew = 0.0;
    }
    if (kurtosis) {
        if (r > 0.0)
            *kurtosis = s4/(s2*s2)*n - 3.0;
        else
            *kurtosis = 0.0;
    }
}

/**
 * gwy_field_stats:
 * @field: A data field.
 * @avg: (out) (optional): Where average height value of the surface should be stored, or %NULL.
 * @ra: (out) (optional): Where average value of irregularities should be stored, or %NULL.
 * @rms: (out) (optional): Where root mean square value of irregularities (Rq) should be stored, or %NULL.
 * @skew: (out) (optional): Where skew (symmetry of height distribution) should be stored, or %NULL.
 * @kurtosis: (out) (optional): Where kurtosis (peakedness of height ditribution) should be stored, or %NULL.
 *
 * Computes basic statistical quantities of a data field.
 *
 * Note the kurtosis returned by this function returns is the excess kurtosis
 * which is zero for the Gaussian distribution (not 3).
 **/
void
gwy_field_stats(GwyField *field,
                gdouble *pavg,
                gdouble *ra,
                gdouble *prms,
                gdouble *skew,
                gdouble *kurtosis)
{
    g_return_if_fail(GWY_IS_FIELD(field));

    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    gdouble avg, rms, sa, s2, s3, s4;
    calc_stats_nomask(field, 0, 0, xres, yres, &avg, &sa, &s2, &s3, &s4);
    sums_to_stats(sa, s2, s3, s4, n, ra, &rms, skew, kurtosis);
    if (pavg)
        *pavg = avg;
    if (prms)
        *prms = rms;

    if (!FCTEST(field, SUM)) {
        FCVAL(field, SUM) = avg*n;
        field->priv->cached |= FCBIT(SUM);
    }
    if (!FCTEST(field, RMS)) {
        FCVAL(field, RMS) = rms;
        field->priv->cached |= FCBIT(RMS);
    }
}

/**
 * gwy_field_area_stats:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @avg: (out) (optional): Where average height value of the surface should be stored, or %NULL.
 * @ra: (out) (optional): Where average value of irregularities should be stored, or %NULL.
 * @rms: (out) (optional): Where root mean square value of irregularities (Rq) should be stored, or %NULL.
 * @skew: (out) (optional): Where skew (symmetry of height distribution) should be stored, or %NULL.
 * @kurtosis: (out) (optional): Where excess kurtosis (peakedness of height ditribution) should be stored, or %NULL.
 *
 * Computes basic statistical quantities of a rectangular part of a data field.
 *
 * Note the kurtosis returned by this function returns is the excess kurtosis which is zero for the Gaussian
 * distribution (not 3).
 **/
void
gwy_field_area_stats(GwyField *field,
                     GwyNield *mask,
                     GwyMaskingType masking,
                     gint col, gint row,
                     gint width, gint height,
                     gdouble *pavg,
                     gdouble *ra,
                     gdouble *rms,
                     gdouble *skew,
                     gdouble *kurtosis)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;

    gint xres = field->xres;
    gdouble avg, sa = 0.0, s2 = 0.0, s3 = 0.0, s4 = 0.0;
    if (!mask) {
        calc_stats_nomask(field, col, row, width, height, &avg, &sa, &s2, &s3, &s4);
        sums_to_stats(sa, s2, s3, s4, width*height, ra, rms, skew, kurtosis);
        if (pavg)
            *pavg = avg;
        return;
    }

    avg = gwy_field_area_mean(field, mask, masking, col, row, width, height);

    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask->priv->data + row*xres + col;
    gsize n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
    reduction(+:sa,s2,s3,s4,n) \
    shared(datapos,maskpos,xres,width,height,avg,masking)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;
        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking)) {
                gdouble dz = drow[j] - avg, dz2 = dz*dz;
                sa += fabs(dz);
                s2 += dz2;
                s3 += dz*dz2;
                s4 += dz2*dz2;
                n++;
            }
        }
    }

    sums_to_stats(sa, s2, s3, s4, n, ra, rms, skew, kurtosis);
    if (pavg)
        *pavg = avg;
}

/**
 * gwy_field_area_count_in_range:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @below: Upper bound to compare data to.  The number of samples less than or equal to @below is stored in @nbelow.
 * @above: Lower bound to compare data to.  The number of samples greater than or equal to @above is stored in
 *         @nabove.
 * @nbelow: (out) (optional): Location to store the number of samples less than or equal to @below, or %NULL.
 * @nabove: (out) (optional): Location to store the number of samples greater than or equal to @above, or %NULL.
 *
 * Counts data samples in given range.
 *
 * No assertion is made about the values of @above and @below, in other words @above may be larger than @below.  To
 * count samples in an open interval instead of a closed interval, exchange @below and @above and then subtract the
 * @nabove and @nbelow from @width*@height to get the complementary counts.
 *
 * With this trick the common task of counting positive values can be realized:
 * <informalexample><programlisting>
 * gwy_field_area_count_in_range(field, NULL,
 *                               col, row, width, height,
 *                               0.0, 0.0, &amp;count, NULL);
 * count = width*height - count;
 * </programlisting></informalexample>
 **/
void
gwy_field_area_count_in_range(GwyField *field,
                              GwyNield *mask,
                              GwyMaskingType masking,
                              gint col, gint row,
                              gint width, gint height,
                              gdouble below,
                              gdouble above,
                              gint *nbelow,
                              gint *nabove)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;

    if (!nabove && !nbelow)
        return;

    gint na = 0, nb = 0;
    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    if (mask) {
        const gint *mpos = mask->priv->data + row*xres + col;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:na,nb) \
            shared(datapos,mpos,masking,xres,width,height,above,below)
#endif
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;
            const gint *mrow = mpos + i*xres;

            for (gint j = 0; j < width; j++) {
                guint c = nielded_included(mrow + j, masking);
                na += c*(drow[j] >= above);
                nb += c*(drow[j] <= below);
            }
        }
    }
    else {
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:na,nb) \
            shared(datapos,xres,width,height,above,below)
#endif
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = datapos + i*xres;

            for (gint j = 0; j < width; j++) {
                na += (drow[j] >= above);
                nb += (drow[j] <= below);
            }
        }
    }

    if (nabove)
        *nabove = na;
    if (nbelow)
        *nbelow = nb;
}

/*
 * class_weight:
 * @hist: A #GwyLine histogram from where to calculate the class probability for @t.
 * @t: A threshold value.
 * @flag: Alternates between %BACKGROUND or %FOREGROUND class probabilities.
 *
 * Returns: The probability for a class given a threshold value.
 **/
static gdouble
class_weight(GwyLine *hist,
             gint t,
             gint flag)
{
    gint len = hist->res;
    const gdouble *data = hist->priv->data;
    gdouble roi = 0.0;
    gdouble total = 0.0;

    for (gint i = 0; i < len; i++)
        total += data[i];

    for (gint i = flag*t; i < (1 - flag)*t + flag*len; i++)
        roi += data[i];

    return roi/total;
}

/*
 * class_mean:
 * @hist: A #GwyLine histogram from where to calculate the class mean for @t.
 * @t: A threshold value.
 * @flag: Alternates between %BACKGROUND or %FOREGROUND class mean.
 *
 * Returns: The mean value for a class given a threshold value.
 **/
static gdouble
class_mean(GwyLine *hist,
           gdouble min, gdouble max,
           gint t,
           gint flag)
{
    gint len = hist->res;
    const gdouble *data = hist->priv->data;
    gdouble roi = 0.0;
    gdouble total = 0.0;

    if (!t && !flag)
        return 0.0;

    for (gint i = flag*t; i < (1 - flag)*t + flag*len; i++) {
        gdouble val = data[i];
        gdouble bin = min + ((i + 0.5) * (max-min)/len);
        roi += bin * val;
        total += val;
    }

    return roi/total;
}

/**
 * gwy_field_otsu_threshold:
 * @field: A data field.
 *
 * Finds Otsu's height threshold for a data field.
 *
 * The Otsu's threshold is optimal in the sense that it minimises the inter-class variances of two classes of pixels:
 * above and below theshold.
 *
 * Returns: The threshold.
 **/
gdouble
gwy_field_otsu_threshold(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);

    /* Getting histogram length and max/min values for the data field */
    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    gdouble min, max;
    gwy_field_min_max(field, &min, &max);
    if (min == max)
        return min;

    guint nstats = floor(13.49*cbrt(n) + 0.5);
    GwyLine *hist = gwy_line_new(nstats, 1, FALSE);
    gwy_field_height_dist(field, hist, nstats);

    /* Calculating the threshold */
    gdouble thresh = 0.0, max_var = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(hist,nstats,min,max,thresh,max_var)
#endif
    for (guint t = 0; t < nstats; t++) {
        /* Getting histogram statistics for background classes */
        gdouble weight_0 = class_weight(hist, t, BACKGROUND_FLAG);
        gdouble mean_0 = class_mean(hist, min, max, t, BACKGROUND_FLAG);
        /* Getting histogram statistics for fogeground classes */
        gdouble weight_1 = class_weight(hist, t, FOREGROUND_FLAG);
        gdouble mean_1 = class_mean(hist, min, max, t, FOREGROUND_FLAG);

        /* Interclass variance */
        gdouble var = weight_0 * weight_1 * (mean_0 - mean_1) * (mean_0 - mean_1);

#ifdef _OPENMP
#pragma omp critical
#endif
        /* Check for greater interclass variance */
        if (var > max_var) {
            max_var = var;
            thresh = min + (t + 0.5)*(max - min)/nstats;
        }
    }

    g_object_unref(hist);

    return thresh;
}

/**
 * gwy_field_area_height_dist:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable resolution is
 *          determined automatically.
 *
 * Calculates distribution of heights in a rectangular part of data field.
 **/
void
gwy_field_area_height_dist(GwyField *field,
                           GwyNield *mask,
                           GwyMaskingType masking,
                           GwyLine *target_line,
                           gint col, gint row,
                           gint width, gint height,
                           gint nstats)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));

    gint xres = field->xres;
    const gdouble *data = field->priv->data;
    gsize nn = width*height;
    if (masking != GWY_MASK_IGNORE) {
        gsize nmasked = gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);
        nn = (masking == GWY_MASK_INCLUDE ? nmasked : nn - nmasked);
    }

    if (nstats < 1)
        nstats = dist_points_for_n_points(nn);
    gwy_line_resize(target_line, nstats);

    gdouble min, max;
    gwy_field_area_min_max(field, nn ? mask : NULL, masking, col, row, width, height, &min, &max);

    /* Set correct units first. */
    gwy_unit_assign(gwy_line_get_unit_x(target_line), gwy_field_get_unit_z(field));
    gwy_unit_power(gwy_line_get_unit_y(target_line), gwy_line_get_unit_x(target_line), -1);

    /* Handle edge cases. */
    if (!nn) {
        gwy_line_clear(target_line);
        gwy_line_set_real(target_line, min < max ? max - min : 1.0);
        return;
    }
    if (!(min < max)) {
        gwy_line_clear(target_line);
        gwy_line_set_real(target_line, min ? fabs(max) : 1.0);
        target_line->priv->data[0] = nstats/gwy_line_get_real(target_line);
        return;
    }

    /* Calculate height distribution */
    gwy_line_set_real(target_line, max - min);
    gwy_line_set_offset(target_line, min);
    guint *counts = g_new(guint, nstats);

    if (mask) {
        gdouble *values = g_new(gdouble, nn);
        nn = 0;
        for (gint i = 0; i < height; i++) {
            const gdouble *drow = data + (i + row)*xres + col;
            const gint *mrow = mask->priv->data + (i + row)*xres + col;

            for (gint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, masking))
                    values[nn++] = drow[j];
            }
        }
        nn = gwy_math_histogram(values, nn, min, max, nstats, counts);
        g_free(values);
    }
    else if (width < xres) {
        gdouble *values = g_new(gdouble, nn);
        for (gint i = 0; i < height; i++)
            gwy_assign(values + i*width, data + (i + row)*xres + col, width);
        nn = gwy_math_histogram(values, nn, min, max, nstats, counts);
        g_free(values);
    }
    else {
        /* Contiguous block, not need to allocate anything. */
        g_assert(width == xres);
        nn = gwy_math_histogram(data + row*xres, nn, min, max, nstats, counts);
    }

    gdouble *ldata = target_line->priv->data;
    for (gint i = 0; i < nstats; i++)
        ldata[i] = counts[i];
    g_free(counts);

    /* Normalise integral to 1. */
    gwy_line_multiply(target_line, nstats/(max - min)/MAX(nn, 1));
}

/**
 * gwy_field_height_dist:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable resolution is
 *          determined automatically.
 *
 * Calculates distribution of heights in a data field.
 **/
void
gwy_field_height_dist(GwyField *field,
                      GwyLine *target_line,
                      gint nstats)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_height_dist(field, NULL, GWY_MASK_IGNORE, target_line, 0, 0, field->xres, field->yres, nstats);
}

/**
 * gwy_field_area_angle_dist:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @orientation: Orientation to compute the slope distribution in.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable resolution is
 *          determined automatically.
 *
 * Calculates distribution of slopes in a rectangular part of data field, with masking.
 **/
void
gwy_field_area_angle_dist(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          GwyLine *target_line,
                          gint col, gint row,
                          gint width, gint height,
                          GwyOrientation orientation,
                          gint nstats)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_LINE(target_line));

    /* Set correct units first */
    gwy_unit_divide(gwy_line_get_unit_x(target_line), gwy_field_get_unit_z(field), gwy_field_get_unit_xy(field));
    gwy_unit_power(gwy_line_get_unit_y(target_line), gwy_line_get_unit_x(target_line), -1);

    gint xres = field->xres, yres = field->yres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask ? mask->priv->data + row*xres + col : NULL;
    gsize nn = 0;

    if (maskpos) {
        if (orientation == GWY_ORIENTATION_VERTICAL) {
            for (gint i = 0; i < height-1; i++) {
                const gint *m1 = maskpos + i*xres, *m2 = m1 + xres;
                for (gint j = 0; j < width; j++)
                    nn += nielded_included(m1 + j, masking)*nielded_included(m2 + j, masking);
            }
        }
        else {
            for (gint i = 0; i < height; i++) {
                const gint *m = maskpos + i*xres;
                for (gint j = 0; j < width-1; j++)
                    nn += nielded_included(m + j, masking)*nielded_included(m + j+1, masking);
            }
        }
    }
    else {
        if (orientation == GWY_ORIENTATION_VERTICAL)
            nn = width*(height - 1);
        else
            nn = (width - 1)*height;
    }

    if (nstats < 1) {
        nstats = floor(3.49*cbrt(nn) + 0.5);
        nstats = MAX(nstats, 2);
    }

    gwy_line_resize(target_line, nstats);

    gdouble q = (orientation == GWY_ORIENTATION_VERTICAL ? yres/field->yreal : xres/field->xreal);
    gdouble *values = g_new(gdouble, MAX(nn, 1));
    if (mask) {
        gsize k = 0;
        if (orientation == GWY_ORIENTATION_VERTICAL) {
            for (gint i = 0; i < height-1; i++) {
                const gdouble *d1 = datapos + i*xres, *d2 = d1 + xres;
                const gint *m1 = maskpos + i*xres, *m2 = m1 + xres;
                for (gint j = 0; j < width; j++) {
                    if (nielded_included(m1 + j, masking) && nielded_included(m2 + j, masking))
                        values[k++] = q*(d2[j] - d1[j]);
                }
            }
        }
        else {
            for (gint i = 0; i < height; i++) {
                const gdouble *d = datapos + i*xres;
                const gint *m = maskpos + i*xres;
                for (gint j = 0; j < width-1; j++) {
                    if (nielded_included(m + j, masking) && nielded_included(m + j+1, masking))
                        values[k++] = q*(d[j+1] - d[j]);
                }
            }
        }
    }
    else {
        gsize k = 0;
        if (orientation == GWY_ORIENTATION_VERTICAL) {
            for (gint i = 0; i < height-1; i++) {
                const gdouble *d1 = datapos + i*xres, *d2 = d1 + xres;
                for (gint j = 0; j < width; j++)
                    values[k++] = q*(d2[j] - d1[j]);
            }
        }
        else {
            for (gint i = 0; i < height; i++) {
                const gdouble *d = datapos + i*xres;
                for (gint j = 0; j < width-1; j++)
                    values[k++] = q*(d[j+1] - d[j]);
            }
        }
    }

    gdouble min = values[0], max = values[0];
    for (gsize k = 1; k < nn; k++) {
        min = fmin(min, values[k]);
        max = fmax(max, values[k]);
    }

    /* Handle edge cases. */
    if (!nn) {
        gwy_line_clear(target_line);
        gwy_line_set_real(target_line, 1.0);
        gwy_line_set_offset(target_line, -0.5);
        return;
    }

    gdouble *ldata = target_line->priv->data;
    if (!(min < max)) {
        gwy_line_clear(target_line);
        gwy_line_set_real(target_line, min ? fabs(max) : 1.0);
        ldata[0] = nstats/gwy_line_get_real(target_line);
        return;
    }

    guint *counts = g_new(guint, nstats);
    nn = gwy_math_histogram(values, nn, min, max, nstats, counts);
    g_free(values);

    for (gint i = 0; i < nstats; i++)
        ldata[i] = counts[i];
    g_free(counts);

    gwy_line_set_real(target_line, max - min);
    gwy_line_set_offset(target_line, min);
    gwy_line_multiply(target_line, nstats/(max - min)/nn);
}

/**
 * gwy_field_angle_dist:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to requested width.
 * @orientation: Orientation to compute the slope distribution in.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable resolution is
 *          determined automatically.
 *
 * Calculates distribution of slopes in a data field.
 **/
void
gwy_field_angle_dist(GwyField *field,
                     GwyLine *target_line,
                     GwyOrientation orientation,
                     gint nstats)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_angle_dist(field, NULL, GWY_MASK_IGNORE, target_line,
                              0, 0, field->xres, field->yres, orientation, nstats);
}

static void
compute_slopes(GwyField *field, gint kernel_size,
               GwyField *xder, GwyField *yder)
{
    GwyPlaneFitQuantity quantites[] = { GWY_PLANE_FIT_BX, GWY_PLANE_FIT_BY };
    GwyField *fields[2];
    gint xres, yres;

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);
    if (kernel_size > 1) {
        fields[0] = xder;
        fields[1] = yder;
        gwy_field_fit_local_planes(field, kernel_size, 2, quantites, fields);
        gwy_field_multiply(xder, xres/field->xreal);
        gwy_field_multiply(yder, yres/field->yreal);
    }
    else {
        gwy_field_derivative_filter(field, xder, GWY_DERIVATIVE_FILTER_XDER, TRUE);
        gwy_field_derivative_filter(field, yder, GWY_DERIVATIVE_FILTER_YDER, TRUE);
    }
}

/**
 * gwy_field_slope_distribution:
 * @field: A data field.
 * @derdist: A data line to fill with angular slope distribution. Its resolution determines resolution of the
 *           distribution.
 * @kernel_size: If positive, local plane fitting will be used for slope computation; if nonpositive, plain central
 *               derivations will be used.
 *
 * Computes angular slope distribution.
 **/
void
gwy_field_slope_distribution(GwyField *field,
                             GwyLine *derdist,
                             gint kernel_size)
{
    GwyField *xder, *yder;
    gdouble *der;
    gint xres, yres, nder;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(derdist));

    /* Set proper units */
    gwy_unit_clear(gwy_line_get_unit_x(derdist));
    gwy_unit_divide(gwy_line_get_unit_y(derdist), gwy_field_get_unit_z(field), gwy_field_get_unit_xy(field));

    nder = derdist->res;
    der = derdist->priv->data;
    xres = field->xres;
    yres = field->yres;
    gwy_clear(der, nder);

    if (kernel_size > xres || kernel_size > yres)
        return;

    xder = gwy_field_new_alike(field, FALSE);
    yder = gwy_field_new_alike(field, FALSE);
    compute_slopes(field, kernel_size, xder, yder);

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(xder,yder,der,xres,yres,kernel_size,nder)
#endif
    {
        gint rowfrom = gwy_omp_chunk_start(yres+1-kernel_size) + kernel_size/2;
        gint rowto = gwy_omp_chunk_end(yres+1-kernel_size) + kernel_size/2;
        gint colfrom = kernel_size/2;
        gint colto = kernel_size/2 + xres+1-kernel_size;
        gint row, col;
        const gdouble *bxdata = xder->priv->data;
        const gdouble *bydata = yder->priv->data;
        gdouble *tder = gwy_omp_if_threads_new0(der, nder);

        for (row = rowfrom; row < rowto; row++) {
            for (col = colfrom; col < colto; col++) {
                gdouble bx = bxdata[row*xres + col];
                gdouble by = bydata[row*xres + col];
                gdouble phi = atan2(by, bx);
                gint iphi = (gint)floor(nder*(phi + G_PI)/(2.0*G_PI));
                iphi = CLAMP(iphi, 0, nder-1);
                tder[iphi] += sqrt(bx*bx + by*by);
            }
        }

        gwy_omp_if_threads_sum_double(der, tder, nder);
    }

    g_object_unref(xder);
    g_object_unref(yder);
}

static inline void
slopes_to_angles(gdouble xder, gdouble yder,
                 gdouble *theta, gdouble *phi)
{
    *phi = atan2(yder, -xder);
    *theta = atan(sqrt(xder*xder + yder*yder));
}

/* Transforms (ϑ, φ) to Cartesian selection coordinates [0,2q], which is [0,2] for the full range of angles. */
static inline void
angles_to_xy(gdouble theta, gdouble phi, gdouble q,
             gdouble *x, gdouble *y)
{
    gdouble rho = G_SQRT2*sin(theta/2.0);
    gdouble c = cos(phi), s = sin(phi);

    *x = rho*c + q;
    *y = -rho*s + q;
}

/**
 * gwy_field_facet_distribution:
 * @field: A data field.
 * @dist: Target data field for the distribution.
 * @half_res: Requested distribution resolution, as half-size. Data field @dist will be resized to size 2*@half_res+1.
 * @size: Neighbourhood size, as in gwy_field_fit_local_planes(); or one for simple derivatives. Odd values are
 *        preferred.
 * @theta_field: Output field for theta angles in corresponding pixels, or %NULL.
 * @phi_field: Output field for phi angles in corresponding pixels, or %NULL.
 *
 * Calculates two-dimensional facet angle distribution for a data field.
 *
 * The distribution field @dist is constructed as an area-preserving projection of local facet orientation
 * distribution on the upper half-sphere. The radial coordinate in the projection is √2*sin(ϑ/2) where ϑ is the
 * polar coordinate on the sphere. The returned field only covers as large part of the sphere as necessary. i.e.
 * ϑ usually does not go to up to π/2. Use the real dimensions of computed @dist to obtain the range.
 *
 * The two optional outputs @theta_field and @phi_field will be resized to the same dimensions as @field and filled
 * with the theta and phi angles in the corresponding pixels as computed by this function. Pass %NULL if you do not
 * have further use for the facet angles.
 *
 * Note that facet distribution is generally meaningful only if the values are the same physical quantity as the
 * lateral dimensions because it relies on slope angles.
 **/
void
gwy_field_facet_distribution(GwyField *field, GwyField *dist,
                             gint half_res, gint size,
                             GwyField *theta_field, GwyField *phi_field)
{
    GwyField *allocated_theta_field = NULL, *allocated_phi_field = NULL;
    gdouble *xd, *yd, *data;
    const gdouble *xdc, *ydc;
    gdouble q, x, y;
    gint i, xres, yres, n, fres = 2*half_res + 1;

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(dist));
    g_return_if_fail(!theta_field || GWY_IS_FIELD(theta_field));
    g_return_if_fail(!phi_field || GWY_IS_FIELD(phi_field));
    g_return_if_fail(half_res >= 0);
    g_return_if_fail(size >= 0);

    xres = gwy_field_get_xres(field);
    yres = gwy_field_get_yres(field);

    if (!theta_field)
        theta_field = allocated_theta_field = gwy_field_new_alike(field, FALSE);
    else
        gwy_field_resize(theta_field, xres, yres);

    if (!phi_field)
        phi_field = allocated_phi_field = gwy_field_new_alike(field, FALSE);
    else
        gwy_field_resize(phi_field, xres, yres);

    compute_slopes(field, size, theta_field, phi_field);
    xd = theta_field->priv->data;
    yd = phi_field->priv->data;
    n = xres*yres;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i) \
            shared(xd,yd,n)
#endif
    for (i = 0; i < n; i++) {
        gdouble theta, phi;

        slopes_to_angles(xd[i], yd[i], &theta, &phi);
        xd[i] = theta;
        yd[i] = phi;
    }
    q = gwy_field_max(theta_field);
    q = MIN(q*1.05, 1.001*G_PI/2.0);
    q = G_SQRT2*sin(q/2.0);

    gwy_field_clear(dist);
    gwy_field_set_xreal(dist, 2.0*q);
    gwy_field_set_yreal(dist, 2.0*q);
    gwy_field_set_xoffset(dist, -q);
    gwy_field_set_yoffset(dist, -q);

    data = dist->priv->data;
    xdc = theta_field->priv->data;
    ydc = phi_field->priv->data;
    for (i = 0; i < n; i++) {
        gint xx, yy;

        angles_to_xy(xdc[i], ydc[i], q, &x, &y);
        x *= half_res/q;
        y *= half_res/q;
        xx = (gint)floor(x - 0.5);
        yy = (gint)floor(y - 0.5);

        if (G_UNLIKELY(xx < 0)) {
            xx = 0;
            x = 0.0;
        }
        else if (G_UNLIKELY(xx >= fres-1)) {
            xx = fres-2;
            x = 1.0;
        }
        else
            x = x - (xx + 0.5);

        if (G_UNLIKELY(yy < 0)) {
            yy = 0;
            y = 0.0;
        }
        else if (G_UNLIKELY(yy >= fres-1)) {
            yy = fres-2;
            y = 1.0;
        }
        else
            y = y - (yy + 0.5);

        data[yy*fres + xx] += (1.0 - x)*(1.0 - y);
        data[yy*fres + xx+1] += x*(1.0 - y);
        data[yy*fres+fres + xx] += (1.0 - x)*y;
        data[yy*fres+fres + xx+1] += x*y;
    }
    gwy_field_invalidate(dist);

    g_clear_object(&allocated_theta_field);
    g_clear_object(&allocated_phi_field);
}

/**
 * gwy_field_median:
 * @field: A data field.
 *
 * Computes median value of a data field.
 *
 * This quantity is cached.
 *
 * Returns: The median value.
 **/
gdouble
gwy_field_median(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0.0);
    gwy_debug("%s", FCTEST(field, MED) ? "cache" : "lame");

    if (!FCTEST(field, MED)) {
        FCVAL(field, MED) = calc_median_nomask(field, 0, 0, field->xres, field->yres);
        field->priv->cached |= FCBIT(MED);
    }

    return FCVAL(field, MED);
}

/**
 * gwy_field_area_median:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Computes median value of a data field area.
 *
 * Returns: The median value.
 **/
gdouble
gwy_field_area_median(GwyField *field,
                      GwyNield *mask,
                      GwyMaskingType masking,
                      gint col, gint row,
                      gint width, gint height)
{
    /* empty_is_noop=TRUE for compatibility */
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0.0;

    gint xres = field->xres, yres = field->yres;
    if (!mask) {
        if (col == 0 && row == 0 && width == xres && height == yres)
            return gwy_field_median(field);
        return calc_median_nomask(field, col, row, width, height);
    }

    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask->priv->data + row*xres + col;
    gdouble *buffer = g_new(gdouble, width*height);
    gsize n = 0;
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;

        for (gint j = 0; j < width; j++) {
            if (nielded_included(mrow + j, masking))
                buffer[n++] = drow[j];
        }
    }

    gdouble median = n ? gwy_math_median(buffer, n) : 0.0;
    g_free(buffer);

    return median;
}

/**
 * gwy_field_normal_coeffs:
 * @field: A data field.
 * @nx: (out) (optional): Where x-component of average normal vector should be stored, or %NULL.
 * @ny: (out) (optional): Where y-component of average normal vector should be stored, or %NULL.
 * @nz: (out) (optional): Where z-component of average normal vector should be stored, or %NULL.
 *
 * Computes average normal vector of a data field.
 **/
void
gwy_field_normal_coeffs(GwyField *field,
                        gdouble *nx, gdouble *ny, gdouble *nz)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_normal_coeffs(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres, nx, ny, nz);
}

/**
 * gwy_field_area_normal_coeffs:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @nx: (out) (optional): Where x-component of average normal vector should be stored, or %NULL.
 * @ny: (out) (optional): Where y-component of average normal vector should be stored, or %NULL.
 * @nz: (out) (optional): Where z-component of average normal vector should be stored, or %NULL.
 *
 * Computes average normal vector of an area of a data field, with masking.
 *
 * Values outside the area or excluded by masking never enter the computation. This is different from functions like
 * gwy_field_area_get_normal_coeffs() which can use outside pixels for two-side derivatives. Two-side
 * derivatives are used for interior pixels, one-sided when two-side are not possible. Normals are not calculated for
 * pixels excluded by masking, even when all their neighbours are included and two-sided derivatives do not actually
 * use the central value.
 *
 * The sign convention is also natural for the Gwyddion coordinate system.
 *
 * Returns: The number of vectors averaged. Zero means failure (typically occuring when masking did not leave any
 *          contiguous patch of pixels for computation of derivatives).
 **/
gint
gwy_field_area_normal_coeffs(GwyField *field,
                             GwyNield *mask, GwyMaskingType masking,
                             gint col, gint row,
                             gint width, gint height,
                             gdouble *nx, gdouble *ny, gdouble *nz)
{
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0;

    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + row*xres + col;
    const gint *maskpos = mask ? mask->priv->data + row*xres + col : NULL;
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gdouble sumdx = 0.0, sumdy = 0.0, sumdz = 0.0;
    gsize n = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:sumdx,sumdy,sumdz,n) \
            shared(datapos,maskpos,width,height,xres,masking,dx,dy)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos ? maskpos + i*xres : NULL;
        for (gint j = 0; j < width; j++) {
            gboolean has_c = nielded_included(mrow + j, masking);
            if (!has_c)
                continue;

            gboolean has_ym = (i > 0) && nielded_included(mrow + j-xres, masking);
            gboolean has_yp = (i < height-1) && nielded_included(mrow + j+xres, masking);
            gboolean has_xm = (j > 0) && nielded_included(mrow + j-1, masking);
            gboolean has_xp = (j < width-1) && nielded_included(mrow + j+1, masking);
            gdouble dzx, dzy;

            if (has_xm)
                dzx = has_xp ? 0.5*(drow[j+1] - drow[j-1]) : drow[j] - drow[j-1];
            else if (has_xp)
                dzx = drow[j+1] - drow[j];
            else
                continue;

            if (has_ym)
                dzy = has_yp ? 0.5*(drow[j+xres] - *(drow + j-xres)) : drow[j] - *(drow + j-xres);
            else if (has_yp)
                dzy = drow[j+xres] - drow[j];
            else
                continue;

            /* Cross product = normal vector */
            gdouble dcx = -dy*dzx;
            gdouble dcy = -dx*dzy;
            gdouble dcz = dx*dy;

            /* Normalize and add */
            gdouble dd = sqrt(dcx*dcx + dcy*dcy + dcz*dcz);
            sumdx += dcx/dd;
            sumdy += dcy/dd;
            sumdz += dcz/dd;
            n++;
        }
    }

    /* average dimensionless normal vector */
    if (n) {
        gdouble len = sqrt(sumdx*sumdx + sumdy*sumdy + sumdz*sumdz);
        sumdx /= len;
        sumdy /= len;
        sumdz /= len;
    }
    else {
        sumdx = sumdy = 0.0;
        sumdz = 1.0;
    }

    if (nx)
        *nx = sumdx;
    if (ny)
        *ny = sumdy;
    if (nz)
        *nz = sumdz;

    return n;
}

/**
 * gwy_field_area_inclination:
 * @field: A data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @theta: (out) (optional): Where theta angle (in radians) should be stored, or %NULL.
 * @phi: (out) (optional): Where phi angle (in radians) should be stored, or %NULL.
 *
 * Calculates the inclination of the image (polar and azimuth angle).
 **/
void
gwy_field_area_inclination(GwyField *field,
                           GwyNield *mask,
                           GwyMaskingType masking,
                           gint col, gint row,
                           gint width, gint height,
                           gdouble *theta,
                           gdouble *phi)
{
    gdouble nx, ny, nz;
    gwy_field_area_normal_coeffs(field, mask, masking, col, row, width, height, &nx, &ny, &nz);

    gdouble nr = hypot(nx, ny);
    if (theta)
        *theta = atan2(nr, nz);
    if (phi)
        *phi = atan2(ny, nx);
}

/**
 * gwy_field_inclination:
 * @field: A data field.
 * @theta: (out) (optional): Where theta angle (in radians) should be stored, or %NULL.
 * @phi: (out) (optional): Where phi angle (in radians) should be stored, or %NULL.
 *
 * Calculates the inclination of the image (polar and azimuth angle).
 **/
void
gwy_field_inclination(GwyField *field,
                      gdouble *theta,
                      gdouble *phi)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_inclination(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres, theta, phi);
}

static gint
extract_field_row_masked(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gdouble *values,
                         gint col, gint row, gint width,
                         gboolean replace_masked,
                         gdouble filler)
{
    gint xres = field->xres;
    const gdouble *d = field->priv->data + row*xres + col;

    if (!mask) {
        gwy_assign(values, d, width);
        return width;
    }

    const gint *m = mask->priv->data + row*xres + col;
    if (replace_masked) {
        for (gint i = 0; i < width; i++)
            values[i] = nielded_included(m + i, masking) ? d[i] : filler;
        return width;
    }

    gint n = 0;
    for (gint i = 0; i < width; i++) {
        if (nielded_included(m + i, masking))
            values[n++] = d[i];
    }

    return n;
}

static gint
extract_field_column_masked(GwyField *field,
                            GwyNield *mask,
                            GwyMaskingType masking,
                            gdouble *values,
                            gint col, gint row, gint height,
                            gboolean replace_masked,
                            gdouble filler)
{
    gint xres = field->xres;
    const gdouble *d = field->priv->data + row*xres + col;

    if (!mask) {
        for (gint i = 0; i < height; i++)
            values[i] = d[xres*i];
        return height;
    }

    const gint *m = mask->priv->data + row*xres + col;
    if (replace_masked) {
        for (gint i = 0; i < height; i++)
            values[i] = nielded_included(m + xres*i, masking) ? d[xres*i] : filler;
        return height;
    }

    gint n = 0;
    for (gint i = 0; i < height; i++) {
        if (nielded_included(m + i*xres, masking))
            values[n++] = d[xres*i];
    }

    return n;
}

static GwyLine*
calc_field_row_linestat_masked(GwyField *field,
                               GwyNield *mask,
                               GwyMaskingType masking,
                               GwyLine *weights,
                               LineStatFunc func,
                               gboolean replace_masked,
                               gdouble filler_value,
                               gint col, gint row,
                               gint width, gint height)
{
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    GwyLine *dline = gwy_line_new(height, height*dy, FALSE);
    dline->off = row*dy + field->yoff;
    gdouble *wdata = NULL, *ldata = dline->priv->data;

    if (weights) {
        gwy_line_make_alike(weights, dline);
        gwy_line_clear(weights);
        gwy_unit_clear(gwy_line_get_unit_y(weights));
        wdata = weights->priv->data;
    }

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(field,ldata,wdata,mask,masking,width,height,col,row,dx,func,filler_value,replace_masked)
#endif
    {
        GwyLine *buf = gwy_line_new(width, width*dx, FALSE);
        gdouble *bufdata = buf->priv->data;
        gint ifrom = gwy_omp_chunk_start(height);
        gint ito = gwy_omp_chunk_end(height);
        gint i, n;

        for (i = ifrom; i < ito; i++) {
            n = extract_field_row_masked(field, mask, masking, bufdata, col, row + i, width,
                                         replace_masked, filler_value);
            if (n) {
                /* Temporarily pretend-shorten the line to avoid reallocations. */
                buf->res = n;
                buf->real = n*dx;
                ldata[i] = func(buf);
                buf->res = width;
                buf->real = width*dx;
                if (wdata)
                    wdata[i] = n;
            }
            else
                ldata[i] = filler_value;
        }

        g_object_unref(buf);
    }

    return dline;
}

static GwyLine*
calc_field_column_linestat_masked(GwyField *field,
                                  GwyNield *mask,
                                  GwyMaskingType masking,
                                  GwyLine *weights,
                                  LineStatFunc func,
                                  gboolean replace_masked,
                                  gdouble filler_value,
                                  gint col, gint row,
                                  gint width, gint height)
{
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    GwyLine *dline = gwy_line_new(width, width*dx, FALSE);
    dline->off = col*dx + field->xoff;
    gdouble *wdata = NULL, *ldata = dline->priv->data;

    if (weights) {
        gwy_line_make_alike(weights, dline);
        gwy_line_clear(weights);
        gwy_unit_clear(gwy_line_get_unit_y(weights));
        wdata = weights->priv->data;
    }

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(field,ldata,wdata,mask,masking,width,height,col,row,dy,func,filler_value,replace_masked)
#endif
    {
        GwyLine *buf = buf = gwy_line_new(height, height*dy, FALSE);
        gdouble *bufdata = buf->priv->data;
        gint ifrom = gwy_omp_chunk_start(width);
        gint ito = gwy_omp_chunk_end(width);
        gint i, n;

        for (i = ifrom; i < ito; i++) {
            n = extract_field_column_masked(field, mask, masking, bufdata, col + i, row, height,
                                            replace_masked, filler_value);
            if (n) {
                /* Temporarily pretend-shorten the line to avoid reallocations. */
                buf->res = n;
                buf->real = n*dy;
                ldata[i] = func(buf);
                buf->res = height;
                buf->real = height*dy;
                if (wdata)
                    wdata[i] = n;
            }
            else
                ldata[i] = filler_value;
        }

        g_object_unref(buf);
    }

    return dline;
}

static gdouble
gwy_line_slope(GwyLine *dline)
{
    gdouble v;

    gwy_line_fit_line(dline, NULL, &v);
    return v*dline->res/dline->real;
}

static gdouble
gwy_line_range(GwyLine *dline)
{
    gdouble min, max;

    gwy_line_min_max(dline, &min, &max);
    return max - min;
}

static gdouble
gwy_line_median_destructive(GwyLine *dline)
{
    return gwy_math_median(dline->priv->data, dline->res);
}

static gdouble
gwy_line_Rt_destructive(GwyLine *dline)
{
    gwy_line_add(dline, -gwy_line_mean(dline));
    return gwy_line_xtm(dline, 1, 1);
}

static gdouble
gwy_line_Rz_destructive(GwyLine *dline)
{
    gwy_line_add(dline, -gwy_line_mean(dline));
    return gwy_line_xtm(dline, 5, 1);
}

/**
 * gwy_field_area_line_stats:
 * @field: A data field.
 * @mask: (nullable): Mask of values to take values into account, or %NULL for full @field.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @weights: (nullable):
 *           A data line to store number of data points contributing to each value in @target_line, or %NULL.  It is
 *           useful when masking is used to possibly exclude values calculated from too few data points.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @quantity: The line quantity to calulate for each row (column).
 * @orientation: Line orientation.  For %GWY_ORIENTATION_HORIZONTAL each @target_line point corresponds to a row of
 *               the area, for %GWY_ORIENTATION_VERTICAL each @target_line point corresponds to a column of the area.
 *
 * Calculates a line quantity for each row or column in a data field area.
 *
 * Returns: (transfer full): A newly created data line with the row on column values.
 **/
GwyLine*
gwy_field_area_line_stats(GwyField *field,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gint col, gint row,
                          gint width, gint height,
                          GwyLineStatQuantity quantity,
                          GwyOrientation orientation,
                          GwyLine *weights)
{
    static const LineStatFunc funcs[] = {
        gwy_line_mean,
        gwy_line_median_destructive,
        gwy_line_min,
        gwy_line_max,
        gwy_line_rms,
        gwy_line_length,
        gwy_line_slope,
        gwy_line_tan_beta0,
        gwy_line_ra,
        gwy_line_Rz_destructive,
        gwy_line_Rt_destructive,
        gwy_line_skew,
        gwy_line_kurtosis,
        gwy_line_range,
        gwy_line_variation,
        gwy_line_min_pos_r,
        gwy_line_max_pos_r,
    };

    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return NULL;
    g_return_val_if_fail(quantity < G_N_ELEMENTS(funcs), NULL);

    LineStatFunc func = funcs[quantity];
    gboolean replace_masked = FALSE;
    gdouble filler_value = 0.0;

    if (quantity == GWY_LINE_STAT_MINPOS) {
        replace_masked = TRUE;
        filler_value = G_MAXDOUBLE;
    }
    else if (quantity == GWY_LINE_STAT_MAXPOS) {
        replace_masked = TRUE;
        filler_value = -G_MAXDOUBLE;
    }

    GwyLine *line;
    if (orientation == GWY_ORIENTATION_VERTICAL) {
        line = calc_field_column_linestat_masked(field, mask, masking, weights, func,
                                                 replace_masked, filler_value, col, row, width, height);
    }
    else {
        line = calc_field_row_linestat_masked(field, mask, masking, weights, func,
                                              replace_masked, filler_value, col, row, width, height);
    }

    GwyUnit *xyunit = gwy_field_get_unit_xy(field);
    GwyUnit *zunit = gwy_field_get_unit_z(field);
    gwy_unit_assign(gwy_line_get_unit_x(line), xyunit);

    switch (quantity) {
        case GWY_LINE_STAT_LENGTH:
        if (!gwy_unit_equal(xyunit, zunit))
            g_warning("Length makes no sense when lateral and value units differ");
        case GWY_LINE_STAT_MEAN:
        case GWY_LINE_STAT_MEDIAN:
        case GWY_LINE_STAT_MINIMUM:
        case GWY_LINE_STAT_MAXIMUM:
        case GWY_LINE_STAT_RMS:
        case GWY_LINE_STAT_RA:
        case GWY_LINE_STAT_RT:
        case GWY_LINE_STAT_RZ:
        case GWY_LINE_STAT_RANGE:
        case GWY_LINE_STAT_VARIATION:
        gwy_unit_assign(gwy_line_get_unit_y(line), zunit);
        break;

        case GWY_LINE_STAT_MINPOS:
        case GWY_LINE_STAT_MAXPOS:
        gwy_unit_assign(gwy_line_get_unit_y(line), xyunit);
        break;

        case GWY_LINE_STAT_SLOPE:
        case GWY_LINE_STAT_TAN_BETA0:
        case GWY_LINE_STAT_SKEW:
        case GWY_LINE_STAT_KURTOSIS:
        gwy_unit_divide(gwy_line_get_unit_y(line), zunit, xyunit);
        break;

        default:
        g_assert_not_reached();
        break;
    }

    if (weights) {
        gwy_unit_assign(gwy_line_get_unit_x(weights), xyunit);
        gwy_unit_clear(gwy_line_get_unit_y(weights));
    }

    return line;
}

/**
 * gwy_field_line_stats:
 * @field: A data field.
 * @target_line: A data line to store the distribution to.  It will be resampled to @field height (width).
 * @quantity: The line quantity to calulate for each row (column).
 * @orientation: Line orientation.  See gwy_field_area_line_stats().
 *
 * Calculates a line quantity for each row or column of a data field.
 **/
void
gwy_field_line_stats(GwyField *field,
                     GwyLine *target_line,
                     GwyLineStatQuantity quantity,
                     GwyOrientation orientation)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    GwyLine *line = gwy_field_area_line_stats(field, NULL, GWY_MASK_IGNORE,
                                              0, 0, field->xres, field->yres,
                                              quantity, orientation, NULL);
    gwy_line_assign(target_line, line);
    g_object_unref(line);
}

/**
 * gwy_field_count_maxima:
 * @field: A data field.
 *
 * Counts the number of regional maxima in a data field.
 *
 * See gwy_field_mark_extrema() for the definition of a regional maximum.
 *
 * Returns: The number of regional maxima.
 **/
guint
gwy_field_count_maxima(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    GwyNield *mask = gwy_field_new_nield_alike(field);
    gwy_field_mark_extrema(field, mask, TRUE);
    gint ngrains = gwy_nield_number_contiguous(mask);
    g_object_unref(mask);
    return ngrains;
}

/**
 * gwy_field_count_minima:
 * @field: A data field
 *
 * Counts the number of regional minima in a data field.
 *
 * See gwy_field_mark_extrema() for the definition of a regional minimum.
 *
 * Returns: The number of regional minima.
 **/
guint
gwy_field_count_minima(GwyField *field)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    GwyNield *mask = gwy_field_new_nield_alike(field);
    gwy_field_mark_extrema(field, mask, FALSE);
    gint ngrains = gwy_nield_number_contiguous(mask);
    g_object_unref(mask);
    return ngrains;
}

/**
 * SECTION: stats
 * @title: Statistics
 * @short_description: Two-dimensional statistical functions
 *
 * Many statistical functions permit to pass masks that determine which values in the data field to take into account
 * or ignore when calculating the statistical characteristics.  Masking mode %GWY_MASK_INCLUDE means that maks values
 * equal to 0.0 and below cause corresponding data field samples to be ignored, values equal to 1.0 and above cause
 * inclusion of corresponding data field samples.  The behaviour for values inside interval (0.0, 1.0) is undefined.
 * In mode @GWY_MASK_EXCLUDE, the meaning of mask is inverted, as if all mask values x were replaced with 1-x.  The
 * mask field is ignored in mode @GWY_MASK_IGNORE, i.e. the same behaviour occurs as with %NULL mask argument.
 **/

/* 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 : */
