/*
 *  $Id: correct.c 29862 2026-04-21 15:29:47Z yeti-dn $
 *  Copyright (C) 2004-2025 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 <stdlib.h>
#include <string.h>
#include <glib/gi18n-lib.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/shaped.h"
#include "libgwyddion/linestats.h"
#include "libgwyddion/level.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/filters.h"
#include "libgwyddion/correct.h"
#include "libgwyddion/interpolation.h"
#include "libgwyddion/watershed.h"
#include "libgwyddion/types.h"

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

/* Lower symmetric part indexing */
/* i MUST be greater or equal than j */
#define SLi(a, i, j) a[(i)*((i) + 1)/2 + (j)]

typedef struct {
    gdouble max;
    gdouble x;
    gdouble y;
    gdouble d;
    gdouble q;
    guint basecount;
} LatticeMaximumInfo;

static void    gwy_field_distort_internal(GwyField *source,
                                          GwyField *dest,
                                          GwyInterpolationType interp,
                                          GwyExteriorType exterior,
                                          gdouble fill_value,
                                          const GwyXY *coords,
                                          GwyCoordTransform2DFunc invtrans,
                                          gpointer user_data);
static gdouble unrotate_refine_correction(GwyLine *derdist,
                                          guint m,
                                          gdouble phi);
static void    compute_fourier_coeffs    (gint nder,
                                          const gdouble *der,
                                          gdouble baseline,
                                          guint symmetry,
                                          gdouble *st,
                                          gdouble *ct);

/**
 * gwy_field_mark_outliers:
 * @field: A data field.
 * @mask: A data field to be filled with mask.
 * @thresh: Threshold deviation.
 *
 * Creates mask of data that are above or below @thresh*sigma from average height.
 *
 * Sigma denotes root-mean square deviation of heights. This criterium corresponds to the usual Gaussian distribution
 * outliers detection if @thresh is 3.
 **/
void
gwy_field_mark_outliers(GwyField *field,
                        GwyNield *mask,
                        gdouble thresh)
{
    gwy_field_mark_outliers2(field, mask, thresh, thresh);
}

static void
mark_outside_range(GwyField *field,
                   GwyNield *mask,
                   gdouble threshold_low,
                   gdouble threshold_high)
{
    gsize n = (gsize)field->xres * (gsize)field->yres;
    const gdouble *d = field->priv->data;
    gint *m = mask->priv->data;

    for (gsize i = 0; i < n; i++)
        m[i] = !(d[i] >= threshold_low && d[i] <= threshold_high);

    gwy_nield_invalidate(mask);
}

/**
 * gwy_field_mark_outliers2:
 * @field: A data field.
 * @mask: A data field to be filled with mask.
 * @thresh_low: Lower threshold deviation.
 * @thresh_high: Upper threshold deviation.
 *
 * Creates mask of data that are above or below multiples of rms from average height.
 *
 * Data that are below @mean-@thresh_low*@sigma or above @mean+@thresh_high*@sigma are marked as outliers, where
 * @sigma denotes the root-mean square deviation of data values.
 *
 * See gwy_field_mark_outliers_iqr() for a slower but more robust version which uses rank-based statistics. See also
 * gwy_nield_mark_range() for marking inside/oustide given range.
 **/
void
gwy_field_mark_outliers2(GwyField *field,
                         GwyNield *mask,
                         gdouble thresh_low,
                         gdouble thresh_high)
{
    if (!_gwy_field_check_mask(field, &mask, NULL))
        return;

    gdouble avg, rms;
    gwy_field_stats(field, &avg, NULL, &rms, NULL, NULL);
    gdouble criterium_low = avg - rms*thresh_low;
    gdouble criterium_high = avg + rms*thresh_high;
    mark_outside_range(field, mask, criterium_low, criterium_high);
}

/**
 * gwy_field_mark_outliers_iqr:
 * @field: A data field.
 * @mask: A data field to be filled with mask.
 * @thresh_low: Lower threshold deviation.
 * @thresh_high: Upper threshold deviation.
 *
 * Creates mask of data that are above or below multiples of the inter-quartile range.
 *
 * Data that are below @quartile_25-@thresh_low*@iqr or above @quartile_75+@thresh_high*@iqr are marked as outliers,
 * where @iqr denotes the inter-quartile range of data values. Note that zero thresholds already remove everything
 * outside the IQR.
 *
 * See gwy_field_mark_outliers2() for a faster but less robust version which uses mean and standard deviation. See
 * also gwy_nield_mark_range() for marking inside/oustide given range.
 **/
void
gwy_field_mark_outliers_iqr(GwyField *field,
                            GwyNield *mask,
                            gdouble thresh_low,
                            gdouble thresh_high)
{
    if (!_gwy_field_check_mask(field, &mask, NULL))
        return;

    gint xres = field->xres, yres = field->yres;
    gdouble *buf = g_memdup(field->priv->data, xres*yres*sizeof(gdouble));

    gdouble p[2] = { 25, 75 }, pv[2];
    gwy_math_percentiles(buf, xres*yres, GWY_PERCENTILE_INTERPOLATION_LINEAR, p, 2, pv);
    g_free(buf);
    gdouble iqr = fabs(pv[1] - pv[0]);
    gdouble criterium_low = pv[0] - thresh_low*iqr;
    gdouble criterium_high = pv[1] + thresh_high*iqr;
    mark_outside_range(field, mask, criterium_low, criterium_high);
}

/**
 * gwy_field_correct_average:
 * @field: A data field.
 * @mask: Mask of places to be corrected.
 *
 * Fills data under mask with the average value of unmasked data.
 *
 * The function calculates the average value of all unmasked pixels in @field and then fills all the masked pixels
 * with this average value. It can be useful as the first rough step of replacement of data under the mask since it
 * is much faster than, for example, gwy_field_laplace_solve().
 *
 * If all data are masked the field is filled with zeroes.
 **/
void
gwy_field_correct_average(GwyField *field,
                          GwyNield *mask)
{
    if (!_gwy_field_check_mask(field, &mask, NULL))
        return;
    gint xres = field->xres, yres = field->yres;
    gdouble avg = gwy_field_area_mean(field, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres);
    gwy_field_area_fill(field, mask, GWY_MASK_INCLUDE, 0, 0, xres, yres, avg);
}

/**
 * gwy_field_unrotate_find_corrections:
 * @derdist: Angular derivation distribution (normally obrained from gwy_field_slope_distribution()).
 * @correction: Corrections for particular symmetry types will be stored here (indexed by GwyPlaneSymmetry).
 *              @correction[0] contains the most probable correction.  All angles are in radians.
 *
 * Finds rotation corrections.
 *
 * Rotation correction is computed for for all symmetry types.
 * In addition an estimate is made about the prevalent one.
 *
 * Returns: The estimate type of prevalent symmetry.
 **/
GwyPlaneSymmetry
gwy_field_unrotate_find_corrections(GwyLine *derdist,
                                    gdouble *correction)
{
    static const guint symm[] = { 2, 3, 4, 6 };

    g_return_val_if_fail(GWY_IS_LINE(derdist), GWY_SYMMETRY_AUTO);

    gdouble sint[G_N_ELEMENTS(symm)], cost[G_N_ELEMENTS(symm)];
    gint nder = derdist->res;
    const gdouble *der = derdist->priv->data;
    gdouble avg = gwy_line_mean(derdist);

    GwyPlaneSymmetry guess = GWY_SYMMETRY_AUTO;
    gdouble max = -G_MAXDOUBLE;
    for (guint j = 0; j < G_N_ELEMENTS(symm); j++) {
        guint m = symm[j];
        compute_fourier_coeffs(nder, der, avg, m, sint+j, cost+j);
        gdouble phi = atan2(-sint[j], cost[j]);
        gdouble total = hypot(sint[j], cost[j]);

        gwy_debug("sc%d = (%f, %f), total%d = (%f, %f)", m, sint[j], cost[j], m, total, gwy_rad2deg(phi));

        phi /= 2*G_PI*m;
        phi = unrotate_refine_correction(derdist, m, phi);
        GwyPlaneSymmetry t;
        /*
         *             range from             smallest possible
         *  symmetry   compute_correction()   range                ratio
         *    m        -1/2m .. 1/2m
         *
         *    2        -1/4  .. 1/4           -1/8  .. 1/8         1/2
         *    3        -1/6  .. 1/6           -1/12 .. 1/12        1/2
         *    4        -1/8  .. 1/8           -1/8  .. 1/8 (*)     1
         *    6        -1/12 .. 1/12          -1/12 .. 1/12        1
         *
         *  (*) not counting rhombic
         */
        if (m == 2) {
            t = GWY_SYMMETRY_PARALLEL;
            /* align with any x or y */
            if (phi >= 0.25/m)
                phi -= 0.5/m;
            else if (phi <= -0.25/m)
                phi += 0.5/m;
            correction[t] = phi;
            total /= 1.25;
        }
        else if (m == 3) {
            t = GWY_SYMMETRY_TRIANGULAR;
            /* align with any x or y */
            if (phi >= 0.125/m)
                phi -= 0.25/m;
            else if (phi <= -0.125/m)
                phi += 0.25/m;
            correction[t] = phi;
        }
        else if (m == 4) {
            t = GWY_SYMMETRY_SQUARE;
            correction[t] = phi;
            /* decide square/rhombic */
            phi += 0.5/m;
            if (phi > 0.5/m)
                phi -= 1.0/m;
            t = GWY_SYMMETRY_RHOMBIC;
            correction[t] = phi;
            if (fabs(phi) > fabs(correction[GWY_SYMMETRY_SQUARE]))
                t = GWY_SYMMETRY_SQUARE;
            total /= 1.4;
        }
        else if (m == 6) {
            t = GWY_SYMMETRY_HEXAGONAL;
            correction[t] = phi;
        }
        else {
            g_assert_not_reached();
        }

        if (total > max) {
            max = total;
            guess = t;
        }
    }

    g_assert(guess != GWY_SYMMETRY_AUTO);
    gwy_debug("SELECTED: %d", guess);
    correction[GWY_SYMMETRY_AUTO] = correction[guess];

    for (guint j = 0; j < GWY_SYMMETRY_LAST; j++) {
        gwy_debug("FINAL %d: (%f, %f)", j, correction[j], 360*correction[j]);
        correction[j] *= 2.0*G_PI;
    }

    return guess;
}

