/*
 *  $Id: filters.c 30078 2026-06-16 17:25:09Z yeti-dn $
 *  Copyright (C) 2003-2026 David Necas (Yeti), Petr Klapetek.
 *  E-mail: yeti@gwyddion.net, klapetek@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 "config.h"
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/filters.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/extend.h"
#include "libgwyddion/inttrans.h"
#include "libgwyddion/simplefft.h"

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

static void thin_field(GwyField *field);

/**
 * gwy_field_normalize:
 * @field: A data field.
 *
 * Normalizes data in a data field to range 0.0 to 1.0.
 *
 * It is equivalent to gwy_field_renormalize(@field, 1.0, 0.0);
 *
 * If @field is filled with only one value, it is changed to 0.0.
 **/
void
gwy_field_normalize(GwyField *field)
{
    gdouble min, max;
    gdouble *p;
    gint xres, yres, i;

    g_return_if_fail(GWY_IS_FIELD(field));

    gwy_field_min_max(field, &min, &max);
    if (min == max) {
        gwy_field_clear(field);
        return;
    }
    if (!min) {
        if (max != 1.0)
            gwy_field_multiply(field, 1.0/max);
        return;
    }

    /* The general case */
    max -= min;
    xres = field->xres;
    yres = field->yres;
    for (i = xres*yres, p = field->priv->data; i; i--, p++)
        *p = (*p - min)/max;

    /* We can transform some stats */
    field->priv->cached &= FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED) | FCBIT(ARF) | FCBIT(ART);
    FCVAL(field, MIN) = 0.0;
    FCVAL(field, MAX) = 1.0;
    FCVAL(field, SUM) /= (FCVAL(field, SUM) - xres*yres*min)/max;
    FCVAL(field, RMS) /= max;
    FCVAL(field, MED) = (FCVAL(field, MED) - min)/max;
    FCVAL(field, ART) = (FCVAL(field, ART) - min)/max;
    FCVAL(field, ARF) = (FCVAL(field, ARF) - min)/max;
    /* There is a transformation formula for MSQ, but it can be prone to ugly cancellation errors. */
}

/**
 * gwy_field_renormalize:
 * @field: A data field.
 * @range: New data interval size.
 * @offset: New data interval offset.
 *
 * Transforms data in a data field with linear function to given range.
 *
 * When @range is positive, the new data range is (@offset, @offset+@range); when @range is negative, the new data
 * range is (@offset-@range, @offset). In neither case the data are flipped. Negative range only means a different
 * selection of boundaries.
 *
 * When @range is zero, this method is equivalent to gwy_field_fill(@field, @offset).
 **/
void
gwy_field_renormalize(GwyField *field,
                      gdouble range,
                      gdouble offset)
{
    gdouble min, max;

    g_return_if_fail(GWY_IS_FIELD(field));

    if (!range) {
        gwy_field_fill(field, offset);
        return;
    }

    gwy_field_min_max(field, &min, &max);
    if (min == max) {
        gwy_field_fill(field, offset);
        return;
    }

    if ((range > 0 && min == offset && min + range == max) || (range < 0 && max == offset && min - range == max))
        return;

    /* The general case */
    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    gdouble *d = field->priv->data;

    if (range > 0) {
        max -= min;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(d,n,min,max,range,offset)
#endif
        for (gsize i = 0; i < n; i++)
            d[i] = (d[i] - min)/max*range + offset;

        /* We can transform stats */
        field->priv->cached &= FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED);
        FCVAL(field, MIN) = offset;
        FCVAL(field, MAX) = offset + range;
        gdouble v = FCVAL(field, SUM);
        FCVAL(field, SUM) = (v - n*min)/max*range + offset*n;
        FCVAL(field, RMS) = FCVAL(field, RMS)/max*range;
        FCVAL(field, MED) = (FCVAL(field, MED) - min)/max*range + offset;
        /* FIXME: we can recompute ARF and ART too */
        /* There is a transformation formula for MSQ, but it can be prone to ugly cancellation errors. */
    }
    else {
        min = max - min;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(d,n,min,max,range,offset)
#endif
        for (gsize i = 0; i < n; i++)
            d[i] = (max - d[i])/min*range + offset;

        /* We can transform stats */
        field->priv->cached &= FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED);
        FCVAL(field, MIN) = offset + range;
        FCVAL(field, MAX) = offset;
        gdouble v = FCVAL(field, SUM);
        FCVAL(field, SUM) = (xres*yres*max - v)/min*range + offset*xres*yres;
        FCVAL(field, RMS) = FCVAL(field, RMS)/min*(-range);
        FCVAL(field, MED) = (max - FCVAL(field, MED))/min*range + offset;
        /* FIXME: we can recompute ARF and ART too */
        /* There is a transformation formula for MSQ, but it can be prone to ugly cancellation errors. */
    }
}

/**
 * gwy_field_area_renormalize:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @range: New data interval size.
 * @offset: New data interval offset.
 *
 * Transforms data in a part of a data field with linear function to given range.
 *
 * When @range is positive, the new data range is (@offset, @offset+@range); when @range is negative, the new data
 * range is (@offset-@range, @offset). In neither case the data are flipped, negative range only means different
 * selection of boundaries.
 *
 * When @range is zero, this method is equivalent to gwy_field_fill(@field, @offset).
 **/
