/*
 *  $Id: filters--small.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.
 *
 *  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/stats.h"
#include "libgwyddion/arithmetic.h"

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

// We do a convolution. So one of the arguments need to be flipped.
static inline gdouble
convolve_3x3(const gdouble *prev, const gdouble *row, const gdouble *next,
             const gdouble *kernel)
{
    return (kernel[8]*prev[0] + kernel[7]*prev[1] + kernel[6]*prev[2]
            + kernel[5]*row[0] + kernel[4]*row[1] + kernel[3]*row[2]
            + kernel[2]*next[0] + kernel[1]*next[1] + kernel[0]*next[2]);
}

static inline gdouble
convolve_5x5(const gdouble *prev2, const gdouble *prev,
             const gdouble *row,
             const gdouble *next, const gdouble *next2,
             const gdouble *kernel)
{
    return (kernel[24]*prev2[0] + kernel[23]*prev2[1] + kernel[22]*prev2[2] + kernel[21]*prev2[3] + kernel[20]*prev2[4]
            + kernel[19]*prev[0] + kernel[18]*prev[1] + kernel[17]*prev[2] + kernel[16]*prev[3] + kernel[15]*prev[4]
            + kernel[14]*row[0] + kernel[13]*row[1] + kernel[12]*row[2] + kernel[11]*row[3] + kernel[10]*row[4]
            + kernel[9]*next[0] + kernel[8]*next[1] + kernel[7]*next[2] + kernel[6]*next[3] + kernel[5]*next[4]
            + kernel[4]*next2[0] + kernel[3]*next2[1] + kernel[2]*next2[2] + kernel[1]*next2[3] + kernel[0]*next2[4]);
}

/**
 * gwy_field_area_filter_3x3:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can be @field itself and in any case it must have the same
 *          size.
 * @mask: Mask specifying which pixels to apply the filter to.
 * @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).
 * @kernel: (array size=9): Filter kernel.
 *
 * Applies a 3×3 convolution filter to a data field area.
 *
 * The filter is appled as a convolution filter. For asymmetrical filters, for example derivatives, the signs needs to
 * be [1, 0, -1] instead of [-1, 0, 1] to get the usual derivative sign.
 *
 * The mask, if given, determines which pixels are modified by the filter, not which are used as the inpot data (which
 * would hardly make any sense for irregular masked regions). If @target differs from @source, pixels excluded
 * according to the current masking mode are unchanged.
 **/
void
gwy_field_area_filter_3x3(GwyField *field,
                          GwyField *target,
                          GwyNield *mask,
                          GwyMaskingType masking,
                          gint col, gint row,
                          gint width, gint height,
                          const gdouble *kernel)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_FIELD(target));
    // Allow the target to also be just the size of the area because it is convenient for us. But do not announce it.
    gboolean target_is_area = (target->xres == width && target->yres == height);
    if (!target_is_area && !_gwy_field_check_other(field, target, FALSE))
        return;
    g_return_if_fail(kernel);

    gint xres = field->xres, yres = field->yres, txres = target->xres;
    const gdouble *sdata = field->priv->data + row*xres + col;
    gdouble *tdata = target->priv->data + (target_is_area ? 0 : row*txres + col);
    const gint *mdata = mask ? mask->priv->data + row*txres + col : NULL;
    // Use a temporary buffer when overwriting the source to safely paralelise.
    if (target == field) {
        tdata = g_new(gdouble, width*height);
        txres = width;
        // Initialise the buffer with data from the target (conceptually – it is the same field as the source). This
        // is not necessary when no mask is applied because we set all pixels in the rectangle.
        if (mask) {
            for (gint i = 0; i < height; i++)
                gwy_assign(tdata + i*width, target->priv->data + (i + row)*xres + col, width);
        }
    }

    gint jfrom = col ? 0 : 1, jto = col + width == xres ? width-1 : width;
    if (mdata) {
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(xres,yres,txres,col,row,width,height,jfrom,jto,sdata,tdata,mdata,masking,kernel)
#endif
        for (gint i = 0; i < height; i++) {
            gint im = MAX(row + i-1, 0) - row, ip = MIN(row + i+1, yres-1) - row;
            // Point to the first pixels to use in the convolution (not to the central pixel). Meaning row[0] may
            // be outside the image (as well as row[xres+1], but not row[xres]).
            const gdouble *srowm = sdata + im*xres - 1, *srow = sdata + i*xres - 1, *srowp = sdata + ip*xres - 1;
            gdouble *trow = tdata + i*txres;
            const gint *mrow = mdata + i*xres;

            // Handle the silly xres=1 case completely separately.
            if (xres == 1) {
                if (nielded_included(mrow + 0, masking)) {
                    gdouble sm[3] = { srowm[1], srowm[1], srowm[1] };
                    gdouble s[3] = { srow[1], srow[1], srow[1] };
                    gdouble sp[3] = { srowp[1], srowp[1], srowp[1] };
                    trow[0] = convolve_3x3(sm, s, sp, kernel);
                }
                continue;
            }

            // Handle the first pixel separately if we went outside the image.
            if (jfrom) {
                if (nielded_included(mrow + 0, masking)) {
                    gdouble sm[3] = { srowm[1], srowm[1], srowm[2] };
                    gdouble s[3] = { srow[1], srow[1], srow[2] };
                    gdouble sp[3] = { srowp[1], srowp[1], srowp[2] };
                    trow[0] = convolve_3x3(sm, s, sp, kernel);
                }
            }

            // Interior. The part we want to be tight.
            for (gint j = jfrom; j < jto; j++) {
                if (nielded_included(mrow + j, masking))
                    trow[j] = convolve_3x3(srowm + j, srow + j, srowp + j, kernel);
            }

            // Handle the last pixel separately if we went outside the image.
            if (jto != width) {
                if (nielded_included(mrow + width-1, masking)) {
                    gdouble sm[3] = { srowm[width-1], srowm[width], srowm[width] };
                    gdouble s[3] = { srow[width-1], srow[width], srow[width] };
                    gdouble sp[3] = { srowp[width-1], srowp[width], srowp[width] };
                    trow[width-1] = convolve_3x3(sm, s, sp, kernel);
                }
            }
        }
    }
    else {
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(xres,yres,txres,col,row,width,height,jfrom,jto,sdata,tdata,mdata,masking,kernel)
#endif
        for (gint i = 0; i < height; i++) {
            gint im = MAX(row + i-1, 0) - row, ip = MIN(row + i+1, yres-1) - row;
            const gdouble *srowm = sdata + im*xres - 1, *srow = sdata + i*xres - 1, *srowp = sdata + ip*xres - 1;
            gdouble *trow = tdata + i*txres;

            // Handle the silly xres=1 case completely separately.
            if (xres == 1) {
                gdouble sm[3] = { srowm[1], srowm[1], srowm[1] };
                gdouble s[3] = { srow[1], srow[1], srow[1] };
                gdouble sp[3] = { srowp[1], srowp[1], srowp[1] };
                trow[0] = convolve_3x3(sm, s, sp, kernel);
                continue;
            }

            // Handle the first pixel separately if we went outside the image.
            if (jfrom) {
                gdouble sm[3] = { srowm[1], srowm[1], srowm[2] };
                gdouble s[3] = { srow[1], srow[1], srow[2] };
                gdouble sp[3] = { srowp[1], srowp[1], srowp[2] };
                trow[0] = convolve_3x3(sm, s, sp, kernel);
            }

            // Interior. The part we want to be tight.
            for (gint j = jfrom; j < jto; j++)
                trow[j] = convolve_3x3(srowm + j, srow + j, srowp + j, kernel);

            // Handle the last pixel separately if we went outside the image.
            if (jto != width) {
                gdouble sm[3] = { srowm[width-1], srowm[width], srowm[width] };
                gdouble s[3] = { srow[width-1], srow[width], srow[width] };
                gdouble sp[3] = { srowp[width-1], srowp[width], srowp[width] };
                trow[width-1] = convolve_3x3(sm, s, sp, kernel);
            }
        }
    }

    if (target == field) {
        for (gint i = 0; i < height; i++)
            gwy_assign(target->priv->data + (i + row)*xres + col, tdata + i*width, width);
        g_free(tdata);
    }
    gwy_field_invalidate(target);
}