static void
compute_fourier_coeffs(gint nder, const gdouble *der, gdouble baseline,
                       guint symmetry,
                       gdouble *st, gdouble *ct)
{
    guint i;
    gdouble q, sint, cost;

    q = 2*G_PI/nder*symmetry;
    sint = cost = 0.0;
    for (i = 0; i < nder; i++) {
        sint += sin(q*(i + 0.5))*(der[i] - baseline);
        cost += cos(q*(i + 0.5))*(der[i] - baseline);
    }

    *st = sint;
    *ct = cost;
}

/**
 * unrotate_refine_correction:
 * @derdist: Angular derivation distribution (as in Slope dist. graph).
 * @m: Symmetry.
 * @phi: Initial correction guess (in the range 0..1!).
 *
 * Compute correction assuming symmetry @m and initial guess @phi.
 *
 * Returns: The correction (again in the range 0..1!).
 **/
static gdouble
unrotate_refine_correction(GwyLine *derdist,
                           guint m, gdouble phi)
{
    gdouble sum, wsum;
    guint i, j;

    gint nder = derdist->res;
    const gdouble *der = derdist->priv->data;

    phi -= floor(phi) + 1.0;
    sum = wsum = 0.0;
    for (j = 0; j < m; j++) {
        gdouble low = (j + 5.0/6.0)/m - phi;
        gdouble high = (j + 7.0/6.0)/m - phi;
        gdouble s, w;
        guint ilow, ihigh;

        ilow = (guint)floor(low*nder);
        ihigh = (guint)floor(high*nder);
        gwy_debug("[%u] peak %u low = %f, high = %f, %u, %u", m, j, low, high, ilow, ihigh);
        s = w = 0.0;
        for (i = ilow; i <= ihigh; i++) {
            s += (i + 0.5)*der[i % nder];
            w += der[i % nder];
        }

        s /= nder*w;
        gwy_debug("[%u] peak %u center: %f", m, j, 360*s);
        sum += (s - (gdouble)j/m)*w*w;
        wsum += w*w;
    }
    phi = sum/wsum;
    gwy_debug("[%u] FITTED phi = %f (%f)", m, phi, 360*phi);
    phi = fmod(phi + 1.0, 1.0/m);
    if (phi > 0.5/m)
        phi -= 1.0/m;
    gwy_debug("[%u] MINIMIZED phi = %f (%f)", m, phi, 360*phi);

    return phi;
}

/**
 * gwy_field_sample_distorted:
 * @source: Source data field.
 * @dest: Destination data field.
 * @coords: Array of @source coordinates with the same number of items as @dest, ordered as data field data. See
 *          gwy_field_distort() for coordinate convention discussion.
 * @interp: Interpolation type to use.
 * @exterior: Exterior pixels handling.
 * @fill_value: The value to use with @GWY_EXTERIOR_FIXED_VALUE.
 *
 * Resamples a data field in an arbitrarily distorted manner.
 *
 * Each item in @coords corresponds to one pixel in @dest and gives the coordinates in @source defining the value to
 * set in this pixel.
 *
 * The %GWY_EXTERIOR_LAPLACE exterior type cannot be used with this function.
 **/
void
gwy_field_sample_distorted(GwyField *source,
                           GwyField *dest,
                           const GwyXY *coords,
                           GwyInterpolationType interp,
                           GwyExteriorType exterior,
                           gdouble fill_value)
{
    gwy_field_distort_internal(source, dest, interp, exterior, fill_value, coords, NULL, NULL);
}

/**
 * gwy_field_distort:
 * @source: Source data field.
 * @dest: Destination data field.
 * @invtrans: (scope call):
 *            Inverse transform function, that is the transformation from new coordinates to old coordinates.   It
 *            gets (@j+0.5, @i+0.5), where @i and @j are the new row and column indices, passed as the input
 *            coordinates.  The output coordinates should follow the same convention.  Unless a special exterior
 *            handling is required, the transform function does not need to concern itself with coordinates being
 *            outside of the data.
 * @user_data: Pointer passed as @user_data to @invtrans.
 * @interp: Interpolation type to use.
 * @exterior: Exterior pixels handling.
 * @fill_value: The value to use with @GWY_EXTERIOR_FIXED_VALUE.
 *
 * Distorts a data field in the horizontal plane.
 *
 * Note the transform function @invtrans is the inverse transform, in other words it calculates the old coordinates
 * from the new coordinates (the transform would not be uniquely defined the other way round).
 *
 * The %GWY_EXTERIOR_LAPLACE exterior type cannot be used with this function.
 **/
void
gwy_field_distort(GwyField *source,
                  GwyField *dest,
                  GwyCoordTransform2DFunc invtrans,
                  gpointer user_data,
                  GwyInterpolationType interp,
                  GwyExteriorType exterior,
                  gdouble fill_value)
{
    gwy_field_distort_internal(source, dest, interp, exterior, fill_value, NULL, invtrans, user_data);
}

static gboolean
interpolate_in_neigbourhood(const gdouble *cdata, gint xres, gint yres,
                            GwyInterpolationType interp, GwyExteriorType exterior, gdouble fill_value,
                            gint sf, gint st, gint suplen,
                            gdouble x, gdouble y, gdouble *coeff,
                            gdouble *v)
{
    gint oldi, oldj, i, j, ii, jj;

    if (fmax(fmin(y, yres), 0) != y || fmax(fmin(x, xres), 0) != x) {
        if (exterior == GWY_EXTERIOR_BORDER) {
            x = CLAMP(x, 0, xres);
            y = CLAMP(y, 0, yres);
        }
        else if (exterior == GWY_EXTERIOR_PERIODIC) {
            x = (x > 0) ? fmod(x, xres) : fmod(x, xres) + xres;
            y = (y > 0) ? fmod(y, yres) : fmod(y, yres) + yres;
        }
        else if (exterior == GWY_EXTERIOR_FIXED_VALUE) {
            *v = fill_value;
            return TRUE;
        }
        else if (exterior == GWY_EXTERIOR_UNDEFINED)
            return FALSE;

        /* Mirror extension is what the interpolation code does by default.  Do not need to adjust anything.  */
    }
    oldi = (gint)floor(y);
    y -= oldi;
    oldj = (gint)floor(x);
    x -= oldj;
    for (i = sf; i <= st; i++) {
        ii = (oldi + i + 2*st*yres) % (2*yres);
        if (G_UNLIKELY(ii >= yres))
            ii = 2*yres-1 - ii;
        for (j = sf; j <= st; j++) {
            jj = (oldj + j + 2*st*xres) % (2*xres);
            if (G_UNLIKELY(jj >= xres))
                jj = 2*xres-1 - jj;
            coeff[(i - sf)*suplen + j - sf] = cdata[ii*xres + jj];
        }
    }

    *v = gwy_interpolation_interpolate_2d(x, y, suplen, coeff, interp);
    return TRUE;
}