void
gwy_field_area_renormalize(GwyField *dfield,
                           gint col, gint row,
                           gint width, gint height,
                           gdouble range,
                           gdouble offset)
{
    gdouble min, max;

    if (!_gwy_field_check_area(dfield, col, row, width, height, TRUE))
        return;

    gssize xres = dfield->xres, yres = dfield->yres;
    if (col == 0 && row == 0 && width == xres && height == yres) {
        gwy_field_renormalize(dfield, range, offset);
        return;
    }

    if (!range) {
        gwy_field_area_fill(dfield, NULL, GWY_MASK_IGNORE, col, row, width, height, offset);
        return;
    }

    gwy_field_area_min_max(dfield, NULL, GWY_MASK_IGNORE, col, row, width, height, &min, &max);
    if (min == max) {
        gwy_field_area_fill(dfield, NULL, GWY_MASK_IGNORE, col, row, width, height, offset);
        return;
    }

    if ((range > 0 && min == offset && min + range == max) || (range < 0 && max == offset && min - range == max))
        return;

    /* The general case */
    gdouble *d = dfield->priv->data;
    if (range > 0) {
        max -= min;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(d,width,height,col,row,xres,min,max,range,offset)
#endif
        for (gint i = 0; i < height; i++) {
            gdouble *r = d + (i + row)*xres + col;
            for (gint j = 0; j < width; j++)
                r[j] = (r[j] - min)/max*range + offset;
        }
    }
    else {
        min = max - min;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(d,width,height,col,row,xres,min,max,range,offset)
#endif
        for (gint i = 0; i < height; i++) {
            gdouble *r = d + (i + row)*xres + col;
            for (gint j = 0; j < width; j++)
                r[j] = (max - r[j])/min*range + offset;
        }
    }

    gwy_field_invalidate(dfield);
}

/**
 * gwy_field_threshold:
 * @field: A data field.
 * @threshold: Threshold value.
 * @below: Lower replacement value.
 * @above: Upper replacement value.
 *
 * Tresholds values of a data field.
 *
 * Values smaller than @threshold are set to value @below, values higher than @threshold or equal to it are set to
 * value @above.
 *
 * Returns: The total number of values above threshold.
 **/
gsize
gwy_field_threshold(GwyField *field,
                    gdouble threshold, gdouble below, gdouble above)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);

    gint xres = field->xres, yres = field->yres;
    gsize n = (gsize)xres * (gsize)yres;
    gdouble *d = field->priv->data;
    gsize tot = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:tot) \
            shared(d,n,threshold,below,above)
#endif
    for (gsize i = 0; i < n; i++) {
        if (d[i] < threshold)
            d[i] = below;
        else {
            d[i] = above;
            tot++;
        }
    }

    /* We can precompute stats */
    field->priv->cached = FCBIT(MIN) | FCBIT(MAX) | FCBIT(SUM) | FCBIT(RMS) | FCBIT(MED);
    if (tot == n)
        FCVAL(field, MIN) = FCVAL(field, MAX) = above;
    else if (tot == 0)
        FCVAL(field, MIN) = FCVAL(field, MAX) = below;
    else {
        FCVAL(field, MIN) = MIN(above, below);
        FCVAL(field, MAX) = MAX(above, below);
    }
    FCVAL(field, SUM) = tot*above + (n - tot)*below;
    FCVAL(field, RMS) = (above - below)*(above - below) * tot/(gdouble)n * (n - tot)/(gdouble)n;
    /* FIXME: may be incorrect for tot == n/2(?) */
    FCVAL(field, MED) = tot > n/2 ? above : below;

    return tot;
}


/**
 * gwy_field_area_threshold:
 * @field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @threshold: Threshold value.
 * @below: Lower replacement value.
 * @above: Upper replacement value.
 *
 * Tresholds values of a rectangular part of a data field.
 *
 * Values smaller than @threshold are set to value @below, values higher than @threshold or equal to it are set to
 * value @above
 *
 * Returns: The total number of values above threshold.
 **/
gsize
gwy_field_area_threshold(GwyField *field,
                         gint col, gint row, gint width, gint height,
                         gdouble threshold, gdouble below, gdouble above)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE))
        return 0;

    gdouble *d = field->priv->data;
    gint xres = field->xres;
    gsize tot = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:tot) \
            shared(d,width,height,xres,col,row,threshold,below,above)
#endif
    for (gint i = 0; i < height; i++) {
        gdouble *drow = d + (row + i)*xres + col;

        for (gint j = 0; j < width; j++) {
            if (drow[j] < threshold)
                drow[j] = below;
            else {
                drow[j] = above;
                tot++;
            }
        }
    }
    gwy_field_invalidate(field);

    return tot;
}

/**
 * gwy_field_clamp:
 * @field: A data field.
 * @bottom: Lower limit value.
 * @top: Upper limit value.
 *
 * Limits data field values to a range.
 *
 * Returns: The number of changed values, i.e., values that were outside [@bottom, @top].
 **/
gsize
gwy_field_clamp(GwyField *field,
                gdouble bottom, gdouble top)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), 0);
    g_return_val_if_fail(bottom <= top, 0);

    gdouble *d = field->priv->data;
    gsize n = (gsize)field->xres * (gsize)field->yres;
    gsize tot = 0;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:tot) \
            shared(d,n,bottom,top)
#endif
    for (gsize i = 0; i < n; i++) {
        if (d[i] < bottom) {
            d[i] = bottom;
            tot++;
        }
        else if (d[i] > top) {
            d[i] = top;
            tot++;
        }
    }
    if (tot) {
        /* We can precompute stats */
        field->priv->cached &= FCBIT(MIN) | FCBIT(MAX) | FCBIT(MED);
        FCVAL(field, MIN) = MAX(bottom, FCVAL(field, MIN));
        FCVAL(field, MAX) = MIN(top, FCVAL(field, MAX));
        if (FCTEST(field, MED) && (FCVAL(field, MED) < bottom || FCVAL(field, MED) > top))
            field->priv->cached &= ~FCBIT(MED);
    }

    return tot;
}