/**
 * gwy_field_filter_3x3:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can be @field itself and in any case it must have the same
 *          size.
 * @kernel: (array size=9): Filter kernel.
 *
 * Applies a 3×3 convolution filter to a data field.
 *
 * The filter is appled as a convolution filter. For asymmetrical filters, for example derivatives, the signs needs to
 * be [1, 0, -1] instead of [-1, 0, 1] to get the usual derivative sign.
 **/
void
gwy_field_filter_3x3(GwyField *field,
                     GwyField *target,
                     const gdouble *kernel)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres, kernel);
}

void
_gwy_field_area_filter_5x5(GwyField *field,
                           GwyField *target,
                           GwyNield *mask,
                           GwyMaskingType masking,
                           gint col, gint row,
                           gint width, gint height,
                           const gdouble *kernel,
                           Filter5x5Func filterfunc)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(GWY_IS_FIELD(target));
    // Allow the target to also be just the size of the area because it is convenient for us. But do not announce it.
    gboolean target_is_area = (target->xres == width && target->yres == height);
    if (!target_is_area && !_gwy_field_check_other(field, target, FALSE))
        return;
    g_return_if_fail(!kernel ^ !filterfunc);

    gint xres = field->xres, yres = field->yres, txres = target->xres;
    const gdouble *sdata = field->priv->data + row*xres + col;
    gdouble *tdata = target->priv->data + (target_is_area ? 0 : row*txres + col);
    const gint *mdata = mask ? mask->priv->data + row*txres + col : NULL;
    // Use a temporary buffer when overwriting the source to safely paralelise.
    if (target == field) {
        tdata = g_new(gdouble, width*height);
        txres = width;
        // Initialise the buffer with data from the target (conceptually – it is the same field as the source). This
        // is not necessary when no mask is applied because we set all pixels in the rectangle.
        if (mask) {
            for (gint i = 0; i < height; i++)
                gwy_assign(tdata + i*width, target->priv->data + (i + row)*xres + col, width);
        }
    }

    gint jfrom = CLAMP(2-col, 0, width), jto = CLAMP(xres-2 - col, 0, width);
    // Merge borders.
    if (jfrom >= jto)
        jfrom = width;

    if (mdata) {
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(xres,yres,txres,col,row,width,height,jfrom,jto,sdata,tdata,mdata,masking,kernel,filterfunc)
#endif
        for (gint i = 0; i < height; i++) {
            gint imm = MAX(row + i-2, 0) - row, im = MAX(row + i-1, 0) - row;
            gint ipp = MIN(row + i+2, yres-1) - row, ip = MIN(row + i+1, yres-1) - row;
            // Point to the first pixels to use in the convolution (not to the central pixel). Meaning row[0] and
            // row[1] may be outside the image (as well as row[xres+2], but not row[xres] and row[xres+1]).
            const gdouble *srowmm = sdata + imm*xres - 2, *srowm = sdata + im*xres - 2;
            const gdouble *srow = sdata + i*xres - 2;
            const gdouble *srowp = sdata + ip*xres - 2, *srowpp = sdata + ipp*xres - 2;
            gdouble *trow = tdata + i*txres;
            const gint *mrow = mdata + i*xres;

            // Handle the initial pixels separately if we went outside the image. However, if there is no inner
            // part the initial and final part can overlap. In such case initial part is extended to include the
            // entire with and skip the other two.
            for (gint j = 0; j < jfrom; j++) {
                if (nielded_included(mrow + j, masking)) {
                    gdouble smm[5], sm[5], s[5], sp[5], spp[5];
                    for (gint jj = 0; jj < 5; jj++) {
                        gint jclamp = CLAMP(j + jj, 2, xres+1 - col);
                        smm[jj] = srowmm[jclamp];
                        sm[jj] = srowm[jclamp];
                        s[jj] = srow[jclamp];
                        sp[jj] = srowp[jclamp];
                        spp[jj] = srowpp[jclamp];
                    }
                    if (filterfunc)
                        trow[j] = filterfunc(smm, sm, s, sp, spp);
                    else
                        trow[j] = convolve_5x5(smm, sm, s, sp, spp, kernel);
                }
            }
            if (jfrom == width)
                continue;

            // Interior. The part we want to be tight.
            if (filterfunc) {
                for (gint j = jfrom; j < jto; j++) {
                    if (nielded_included(mrow + j, masking))
                        trow[j] = filterfunc(srowmm + j, srowm + j, srow + j, srowp + j, srowpp + j);
                }
            }
            else {
                for (gint j = jfrom; j < jto; j++) {
                    if (nielded_included(mrow + j, masking))
                        trow[j] = convolve_5x5(srowmm + j, srowm + j, srow + j, srowp + j, srowpp + j, kernel);
                }
            }

            // Handle the final pixels separately if we went outside the image.
            // We know the final part does not overlap the initial part as this has already been handled.
            for (gint j = jto; j < width; j++) {
                if (nielded_included(mrow + j, masking)) {
                    gdouble smm[5], sm[5], s[5], sp[5], spp[5];
                    for (gint jj = 0; jj < 5; jj++) {
                        // The lower bound of CLAMP() should be already handled by the MAX() above.
                        gint jclamp = CLAMP(j + jj, 2, xres+1 - col);
                        smm[jj] = srowmm[jclamp];
                        sm[jj] = srowm[jclamp];
                        s[jj] = srow[jclamp];
                        sp[jj] = srowp[jclamp];
                        spp[jj] = srowpp[jclamp];
                    }
                    if (filterfunc)
                        trow[j] = filterfunc(smm, sm, s, sp, spp);
                    else
                        trow[j] = convolve_5x5(smm, sm, s, sp, spp, kernel);
                }
            }
        }
    }
    else {
#ifdef _OPENMP
#pragma omp parallel for if (gwy_threads_are_enabled()) default(none) \
            shared(xres,yres,txres,col,row,width,height,jfrom,jto,sdata,tdata,mdata,masking,kernel,filterfunc)
#endif
        for (gint i = 0; i < height; i++) {
            gint imm = MAX(row + i-2, 0) - row, im = MAX(row + i-1, 0) - row;
            gint ipp = MIN(row + i+2, yres-1) - row, ip = MIN(row + i+1, yres-1) - row;
            // Point to the first pixels to use in the convolution (not to the central pixel). Meaning row[0] and
            // row[1] may be outside the image (as well as row[xres+2], but not row[xres] and row[xres+1]).
            const gdouble *srowmm = sdata + imm*xres - 2, *srowm = sdata + im*xres - 2;
            const gdouble *srow = sdata + i*xres - 2;
            const gdouble *srowp = sdata + ip*xres - 2, *srowpp = sdata + ipp*xres - 2;
            gdouble *trow = tdata + i*txres;

            // Handle the initial pixels separately if we went outside the image. However, if there is no inner
            // part the initial and final part can overlap. In such case initial part is extended to include the
            // entire with and skip the other two.
            for (gint j = 0; j < jfrom; j++) {
                gdouble smm[5], sm[5], s[5], sp[5], spp[5];
                for (gint jj = 0; jj < 5; jj++) {
                    gint jclamp = CLAMP(j + jj, 2, xres+1);
                    smm[jj] = srowmm[jclamp];
                    sm[jj] = srowm[jclamp];
                    s[jj] = srow[jclamp];
                    sp[jj] = srowp[jclamp];
                    spp[jj] = srowpp[jclamp];
                }
                if (filterfunc)
                    trow[j] = filterfunc(smm, sm, s, sp, spp);
                else
                    trow[j] = convolve_5x5(smm, sm, s, sp, spp, kernel);
            }
            if (jfrom == width)
                continue;

            // Interior. The part we want to be tight.
            if (filterfunc) {
                for (gint j = jfrom; j < jto; j++)
                    trow[j] = filterfunc(srowmm + j, srowm + j, srow + j, srowp + j, srowpp + j);
            }
            else {
                for (gint j = jfrom; j < jto; j++)
                    trow[j] = convolve_5x5(srowmm + j, srowm + j, srow + j, srowp + j, srowpp + j, kernel);
            }

            // Handle the final pixels separately if we went outside the image.
            // We know the final part does not overlap the initial part as this has already been handled.
            for (gint j = jto; j < width; j++) {
                gdouble smm[5], sm[5], s[5], sp[5], spp[5];
                for (gint jj = 0; jj < 5; jj++) {
                    // The lower bound of CLAMP() should be already handled by the MAX() above.
                    gint jclamp = CLAMP(j + jj, 2, xres+1);
                    smm[jj] = srowmm[jclamp];
                    sm[jj] = srowm[jclamp];
                    s[jj] = srow[jclamp];
                    sp[jj] = srowp[jclamp];
                    spp[jj] = srowpp[jclamp];
                }
                if (filterfunc)
                    trow[j] = filterfunc(smm, sm, s, sp, spp);
                else
                    trow[j] = convolve_5x5(smm, sm, s, sp, spp, kernel);
            }
        }
    }

    if (target == field) {
        for (gint i = 0; i < height; i++)
            gwy_assign(target->priv->data + (i + row)*xres + col, tdata + i*width, width);
        g_free(tdata);
    }
    gwy_field_invalidate(target);
}