static void
gwy_field_distort_internal(GwyField *source,
                           GwyField *dest,
                           GwyInterpolationType interp,
                           GwyExteriorType exterior,
                           gdouble fill_value,
                           const GwyXY *coords,
                           GwyCoordTransform2DFunc invtrans,
                           gpointer user_data)
{
    GwyField *coeffield;
    GwyXY *my_coords = NULL;
    gdouble *data;
    const gdouble *cdata;
    gint xres, yres, newxres, newyres;
    gint suplen, sf, st;

    g_return_if_fail(GWY_IS_FIELD(source));
    g_return_if_fail(GWY_IS_FIELD(dest));
    g_return_if_fail(coords || invtrans);
    g_return_if_fail(!coords || !invtrans);

    suplen = gwy_interpolation_support_size(interp);
    g_return_if_fail(suplen > 0);
    sf = -((suplen - 1)/2);
    st = suplen/2;

    if (gwy_enum_sanitize_value(exterior, GWY_TYPE_EXTERIOR_TYPE) != exterior) {
        g_critical("Invalid exterior type.");
        return;
    }
    if (exterior == GWY_EXTERIOR_LAPLACE) {
        g_warning("Laplace exterior cannot be used with distortions.  Using border extension.");
        exterior = GWY_EXTERIOR_BORDER;
    }

    xres = gwy_field_get_xres(source);
    yres = gwy_field_get_yres(source);
    newxres = gwy_field_get_xres(dest);
    newyres = gwy_field_get_yres(dest);

    if (gwy_interpolation_has_interpolating_basis(interp))
        coeffield = g_object_ref(source);
    else {
        coeffield = gwy_field_copy(source);
        gwy_interpolation_resolve_coeffs_2d(xres, yres, xres, gwy_field_get_data(coeffield), interp);
    }

    data = gwy_field_get_data(dest);
    cdata = gwy_field_get_data_const(coeffield);

    if (!coords) {
        gint newi;

        my_coords = g_new(GwyXY, newxres*newyres);
#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(my_coords,invtrans,user_data,newxres,newyres) \
            private(newi)
#endif
        for (newi = 0; newi < newyres; newi++) {
            gint newj, k = newi*newxres;

            for (newj = 0; newj < newxres; newj++, k++)
                invtrans(newj + 0.5, newi + 0.5, &my_coords[k].x, &my_coords[k].y, user_data);
        }
        coords = my_coords;
    }

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(data,cdata,xres,yres,newxres,newyres,suplen,sf,st,coords,interp,exterior,fill_value)
#endif
    {
        gdouble *coeff = g_new(gdouble, suplen*suplen);
        gint ifrom = gwy_omp_chunk_start(newyres);
        gint ito = gwy_omp_chunk_end(newyres);
        gint newi, newj;
        gdouble x, y, v;

        for (newi = ifrom; newi < ito; newi++) {
            for (newj = 0; newj < newxres; newj++) {
                x = coords[newi*newxres + newj].x - 0.5;
                y = coords[newi*newxres + newj].y - 0.5;
                if (interpolate_in_neigbourhood(cdata, xres, yres, interp, exterior, fill_value,
                                                sf, st, suplen, x, y, coeff, &v))
                    data[newj + newxres*newi] = v;
            }
        }
        g_free(coeff);
    }

    g_object_unref(coeffield);
    g_free(my_coords);
}

/**
 * gwy_field_affine:
 * @source: Source data field.
 * @dest: Destination data field.
 * @invtrans: (array fixed-size=6):
 *            Inverse transform, that is the transformation from new pixel coordinates to old pixel coordinates,
 *            represented as (@j+0.5, @i+0.5), where @i and @j are the row and column indices.  It is represented as
 *            a six-element array [@axx, @axy, @ayx, @ayy, @bx, @by] where @axy is the coefficient from @x to @y.
 * @interp: Interpolation type to use.
 * @exterior: Exterior pixels handling.
 * @fill_value: The value to use with @GWY_EXTERIOR_FIXED_VALUE.
 *
 * Performs an affine transformation of a data field in the horizontal plane.
 *
 * Note the transform @invtrans is the inverse transform, in other words it calculates the old coordinates from the
 * new coordinates.  This way even degenerate (non-invertible) transforms can be meaningfully used. Also note that the
 * (column, row) coordinate system is left-handed.
 *
 * The %GWY_EXTERIOR_LAPLACE exterior type cannot be used with this function.
 **/
void
gwy_field_affine(GwyField *source,
                 GwyField *dest,
                 const gdouble *invtrans,
                 GwyInterpolationType interp,
                 GwyExteriorType exterior,
                 gdouble fill_value)
{
    GwyField *coeffield;
    gdouble *data;
    const gdouble *cdata;
    gint xres, yres, newxres, newyres;
    gint suplen, sf, st;
    gdouble axx, axy, ayx, ayy, bx, by;

    g_return_if_fail(GWY_IS_FIELD(source));
    g_return_if_fail(GWY_IS_FIELD(dest));
    g_return_if_fail(invtrans);

    axx = invtrans[0];
    axy = invtrans[1];
    ayx = invtrans[2];
    ayy = invtrans[3];
    bx = invtrans[4];
    by = invtrans[5];

    suplen = gwy_interpolation_support_size(interp);
    g_return_if_fail(suplen > 0);
    sf = -((suplen - 1)/2);
    st = suplen/2;

    if (gwy_enum_sanitize_value(exterior, GWY_TYPE_EXTERIOR_TYPE) != exterior) {
        g_critical("Invalid exterior type.");
        return;
    }
    if (exterior == GWY_EXTERIOR_LAPLACE) {
        g_warning("Laplace exterior cannot be used with distortions.  Using border extension.");
        exterior = GWY_EXTERIOR_BORDER;
    }

    xres = gwy_field_get_xres(source);
    yres = gwy_field_get_yres(source);
    newxres = gwy_field_get_xres(dest);
    newyres = gwy_field_get_yres(dest);

    if (gwy_interpolation_has_interpolating_basis(interp))
        coeffield = g_object_ref(source);
    else {
        coeffield = gwy_field_copy(source);
        gwy_interpolation_resolve_coeffs_2d(xres, yres, xres, gwy_field_get_data(coeffield), interp);
    }

    data = gwy_field_get_data(dest);
    cdata = gwy_field_get_data_const(coeffield);

    /* Incorporate the half-pixel shifts to bx and by */
    bx += 0.5*(axx + axy - 1.0);
    by += 0.5*(ayx + ayy - 1.0);
#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(data,cdata,xres,yres,newxres,newyres,suplen,sf,st,axx,axy,ayx,ayy,bx,by,interp,exterior,fill_value)
#endif
    {
        gdouble *coeff = g_new(gdouble, suplen*suplen);
        gint ifrom = gwy_omp_chunk_start(newyres);
        gint ito = gwy_omp_chunk_end(newyres);
        gint newi, newj;
        gdouble x, y, v;

        for (newi = ifrom; newi < ito; newi++) {
            for (newj = 0; newj < newxres; newj++) {
                x = axx*newj + axy*newi + bx;
                y = ayx*newj + ayy*newi + by;
                if (interpolate_in_neigbourhood(cdata, xres, yres, interp, exterior, fill_value,
                                                sf, st, suplen, x, y, coeff, &v))
                    data[newj + newxres*newi] = v;
            }
        }
        g_free(coeff);
    }

    g_object_unref(coeffield);
}

static gdouble
matrix2_det(const gdouble *m)
{
    return m[0]*m[3] - m[1]*m[2];
}

/* Permit dest = src */
static void
matrix2_vector2(gdouble *dest, const gdouble *m, const gdouble *src)
{
    gdouble xy[2];

    xy[0] = m[0]*src[0] + m[1]*src[1];
    xy[1] = m[2]*src[0] + m[3]*src[1];
    dest[0] = xy[0];
    dest[1] = xy[1];
}

/* Permit dest = src */
static void
matrix2_matrix2(gdouble *dest, const gdouble *m, const gdouble *src)
{
    gdouble xy[4];

    xy[0] = m[0]*src[0] + m[1]*src[2];
    xy[1] = m[0]*src[1] + m[1]*src[3];
    xy[2] = m[2]*src[0] + m[3]*src[2];
    xy[3] = m[2]*src[1] + m[3]*src[3];
    dest[0] = xy[0];
    dest[1] = xy[1];
    dest[2] = xy[2];
    dest[3] = xy[3];
}

/* Permit dest = src */
static void
invert_matrix2(gdouble *dest, const gdouble *src)
{
    gdouble D = matrix2_det(src);
    gdouble xy[4];

    gwy_debug("D %g", D);
    xy[0] = src[3]/D;
    xy[1] = -src[1]/D;
    xy[2] = -src[2]/D;
    xy[3] = src[0]/D;
    dest[0] = xy[0];
    dest[1] = xy[1];
    dest[2] = xy[2];
    dest[3] = xy[3];
}

static void
corner_max(gdouble x, gdouble y, const gdouble *m, gdouble *vmax)
{
    gdouble v[2];

    v[0] = x;
    v[1] = y;
    matrix2_vector2(v, m, v);
    vmax[0] = MAX(vmax[0], fabs(v[0]));
    vmax[1] = MAX(vmax[1], fabs(v[1]));
}

static void
solve_transform_real(const gdouble *a1a2, const gdouble *a1a2_corr, gdouble *m)
{
    gdouble tmp[4];
    tmp[0] = a1a2[0];
    tmp[1] = a1a2[2];
    tmp[2] = a1a2[1];
    tmp[3] = a1a2[3];
    invert_matrix2(m, tmp);
    tmp[0] = a1a2_corr[0];
    tmp[1] = a1a2_corr[2];
    tmp[2] = a1a2_corr[1];
    tmp[3] = a1a2_corr[3];
    matrix2_matrix2(m, tmp, m);
}