/**
 * gwy_field_area_clamp:
 * @field: A data field.
 * @mask: (nullable): Mask of values to modify, or %NULL for full @field.
 * @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).
 * @bottom: Lower limit value.
 * @top: Upper limit value.
 *
 * Limits values in a rectangular part of a data field to a range.
 *
 * Returns: The number of changed values, i.e., values that were outside [@bottom, @top].
 **/
gsize
gwy_field_area_clamp(GwyField *field,
                     GwyNield *mask, GwyMaskingType masking,
                     gint col, gint row,
                     gint width, gint height,
                     gdouble bottom, gdouble top)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return 0;

    gdouble *d = field->priv->data;
    const gint *m = mask ? mask->priv->data : NULL;
    gint xres = field->xres;
    gsize tot = 0;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
    reduction(+:tot) \
    shared(d,m,xres,width,height,col,row,masking,bottom,top)
#endif
    for (gint i = 0; i < height; i++) {
        gdouble *drow = d + (row + i)*xres + col;

        if (m) {
            const gint *mrow = m ? m + (row + i)*xres + col : NULL;

            for (gint j = 0; j < width; j++) {
                if (!nielded_included(mrow + j, masking))
                    continue;

                if (drow[j] < bottom) {
                    drow[j] = bottom;
                    tot++;
                }
                else if (drow[j] > top) {
                    drow[j] = top;
                    tot++;
                }
            }
        }
        else {
            for (gint j = 0; j < width; j++) {
                if (drow[j] < bottom) {
                    drow[j] = bottom;
                    tot++;
                }
                else if (drow[j] > top) {
                    drow[j] = top;
                    tot++;
                }
            }
        }
    }
    if (tot)
        gwy_field_invalidate(field);

    return tot;
}

/**
 * gwy_field_area_gather:
 * @field: A data field.
 * @result: A data field to put the result to, it may be @field itself.
 * @buffer: (nullable):
 *          A data field to use as a scratch area, its size must be at least @width*@height.  May be %NULL to allocate
 *          a private temporary buffer.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @hsize: Horizontal size of gathered area.  The area is centered around each sample if @hsize is odd, it extends one
 *         pixel more to the right if @hsize is even.
 * @vsize: Vertical size of gathered area.  The area is centered around each sample if @vsize is odd, it extends one
 *         pixel more down if @vsize is even.
 * @average: %TRUE to divide resulting sums by the number of involved samples to get averages instead of sums.
 *
 * Sums or averages values in reactangular areas around each sample in a data field.
 *
 * When the gathered area extends out of calculation area, only samples from their intersection are taken into the
 * local sum (or average).
 *
 * There are no restrictions on values of @hsize and @vsize with regard to @width and @height, but they have to be
 * positive.
 *
 * The result is calculated by the means of two-dimensional rolling sums. One one hand it means the calculation time
 * depends linearly on (@width + @hsize)*(@height + @vsize) instead of @width*@hsize*@height*@vsize.  On the other
 * hand it means absolute rounding errors of all output values are given by the largest input values, that is relative
 * precision of results small in absolute value may be poor.
 **/