static gdouble
kuwahara_5x5(const gdouble *pp, const gdouble *p,
             const gdouble *r,
             const gdouble *n, const gdouble *nn)
{
    gdouble s1 = pp[0] + pp[1] + p[0] + p[1];
    gdouble s2 = pp[3] + pp[4] + p[3] + p[4];
    gdouble s3 = n[0] + n[1] + nn[0] + nn[1];
    gdouble s4 = n[3] + n[4] + nn[3] + nn[4];
    gdouble s12 = pp[2] + p[2], s13 = r[0] + r[1], s24 = r[3] + r[4], s34 = n[2] + nn[2];
    gdouble c = r[2];

    // Means of the overlapping 4 corner squares. Calculate them first because of cancellation errors.
    gdouble mu1 = (s1 + s12 + s13 + c)/9.0;
    gdouble mu2 = (s2 + s12 + s24 + c)/9.0;
    gdouble mu3 = (s3 + s34 + s13 + c)/9.0;
    gdouble mu4 = (s4 + s34 + s24 + c)/9.0;

    // Variances in the same squares (unnormalised, just sums of squares).
    gdouble var1 = ((pp[0] - mu1)*(pp[0] - mu1) + (pp[1] - mu1)*(pp[1] - mu1) + (pp[2] - mu1)*(pp[2] - mu1)
                    + (p[0] - mu1)*(p[0] - mu1) + (p[1] - mu1)*(p[1] - mu1) + (p[2] - mu1)*(p[2] - mu1)
                    + (r[0] - mu1)*(r[0] - mu1) + (r[1] - mu1)*(r[1] - mu1) + (r[2] - mu1)*(r[2] - mu1));
    gdouble var2 = ((pp[2] - mu2)*(pp[2] - mu2) + (pp[3] - mu2)*(pp[3] - mu2) + (pp[4] - mu2)*(pp[4] - mu2)
                    + (p[2] - mu2)*(p[2] - mu2) + (p[3] - mu2)*(p[3] - mu2) + (p[4] - mu2)*(p[4] - mu2)
                    + (r[2] - mu2)*(r[2] - mu2) + (r[3] - mu2)*(r[3] - mu2) + (r[4] - mu2)*(r[4] - mu2));
    gdouble var3 = ((r[0] - mu3)*(r[0] - mu3) + (r[1] - mu3)*(r[1] - mu3) + (r[2] - mu3)*(r[2] - mu3)
                    + (n[0] - mu3)*(n[0] - mu3) + (n[1] - mu3)*(n[1] - mu3) + (n[2] - mu3)*(n[2] - mu3)
                    + (nn[0] - mu3)*(nn[0] - mu3) + (nn[1] - mu3)*(nn[1] - mu3) + (nn[2] - mu3)*(nn[2] - mu3));
    gdouble var4 = ((r[2] - mu4)*(r[2] - mu4) + (r[3] - mu4)*(r[3] - mu4) + (r[4] - mu4)*(r[4] - mu4)
                    + (n[2] - mu4)*(n[2] - mu4) + (n[3] - mu4)*(n[3] - mu4) + (n[4] - mu4)*(n[4] - mu4)
                    + (nn[2] - mu4)*(nn[2] - mu4) + (nn[3] - mu4)*(nn[3] - mu4) + (nn[4] - mu4)*(nn[4] - mu4));

    if (var1 <= var2) {
        if (var1 <= var3)
            return var1 <= var4 ? mu1 : mu4;
        else
            return var3 <= var4 ? mu3 : mu4;
    }
    else {
        if (var2 <= var3)
            return var2 <= var4 ? mu2 : mu4;
        else
            return var3 <= var4 ? mu3 : mu4;
    }
}