/**
 * gwy_field_affine_prepare:
 * @source: Source data field.
 * @dest: Destination data field.
 * @a1a2: (array fixed-size=4):
 *        Lattice vectors (or generally base vectors) in @source, as an array of four components: @x1, @y1, @x2 and
 *        @y2.
 * @a1a2_corr: (inout) (array fixed-size=4):
 *             Correct lattice vectors (or generally base vectors) @dest should have after the affine transform, in
 *             the same form as @a1a2.
 * @invtrans: (array fixed-size=6):
 *            Inverse transform as an array of six values to be filled according to gwy_field_affine()
 *            specification.
 * @scaling: How (or if) to scale the correct lattice vectors.
 * @prevent_rotation: %TRUE to prevent rotation of the data by rotating @a1a2_corr as a whole to a direction
 *                    preserving the data orientation.  %FALSE to take @a1a2_corr as given.
 * @oversampling: Oversampling factor.  Values larger than 1 mean smaller pixels (and more of them) in @dest, values
 *                smaller than 1 the opposite.  Pass 1.0 for the default pixel size choice.
 *
 * Resolves an affine transformation of a data field in the horizontal plane.
 *
 * This function calculates suitable arguments for gwy_field_affine() from given images and lattice vectors (in
 * real coordinates).
 *
 * Data field @dest will be resized and its real dimensions and units set in anticipation of gwy_field_affine().
 * Its contents will be destroyed.
 *
 * Note that @a1a2_corr is an input-output parameter.  In general, the vectors will be modified according to @scaling
 * and @prevent_rotation to the actual vectors in @dest after the transformation.  Only if @prevent_rotation is %FALSE
 * and @scaling is %GWY_AFFINE_SCALING_AS_GIVEN the vectors are preserved.
 **/