void
gwy_field_area_gather(GwyField *field,
                      GwyField *result,
                      GwyField *buffer,
                      gint hsize,
                      gint vsize,
                      gboolean average,
                      gint col, gint row,
                      gint width, gint height)
{
    gint m;
    gdouble v;

    g_return_if_fail(hsize > 0 && vsize > 0);
    if (!_gwy_field_check_area(field, col, row, width, height, FALSE)
        || !_gwy_field_check_other(field, result, FALSE))
        return;
    gint xres = field->xres;
    if (buffer) {
        if (!_gwy_field_check_other(field, buffer, FALSE))
            return;
        g_object_ref(buffer);
    }
    else
        buffer = gwy_field_new(width, height, 1.0, 1.0, FALSE);

    /* Extension to the left and to the right (for asymmetric sizes extend to the right more) */
    gint hs2m = (hsize - 1)/2;
    gint hs2p = hsize/2;
    gint vs2m = (vsize - 1)/2;
    gint vs2p = vsize/2;

    /* Row-wise sums */
    /* FIXME: This is inefficient, split the inner loops to explicitly according to the conditions inside */
    const gdouble *fdata = field->priv->data;
    gdouble *bdata = buffer->priv->data;
    for (gint i = 0; i < height; i++) {
        const gdouble *srow = fdata + (i + row)*xres + col;
        gdouble *drow = bdata + i*width;

        /* Left half */
        drow[0] = 0.0;
        m = MIN(hs2p, width-1);
        for (gint j = 0; j <= m; j++)
            drow[0] += srow[j];
        for (gint j = 1; j < width/2; j++) {
            v = ((j + hs2p < width ? srow[j + hs2p] : 0.0) - (j-1 - hs2m >= 0 ? srow[j-1 - hs2m] : 0.0));
            drow[j] = drow[j-1] + v;
        }

        /* Right half */
        drow[width-1] = 0.0;
        m = width-1 - MIN(hs2m, width-1);
        for (gint j = width-1; j >= m; j--)
            drow[width-1] += srow[j];
        for (gint j = width-2; j >= width/2; j--) {
            v = ((j - hs2m >= 0 ? srow[j - hs2m] : 0.0) - (j+1 + hs2p < width ? srow[j+1 + hs2p] : 0.0));
            drow[j] = drow[j+1] + v;
        }
    }

    /* Column-wise sums (but iterate row-wise to access memory linearly) */
    /* Top half */
    gdouble *r = result->priv->data;
    gdouble *drow = r + row*xres + col;
    gwy_clear(drow, width);
    m = MIN(vs2p, height-1);
    for (gint i = 0; i <= m; i++) {
        gdouble *srow = bdata + i*width;
        for (gint j = 0; j < width; j++)
            drow[j] += srow[j];
    }
    for (gint i = 1; i < height/2; i++) {
        drow = r + (i + row)*xres + col;
        if (i + vs2p < height) {
            gdouble *srow = bdata + (i + vs2p)*width;
            if (i-1 - vs2m >= 0) {
                const gdouble *trow = bdata + (i-1 - vs2m)*width;
                for (gint j = 0; j < width; j++)
                    drow[j] = *(drow + j - xres) + (srow[j] - trow[j]);
            }
            else {
                for (gint j = 0; j < width; j++)
                    drow[j] = *(drow + j - xres) + srow[j];
            }
        }
        else {
            if (G_UNLIKELY(i-1 - vs2m >= 0)) {
                g_warning("Me thinks pure subtraction cannot occur.");
                const gdouble *trow = bdata + (i-1 - vs2m)*width;
                for (gint j = 0; j < width; j++)
                    drow[j] = *(drow + j - xres) - trow[j];
            }
            else {
                for (gint j = 0; j < width; j++)
                    drow[j] = *(drow + j - xres);
            }
        }
    }

    /* Bottom half */
    drow = r + (height-1 + row)*xres + col;
    gwy_clear(drow, width);
    m = height-1 - MIN(vs2m, height-1);
    for (gint i = height-1; i >= m; i--) {
        gdouble *srow = bdata + i*width;
        for (gint j = 0; j < width; j++)
            drow[j] += srow[j];
    }
    for (gint i = height-2; i >= height/2; i--) {
        drow = r + (i + row)*xres + col;
        if (i+1 + vs2p < height) {
            gdouble *srow = bdata + (i+1 + vs2p)*width;
            if (G_LIKELY(i - vs2m >= 0)) {
                const gdouble *trow = bdata + (i - vs2m)*width;
                for (gint j = 0; j < width; j++)
                    drow[j] = drow[j + xres] + (trow[j] - srow[j]);
            }
            else {
                g_warning("Me thinks pure subtraction cannot occur.");
                for (gint j = 0; j < width; j++)
                    drow[j] = drow[j + xres] - srow[j];
            }
        }
        else {
            if (i - vs2m >= 0) {
                const gdouble *trow = bdata + (i - vs2m)*width;
                for (gint j = 0; j < width; j++)
                    drow[j] = drow[j + xres] + trow[j];
            }
            else {
                for (gint j = 0; j < width; j++)
                    drow[j] = drow[j + xres];
            }
        }
    }

    gwy_field_invalidate(result);
    g_object_unref(buffer);

    if (!average)
        return;

    /* Divide sums by the numbers of pixels that entered them */
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(r,width,height,xres,row,col,vsize,hsize,vs2m,vs2p,hs2m,hs2p)
#endif
    for (gint i = 0; i < height; i++) {
        gint iw;

        if (i <= vs2m)
            iw = vs2p + 1 + i;
        else if (i >= height-1 - vs2p)
            iw = vs2m + height - i;
        else
            iw = vsize;
        iw = MIN(iw, height);

        for (gint j = 0; j < width; j++) {
            gint jw;

            if (j <= hs2m)
                jw = hs2p + 1 + j;
            else if (j >= width-1 - hs2p)
                jw = hs2m + width - j;
            else
                jw = hsize;
            jw = MIN(jw, width);

            r[(i + row)*xres + j + col] /= iw*jw;
        }
    }
}

/**
 * gwy_field_area_filter_mean:
 * @field: A data field to apply the filter to.
 * @size: Averaged area size.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Filters a rectangular part of a data field with mean filter of size @size.
 *
 * This method is a simple gwy_field_area_gather() wrapper, so the kernel is square.  Use convolution
 * gwy_field_area_ext_convolve() to perform a mean filter with different, for instance circular, kernel.
 **/
void
gwy_field_area_filter_mean(GwyField *field,
                           gint size,
                           gint col, gint row,
                           gint width, gint height)
{
    gwy_field_area_gather(field, field, NULL, size, size, TRUE, col, row, width, height);
}

/**
 * gwy_field_filter_mean:
 * @field: A data field to apply the filter to.
 * @size: Averaged area size.
 *
 * Filters a data field with mean filter of size @size.
 *
 * This method is a simple gwy_field_area_gather() wrapper, so the kernel is square.  Use convolution
 * gwy_field_area_ext_convolve() to perform a mean filter with different, for instance circular, kernel.
 **/
void
gwy_field_filter_mean(GwyField *field,
                      gint size)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_filter_mean(field, size, 0, 0, field->xres, field->yres);
}

/**
 * gwy_field_filter_rms:
 * @field: A data field to apply RMS filter to.
 * @target: A data field to put the shade to.  It will be resized to match @field. It can be @field itself.
 * @size: Area size.
 *
 * Filters a data field with RMS filter of size @size.
 *
 * RMS filter computes root mean square in given area.
 **/
void
gwy_field_filter_rms(GwyField *field,
                     GwyField *target,
                     gint size)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));
    g_return_if_fail(size > 0);

    if (size == 1) {
        gwy_field_clear(target);
        return;
    }

    gwy_field_make_alike(target, field);
    GwyField *avg = gwy_field_new_alike(field, FALSE);
    gwy_field_multiply_fields(avg, field, field);

    GwyField *buffer = gwy_field_new_alike(field, FALSE);
    gint xres = field->xres, yres = field->yres;
    gwy_field_area_gather(avg, target, buffer, size, size, TRUE, 0, 0, xres, yres);
    gwy_field_area_gather(field, avg, buffer, size, size, TRUE, 0, 0, xres, yres);
    // After this dance, we have averages in avg and average squares in target.
    g_object_unref(buffer);

    for (gint i = 0; i < yres; i++) {
        const gdouble *arow = avg->priv->data + i*xres;
        gdouble *trow = target->priv->data + i*xres;
        for (gint j = 0; j < xres; j++) {
            gdouble v = trow[j] - arow[j]*arow[j];
            trow[j] = sqrt(fmax(v, 0.0));
        }
    }
    g_object_unref(avg);

    gwy_field_invalidate(target);
}