static gdouble
mlv_5x5(const gdouble *pp, const gdouble *p,
        const gdouble *r,
        const gdouble *n, const gdouble *nn)
{
    const gdouble *data[5] = { pp, p, r, n, nn };

    gdouble mu[9];
    for (gint i = 0; i < 3; i++) {
        for (gint j = 0; j < 3; j++) {
            gdouble s = 0.0;
            for (gint ii = 0; ii < 3; ii++) {
                for (gint jj = 0; jj < 3; jj++)
                    s += data[i + ii][j + jj];
            }
            mu[3*i + j] = s/9.0;
        }
    }

    gdouble var[9];
    for (gint i = 0; i < 3; i++) {
        for (gint j = 0; j < 3; j++) {
            gdouble s = 0.0, m = mu[3*i + j];
            for (gint ii = 0; ii < 3; ii++) {
                for (gint jj = 0; jj < 3; jj++) {
                    gdouble d = data[i + ii][j + jj] - m;
                    s += d*d;
                }
            }
            var[3*i + j] = s/9.0;
        }
    }

    // Decrease the variance for the central pixel especially when all are similar.
    gdouble c2 = 0.0;
    for (gint ii = 0; ii < 3; ii++) {
        for (gint jj = 0; jj < 3; jj++) {
            gdouble d = data[1 + ii][1 + jj];
            c2 += d*d;
        }
    }
    var[3*1 + 1] -= DBL_EPSILON*c2;

    gint i0 = 0;
    for (gint i = 1; i < 9; i++) {
        if (var[i] < var[i0])
            i0 = i;
    }
    return mu[i0];
}

/**
 * gwy_field_area_enhance_filter:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can be @field itself and in any case it must have the same
 *          size.
 * @mask: Mask specifying which pixels to apply the filter to.
 * @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).
 * @filter_type: Predefined denoising filter type.
 *
 * Applies a simple enhance filter to a data field area.
 *
 * The mask, if given, determines which pixels are modified by the filter, not which are used as the inpot data (which
 * would hardly make any sense for irregular masked regions). If @target differs from @source, pixels excluded
 * according to the current masking mode are unchanged.
 **/