void
gwy_field_affine_prepare(GwyField *source,
                         GwyField *dest,
                         const gdouble *a1a2,
                         gdouble *a1a2_corr,
                         gdouble *invtrans,
                         GwyAffineScalingType scaling,
                         gboolean prevent_rotation,
                         gdouble oversampling)
{
    gdouble dx, dy, sdx, sdy, alpha, q;
    gdouble vmax[2], tmp[4];
    guint xres, yres, i;

    g_return_if_fail(GWY_IS_FIELD(source));
    g_return_if_fail(GWY_IS_FIELD(dest));
    g_return_if_fail(a1a2);
    g_return_if_fail(a1a2_corr);
    g_return_if_fail(invtrans);

    if (!(oversampling > 0.0)) {
        g_warning("Oversampling must be positive.");
        oversampling = 1.0;
    }
    sdx = source->xreal/source->xres;
    sdy = source->yreal/source->yres;

    gwy_debug("a1a2 %g %g %g %g", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    gwy_debug("a1a2_corr %g %g %g %g", a1a2_corr[0], a1a2_corr[1], a1a2_corr[2], a1a2_corr[3]);
    /* This is an approximate rotation correction to get the base more or less
     * oriented in the plane as expected and not upside down. */
    if (prevent_rotation) {
        alpha = atan2(-a1a2[1], a1a2[0]);
        tmp[0] = tmp[3] = cos(alpha);
        tmp[1] = sin(alpha);
        tmp[2] = -sin(alpha);
        matrix2_vector2(a1a2_corr, tmp, a1a2_corr);
        matrix2_vector2(a1a2_corr + 2, tmp, a1a2_corr + 2);
    }

    solve_transform_real(a1a2, a1a2_corr, invtrans);
    gwy_debug("invtrans %g %g %g %g", invtrans[0], invtrans[1], invtrans[2], invtrans[3]);

    /* This is the exact rotation correction. */
    if (prevent_rotation) {
        alpha = atan2(invtrans[2], invtrans[0]);
        gwy_debug("alpha %g", alpha);
        tmp[0] = tmp[3] = cos(alpha);
        tmp[1] = sin(alpha);
        tmp[2] = -sin(alpha);
        matrix2_matrix2(invtrans, tmp, invtrans);
    }

    if (scaling == GWY_AFFINE_SCALING_PRESERVE_AREA)
        q = 1.0/sqrt(fabs(matrix2_det(invtrans)));
    else if (scaling == GWY_AFFINE_SCALING_PRESERVE_X)
        q = 1.0/hypot(invtrans[0], invtrans[2]);
    else
        q = 1.0;

    for (i = 0; i < 4; i++) {
        invtrans[i] *= q;
        /* To create the corrected lattice selection on result. */
        a1a2_corr[i] *= q;
    }
    gwy_debug("invtrans %g %g %g %g",
              invtrans[0], invtrans[1], invtrans[2], invtrans[3]);

    vmax[0] = vmax[1] = 0.0;
    corner_max(source->xreal, source->yreal, invtrans, vmax);
    corner_max(-source->xreal, source->yreal, invtrans, vmax);
    corner_max(source->xreal, -source->yreal, invtrans, vmax);
    corner_max(-source->xreal, -source->yreal, invtrans, vmax);

    /* Prevent information loss by using a sufficient resolution to represent
     * original pixels. */
    tmp[0] = sdx;
    tmp[1] = tmp[2] = 0.0;
    tmp[3] = sdy;
    gwy_debug("dxdy %g %g", tmp[0], tmp[3]);
    matrix2_matrix2(tmp, invtrans, tmp);
    gwy_debug("pix_corr %g %g %g %g", tmp[0], tmp[1], tmp[2], tmp[3]);
    dx = hypot(tmp[0]/G_SQRT2, tmp[1]/G_SQRT2);
    dy = hypot(tmp[2]/G_SQRT2, tmp[3]/G_SQRT2);
    dx = dy = MIN(dx, dy)/oversampling;
    xres = GWY_ROUND(vmax[0]/dx);
    yres = GWY_ROUND(vmax[1]/dy);
    gwy_debug("dxdy_corr %g %g", dx, dy);
    gwy_debug("res %u %u", xres, yres);

    gwy_field_resize(dest, xres, yres);
    dest->xreal = dx*xres;
    dest->yreal = dy*yres;
    /* We could preserve the centre but that would be strange for the typical case when there are no offsets. */
    dest->xoff = dest->yoff = 0.0;
    gwy_field_copy_units(source, dest);

    /* So far, we used invtrans as a temporary matrix.  Now really fill it with
     * the inverse transformation. */
    invert_matrix2(invtrans, invtrans);
    gwy_debug("minv %g %g %g %g", invtrans[0], invtrans[1], invtrans[2], invtrans[3]);

    /* Multiply from right by pixel-to-real matrix in the corrected field. */
    tmp[0] = dx;
    tmp[1] = tmp[2] = 0.0;
    tmp[3] = dy;
    matrix2_matrix2(invtrans, invtrans, tmp);
    /* and from left by real-to-pixel matrix in the original field. */
    tmp[0] = 1.0/sdx;
    tmp[1] = tmp[2] = 0.0;
    tmp[3] = 1.0/sdy;
    matrix2_matrix2(invtrans, tmp, invtrans);
    gwy_debug("minvpix %g %g %g %g", invtrans[0], invtrans[1], invtrans[2], invtrans[3]);

    invtrans[4] = 0.5*xres;
    invtrans[5] = 0.5*yres;
    matrix2_vector2(invtrans + 4, invtrans, invtrans + 4);
    invtrans[4] = 0.5*source->xres - invtrans[4];
    invtrans[5] = 0.5*source->yres - invtrans[5];
    gwy_debug("b %g %g", invtrans[4], invtrans[5]);
}

static void
maybe_swap_axes(gdouble *a1a2)
{
    gdouble phi;

    phi = fmod(atan2(a1a2[1], a1a2[0]) + 4.0*G_PI - atan2(a1a2[3], a1a2[2]), 2.0*G_PI);
    if (phi > G_PI) {
        GWY_SWAP(gdouble, a1a2[0], a1a2[2]);
        GWY_SWAP(gdouble, a1a2[1], a1a2[3]);
    }
}

static gboolean
transform_vectors_real_freq(gdouble *xy)
{
    gdouble D = matrix2_det(xy);
    gdouble a = fabs(xy[0]*xy[3]) + fabs(xy[1]*xy[2]);

    if (fabs(D)/a < 1e-9)
        return FALSE;

    invert_matrix2(xy, xy);
    /* Transpose. */
    GWY_SWAP(gdouble, xy[1], xy[2]);
    return TRUE;
}

static gint
compare_maxima(gconstpointer pa, gconstpointer pb)
{
    const LatticeMaximumInfo *a = (const LatticeMaximumInfo*)pa;
    const LatticeMaximumInfo *b = (const LatticeMaximumInfo*)pb;

    if (a->basecount*a->q > b->basecount*b->q)
        return -1;
    if (a->basecount*a->q < b->basecount*b->q)
        return 1;

    if (a->q > b->q)
        return -1;
    if (a->q < b->q)
        return 1;

    /* Ensure comparison stability.  This should play no role in significance
     * sorting. */
    if (a->y < b->y)
        return -1;
    if (a->y > b->y)
        return 1;
    if (a->x < b->x)
        return -1;
    if (a->x > b->x)
        return 1;
    return 0;
}

/* @dfield is ACF or PSDF here. */
static gboolean
guess_lattice(GwyField *dfield, gdouble *a1a2, gboolean is_psdf)
{
    enum { nquantities = 3 };
    GwyGrainQuantity quantities[nquantities] = {
        GWY_GRAIN_MAXIMUM,
        GWY_GRAIN_CENTER_X,
        GWY_GRAIN_CENTER_Y,
    };
    GwyField *smoothed = NULL;
    gdouble *values[nquantities];
    LatticeMaximumInfo *maxima;
    guint i, j, k;
    gdouble dh, cphi, sphi, phi, l1, l2, d, x, y;
    gboolean ok = FALSE;

    /* Mark local maxima. */
    GwyNield *mask = gwy_field_new_nield_alike(dfield);
    if (is_psdf) {
        smoothed = gwy_field_copy(dfield);
        gwy_field_filter_gaussian(smoothed, 1.2);
        dfield = smoothed;
    }

    gwy_field_mark_extrema(dfield, mask, TRUE);
    gint ngrains = gwy_nield_number_contiguous(mask);

    /* Find the position and value of each. */
    for (i = 0; i < nquantities; i++)
        values[i] = g_new(gdouble, ngrains+1);

    gwy_nield_grain_quantities(mask, dfield, quantities, nquantities, values);
    g_clear_object(&mask);
    g_clear_object(&smoothed);

    maxima = g_new(LatticeMaximumInfo, ngrains);
    dh = hypot(dfield->xreal/dfield->xres, dfield->yreal/dfield->yres);
    for (i = 0; i < ngrains; i++) {
        maxima[i].max = values[0][i+1];
        maxima[i].x = values[1][i+1];
        maxima[i].y = values[2][i+1];
        maxima[i].d = hypot(maxima[i].x, maxima[i].y);
        maxima[i].q = maxima[i].max/(maxima[i].d + 5.0*dh);
        maxima[i].basecount = 0;
    }
    for (i = 0; i < nquantities; i++)
        g_free(values[i]);

    /* Remove the central peak, i.e. anything too close to the centre */
    i = j = 0;
    while (i < ngrains) {
        d = maxima[i].d;
        maxima[j] = maxima[i];
        if (d >= 1.8*dh)
            j++;
        i++;
    }
    ngrains = j;

    if ((is_psdf && ngrains < 4) || (!is_psdf && ngrains < 14)) {
        gwy_debug("Too few maxima (after centre removal): %d.", ngrains);
        g_free(maxima);
        return FALSE;
    }

    qsort(maxima, ngrains, sizeof(LatticeMaximumInfo), compare_maxima);
#ifdef DEBUG
    for (i = 0; i < ngrains; i++) {
        gwy_debug("[%u] (%g, %g) %g :: %g", i, maxima[i].x, maxima[i].y, maxima[i].max, maxima[i].q);
    }
#endif

    /* Remove anything with direction opposite to the first vector.  But we must carefully accept ortohogonal vectors.
     * This is just a half-plane selection though it influences the preferred vectors, of course. */
    gwy_debug("Base-plane selector [%u] (%g, %g) %g", 0, maxima[0].x, maxima[0].y, maxima[0].max);
    cphi = maxima[0].x/maxima[0].d;
    sphi = maxima[0].y/maxima[0].d;
    i = j = 1;
    while (i < ngrains) {
        x = cphi*maxima[i].x + sphi*maxima[i].y;
        y = cphi*maxima[i].y - sphi*maxima[i].x;
        maxima[j] = maxima[i];
        if (x > 1e-9*dh || (x > -1e-9*dh && y > 1e-9*dh))
            j++;
        i++;
    }
    ngrains = j;

    if ((is_psdf && ngrains < 2) || (!is_psdf && ngrains < 7)) {
        gwy_debug("Too few maxima (after half-plane removal): %d.", ngrains);
        g_free(maxima);
        return FALSE;
    }

    /* Locate the most important maxima. */
    ngrains = MIN(ngrains, 12);
    for (i = 0; i < ngrains; i++) {
        for (j = i+1; j < ngrains; j++) {
            x = maxima[i].x + maxima[j].x;
            y = maxima[i].y + maxima[j].y;
            for (k = 0; k < ngrains; k++) {
                if (fabs(maxima[k].x - x) < dh && fabs(maxima[k].y - y) < dh) {
                    maxima[i].basecount++;
                    maxima[j].basecount++;
                }
            }
        }
    }
    qsort(maxima, ngrains, sizeof(LatticeMaximumInfo), compare_maxima);
#ifdef DEBUG
    for (i = 0; i < ngrains; i++) {
        gwy_debug("[%u] (%g, %g) %g #%u", i, maxima[i].x, maxima[i].y, maxima[i].max, maxima[i].basecount);
    }
#endif

    if (!is_psdf && maxima[1].basecount < 3) {
        g_free(maxima);
        return FALSE;
    }

    a1a2[0] = maxima[0].x;
    a1a2[1] = maxima[0].y;
    dh = maxima[0].d;
    /* Exclude maxima that appear to be collinear with the first one,
     * otherwise take the next one with the highest basecount. */
    for (i = 1; i < ngrains; i++) {
        for (k = 2; k < 5; k++) {
            if (fabs(maxima[i].x/k - a1a2[0]) < 0.2*dh && fabs(maxima[i].y/k - a1a2[1]) < 0.2*dh) {
                gwy_debug("Excluding #%u for collinearity (%u).", i, k);
                break;
            }
        }
        if (k == 5) {
            a1a2[2] = maxima[i].x;
            a1a2[3] = maxima[i].y;
            ok = TRUE;
            break;
        }
    }

    g_free(maxima);
    if (!ok)
        return FALSE;

    /* Try to choose some sensible vectors among the equivalent choices. It does not guarantee the choice a human
     * would make but at least make a reasonable one... */
    for (i = 0; i < 4; i++)
        a1a2[i] = -a1a2[i];
    if (is_psdf)
        transform_vectors_real_freq(a1a2);
    maybe_swap_axes(a1a2);
    l1 = hypot(a1a2[0], a1a2[1]);
    l2 = hypot(a1a2[2], a1a2[3]);
    phi = acos((a1a2[0]*a1a2[2] + a1a2[1]*a1a2[3])/(l1*l2));
    if (phi > 0.5*G_PI) {
        if (a1a2[0]*a1a2[3] - a1a2[1]*a1a2[2] > 0.0) {
            a1a2[2] = -a1a2[2];
            a1a2[3] = -a1a2[3];
        }
        else {
            a1a2[0] = -a1a2[0];
            a1a2[1] = -a1a2[1];
        }
    }
    maybe_swap_axes(a1a2);

    return TRUE;
}

static gdouble
refine_from_multiple(GwyField *dfield, gdouble *a1a2,
                     gint peakrange, gint dist2limit, gint xwinsize, gint ywinsize)
{
    gint i, j, n, nex, xres, yres, ii, jj;
    gdouble sii, sij, sjj, six, sjx, siy, sjy, xytmp[2];
    gdouble w, D, dx, dy, xoff, yoff;

    xoff = dfield->xoff;
    yoff = dfield->yoff;
    xres = dfield->xres;
    yres = dfield->yres;
    dx = dfield->xreal/xres;
    dy = dfield->yreal/yres;
    n = nex = 0;
    sii = sij = sjj = six = sjx = siy = sjy = 0.0;
    for (i = -peakrange; i <= peakrange; i++) {
        for (j = -peakrange; j <= peakrange; j++) {
            w = i*i + j*j;
            if (w > dist2limit || w == 0)
                continue;

            xytmp[0] = i*a1a2[0] + j*a1a2[2];
            xytmp[1] = i*a1a2[1] + j*a1a2[3];

            xytmp[0] = (xytmp[0] - xoff)/dx;
            xytmp[1] = (xytmp[1] - yoff)/dy;
            /* Do not go outside the field. */
            if (xytmp[0] < 1 || xytmp[0] > xres-2
                || xytmp[1] < 1 || xytmp[1] > yres-2)
                continue;

            nex++;
            if (!gwy_field_local_maximum(dfield, xytmp + 0, xytmp + 1, xwinsize, ywinsize))
                continue;

            jj = GWY_ROUND(xytmp[0]);
            ii = GWY_ROUND(xytmp[1]);
            if (jj < 0 || jj >= xres || ii < 0 || ii >= yres)
                continue;

            xytmp[0] = (xytmp[0] + 0.5)*dx + xoff;
            xytmp[1] = (xytmp[1] + 0.5)*dy + yoff;

            w = dfield->priv->data[ii*xres + jj]/w;
            sii += i*i*w;
            sij += i*j*w;
            sjj += j*j*w;
            six += i*xytmp[0]*w;
            sjx += j*xytmp[0]*w;
            siy += i*xytmp[1]*w;
            sjy += j*xytmp[1]*w;
            n++;
        }
    }
    gwy_debug("nex=%d, n=%d", nex, n);

    if (!n)
        return 0.0;

    D = sii*sjj - sij*sij;
    a1a2[0] = (six*sjj - sjx*sij)/D;
    a1a2[2] = (sjx*sii - six*sij)/D;
    a1a2[1] = (siy*sjj - sjy*sij)/D;
    a1a2[3] = (sjy*sii - siy*sij)/D;

    return (gdouble)n/nex;
}

static gboolean
refine_lattice(GwyField *dfield, gdouble *a1a2, gboolean is_psdf)
{
    gint xwinsize, ywinsize;
    gdouble r, dx, dy;

    dx = dfield->xreal/dfield->xres;
    dy = dfield->yreal/dfield->yres;
    xwinsize = (gint)(0.32*MAX(fabs(a1a2[0]), fabs(a1a2[2]))/dx + 0.5);
    ywinsize = (gint)(0.32*MAX(fabs(a1a2[1]), fabs(a1a2[3]))/dy + 0.5);
    gwy_debug("window size: %dx%d", xwinsize, ywinsize);

    r = refine_from_multiple(dfield, a1a2, 1, 2, xwinsize, ywinsize);
    gwy_debug("refine1(%g): (%g, %g) (%g, %g)", r, a1a2[0], a1a2[1], a1a2[2], a1a2[3]);

    if (is_psdf)
        return r >= 0.75;

    if (r == 1.0) {
        xwinsize = 5*xwinsize/6;
        ywinsize = 5*ywinsize/6;
        r = refine_from_multiple(dfield, a1a2, 3, 25, xwinsize, ywinsize);
        gwy_debug("refine3(%g): (%g, %g) (%g, %g)", r, a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    }

    if (r == 1.0) {
        xwinsize = 7*xwinsize/8;
        ywinsize = 7*ywinsize/8;
        r = refine_from_multiple(dfield, a1a2, 5, 29, xwinsize, ywinsize);
        gwy_debug("refine5(%g): (%g, %g) (%g, %g)", r, a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    }

    return r >= 0.5;
}

/**
 * gwy_field_measure_lattice_acf:
 * @acf2d: Data field containing two-dimensional autocorrelation function.
 * @a1a2: (array fixed-size=4):
 *        Lattice vectors as an array of four components: @x1, @y1, @x2 and @y2 (in real coordinates).
 *
 * Estimates or improves estimate of lattice vectors from a 2D ACF field.
 *
 * Note that the 2D ACF of a data field has to be passed, not the data field itself.  The correlation function can be
 * for instance calculated by gwy_field_acf_2d(). However, you can calculate and/or process the correlation
 * function in any way you see fit.
 *
 * When the vectors in @a1a2 are zero the function attempts to estimate the lattice from scratch.  But if @a1a2
 * contains two non-zero vectors it takes them as approximate lattice vectors to improve.
 *
 * If the function return %FALSE the array @a1a2 is filled with useless values and must be ignored.
 *
 * Returns: %TRUE if good lattice vectors were found, %FALSE on failure.
 **/
gboolean
gwy_field_measure_lattice_acf(GwyField *acf2d, gdouble *a1a2)
{
    gdouble dx, dy;
    guint i;

    if ((a1a2[0] == 0.0 && a1a2[1] == 0.0) || (a1a2[2] == 0.0 && a1a2[3] == 0.0)) {
        if (!guess_lattice(acf2d, a1a2, FALSE))
            return FALSE;
        gwy_debug("guess: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    }

    for (i = 0; i < 4; i++) {
        if (gwy_isnan(a1a2[i]) || gwy_isinf(a1a2[i])) {
            gwy_debug("inf/nan 1");
            return FALSE;
        }
    }

    if (!refine_lattice(acf2d, a1a2, FALSE)) {
        gwy_debug("failed to refine");
        return FALSE;
    }

    for (i = 0; i < 4; i++) {
        if (gwy_isnan(a1a2[i]) || gwy_isinf(a1a2[i])) {
            gwy_debug("inf/nan 2");
            return FALSE;
        }
    }

    /* For very skewed lattices refine() can produce two of the same vector. */
    dx = acf2d->xreal/acf2d->xres;
    dy = acf2d->yreal/acf2d->yres;
    if (hypot(a1a2[0] - a1a2[2], a1a2[1] - a1a2[3]) < 1.8*hypot(dx, dy)) {
        gwy_debug("too skewed");
        return FALSE;
    }

    return TRUE;
}

/**
 * gwy_field_measure_lattice_psdf:
 * @psdf2d: Data field containing two-dimensional power spectrum density function (or alternatively Fourier
 *          coefficient modulus).
 * @a1a2: (array fixed-size=4):
 *        Lattice vectors as an array of four components: @x1, @y1, @x2 and @y2 (in real coordinates).
 *
 * Estimates or improves estimate of lattice vectors from a 2D PSDF field.
 *
 * Note that the 2D PSDF of a data field has to be passed, not the data field itself.  The spectral density can be for
 * instance calculated by gwy_field_fft_2d() and summing the squares of real and imaginary parts However, you can
 * calculate and/or process the spectral density in any way you see fit.
 *
 * When the vectors in @a1a2 are zero the function attempts to estimate the lattice from scratch.  But if @a1a2
 * contains two non-zero vectors it takes them as approximate lattice vectors to improve.
 *
 * If the function return %FALSE the array @a1a2 is filled with useless values and must be ignored.
 *
 * Returns: %TRUE if good lattice vectors were found, %FALSE on failure.
 **/
gboolean
gwy_field_measure_lattice_psdf(GwyField *psdf2d, gdouble *a1a2)
{
    gdouble dx, dy;
    guint i;

    gwy_debug("input: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    if ((a1a2[0] == 0.0 && a1a2[1] == 0.0) || (a1a2[2] == 0.0 && a1a2[3] == 0.0)) {
        if (!guess_lattice(psdf2d, a1a2, TRUE))
            return FALSE;
        gwy_debug("guess: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    }

    transform_vectors_real_freq(a1a2);
    gwy_debug("freq: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    for (i = 0; i < 4; i++) {
        if (gwy_isnan(a1a2[i]) || gwy_isinf(a1a2[i]))
            return FALSE;
    }

    if (!refine_lattice(psdf2d, a1a2, TRUE))
        return FALSE;

    gwy_debug("refined: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    transform_vectors_real_freq(a1a2);
    gwy_debug("real: (%g, %g) (%g, %g)", a1a2[0], a1a2[1], a1a2[2], a1a2[3]);
    for (i = 0; i < 4; i++) {
        if (gwy_isnan(a1a2[i]) || gwy_isinf(a1a2[i]))
            return FALSE;
    }

    /* For very skewed lattices refine() can produce two of the same vector. */
    dx = 1.0/psdf2d->xreal;
    dy = 1.0/psdf2d->yreal;
    if (hypot(a1a2[0] - a1a2[2], a1a2[1] - a1a2[3]) < 1.8*hypot(dx, dy))
        return FALSE;

    return TRUE;
}

static void
mark_scars(GwyField *field, GwyNield *result, GwyField *buffer,
           gdouble rms,
           gdouble threshold_high, gdouble threshold_low, gdouble min_scar_len, gdouble max_scar_width,
           gboolean negative)
{
    gint xres = field->xres, yres = field->yres;
    const gdouble *d = field->priv->data;
    gdouble *b = buffer->priv->data;
    gint *m = result->priv->data;

    /* Initial scar search. */
    for (gint i = 0; i < yres - (max_scar_width + 1); i++) {
        for (gint j = 0; j < xres; j++) {
            const gdouble *row = d + i*xres + j;

            if (negative) {
                gdouble top = row[0];
                gdouble bottom = row[xres];
                gint k;
                for (k = 1; k <= max_scar_width; k++) {
                    top = MIN(row[0], row[xres*(k + 1)]);
                    bottom = MAX(bottom, row[xres*k]);
                    if (top - bottom >= threshold_low*rms)
                        break;
                }
                if (k <= max_scar_width) {
                    gdouble *brow = b + i*xres + j;

                    while (k) {
                        brow[k*xres] = fmax(brow[k*xres], (top - row[k*xres])/rms);
                        k--;
                    }
                }
            }
            else {
                gdouble bottom = row[0];
                gdouble top = row[xres];
                gint k;
                for (k = 1; k <= max_scar_width; k++) {
                    bottom = MAX(row[0], row[xres*(k + 1)]);
                    top = MIN(top, row[xres*k]);
                    if (top - bottom >= threshold_low*rms)
                        break;
                }
                if (k <= max_scar_width) {
                    gdouble *brow = b + i*xres + j;

                    while (k) {
                        brow[k*xres] = fmax(brow[k*xres], (row[k*xres] - bottom)/rms);
                        k--;
                    }
                }
            }
        }
    }

    /* Expand high threshold to neighbouring low threshold. */
    for (gint i = 0; i < yres; i++) {
        gdouble *brow = b + i*xres;

        for (gint j = 1; j < xres; j++) {
            if (brow[j] >= threshold_low && brow[j-1] >= threshold_high)
                brow[j] = threshold_high;
        }
        for (gint j = xres-1; j > 0; j--) {
            if (brow[j-1] >= threshold_low && brow[j] >= threshold_high)
                brow[j-1] = threshold_high;
        }
    }

    /* Kill too short segments and set the integer mask. */
    for (gint i = 0; i < yres; i++) {
        gdouble *brow = b + i*xres;
        gint *mrow = m + i*xres;

        gint k = 0, j;
        for (j = 0; j < xres; j++) {
            if (brow[j] >= threshold_high) {
                mrow[j] = 1;
                k++;
                continue;
            }
            if (k && k < min_scar_len) {
                while (k) {
                    mrow[j-k] = 0;
                    k--;
                }
            }
            mrow[j] = 0;
            k = 0;
        }
        if (k && k < min_scar_len) {
            while (k) {
                mrow[j-k] = 0;
                k--;
            }
        }
    }
}

/**
 * gwy_field_mark_scars:
 * @field: A data field to find scars in.
 * @result: A number field to store the result to (it is resized to match @field).
 * @threshold_high: Miminum relative step for scar marking, must be positive.
 * @threshold_low: Definite relative step for scar marking, must be at least equal to @threshold_high.
 * @min_scar_len: Minimum length of a scar, shorter ones are discarded (must be at least one).
 * @max_scar_width: Maximum width of a scar, must be at least one.
 * @sign: Sign(s) of features to mark.
 *
 * Find and marks scars in a data field.
 *
 * Scars are linear horizontal defects, consisting of shifted values. Zero or negative values in @result siginify
 * normal data, positive values siginify samples that are part of a scar.
 *
 * The function operates on rows. Use gwy_field_transpose() if you need a column-wise marking.
 **/
void
gwy_field_mark_scars(GwyField *field,
                     GwyNield *result,
                     gdouble threshold_high,
                     gdouble threshold_low,
                     gdouble min_scar_len,
                     gdouble max_scar_width,
                     GwySignFlags sign)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_NIELD(result));

    gint xres = field->xres, yres = field->yres;

    min_scar_len = MAX(min_scar_len, 1);
    max_scar_width = MIN(max_scar_width, yres - 2);
    threshold_high = MAX(threshold_high, threshold_low);
    sign &= GWY_SIGN_BOTH;

    gwy_nield_resize(result, xres, yres);
    gwy_nield_clear(result);

    if (min_scar_len > xres || max_scar_width < 1 || threshold_low <= 0.0 || !sign)
        return;

    /* Compute ‘vertical rms’. */
    const gdouble *d = field->priv->data;
    gdouble rms = 0.0;
    for (gint i = 0; i < yres-1; i++) {
        const gdouble *row = d + i*xres;

        for (gint j = 0; j < xres; j++) {
            gdouble z = row[j] - row[j + xres];

            rms += z*z;
        }
    }
    rms = sqrt(rms/(xres*yres));
    if (rms == 0.0)
        return;

    GwyField *buffer = gwy_field_new_alike(field, TRUE);
    if (sign == GWY_SIGN_BOTH) {
        GwyNield *tmp = gwy_nield_new_alike(result);
        mark_scars(field, tmp, buffer, rms, threshold_high, threshold_low, min_scar_len, max_scar_width, TRUE);
        mark_scars(field, result, buffer, rms, threshold_high, threshold_low, min_scar_len, max_scar_width, FALSE);
        gwy_nield_union(result, tmp);
        g_object_unref(tmp);
    }
    else {
        mark_scars(field, result, buffer, rms, threshold_high, threshold_low, min_scar_len, max_scar_width,
                   sign & GWY_SIGN_NEGATIVE);
    }
    g_object_unref(buffer);
}

/**
 * gwy_field_subtract_row_shifts:
 * @field: A data field.
 * @shifts: Data line containing the row shifts.
 *
 * Shifts entire data field rows as specified by given data line.
 *
 * Data line @shifts must have resolution corresponding to the number of @field rows.  Its values are subtracted
 * from individual field rows.
 **/
void
gwy_field_subtract_row_shifts(GwyField *field,
                              GwyLine *shifts)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_LINE(shifts));
    gint xres = field->xres, yres = field->yres;
    g_return_if_fail(shifts->res == yres);

    const gdouble *s = shifts->priv->data;
    gdouble *d = field->priv->data;

    for (gint i = 0; i < yres; i++) {
        gdouble z = s[i];
        for (gint j = xres; j; j--, d++)
            *d -= z;
    }

    gwy_field_invalidate(field);
}

static gdouble
trimmed_mean_or_median(gdouble *array, guint n, gdouble p)
{
    guint nlowest = GWY_ROUND(p*n);
    guint nhighest = GWY_ROUND(p*n);

    if (nlowest + nhighest + 1 >= n)
        return gwy_math_median(array, n);
    return gwy_math_trimmed_mean(array, n, nlowest, nhighest);
}

static void
zero_level_row_shifts(GwyLine *shifts)
{
    gwy_line_add(shifts, -gwy_line_mean(shifts));
}

static void
slope_level_row_shifts(GwyLine *shifts)
{
    gdouble a, b;

    gwy_line_fit_line(shifts, &a, &b);
    gwy_line_line_level(shifts, a, b);
}

/**
 * gwy_field_find_row_shifts_trimmed_mean:
 * @field: A data field.
 * @mask: (nullable): Mask of values to take values into account/exclude, or %NULL for full @field.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @trimfrac: Fraction of lowest values and highest values to discard when trimming.
 * @mincount: Minimum number of values in a row necessary for per-row calculation.  Rows which are essentially
 *            completely masked are not shifted with respect to a global value.  Pass a non-positive number to use an
 *            automatic minimum count.
 *
 * Finds row shifts to misaligned row correction using trimmed row means.
 *
 * For zero @trimfrac the function calculates row means.  For @trimfrac of 1/2 or larger it calculates row medians.
 * Values between correspond to trimmed means.
 *
 * Returns: (transfer full):
 *          A newly created data line containing the row shifts, for instance row means, medians or trimmed means.
 **/
GwyLine*
gwy_field_find_row_shifts_trimmed_mean(GwyField *field,
                                       GwyNield *mask,
                                       GwyMaskingType masking,
                                       gdouble trimfrac,
                                       gint mincount)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return NULL;

    gint xres = field->xres, yres = field->yres;

    if (mincount <= 0)
        mincount = GWY_ROUND(log(xres) + 1);

    GwyLine *shifts = gwy_line_new(yres, field->yreal, FALSE);
    shifts->off = field->yoff;
    gwy_field_copy_units_to_line(field, shifts);
    gdouble total_median = gwy_field_area_median(field, mask, masking, 0, 0, xres, yres);

    const gdouble *d = field->priv->data;
    const gint *m = mask ? mask->priv->data : NULL;
    gdouble *sdata = shifts->priv->data;

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,sdata,xres,yres,masking,shifts,total_median,trimfrac,mincount)
#endif
    {
        gdouble *buf = g_new(gdouble, xres);
        gint ifrom = gwy_omp_chunk_start(yres), ito = gwy_omp_chunk_end(yres);

        for (gint i = ifrom; i < ito; i++) {
            const gdouble *row = d + i*xres;
            if (m) {
                const gint *mrow = m + i*xres;
                gint count = 0;
                for (gint j = 0; j < xres; j++) {
                    if (nielded_included(mrow + j, masking))
                        buf[count++] = row[j];
                }
                if (count >= mincount)
                    sdata[i] = trimmed_mean_or_median(buf, count, trimfrac);
                else
                    sdata[i] = total_median;
            }
            else {
                gwy_assign(buf, row, xres);
                sdata[i] = trimmed_mean_or_median(buf, xres, trimfrac);
            }
        }

        g_free(buf);
    }

    zero_level_row_shifts(shifts);
    return shifts;
}

/**
 * gwy_field_find_row_shifts_trimmed_diff:
 * @field: A data field.
 * @mask: (nullable): Mask of values to take values into account/exclude, or %NULL for full @field.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @trimfrac: Fraction of lowest values and highest values to discard when trimming.
 * @mincount: Minimum number of values in a row necessary for per-row calculation.  Rows which are essentially
 *            completely masked are not shifted with respect to a global value.  Pass a non-positive number to use an
 *            automatic minimum count.
 *
 * Finds row shifts to misaligned row correction using trimmed means of row differences.
 *
 * For zero @trimfrac the function calculates row means.  For @trimfrac of 1/2 or larger it calculates row medians.
 * Values between correspond to trimmed means.
 *
 * Returns: (transfer full):
 *          A newly created data line containing the row shifts, for instance row means, medians or trimmed means.
 **/
GwyLine*
gwy_field_find_row_shifts_trimmed_diff(GwyField *field,
                                       GwyNield *mask,
                                       GwyMaskingType masking,
                                       gdouble trimfrac,
                                       gint mincount)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return NULL;
    g_return_val_if_fail(trimfrac >= 0.0, NULL);

    gint xres = field->xres, yres = field->yres;

    if (mincount <= 0)
        mincount = GWY_ROUND(log(xres) + 1);

    GwyLine *shifts = gwy_line_new(yres, field->yreal, FALSE);
    shifts->off = field->yoff;
    gwy_field_copy_units_to_line(field, shifts);

    const gdouble *d = field->priv->data;
    const gint *m = mask ? mask->priv->data : NULL;
    gdouble *sdata = shifts->priv->data;

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,sdata,xres,yres,masking,shifts,trimfrac,mincount)
#endif
    {
        gdouble *buf = g_new(gdouble, xres);
        gint ifrom = gwy_omp_chunk_start(yres-1);
        gint ito = gwy_omp_chunk_end(yres-1);

        for (gint i = ifrom; i < ito; i++) {
            const gdouble *row = d + i*xres;
            gint count = 0;
            if (m) {
                const gint *mrow = m + i*xres;
                for (gint j = 0; j < xres; j++) {
                    if (nielded_included(mrow + j, masking) && nielded_included(mrow + xres+j, masking))
                        buf[count++] = row[xres + j] - row[j];
                }
            }
            else {
                for (gint j = 0; j < xres; j++)
                    buf[j] = row[xres + j] - row[j];
                count = xres;
            }

            if (count >= mincount)
                sdata[i+1] = trimmed_mean_or_median(buf, count, trimfrac);
            else
                sdata[i+1] = 0.0;
        }
        g_free(buf);
    }

    sdata[0] = 0.0;
    for (gint k = 1; k < yres; k++)
        sdata[k] += sdata[k-1];

    slope_level_row_shifts(shifts);
    return shifts;
}