// FIXME: This produces 0 and 1. So maybe the target should be Nield?
/**
 * gwy_field_filter_canny:
 * @field: A data field to apply the filter to.
 * @target: A data field to put the result to. It can be @field itself. It will be resized to match @field.
 * @threshold: Slope detection threshold (range 0..1).
 *
 * Filters a data field with the Canny edge detector filter.
 **/
void
gwy_field_filter_canny(GwyField *field,
                       GwyField *target,
                       gdouble threshold)
{
    if (!_gwy_field_check_other(field, target, FALSE))
        return;

    gwy_field_make_alike(target, field);
    gwy_unit_clear(gwy_field_get_unit_z(target));

    GwyField *sobel_vertical = gwy_field_new_alike(field, FALSE);
    GwyField *sobel_horizontal = (target == field ? gwy_field_new_alike(field, FALSE) : target);

    gwy_field_derivative_filter(field, sobel_horizontal, GWY_DERIVATIVE_FILTER_HSOBEL, FALSE);
    gwy_field_derivative_filter(field, sobel_vertical, GWY_DERIVATIVE_FILTER_VSOBEL, FALSE);

    gdouble *tdata = target->priv->data;
    gint xres = field->xres, yres = field->yres;
    gsize n = xres*yres;
    const gdouble *svdata = sobel_vertical->priv->data, *shdata = sobel_horizontal->priv->data;
    for (gsize k = 0; k < n; k++)
        tdata[k] = fabs(shdata[k]) + fabs(svdata[k]);

    gwy_field_invalidate(target);

    gdouble min, max;
    gwy_field_min_max(target, &min, &max);
    threshold = min + (max - min)*threshold;

    /* We do not need Sobel field data more, so use sobel_horizontal to store data results. */
    GwyField *result = sobel_horizontal;
    gdouble *resdata = result->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(tdata,resdata,svdata,xres,yres,threshold)
#endif
    for (gint i = 1; i < yres-1; i++) {
        for (gint j = 1; j < xres-1; j++) {
            gint pass = 0;
            if (tdata[i*xres + j] > threshold) {
                gdouble angle = atan2(svdata[i*xres + j], resdata[i*xres + j]);

                if (angle < 0.3925 || angle > 5.8875 || (angle > 2.7475 && angle < 3.5325)) {
                    if (tdata[j + 1 + xres*i] > threshold)
                        pass = 1;
                }
                else if ((angle > 1.178 && angle < 1.9632) || (angle > 4.318 && angle < 5.1049)) {
                    if (tdata[j + 1 + xres*(i + 1)] > threshold)
                        pass = 1;
                }
                else {
                    if (tdata[j + xres*(i + 1)] > threshold)
                        pass = 1;
                }
            }
            resdata[i*xres + j] = pass;
        }
    }
    g_object_unref(sobel_vertical);

    /* The result is now in result (formerly sobel_horizontal) field, which also may be the same as target. */
    if (result != target) {
        gwy_field_copy_data(result, target);
        g_object_unref(result);
    }

    /* Thin the lines. */
    thin_field(target);
}

/**
 * gwy_field_filter_gauss_step:
 * @field: A data field to apply the filter to.
 * @target: A data field to put the result to.  It will be resized to match @field.
 * @sigma: Gaussian filter width (in pixels).
 *
 * Processes a data field with Gaussian step detection filter.
 *
 * The filter is a multi-directional combination of convolutions with Gaussian multiplied by a signed step function.
 *
 * The resulting values correspond roughly to the step height around the pixel.
 **/
void
gwy_field_filter_gauss_step(GwyField *field, GwyField *target, gdouble sigma)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));
    gwy_field_resize(target, field->xres, field->yres);

    GwyField *extfield, *kernel, *freal, *fimag, *kreal, *kimag, *gauss;
    guint ext, extx, exty, extxres, extyres, extn, xres, yres, n, idir, ndirs = 6;
    gdouble *fre, *fim, *g, *k, *kre, *kim, *f, *d;
    gdouble gs;

    g_return_if_fail(sigma >= 0.0);
    if (sigma < 1e-9) {
        gwy_field_clear(target);
        return;
    }

    ext = GWY_ROUND(3.0*sigma) + 1;
    xres = field->xres;
    yres = field->yres;
    n = xres*yres;
    extx = gwy_fft_find_nice_size(xres + ext) - xres;
    exty = gwy_fft_find_nice_size(yres + ext) - yres;
    extfield = gwy_field_extend(field, extx/2, extx - extx/2, exty/2, exty - exty/2,
                                GWY_EXTERIOR_BORDER, 0.0, FALSE);

    extxres = extfield->xres;
    extyres = extfield->yres;

    extn = extxres*extyres;
    freal = gwy_field_new_alike(extfield, FALSE);
    fimag = gwy_field_new_alike(extfield, FALSE);

    kernel = gwy_field_new_alike(extfield, FALSE);
    k = kernel->priv->data;
    kreal = gwy_field_new_alike(extfield, FALSE);
    kimag = gwy_field_new_alike(extfield, FALSE);
    kre = kreal->priv->data;
    kim = kimag->priv->data;

    /* Subtract background on scale much larger than sigma. */
    gauss = gwy_field_new_alike(extfield, FALSE);
    g = gwy_field_get_data(gauss);
    gs = 0.0;
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            reduction(+:gs) \
            shared(extxres,extyres,g,sigma)