void
gwy_field_area_enhance_filter(GwyField *field,
                              GwyField *target,
                              GwyNield *mask,
                              GwyMaskingType masking,
                              gint col,
                              gint row,
                              gint width,
                              gint height,
                              GwyEnhanceFilterType filter_type)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return;

    g_return_if_fail(GWY_IS_FIELD(target));
    gwy_field_make_alike(target, field);

    if (filter_type == GWY_ENHANCE_FILTER_DECHECKER) {
        enum { size = 5 };
        static const gdouble dechecker[size*size] = {
            0.0,        1.0/144.0, -1.0/72.0,  1.0/144.0,  0.0,
            1.0/144.0, -1.0/18.0,   1.0/9.0,  -1.0/18.0,   1.0/144.0,
            -1.0/72.0,   1.0/9.0,    7.0/9.0,   1.0/9.0,   -1.0/72.0,
            1.0/144.0, -1.0/18.0,   1.0/9.0,  -1.0/18.0,   1.0/144.0,
            0.0,        1.0/144.0, -1.0/72.0,  1.0/144.0,  0.0,
        };
        _gwy_field_area_filter_5x5(field, target, mask, masking, col, row, width, height, dechecker, NULL);
    }
    else if (filter_type == GWY_ENHANCE_FILTER_KUWAHARA)
        _gwy_field_area_filter_5x5(field, target, mask, masking, col, row, width, height, NULL, kuwahara_5x5);
    else if (filter_type == GWY_ENHANCE_FILTER_MLV)
        _gwy_field_area_filter_5x5(field, target, mask, masking, col, row, width, height, NULL, mlv_5x5);
    else {
        g_return_if_reached();
    }
}

/**
 * gwy_field_enhance_filter:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can be @field itself and in any case it must have the same
 *          size.
 * @filter_type: Simple predefined filter type.
 *
 * Applies a simple enhance filter to a data field.
 **/
void
gwy_field_enhance_filter(GwyField *field,
                         GwyField *target,
                         GwyEnhanceFilterType filter_type)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    gwy_field_area_enhance_filter(field, target, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres, filter_type);
}

static void
make_kernel_hprewitt(gdouble *kernel, gdouble dx)
{
    gdouble c = 1.0/(6.0*dx);
    kernel[0] = kernel[3] = kernel[6] = c;
    kernel[2] = kernel[5] = kernel[8] = -c;
    kernel[1] = kernel[4] = kernel[7] = 0.0;
}

static void
make_kernel_vprewitt(gdouble *kernel, gdouble dy)
{
    gdouble c = 1.0/(6.0*dy);
    kernel[0] = kernel[1] = kernel[2] = c;
    kernel[6] = kernel[7] = kernel[8] = -c;
    kernel[3] = kernel[4] = kernel[5] = 0.0;
}

static void
make_kernel_hsobel(gdouble *kernel, gdouble dx)
{
    gdouble c = 1.0/(8.0*dx);
    kernel[0] = kernel[6] = c;
    kernel[3] = 2*c;
    kernel[2] = kernel[8] = -c;
    kernel[5] = -2*c;
    kernel[1] = kernel[4] = kernel[7] = 0.0;
}

static void
make_kernel_vsobel(gdouble *kernel, gdouble dy)
{
    gdouble c = 1.0/(8.0*dy);
    kernel[0] = kernel[2] = c;
    kernel[1] = 2*c;
    kernel[6] = kernel[8] = -c;
    kernel[7] = -2*c;
    kernel[3] = kernel[4] = kernel[5] = 0.0;
}

static void
make_kernel_hscharr(gdouble *kernel, gdouble dx)
{
    gdouble c = 1.0/(32.0*dx);
    kernel[0] = kernel[6] = 3*c;
    kernel[3] = 10*c;
    kernel[2] = kernel[8] = -3*c;
    kernel[5] = -10*c;
    kernel[1] = kernel[4] = kernel[7] = 0.0;
}

static void
make_kernel_vscharr(gdouble *kernel, gdouble dy)
{
    gdouble c = 1.0/(32.0*dy);
    kernel[0] = kernel[2] = 3*c;
    kernel[1] = 10*c;
    kernel[6] = kernel[8] = -3*c;
    kernel[7] = -10*c;
    kernel[3] = kernel[4] = kernel[5] = 0.0;
}

static void
make_kernel_sxder2(gdouble *kernel, gdouble dx)
{
    gdouble c = 1.0/(4.0*dx*dx);
    kernel[0] = kernel[2] = kernel[6] = kernel[8] = c;
    kernel[3] = kernel[5] = 2*c;
    kernel[1] = kernel[7] = -2*c;
    kernel[4] = -4*c;
}

static void
make_kernel_syder2(gdouble *kernel, gdouble dy)
{
    gdouble c = 1.0/(4.0*dy*dy);
    kernel[0] = kernel[2] = kernel[6] = kernel[8] = c;
    kernel[1] = kernel[7] = 2*c;
    kernel[3] = kernel[5] = -2*c;
    kernel[4] = -4*c;
}

static void
make_kernel_xyder(gdouble *kernel, gdouble dx, gdouble dy)
{
    gdouble c = 1.0/(4.0*dx*dy);
    kernel[0] = kernel[8] = c;
    kernel[2] = kernel[6] = -c;
    kernel[1] = kernel[3] = kernel[4] = kernel[5] = kernel[7] = 0.0;
}

// Isotropic Laplace. A weird one. If dx = dy when we produce the isotropic kernel with 2/3 weight for the simple
// cardinal-directions-only kernel and 1/3 for the diagonal one. As it should be.
//
// When dx ≠ dy it is much more interesting. There is still a one-parametric family of kernels which give the correct
// derivatives for all quadratic functions. We can choose t as the parameter. To form convex combinations of cardinal
// and diagonal kernels, it must satisfy t < dx², t < dy². Meaning t < min(dx², dy²). A ‘natural’ choice, which reduces
// to the isotropic case for dx = dy, is t = min(dx², dy²)/3. But there are may other ‘natural’ choices, for example
// 2/3 dx² dy²/(dx² + dy²). Any may be bad when one of the steps is much larger than the other. However, we cannot
// really hope for isotropy then anyway.
static void
make_kernel_laplace(gdouble *kernel, gdouble dx, gdouble dy)
{
    gdouble dx2 = dx*dx, dy2 = dy*dy;
    gdouble t = fmin(dx2, dy2)/3.0;
    kernel[1] = kernel[7] = (1.0 - t/dx2)/dy2;
    kernel[3] = kernel[5] = (1.0 - t/dy2)/dx2;
    kernel[0] = kernel[2] = kernel[6] = kernel[8] = 0.5*t/(dx2*dy2);
    kernel[4] = 2.0*t/(dx2*dy2) - 2.0/dx2 - 2.0/dy2;
}