/**
 * gwy_field_row_level_poly:
 * @field: A data field.
 * @mask: (nullable): Mask of values to take values into account/exclude, or %NULL for full @field.
 * @masking: Masking mode to use.  See the introduction for description of masking modes.
 * @degree: Polynomial degree (0 is mean value, 1 is tilt, 2 is bow, etc.).
 * @shifts: (nullable):
 *          Data line where to store the subtracted mean values (constant terms of the polynomials), or %NULL.
 *          It will be resized to match the field vertical dimension.
 *
 * Levels misaligned rows of a data field by fitting and subtracting polynomials.
 *
 * This function does both the fitting and subtraction in one step.
 **/
void
gwy_field_row_level_poly(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint degree,
                         GwyLine *shifts)
{
    if (!_gwy_field_check_mask(field, &mask, &masking))
        return;
    g_return_if_fail(degree >= 0);

    gint xres = field->xres, yres = field->yres;

    if (shifts) {
        g_return_if_fail(GWY_IS_LINE(shifts));
        gwy_line_resize(shifts, yres);
        shifts->real = field->yreal;
        shifts->off = field->yoff;
        gwy_field_copy_units_to_line(field, shifts);
    }

    gdouble xc = 0.5*(xres - 1);
    gdouble avg = gwy_field_mean(field);
    gdouble *d = field->priv->data;
    const gint *m = mask ? mask->priv->data : NULL;

#ifdef _OPENMP
#pragma omp parallel if(gwy_threads_are_enabled()) default(none) \
            shared(d,m,xres,yres,masking,avg,degree,xc,shifts)
#endif
    {
        gdouble *xpowers = g_new(gdouble, 2*degree+1);
        gdouble *zxpowers = g_new(gdouble, degree+1);
        gdouble *matrix = g_new(gdouble, (degree+1)*(degree+2)/2);
        gint ifrom = gwy_omp_chunk_start(yres), ito = gwy_omp_chunk_end(yres);

        for (gint i = ifrom; i < ito; i++) {
            const gint *mrow = m ? m + i*xres : NULL;
            gdouble *drow = d + i*xres;

            gwy_clear(xpowers, 2*degree+1);
            gwy_clear(zxpowers, degree+1);

            for (gint j = 0; j < xres; j++) {
                gdouble p = 1.0, x = j - xc;

                if (!nielded_included(mrow + j, masking))
                    continue;

                for (gint k = 0; k <= degree; k++) {
                    xpowers[k] += p;
                    zxpowers[k] += p*drow[j];
                    p *= x;
                }
                for (gint k = degree+1; k <= 2*degree; k++) {
                    xpowers[k] += p;
                    p *= x;
                }
            }

            /* Solve polynomial coefficients. */
            if (xpowers[0] > degree) {
                for (gint j = 0; j <= degree; j++) {
                    for (gint k = 0; k <= j; k++)
                        SLi(matrix, j, k) = xpowers[j + k];
                }
                gwy_math_choleski_decompose(degree+1, matrix);
                gwy_math_choleski_solve(degree+1, matrix, zxpowers);
            }
            else
                gwy_clear(zxpowers, degree+1);

            /* Subtract. */
            zxpowers[0] -= avg;
            if (shifts)
                shifts->priv->data[i] = zxpowers[0];
            for (gint j = 0; j < xres; j++) {
                gdouble p = 1.0, x = j - xc, z = 0.0;

                for (gint k = 0; k <= degree; k++) {
                    z += p*zxpowers[k];
                    p *= x;
                }

                drow[j] -= z;
            }
        }

        g_free(matrix);
        g_free(zxpowers);
        g_free(xpowers);
    }

    gwy_field_invalidate(field);
}