#endif
    for (guint i = 0; i < extyres; i++) {
        guint m = i*extxres;
        gdouble y = (i < (extyres + 1)/2 ? i : extyres-i)/(10.0*sigma);
        for (guint j = 0; j < extxres; j++, m++) {
            gdouble x = (j < (extxres + 1)/2 ? j : extxres-j)/(10.0*sigma);

            g[m] = exp(-x*x-y*y);
            gs += g[m];
        }
    }
    gwy_field_multiply(gauss, 1.0/gs);

    gwy_field_copy_data(extfield, kernel);
    /* FIXME: We could do this in frequency space and directly modify freal and fimag. */
    gwy_field_fft_convolve(kernel, gauss);
    gwy_field_subtract_fields(extfield, extfield, kernel);

    gwy_field_fft_2d_raw(extfield, NULL, freal, fimag, GWY_TRANSFORM_DIRECTION_FORWARD);
    fre = freal->priv->data;
    fim = fimag->priv->data;

    /* Prepare a Gaussian. */
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(extxres,extyres,g,sigma)
#endif
    for (guint i = 0; i < extyres; i++) {
        guint m = i*extxres;
        gdouble y = (i < (extyres + 1)/2 ? i : extyres-i)/sigma;
        for (guint j = 0; j < extxres; j++, m++) {
            gdouble x = (j < (extxres + 1)/2 ? j : extxres-j)/sigma;

            g[m] = exp(-x*x-y*y);
        }
    }
    g[0] = 0.0;
    gs = gwy_field_sum(gauss);

    /* Convolve with the Gaussian multiplied by rotated signum function. */
    gwy_field_clear(extfield);
    f = extfield->priv->data;
    for (idir = 0; idir < ndirs; idir++) {
        gdouble phi = (idir + 0.5)/ndirs*G_PI;
        gdouble cphi = cos(phi), sphi = sin(phi);

#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(extxres,extyres,g,k,sigma,cphi,sphi)
#endif
        for (guint i = 0; i < extyres; i++) {
            gdouble y = (i < (extyres + 1)/2 ? i/sigma : (extyres-i)/(-sigma));
            guint m = i*extxres;
            for (guint j = 0; j < extxres; j++, m++) {
                gdouble x = (j < (extxres + 1)/2 ? j/sigma : (extxres-j)/(-sigma));
                gdouble v = x*cphi + y*sphi;

                if (G_UNLIKELY(fabs(v) < 1e-9))
                    k[m] = 0.0;
                else
                    k[m] = g[m]*((v > 0.0) - 0.5);
            }
        }

        gwy_field_fft_2d_raw(kernel, NULL, kreal, kimag, GWY_TRANSFORM_DIRECTION_FORWARD);

#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(extn,kre,kim,fre,fim)
#endif
        for (guint i = 0; i < extn; i++) {
            gdouble re = kre[i]*fre[i] - kim[i]*fim[i];
            gdouble im = kre[i]*fim[i] + kim[i]*fre[i];
            kre[i] = re;
            kim[i] = im;
        }
        gwy_field_fft_2d_raw(kreal, kimag, kernel, NULL, GWY_TRANSFORM_DIRECTION_BACKWARD);
        for (guint i = 0; i < extn; i++)
            f[i] += k[i]*k[i];
    }

    gwy_field_area_copy(extfield, target, extx/2, exty/2, xres, yres, 0, 0);

    d = target->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(n,d,gs)
#endif
    for (guint i = 0; i < n; i++)
        d[i] = sqrt(d[i]*n*9.0)/gs;
    gwy_field_invalidate(target);

    g_object_unref(extfield);
    g_object_unref(kreal);
    g_object_unref(kimag);
    g_object_unref(freal);
    g_object_unref(fimag);
    g_object_unref(kernel);
    g_object_unref(gauss);
}

/**
 * gwy_field_area_filter_conservative:
 * @field: A data field to apply the filter to.
 * @size: Filtered area size.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 *
 * Filters a rectangular part of a data field with conservative denoise filter.
 **/
void
gwy_field_area_filter_conservative(GwyField *field,
                                   gint size,
                                   gint col, gint row,
                                   gint width, gint height)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE))
        return;
    g_return_if_fail(size > 0);

    gint xres = field->xres, yres = field->yres;
    if (size == 1)
        return;
    if (size > width || size > height) {
        g_warning("Kernel size larger than field area size.");
        return;
    }

    GwyField *hlp_df = gwy_field_new(width, height, 1.0, 1.0, FALSE);
    gdouble *data = field->priv->data;
    gdouble *hlpdata = hlp_df->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(data,hlpdata,xres,yres,width,height,col,row,size)
#endif
    for (gint i = 0; i < height; i++) {
        gint ifrom = MAX(0, i + row - (size-1)/2);
        gint ito = MIN(yres-1, i + row + size/2);

        for (gint j = 0; j < width; j++) {
            gint jfrom = MAX(0, j + col - (size-1)/2);
            gint jto = MIN(xres-1, j + col + size/2);
            gdouble maxval = -G_MAXDOUBLE, minval = G_MAXDOUBLE;

            for (gint ii = 0; ii <= ito - ifrom; ii++) {
                gdouble *drow = data + (ifrom + ii)*xres + jfrom;

                for (gint jj = 0; jj <= jto - jfrom; jj++) {
                    if (i + row == ii + ifrom && j + col == jj + jfrom)
                        continue;

                    if (drow[jj] < minval)
                        minval = drow[jj];
                    if (drow[jj] > maxval)
                        maxval = drow[jj];
                }
            }

            hlpdata[i*width + j] = CLAMP(data[(i + row)*xres + j + col], minval, maxval);
        }
    }
    /* fix bottom right corner for size == 2 */
    if (size == 2)
        hlpdata[height*width - 1] = data[(row + height-1)*xres + col + width-1];

    gwy_field_area_copy(hlp_df, field, 0, 0, width, height, col, row);
    g_object_unref(hlp_df);
    gwy_field_invalidate(field);
}