static void
xder_filter(GwyField *field, GwyField *xder, gdouble dx, gint nder)
{
    if (!_gwy_field_check_other(field, xder, FALSE))
        return;

    guint xres = field->xres, yres = field->yres;
    if (xres == 1) {
        gwy_field_clear(xder);
        return;
    }

    const gdouble *d = field->priv->data;
    gdouble *bx = xder->priv->data;
    gboolean in_place = (xder == field);
    gdouble dx2 = 2.0*dx, dxdx = dx*dx;

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,bx,dx2,dxdx,xres,yres,nder,in_place)
#endif
    {
        guint ifrom = gwy_omp_chunk_start(yres), ito = gwy_omp_chunk_end(yres);
        // Use a temporary buffer if filtering in place. Yes, we could do with only O(1) extra storage. However, it
        // seems compilers optimise better simple loops with independent iterations than loops with carry-over
        // variables across iterations.
        gdouble *rowbuf = (in_place ? rowbuf = g_new(gdouble, xres) : NULL);

        for (guint i = ifrom; i < ito; i++) {
            const gdouble *row = d + i*xres;
            gdouble *bxrow = bx + i*xres;
            if (!in_place)
                rowbuf = bxrow;

            if (nder == 2) {
                rowbuf[0] = (row[1] - row[0])/dxdx;
                for (guint j = 1; j < xres-1; j++)
                    rowbuf[j] = (row[j-1] - 2*row[j] + row[j+1])/dxdx;
                rowbuf[xres-1] = (row[xres-2] - row[xres-1])/dxdx;
            }
            else {
                rowbuf[0] = (row[1] - row[0])/(0.5*dx2);
                for (guint j = 1; j < xres-1; j++)
                    rowbuf[j] = (row[j+1] - row[j-1])/dx2;
                rowbuf[xres-1] = (row[xres-1] - row[xres-2])/(0.5*dx2);
            }

            if (in_place)
                gwy_assign(bxrow, rowbuf, xres);
        }
        if (in_place)
            g_free(rowbuf);
    }

    gwy_field_invalidate(xder);
}

static void
yder_filter(GwyField *field, GwyField *yder, gdouble dy, gint nder)
{
    if (!_gwy_field_check_other(field, yder, FALSE))
        return;

    guint xres = field->xres, yres = field->yres;
    if (yres == 1) {
        gwy_field_clear(yder);
        return;
    }

    const gdouble *d = field->priv->data;
    gdouble *by = yder->priv->data;
    gboolean in_place = (yder == field);
    gdouble *buf = (in_place ? g_new(gdouble, xres*yres) : by);

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(d,buf,dy,xres,yres,nder,in_place)
#endif
    for (guint i = 0; i < yres; i++) {
        const gdouble *row = d + i*xres, *prev = (i ? row - xres : row), *next = (i < yres-1 ? row + xres : row);
        gdouble dy2 = (nder == 2 ? dy*dy : (i && i < yres-1) ? 2.0*dy : dy);
        gdouble *byrow = buf + i*xres;

        if (nder == 2) {
            for (guint j = 0; j < xres; j++)
                byrow[j] = (prev[j] - 2*row[j] + next[j])/dy2;
        }
        else {
            for (guint j = 0; j < xres; j++)
                byrow[j] = (next[j] - prev[j])/dy2;
        }
    }

    if (in_place) {
        gwy_assign(by, buf, xres*yres);
        g_free(buf);
    }

    gwy_field_invalidate(yder);
}

// TODO: Occasionally we use second derivative filters (see the logistic module). Aeparate dx² and dy² alone are easy
// even with as_derivative=TRUE. A simple mixed derivative with just nonzero corners and divided by dx dy should also
// work with as_derivative=TRUE.
/**
 * gwy_field_derivative_filter:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can also be @field itself. It will be resized if necessary.
 * @filter_type: Simple predefined filter type.
 * @as_derivative: %TRUE to calculate derivatives in physical units, %FALSE to calculate discrete differences.
 *
 * Applies a simple predefined derivative filter to a data field.
 *
 * If @as_derivative is %TRUE, @field sampling steps are used to scale the differences to approximate derivatives.
 * Regardless of @as_derivative, @target real dimensions and units are updated. For derivatives, the units will be the
 * units of derivatives. For discrete differences, @target will have the same units as @field.
 **/