/**
 * gwy_field_distance_blend:
 * @field: A data field.
 * @operand: Another data field to blend into @field.
 * @distances: Distances determining the blending.
 * @col: Area left column.
 * @row: Area top row.
 *
 * Blends one data field into another using distances.
 *
 * The blending preserves @field values everywhere where the distance is non-positive. For positive distances, the
 * larger they are, the more weight @operand gets. Sufficiently far, the contents of @field is effectively replaced
 * with @operand.
 *
 * The distance scaling is pixel-wise. Thereforre, the distance field is usually created for a masked area by
 * gwy_nield_distance_transform().
 *
 * Either @operand and/or @distances can be of the same size of @field, or only covering a part of @field (with top
 * left corner given by @col and @row). When both are smaller than @field, they must be of the same size.
 **/
void
gwy_field_distance_blend(GwyField *field,
                         GwyField *operand,
                         GwyField *distances,
                         gint col,
                         gint row)
{
    g_return_if_fail(GWY_IS_FIELD(field));
    g_return_if_fail(GWY_IS_FIELD(operand));
    g_return_if_fail(GWY_IS_FIELD(distances));

    gint xres = field->xres, width = xres, height = field->yres;
    gboolean operand_is_fullsize = (operand->xres == field->xres && operand->yres == field->yres);
    gboolean distances_is_fullsize = (distances->xres == field->xres && distances->yres == field->yres);
    if (!operand_is_fullsize && !distances_is_fullsize) {
        g_return_if_fail(distances->xres == operand->xres);
        g_return_if_fail(distances->yres == operand->yres);
    }
    if (!operand_is_fullsize) {
        if (!_gwy_field_check_area(field, col, row, operand->xres, operand->yres, FALSE))
            return;
        width = operand->xres;
        height = operand->yres;
    }
    if (!distances_is_fullsize) {
        if (!_gwy_field_check_area(field, col, row, distances->xres, distances->yres, FALSE))
            return;
        width = distances->xres;
        height = distances->yres;
    }
    if (operand_is_fullsize && distances_is_fullsize)
        g_return_if_fail(col == 0 && row == 0);

    const gdouble *opdata = operand->priv->data + (operand_is_fullsize ? row*xres + col : 0);
    const gdouble *distdata = distances->priv->data + (distances_is_fullsize ? row*xres + col : 0);
    gdouble *fdata = field->priv->data + row*xres + col;

#ifdef _OPENMP
#pragma omp parallel for if(gwy_threads_are_enabled()) default(none) \
            shared(fdata,opdata,distdata,width,height,xres,operand_is_fullsize,distances_is_fullsize)
#endif
    for (gint i = 0; i < height; i++) {
        const gdouble *orow = opdata + (operand_is_fullsize ? xres : width)*i;
        const gdouble *drow = distdata + (distances_is_fullsize ? xres : width)*i;
        gdouble *frow = fdata + xres*i;
        for (gint j = 0; j < width; j++) {
            if (drow[j] <= 0.0)
                continue;

            gdouble t = exp(0.167*(1.0 - drow[j]));
            frow[j] *= (1.0 - t);
            frow[j] += t*orow[j];
        }
    }

    gwy_field_invalidate(field);
}