/**
 * gwy_field_filter_conservative:
 * @field: A data field to apply the filter to.
 * @size: Filtered area size.
 *
 * Filters a data field with conservative denoise filter.
 **/
void
gwy_field_filter_conservative(GwyField *field,
                              gint size)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_filter_conservative(field, size, 0, 0, field->xres, field->yres);
}

/* Return 8*number-of-neighbours + number-of-neighbour-segments.
 * Segments must be separated by unset segments, so it is zero for all set
 * neighbourhood. */
static guint
neighbour_segments(const gdouble *d, gint xres, gint k)
{
    static const guint neightable[0x100] = {
        0,  9,  9,  17, 9,  18, 17, 25, 9,  17, 18, 25, 18, 26, 26, 33,
        9,  18, 18, 26, 17, 26, 25, 33, 18, 26, 27, 34, 26, 34, 34, 41,
        9,  18, 18, 26, 18, 27, 26, 34, 17, 25, 26, 33, 26, 34, 34, 41,
        18, 27, 27, 35, 26, 35, 34, 42, 26, 34, 35, 42, 34, 42, 42, 49,
        9,  18, 18, 26, 18, 27, 26, 34, 18, 26, 27, 34, 27, 35, 35, 42,
        18, 27, 27, 35, 26, 35, 34, 42, 27, 35, 36, 43, 35, 43, 43, 50,
        17, 26, 26, 34, 26, 35, 34, 42, 25, 33, 34, 41, 34, 42, 42, 49,
        26, 35, 35, 43, 34, 43, 42, 50, 34, 42, 43, 50, 42, 50, 50, 57,
        9,  18, 18, 26, 18, 27, 26, 34, 18, 26, 27, 34, 27, 35, 35, 42,
        17, 26, 26, 34, 25, 34, 33, 41, 26, 34, 35, 42, 34, 42, 42, 49,
        18, 27, 27, 35, 27, 36, 35, 43, 26, 34, 35, 42, 35, 43, 43, 50,
        26, 35, 35, 43, 34, 43, 42, 50, 34, 42, 43, 50, 42, 50, 50, 57,
        17, 26, 26, 34, 26, 35, 34, 42, 26, 34, 35, 42, 35, 43, 43, 50,
        25, 34, 34, 42, 33, 42, 41, 49, 34, 42, 43, 50, 42, 50, 50, 57,
        25, 34, 34, 42, 34, 43, 42, 50, 33, 41, 42, 49, 42, 50, 50, 57,
        33, 42, 42, 50, 41, 50, 49, 57, 41, 49, 50, 57, 49, 57, 57, 64,
    };
    guint b = 0;

    if (d[k-xres-1] > 0.0)
        b |= 1;
    if (d[k-xres] > 0.0)
        b |= 2;
    if (d[k-xres+1] > 0.0)
        b |= 4;
    if (d[k-1] > 0.0)
        b |= 8;
    if (d[k+1] > 0.0)
        b |= 16;
    if (d[k+xres-1] > 0.0)
        b |= 32;
    if (d[k+xres] > 0.0)
        b |= 64;
    if (d[k+xres+1] > 0.0)
        b |= 128;

    return neightable[b];
}

static gboolean
pixel_thinnable(const gdouble *data, gint xres, gint k)
{
    guint neighval;

    neighval = neighbour_segments(data, xres, k);
    /* One contiguous neighbour segment. */
    if (neighval % 8 != 1)
        return FALSE;

    /* Two to six neighbours. */
    neighval /= 8;
    if (neighval < 2 || neighval > 6)
        return FALSE;

    /* We could pull the first parts into the table in neighbour_segments(), but I am currently too lazy. */
    if (data[k + 1] > 0.0 && data[k - 1] > 0.0 && data[k + xres] > 0.0
        && neighbour_segments(data, xres, k+xres) % 8 == 1)
        return FALSE;

    if (data[k + 1] > 0.0 && data[k - xres] > 0.0 && data[k + xres] > 0.0
        && neighbour_segments(data, xres, k+1) % 8 == 1)
        return FALSE;

    return TRUE;
}

static gint
thinstep(GwyField *field,
         GwyField *buffer)
{
    gint i, j, ch;
    gint xres = field->xres, yres = field->yres;
    gdouble *data = field->priv->data;
    gdouble *bdata = buffer->priv->data;

    gwy_field_clear(buffer);
    ch = 0;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(+:ch) \
            private(i,j) \
            shared(data,bdata,xres,yres)
#endif
    for (i = 2; i < yres-2; i++) {
        for (j = 2; j < xres-2; j++) {
            gint k = i*xres + j;
            if (data[k] > 0.0 && pixel_thinnable(data, xres, k)) {
                ch++;
                bdata[k] = 1.0;
            }
        }
    }
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i,j) \
            shared(data,bdata,xres,yres)
#endif
    for (i = 2; i < yres-1; i++) {
        for (j = 2; j < xres-1; j++) {
            gint k = i*xres + j;
            if (bdata[k] > 0.0)
                data[k] = 0.0;
        }
    }
    gwy_field_invalidate(field);

    return ch;
}

/* XXX: Could be an alternative public function to gwy_field_thin().  It is more aggressive and loses some
 * branches, but this can be a good thing.  But for that it would be nice to implement it using a pixel queue so that
 * we do not have to repeatedly scan the entire data field. */