void
gwy_field_derivative_filter(GwyField *field,
                            GwyField *target,
                            GwyDerivativeFilterType filter_type,
                            gboolean as_derivative)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));

    gint xres = field->xres, yres = field->yres;
    gdouble dx = as_derivative ? field->xreal/xres : 1.0;
    gdouble dy = as_derivative ? field->yreal/yres : 1.0;
    gdouble kernel[9];
    gint derivative = 1;

    gwy_field_make_alike(target, field);
    if (filter_type == GWY_DERIVATIVE_FILTER_XDER)
        xder_filter(field, target, dx, 1);
    else if (filter_type == GWY_DERIVATIVE_FILTER_YDER)
        yder_filter(field, target, dy, 1);
    else if (filter_type == GWY_DERIVATIVE_FILTER_XDER2)
        xder_filter(field, target, dx, 2);
    else if (filter_type == GWY_DERIVATIVE_FILTER_YDER2)
        yder_filter(field, target, dy, 2);
    else if (filter_type == GWY_DERIVATIVE_FILTER_DER) {
        GwyField *buf = gwy_field_new(xres, yres, xres, yres, FALSE);
        xder_filter(field, buf, dx, 1);
        yder_filter(field, target, dy, 1);
        gwy_field_hypot_of_fields(target, target, buf);
        g_object_unref(buf);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_HPREWITT) {
        make_kernel_hprewitt(kernel, dx);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_VPREWITT) {
        make_kernel_vprewitt(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_PREWITT) {
        GwyField *buf = gwy_field_new(xres, yres, xres, yres, FALSE);
        make_kernel_hprewitt(kernel, dx);
        gwy_field_area_filter_3x3(field, buf, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        make_kernel_vprewitt(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        gwy_field_hypot_of_fields(target, target, buf);
        g_object_unref(buf);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_HSOBEL) {
        make_kernel_hsobel(kernel, dx);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_VSOBEL) {
        make_kernel_vsobel(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_SOBEL) {
        GwyField *buf = gwy_field_new(xres, yres, xres, yres, FALSE);
        make_kernel_hsobel(kernel, dx);
        gwy_field_area_filter_3x3(field, buf, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        make_kernel_vsobel(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        gwy_field_hypot_of_fields(target, target, buf);
        g_object_unref(buf);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_HSCHARR) {
        make_kernel_hscharr(kernel, dx);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_VSCHARR) {
        make_kernel_vscharr(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_SCHARR) {
        GwyField *buf = gwy_field_new(xres, yres, xres, yres, FALSE);
        make_kernel_hscharr(kernel, dx);
        gwy_field_area_filter_3x3(field, buf, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        make_kernel_vscharr(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        gwy_field_hypot_of_fields(target, target, buf);
        g_object_unref(buf);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_SXDER2) {
        make_kernel_sxder2(kernel, dx);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_SYDER2) {
        make_kernel_syder2(kernel, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_XYDER) {
        make_kernel_xyder(kernel, dx, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_DET2DER) {
        GwyField *buf = gwy_field_new(xres, yres, xres, yres, FALSE);
        GwyField *buf2 = (target == field ? gwy_field_new_alike(buf, FALSE) : g_object_ref(target));
        make_kernel_sxder2(kernel, dx);
        gwy_field_area_filter_3x3(field, buf, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        make_kernel_syder2(kernel, dy);
        gwy_field_area_filter_3x3(field, buf2, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        gwy_field_multiply_fields(buf2, buf, buf2);
        make_kernel_xyder(kernel, dx, dy);
        gwy_field_area_filter_3x3(field, buf, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        gwy_field_multiply_fields(buf, buf, buf);
        gwy_field_subtract_fields(target, buf2, buf);
        g_object_unref(buf);
        g_object_unref(buf2);
    }
    else if (filter_type == GWY_DERIVATIVE_FILTER_LAPLACE) {
        make_kernel_laplace(kernel, dx, dy);
        gwy_field_area_filter_3x3(field, target, NULL, GWY_MASK_IGNORE, 0, 0, xres, yres, kernel);
        derivative = 2;
    }
    else {
        g_assert_not_reached();
    }

    if (as_derivative) {
        gwy_unit_power_multiply(gwy_field_get_unit_z(target),
                                gwy_field_get_unit_z(field), 1,
                                gwy_field_get_unit_xy(field), -derivative);
    }
}

/**
 * gwy_field_laplacian_of_gaussians:
 * @field: A data field to apply the filter to.
 * @target: Target data field where to put the result. It can also be @field itself. It will be resized if necessary.
 *
 * Applies a simple predefined Laplacian of Gaussians filter to a data field.
 *
 * Dimensions and units of @target are updated. Since this is a discrete derivative filter, the units of @target are
 * kept the same as @field.
 **/
void
gwy_field_laplacian_of_gaussians(GwyField *field, GwyField *target)
{
    enum { size = 5 };
    // This one had originally -902 in the centre, but that was not giving zero on a constant. Changing it to -900
    // probably makes the filter a slightly worse. However, it is now invariant-preseving. The normalisation is such
    // that when applied to i²+j² it gives 4 = (∂²/∂i² + ∂²/∂j²).
    const gdouble kernel[size*size] = {
          1.0/223.0, -12.0/223.0,    3.0/223.0, -12.0/223.0,   1.0/223.0,
        -12.0/223.0,  78.0/223.0,  167.0/223.0,  78.0/223.0, -12.0/223.0,
          3.0/223.0, 167.0/223.0, -900.0/223.0, 167.0/223.0,   3.0/223.0,
        -12.0/223.0,  78.0/223.0,  167.0/223.0,  78.0/223.0, -12.0/223.0,
          1.0/223.0, -12.0/223.0,    3.0/223.0, -12.0/223.0,   1.0/223.0,
    };

    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(target));
    gwy_field_make_alike(target, field);
    _gwy_field_area_filter_5x5(field, target, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres, kernel, NULL);
}

/**
 * GwyEnhanceFilterType:
 * @GWY_ENHANCE_FILTER_KUWAHARA: Kuwahara edge-preserving smoothing filter.
 * @GWY_ENHANCE_FILTER_MLV: Schulze Mean of Least Variance edge-enhancing filter.
 * @GWY_ENHANCE_FILTER_DECHECKER: Checker pattern suppression filter.
 *
 * Type of simple predefined denoising filter type.
 *
 * These filters are used with gwy_field_enhance_filter() and gwy_field_area_enhance_filter().
 *
 * The dechecker filter is a convolution filter with the following 5×5 kernel:
 * |[
 *  0        1/144   -1/72    1/144     0
 *  1/144   -1/18     1/9    -1/18      1/144
 * -1/72     1/9      7/9     1/9      -1/72
 *  1/144   -1/18     1/9    -1/18      1/144
 *  0        1/144   -1/72    1/144     0
 * ]|
 *
 * The Kuwahara filter works on blocks 5×5. In each such block the corner 3×3 sub-block with the lowest variance is
 * chosen and its mean value is the filter result. Notably, the central 3×3 sub-block is not among the options. So,
 * while the filter enhances edges, it does not preserve linear gradients and is susceptible to rounding errors when
 * all the variances are equal.
 *
 * Schulze Mean of Least Variance (MLV) edge-enhancing filter is an improved version of the Kuwahara filter. It
 * considers all nine 3×3 sub-blocks.
 **/

/* These are not derivatives, keep them separate. LoG is a sort of smoothed derivative, but we do not know how to
 * decompose it for dx ≠ dy because the Gaussian part already mixes x and y.
 * @GWY_DERIVATIVE_FILTER_LOG: Laplacian of Gaussians (5×5).
 * @GWY_DERIVATIVE_FILTER_CANNY: Canny edge detection filter.
 * @GWY_DERIVATIVE_FILTER_STEP: Canny edge detection filter.
 * @GWY_DERIVATIVE_FILTER_NONLINEARITY: Canny edge detection filter.
 */

/**
 * GwyDerivativeFilterType:
 * @GWY_DERIVATIVE_FILTER_XDER: Simple symmetrical horizontal derivative (except edges, where it is one-sided).
 * @GWY_DERIVATIVE_FILTER_YDER: Simple symmetrical vertical derivative (except edges, where it is one-sided).
 * @GWY_DERIVATIVE_FILTER_DER: Simple total gradient (except edges, where it is one-sided).
 * @GWY_DERIVATIVE_FILTER_HPREWITT: Horizontal Prewitt derivative filter.
 * @GWY_DERIVATIVE_FILTER_VPREWITT: Vertical Prewitt derivative filter.
 * @GWY_DERIVATIVE_FILTER_PREWITT: Total Prewitt filter (hypotenuse of horizontal and vertical).
 * @GWY_DERIVATIVE_FILTER_HSOBEL: Horizontal Sobel derivative filter.
 * @GWY_DERIVATIVE_FILTER_VSOBEL: Vertical Sobel derivative filter.
 * @GWY_DERIVATIVE_FILTER_SOBEL: Total Sobel filter (hypotenuse of horizontal and vertical).
 * @GWY_DERIVATIVE_FILTER_HSCHARR: Horizontal Scharr derivative filter.
 * @GWY_DERIVATIVE_FILTER_VSCHARR: Vertical Scharr derivative filter.
 * @GWY_DERIVATIVE_FILTER_SCHARR: Total Scharr filter (hypotenuse of horizontal and vertical).
 * @GWY_DERIVATIVE_FILTER_LAPLACE: Simple isotropic Laplacian filter.
 * @GWY_DERIVATIVE_FILTER_XDER2: Simple symmetrical second horizontal derivative.
 * @GWY_DERIVATIVE_FILTER_YDER2: Simple symmetrical second vertical derivative.
 * @GWY_DERIVATIVE_FILTER_XYDER: Simple symmetrical second horizontal derivative.
 * @GWY_DERIVATIVE_FILTER_SXDER2: Smooth symmetrical second horizontal derivative.
 * @GWY_DERIVATIVE_FILTER_SYDER2: Smooth symmetrical second vertical derivative.
 * @GWY_DERIVATIVE_FILTER_DET2DER: Determinant-of-Hessian second derviative filter.
 *
 * Type of simple predefined derivative-type filter type.
 *
 * These filters are used with gwy_field_derivative_filter(). All can be used to calculate either discrete derivatives
 * or approximate real derivatives, which take into account sampling steps.
 *
 * The Laplacian filters have the following 3×3 kernel (corresponding to 1/3 weight):
 * |[
 * 1/20   1/5   1/20
 * 1/5   -1     1/5
 * 1/20   1/5   1/20
 * ]|
 *
 * The Sobel derivative filters have the following 3×3 kernels:
 * |[
 * Horizontal                Vertical
 * 1/8   0   -1/8            1/8    1/4    1/8
 * 1/4   0   -1/4            0      0      0
 * 1/8   0   -1/8           -1/8   -1/4   -1/4
 * ]|
 *
 * The Prewitt derivative filters have the following 3×3 kernels:
 * |[
 * Horizontal                Vertical
 * 1/6   0   -1/6            1/6    1/6    1/6
 * 1/6   0   -1/6            0      0      0
 * 1/6   0   -1/6           -1/6   -1/6   -1/6
 * ]|
 *
 * The Scharr derivative filters have the following 3×3 kernels:
 * |[
 * Horizontal                Vertical
 * 3/32   0   -3/32          3/32   5/16   3/32
 * 5/16   0   -5/16          0      0      0
 * 3/32   0   -3/32         -3/32  -5/16  -3/32
 * ]|
 *
 * The XY derivative filter has the following 3×3 kernels:
 * |[
 * -1/4   0    1/4
 *  0     0    0
 *  1/4   0   -1/4
 * ]|
 *
 * The smooth (or slightly smoother) second derivative filters have the following 3×3 kernels:
 * |[
 * Horizontal                Vertical
 * 1/4   -1/2   1/4          1/4    1/2    1/4
 * 1/2   -1     1/2         -1/2   -1     -1/2
 * 1/4   -1/2   1/4          1/4    1/2    1/4
 * ]|
 *
 * The absolute value of the derivative is calculated as hypotenuse of the derivatives calculated by the corresponding
 * directional filters.
 **/


// XXX: Description currently unused. These filters are only in modules.
/*
 * The step filter also works on blocks 5×5, excluding their corners so that an approximately circular area is
 * processed.  The 21 remaining values are then sorted and the difference between the 16th and 6th value forms the
 * filter result.
 *
 * The local non-linearity filter works on the same neighbourhoods as the step filter.  It calculates the residuum
 * after fitting a plane through the neighbourhood, but normalised by the area of this plane.
 */

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