/**
 * gwy_plane_symmetry_get_enum:
 *
 * Returns #GwyEnum for the #GwyPlaneSymmetry enum type.
 *
 * Returns: %NULL-terminated #GwyEnum which must not be modified nor freed.
 **/
const GwyEnum*
gwy_plane_symmetry_get_enum(void)
{
    static const GwyEnum entries[] = {
        { N_("Detected"),               GWY_SYMMETRY_AUTO,       },
        { N_("Parallel"),               GWY_SYMMETRY_PARALLEL,   },
        { N_("Triangular"),             GWY_SYMMETRY_TRIANGULAR, },
        { gwy_NC("symmetry", "Square"), GWY_SYMMETRY_SQUARE,     },
        { N_("Rhombic"),                GWY_SYMMETRY_RHOMBIC,    },
        { N_("Hexagonal"),              GWY_SYMMETRY_HEXAGONAL,  },
        { NULL,                         0,                       },
    };
    return entries;
};

/**
 * SECTION:correct
 * @title: Correction
 * @short_description: Data correction
 **/

/**
 * GwyCoordTransform2DFunc:
 * @x: Old x coordinate.
 * @y: Old y coordinate.
 * @px: Location to store new x coordinate.
 * @py: Location to store new y coordinate.
 * @user_data: User data passed to the caller function.
 *
 * The type of two-dimensional coordinate transform function.
 **/

/**
 * GwyPlaneSymmetry:
 * @GWY_SYMMETRY_AUTO: Automatic symmetry selection.
 * @GWY_SYMMETRY_PARALLEL: Parallel symmetry, there is one prevalent direction (bilateral).
 * @GWY_SYMMETRY_TRIANGULAR: Triangular symmetry, there are three prevalent directions (unilateral) by 120 degrees.
 * @GWY_SYMMETRY_SQUARE: Square symmetry, two prevalent directions (bilateral) oriented approximately along image
 *                       sides.
 * @GWY_SYMMETRY_RHOMBIC: Rhombic symmetry, two prevalent directions (bilateral) oriented approximately along
 *                        diagonals.
 * @GWY_SYMMETRY_HEXAGONAL: Hexagonal symmetry, three prevalent directions (bilateral) by 120 degrees.
 * @GWY_SYMMETRY_LAST: The number of symmetries.
 *
 * Plane symmetry types for rotation correction.
 **/

/**
 * GwyAffineScalingType:
 * @GWY_AFFINE_SCALING_AS_GIVEN: Correct lattice vectors lengths are taken as given.
 * @GWY_AFFINE_SCALING_PRESERVE_AREA: Correct lattice vectors are scaled to make the transformation area-preserving.
 * @GWY_AFFINE_SCALING_PRESERVE_X: Correct lattice vectors are scaled to preserve the scale along @x-axis.
 *
 * Type of lattice vector scaling in affine transform preparation.
 **/

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