static void
thin_field(GwyField *field)
{
    gint xres = field->xres, yres = field->yres;

    gwy_field_area_clear(field, 0, 0, xres, 1);
    gwy_field_area_clear(field, 0, 0, 1, yres);
    gwy_field_area_clear(field, xres-1, 0, 1, yres);
    gwy_field_area_clear(field, 0, yres-1, xres, 1);

    GwyField *buffer = gwy_field_new_alike(field, FALSE);
    while (thinstep(field, buffer))
        ;
    g_object_unref(buffer);

    gwy_field_invalidate(field);
}

/**
 * gwy_field_shade:
 * @field: A data field.
 * @target: A data field to put the shade to.  It will be resized to match @field.
 * @theta: Shading angle (in radians, from north pole).
 * @phi: Shade orientation in xy plane (in radians, counterclockwise).
 *
 * Shades a data field.
 **/
void
gwy_field_shade(GwyField *field,
                GwyField *target,
                gdouble theta, gdouble phi)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));

    gint xres = field->xres, yres = field->yres;
    gwy_field_resize(target, xres, yres);
    gdouble *tdata = target->priv->data;

    gdouble max = -G_MAXDOUBLE;
    gdouble cphi = cos(phi);
    gdouble sphi = sin(phi);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            reduction(max:max) \
            shared(field,tdata,xres,yres,cphi,sphi)
#endif
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gdouble v = _gwy_field_xder(field, j, i)*cphi + _gwy_field_yder(field, j, i)*sphi;
            tdata[j + xres*i] = -v;
            if (max < v)
                max = v;
        }
    }

    gdouble maxval = theta/max;
    gsize n = (gsize)xres * (gsize)yres;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(tdata,n,max,maxval)
#endif
    for (gsize i = 0; i < n; i++)
        tdata[i] = max - fabs(maxval - tdata[i]);

    gwy_field_invalidate(target);
}

/**
 * gwy_field_filter_harris:
 * @x_gradient: Data field with pre-calculated horizontal derivative.
 * @y_gradient: Data field with pre-calculated vertical derivative.
 * @result: Data field for the result.
 * @neighbourhood: Neighbourhood size.
 * @alpha: Sensitivity parameter (the squared trace is multiplied by it).
 *
 * Applies Harris corner detection filter to a pair of gradient data fields.
 *
 * All passed data field must have the same size.
 **/
void
gwy_field_filter_harris(GwyField *x_gradient,
                        GwyField *y_gradient,
                        GwyField *result,
                        gint neighbourhood,
                        gdouble alpha)
{
    if (!_gwy_field_check_other(result, x_gradient, FALSE)
        || !_gwy_field_check_other(result, y_gradient, FALSE))
        return;
    g_return_if_fail(neighbourhood > 0);

    gdouble mult, sigma;
    gint i, j;
    GwyField *xx, *xy, *yy;
    gdouble *xxdata, *xydata, *yydata, *xg, *yg, *r;
    gint yres = result->yres, xres = result->xres;

    if (2*neighbourhood >= MIN(xres, yres)) {
        gwy_field_clear(result);
        return;
    }

    mult = (fabs(gwy_field_max(x_gradient) - gwy_field_min(x_gradient))
            + fabs(gwy_field_max(y_gradient) - gwy_field_min(y_gradient)));
    mult = 1.0/(mult*mult);

    xx = result;
    xy = gwy_field_new_alike(result, TRUE);
    yy = gwy_field_new_alike(result, TRUE);
    xxdata = xx->priv->data;
    xydata = xy->priv->data;
    yydata = yy->priv->data;
    xg = x_gradient->priv->data;
    yg = y_gradient->priv->data;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i,j) \
            shared(xg,yg,xxdata,xydata,yydata,xres,yres,neighbourhood,mult)
#endif
    for (i = neighbourhood; i < yres - neighbourhood; i++) {
         for (j = neighbourhood; j < xres - neighbourhood; j++) {
             gint k = i*xres + j;
             gdouble vx = xg[k], vy = yg[k];
             xxdata[k] = vx*vx*mult;
             xydata[k] = vx*vy*mult;
             yydata[k] = vy*vy*mult;
         }
    }

    sigma = neighbourhood/5.0;
    gwy_field_filter_gaussian(xx, sigma);
    gwy_field_filter_gaussian(xy, sigma);
    gwy_field_filter_gaussian(yy, sigma);

    r = result->priv->data;
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            private(i,j) \
            shared(r,xxdata,xydata,yydata,xres,yres,neighbourhood,alpha)
#endif
    for (i = neighbourhood; i < yres - neighbourhood; i++) {
         for (j = neighbourhood; j < xres - neighbourhood; j++) {
             gint k = i*xres + j;
             gdouble pxx = xxdata[k], pxy = xydata[k], pyy = yydata[k];
             gdouble det = pxx*pyy - pxy*pxy, trace = pxx + pyy;
             r[k] = det - alpha*trace*trace;
          }
    }

    gwy_field_area_clear(result, 0, 0, xres, neighbourhood);
    gwy_field_area_clear(result, 0, neighbourhood, neighbourhood, yres-2*neighbourhood);
    gwy_field_area_clear(result, xres-neighbourhood, neighbourhood, neighbourhood, yres-2*neighbourhood);
    gwy_field_area_clear(result, 0, yres-neighbourhood, xres, neighbourhood);

    /* xx = result */
    g_object_unref(xy);
    g_object_unref(yy);
}

/**
 * SECTION:filters
 * @title: filters
 * @short_description: Convolution and other 2D data filters
 *
 * Filters are point-wise operations, such as thresholding, or generally local operations producing a value based on
 * the data in the vicinity of each point: gradients, step detectors and convolutions.  Some simple common point-wise
 * operations, e.g. value inversion, are also found in base #GwyField methods.
 **/

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