/*
 *  $Id: grain-quantities.c 30069 2026-06-15 15:53:33Z 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.
 */

// FIXME: Now that grains may not be contiguous and may touch, modify the evaluations to work correctly in such cases.
//        The major remaining issue seem to be inscribed discs (see also old Gwyddion 3 which has a mean edge distance
//        quantity in addition and something called shape number, although otherwise it is similar and of no help with
//        non-contiguous grains).
//
//        Martin diameter compute do something odd, but that is because it is ill-defined for non-contiguous grains,
//        so no one cares.

#include "config.h"
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/math.h"
#include "libgwyddion/linestats.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/correct.h"
#include "libgwyddion/grains.h"

#include "libgwyddion/internal.h"

typedef struct {
    gdouble xa;
    gdouble ya;
    gdouble xb;
    gdouble yb;
    gdouble r2;
} Edge;

typedef struct {
    guint size;
    guint len;
    Edge *edges;
} EdgeQueue;

typedef struct {
    gdouble x;
    gdouble y;
    gdouble R2;
    guint size;   /* For candidate sorting. */
} GrainDisc;

// The row and column are upscaled 2× in most of the convex hulls computation, which help doing everything in integer
// arithmetic then.
typedef struct {
    gint col;
    gint row;
    gint gno;
} CHullVertex;

enum {
    NDIRECTIONS = 12,
    NCHDIRECTIONS = 10,
};

static const gdouble shift_directions[NDIRECTIONS*2] = {
    1.0, 0.0,
    0.9914448613738104, 0.1305261922200516,
    0.9659258262890683, 0.2588190451025207,
    0.9238795325112867, 0.3826834323650898,
    0.8660254037844387, 0.5,
    0.7933533402912352, 0.6087614290087207,
    0.7071067811865476, 0.7071067811865475,
    0.6087614290087207, 0.7933533402912352,
    0.5,                0.8660254037844386,
    0.3826834323650898, 0.9238795325112867,
    0.2588190451025207, 0.9659258262890683,
    0.1305261922200517, 0.9914448613738104,
};

static const gint chull_directions[2*NCHDIRECTIONS] = {
    8, 1,
    2, 1,
    1, 1,
    1, 2,
    1, 8,
    -1, 8,
    -1, 2,
    -1, 1,
    -2, 1,
    -8, 1,
};

static gdouble*  ensure_buffer      (GPtrArray *buffers,
                                     GwyGrainQuantity quantity,
                                     gdouble **quantity_data,
                                     guint ngrains,
                                     guint multiplicity,
                                     gdouble fillvalue);
static void      calculate_grain_aux(GwyNield *nield,
                                     GwyField *field,
                                     const guint *sizes,
                                     gdouble *min,
                                     gdouble *max,
                                     gdouble *xvalue,
                                     gdouble *yvalue,
                                     gdouble *zvalue,
                                     gdouble *linear,
                                     gdouble *quadratic);
static GrainDisc recurse_welzl      (GwyXY *Pset,
                                     guint Psize,
                                     GwyXY *Rset,
                                     guint Rsize,
                                     gdouble eps);

static void
check_corner_candidate(gint j, gint i, GridPoint *dirmax, GridPoint *dirmin)
{
    // If there is a negative value, the entire block is “empty”. This is the first corner ever encountered. So just
    // fill it everywhere.
    if (dirmax[0].i < 0) {
        for (guint k = 0; k < NCHDIRECTIONS; k++) {
            dirmax[k].j = dirmin[k].j = j;
            dirmax[k].i = dirmin[k].i = i;
        }
        return;
    }

    for (guint k = 0; k < NCHDIRECTIONS; k++) {
        // All comparisons here are done within the one direction. So the vector norm is irrelevant, allowing to doing
        // everything in integer arithmetic.
        gint cosa = chull_directions[2*k], sina = chull_directions[2*k + 1];
        gint curr_min = dirmin[k].j*cosa + dirmin[k].i*sina;
        gint curr_max = dirmax[k].j*cosa + dirmax[k].i*sina;
        gint projection = j*cosa + i*sina;
        if (projection < curr_min) {
            dirmin[k].j = j;
            dirmin[k].i = i;
        }
        if (projection > curr_max) {
            dirmax[k].j = j;
            dirmax[k].i = i;
        }
    }
}

static inline guint
sticking_out_edges(const gint *grains, gint xres, gint yres,
                   gint col, gint row, gint k, gint gno)
{
    // The bit order is the usual upper = 1, right = 2, lower = 4, left = 8.
    return ((guint)(!row || grains[k-xres] != gno)
            | ((guint)(col == xres-1 || grains[k+1] != gno) << 1)
            | ((guint)(row == yres-1 || grains[k+xres] != gno) << 2)
            | ((guint)(!col || grains[k-1] != gno)) << 3);
}

static CHullVertex*
approximate_convex_hull(const gint *grains, guint ngrains,
                        gint xres, gint yres,
                        const guint *grain_types,
                        guint *asizes)
{
    // Start by finding extermal points in NCHDIRECTIONS basic directions. By putting everything to one block, we
    // naturally get sorted extermal points.
    GridPoint *minmax_points = g_new(GridPoint, 2*NCHDIRECTIONS*(ngrains + 1));

    for (gint i = 0; i < 2*NCHDIRECTIONS*(ngrains + 1); i++)
        minmax_points[i].j = minmax_points[i].i = -1;

    GArray *vertices = g_array_new(FALSE, FALSE, sizeof(CHullVertex));
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint k = i*xres + j;
            gint gno = grains[k];
            if (gno <= 0 || grain_types[gno] != G_MAXUINT)
                continue;

            GridPoint *max_gno = minmax_points + 2*NCHDIRECTIONS*gno;
            GridPoint *min_gno = max_gno + NCHDIRECTIONS;
            guint edges = sticking_out_edges(grains, xres, yres, j, i, k, gno);
            // Only check outer corners. Inside points, inner corners or even points in straight edges can all be
            // ignored because they cannot be extremal (they can at best be equal to an outer corner extremal value).
            if (edges & 9)
                check_corner_candidate(j, i, min_gno, max_gno);
            if (edges & 3)
                check_corner_candidate(j+1, i, min_gno, max_gno);
            if (edges & 6)
                check_corner_candidate(j+1, i+1, min_gno, max_gno);
            if (edges & 12)
                check_corner_candidate(j, i+1, min_gno, max_gno);
        }
    }

    // Clean up the approximate convex hull by removing duplicates.
    asizes[0] = asizes[1] = 0;
    for (guint gno = 1; gno <= ngrains; gno++) {
        if (grain_types[gno] != G_MAXUINT) {
            asizes[gno+1] = asizes[gno];
            continue;
        }

        GridPoint *mmblock = minmax_points + 2*NCHDIRECTIONS*gno;
        for (guint k = 0; k < 2*NCHDIRECTIONS; k++) {
            if (k) {
                if (mmblock[k].j == mmblock[k-1].j && mmblock[k].i == mmblock[k-1].i)
                    continue;
                if (mmblock[k].j == mmblock[0].j && mmblock[k].i == mmblock[0].i)
                    continue;
            }
            CHullVertex v = { .col = mmblock[k].j, .row = mmblock[k].i, .gno = gno };
            g_array_append_val(vertices, v);
        }
        asizes[gno+1] = vertices->len;
    }
    g_free(minmax_points);

    return (CHullVertex*)g_array_free(vertices, FALSE);
}

// Check if @v is outside triangle formed by the origin, @a and @b. The origin is the grain midpoint, where we
// shift all the coordinates. Outside means not even on the boundary.
static gboolean
is_outside_sector_triangle(const CHullVertex *a, const CHullVertex *b,
                           const CHullVertex *v)
{
    gint ax = a->col, ay = a->row, bx = b->col, by = b->row, vx = v->col, vy = v->row;
    // Wrong side of origin--a to be inside.
    if (vx*ay - ax*vy > 0)
        return TRUE;
    // Wrong side of origin--b to be inside.
    if (vx*by - bx*vy < 0)
        return TRUE;
    // Wrong side of a--b to be inside.
    gint avx = vx - ax, avy = vy - ay, abx = bx - ax, aby = by - ay;
    if (avx*aby - abx*avy > 0)
        return TRUE;

    return FALSE;
}

// Function takes upscaled and grain-centred j and i.
static void
add_vertex_if_it_sticks_out(GArray *vertices,
                            const CHullVertex *vblock, guint len,
                            gint j, gint i, gint gno)
{
    CHullVertex v = { .gno = gno, .col = j, .row = i };
    for (guint k = 0; k < len-1; k++) {
        if (!is_outside_sector_triangle(vblock + k, vblock + k+1, &v))
            return;
    }
    if (!is_outside_sector_triangle(vblock + len-1, vblock, &v))
        return;

    g_array_append_val(vertices, v);
}

static gint
compare_vertices_grain_number(gconstpointer pa, gconstpointer pb)
{
    const CHullVertex *a = (const CHullVertex*)pa, *b = (const CHullVertex*)pb;
    if (a->gno < b->gno)
        return -1;
    if (a->gno > b->gno)
        return 1;
    // If the grain number is the same we do not care about the order.
    return 0;
}

static gint
compare_vertices_angle(gconstpointer pa, gconstpointer pb)
{
    const CHullVertex *a = (const CHullVertex*)pa, *b = (const CHullVertex*)pb;
    // If one is in the upper half-plane and the other in the lower half-plane, then whichever is in the upper one
    // comes first because we put the splitting line along the positive x-axis.
    gint rowprod = a->row*b->row;
    if (rowprod < 0)
        return b->row;

    // Positive x-axis comes before anything. This really has to be special-cased for the other vector in lower
    // half-plane, but we just special-case it fully.
    if (!rowprod) {
        if (!a->row && a->col > 0 && b->row)
            return -1;
        if (!b->row && b->col > 0 && a->row)
            return 1;
    }

    // They are both in the upper plane or both in the lower plane (either plane including the horizontal directions).
    // Try to use the cross product since we they not straddle the positive x-axis.
    gint cprod = a->row*b->col - b->row*a->col;
    if (cprod != 0)
        return cprod;

    // If the cross-product is zero, they are:
    // (a) Parallel (which is difficult to create because it requires two sticking-out corners on the same line from
    //     grain midpoint, but possible with very large grains). Any can come first, so return whatever.
    // (b) Antiparallel and both horizontal (if they were antiparallel in any other way, then the very first condition
    //     would already caught it). The one with larger col is along the positive x-axis and comes first.
    return b->col - a->col;
}

static void
clean_up_convex_hull(GArray *vertices)
{
    g_array_sort(vertices, compare_vertices_angle);

    guint nnew = vertices->len, nvertices = vertices->len;
    g_array_set_size(vertices, 2*nvertices);
    CHullVertex *vert = &g_array_index(vertices, CHullVertex, 0);
    CHullVertex *newvert = &g_array_index(vertices, CHullVertex, nvertices);

    do {
        nvertices = nnew;
        nnew = 0;

        if (is_outside_sector_triangle(vert + nvertices-1, vert + 1, vert + 0))
            newvert[nnew++] = vert[0];
        for (guint k = 1; k < nvertices-1; k++) {
            if (is_outside_sector_triangle(vert + k-1, vert + k+1, vert + k))
                newvert[nnew++] = vert[k];
        }
        if (is_outside_sector_triangle(vert + nvertices-2, vert + 0, vert + nvertices-1))
            newvert[nnew++] = vert[nvertices-1];

        GWY_SWAP(CHullVertex*, vert, newvert);
    } while (nnew < nvertices);

    // When newv == vertices, the two vertex lists are idencital. Hence, we do not care whether the list at the
    // beginning of the array played the role of vert of newvert. Just truncate the array.
    g_array_set_size(vertices, nvertices);
}

// The return value is 16*n + bbbb, where n is the number of corners occupied by the grain and bbbb is a bit mask
// saying which corners are occupied (in the usual ul, ur, lr, ll order).
static inline guint
bbox_grain_corners(const gint *grains, gint xres, gint gno, const gint *bbox)
{
    gint col = bbox[0], row = bbox[1], w = bbox[2], h = bbox[3];
    gint k = row*xres + col;
    return (17*(grains[k] == gno)
            + 18*(grains[k + w-1] == gno)
            + 20*(grains[k + (h-1)*xres + w-1] == gno)
            + 24*(grains[k + (h-1)*xres] == gno));
}

// The returned array contains (j, i) pairs of all pixel corners in the convex hulls of all grains. The starting
// position for grain gno fill be filled to csizes[gno], which must have at least ngrains+2 elements allocated as
// the total length is filled to csizes[ngrains+1].
static gint*
find_grain_convex_hulls(const gint *grains, guint ngrains,
                        gint xres, gint yres,
                        const gint *bboxes,
                        guint *csizes)
{
    guint *asizes = g_new(guint, ngrains+2);
    // We only fill csizes in the last stage. Use it meanwhile to mark trivial grains. For convenience, 0 and 4 are
    // the final convex hull sizes; only G_MAXUINT means not known yet. Also, store the information at the next
    // index to avoid overwriting it more easily.
    guint *grain_types = csizes + 1;
    for (guint gno = 1; gno <= ngrains; gno++) {
        if (bboxes[4*gno + 2] < 0)
            grain_types[gno] = 0;
        else if (bbox_grain_corners(grains, xres, gno, bboxes + 4*gno) == 4*16 + 15)
            grain_types[gno] = 4;
        else
            grain_types[gno] = G_MAXUINT;
    }

    CHullVertex *approx_vertices = approximate_convex_hull(grains, ngrains, xres, yres, grain_types, asizes);
    GridPoint *midpoints = g_new(GridPoint, ngrains + 1);
    for (guint gno = 1; gno <= ngrains; gno++) {
        if (grain_types[gno] != G_MAXUINT)
            continue;

        // If rows and columns are upscaled by factor 2, the approximate convex hull interior always contains a grid
        // point, even for (originally) 1×1 grains and other weird shapes. We choose it as the midpoint and move the
        // origin of coordinates for the grain there. Then ALL calculations we need are integer cross-products, so no
        // rounding errors.
        CHullVertex *vblock = approx_vertices + asizes[gno];
        guint len = asizes[gno+1] - asizes[gno];

        gint sx = 0, sy = 0;
        for (guint k = 0; k < len; k++) {
            vblock[k].col *= 2;
            vblock[k].row *= 2;
            sx += vblock[k].col;
            sy += vblock[k].row;
        }

        gint mcol = midpoints[gno].j = (sx + len/3)/len;
        gint mrow = midpoints[gno].i = (sy + len/3)/len;
        for (guint k = 0; k < len; k++) {
            vblock[k].col -= mcol;
            vblock[k].row -= mrow;
        }
    }

    // Add vertices sticking out the approximate initial convex hulls. This is still O(1) per pixel because we do not
    // augment the approximate convex hulls along the way.
    GArray *more_vertices = g_array_new(FALSE, FALSE, sizeof(CHullVertex));
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint k = i*xres + j;
            gint gno = grains[k];
            if (gno <= 0 || grain_types[gno] != G_MAXUINT)
                continue;

            guint edges = sticking_out_edges(grains, xres, yres, j, i, k, gno);
            CHullVertex *vblock = approx_vertices + asizes[gno];
            guint len = asizes[gno+1] - asizes[gno];
            gint iup = 2*i - midpoints[gno].i;
            gint jup = 2*j - midpoints[gno].j;
            // Only check outer corners. Inside points, inner corners or even points in straight edges can all be
            // ignored because they cannot be extremal (they can at best be equal to an outer corner extremal value).
            if (edges & 9)
                add_vertex_if_it_sticks_out(more_vertices, vblock, len, jup, iup, gno);
            if (edges & 3)
                add_vertex_if_it_sticks_out(more_vertices, vblock, len, jup+2, iup, gno);
            if (edges & 6)
                add_vertex_if_it_sticks_out(more_vertices, vblock, len, jup+2, iup+2, gno);
            if (edges & 12)
                add_vertex_if_it_sticks_out(more_vertices, vblock, len, jup, iup+2, gno);
        }
    }
    g_array_sort(more_vertices, compare_vertices_grain_number);

    GArray *grain_vertices = g_array_new(FALSE, FALSE, sizeof(CHullVertex));
    GArray *chull = g_array_new(FALSE, FALSE, sizeof(gint));
    guint more_pos = 0;
    csizes[0] = csizes[1] = 0;

    for (guint gno = 1; gno <= ngrains; gno++) {
        // NB: Assigning to csizes[gno+1] overwrites grain_types[gno], but grain_types[gno+1] is still safe thanks to
        // the shift by 1.
        if (!grain_types[gno]) {
            csizes[gno+1] = csizes[gno];
            continue;
        }
        else if (grain_types[gno] == 4) {
            // Rectangular grain 1×N or M×1.
            guint len = 4, prev_chull_len = chull->len;
            g_array_set_size(chull, prev_chull_len + 2*len);
            gint *grain_points = &g_array_index(chull, gint, prev_chull_len);
            gint col = bboxes[4*gno], row = bboxes[4*gno + 1], w = bboxes[4*gno + 2], h = bboxes[4*gno + 3];
            grain_points[2] = grain_points[4] = col;
            grain_points[0] = grain_points[6] = col + w;
            grain_points[1] = grain_points[3] = row;
            grain_points[5] = grain_points[7] = row + h;
            csizes[gno+1] = chull->len/2;
            continue;
        }

        guint more_len = 0;
        while (more_pos + more_len < more_vertices->len
               && g_array_index(more_vertices, CHullVertex, more_pos + more_len).gno == gno)
            more_len++;

        g_array_set_size(grain_vertices, 0);
        g_array_append_vals(grain_vertices, approx_vertices + asizes[gno], asizes[gno+1] - asizes[gno]);
        if (more_len)
            g_array_append_vals(grain_vertices, &g_array_index(more_vertices, CHullVertex, more_pos), more_len);

        clean_up_convex_hull(grain_vertices);

        guint len = grain_vertices->len, prev_chull_len = chull->len;
        // Proactively expand the array to hold all the grain vertices.
        g_array_set_size(chull, prev_chull_len + 2*len);
        gint *grain_points = &g_array_index(chull, gint, prev_chull_len);

        // Reverse the order to anti-clockwise, as promised.
        gint mx = midpoints[gno].j, my = midpoints[gno].i;
        for (guint k = 0; k < len; k++) {
            const CHullVertex *v = &g_array_index(grain_vertices, CHullVertex, len-1 - k);
            grain_points[2*k] = (v->col + mx)/2;
            grain_points[2*k + 1] = (v->row + my)/2;
        }

        more_pos += more_len;
        csizes[gno+1] = chull->len/2;
    }

    g_array_free(grain_vertices, TRUE);
    g_array_free(more_vertices, TRUE);

    g_free(asizes);
    g_free(approx_vertices);
    g_free(midpoints);

    return (gint*)g_array_free(chull, FALSE);
}

/**
 * gwy_nield_convex_hulls:
 * @nield: A number field.
 * @cindex: Array of size at least @maxgno+2.  It will be filled with block starts in the returned array.
 *
 * Finds convex hulls of all grains in a number field.
 *
 * Use gwy_nield_max() to obtain @maxgno and allocate correct @cindex.
 *
 * The returned array contains pairs (col,row) of pixel vertices of the convex hull, concatenated all one after
 * another. In each block, the vertices are enumerated in the anti-clockwise order.
 *
 * The column and row coordinates come from the intervals [0,@xres] and [0,@yres]. Note that these interval are
 * inclusive on both ends. The convex hull covers the full pixels as little rectangles (not just their centres or
 * other selected points).
 *
 * The block for grain with number @i starts at position @cindex[@i] and ends one before position @cindex[@i+1] where
 * the next block starts. The end is also stored for the last block, which does not have any next block, and gives the
 * total size of the returned array.
 *
 * The convex hull of the no-grain area (non-positive values) is not computed. Therefore, the first two elements of
 * @cindex will be always zeros.
 *
 * Returns: (transfer full):
 *          A newly allocated array of size given by @cindex[@maxgno+1].
 **/
gint*
gwy_nield_convex_hulls(GwyNield *nield, guint *cindex)
{
    g_return_val_if_fail(GWY_IS_NIELD(nield), NULL);
    g_return_val_if_fail(cindex, NULL);

    gint ngrains = gwy_nield_max(nield);
    ngrains = MAX(ngrains, 0);
    const gint *bboxes = _gwy_nield_ensure_bboxes(nield);
    return find_grain_convex_hulls(nield->priv->data, ngrains, nield->xres, nield->yres, bboxes, cindex);
}

/**
 * grain_maximum_projection:
 * @vertices: Convex hull vertex list.
 * @nvertices: Number of vertices in @vertices.
 * @vx: Location to store vector x component to.
 * @vy: Location to store vector y component to.
 * @cx: Location to store line centre x component to.
 * @cy: Location to store line centre y component to.
 *
 * Given a list of integer convex hull vertices, return a vector between the two most distant vertices.
 *
 * The vector is always the vector between two vertices. It may not be unique, for example for square grains.
 **/
static void
grain_maximum_projection(const GwyXY *vertices, guint nvertices,
                         gdouble *vx, gdouble *vy,
                         gdouble *cx, gdouble *cy)
{
    gdouble vprev = -G_MAXDOUBLE;
    GwyXY a = vertices[0];
    guint g2, g1max = 0, g2max = 0;

    // Find the most distant vertex to the first vertex by brute force.
    for (g2 = 1; g2 < nvertices; g2++) {
        gdouble v = gwy_xy_dist2(&a, vertices + g2);
        if (v < vprev)
            break;
        vprev = v;
    }
    g2max = g2 = g2-1;
    gdouble vmax = vprev;

    // Now move the first point along the perimeter, knowing the most distant point needs to move along the
    // perimeter in the same direction (or stay where it is).
    for (guint g1 = 1; g1 < nvertices; g1++) {
        gdouble vnext, v;
        guint g2next = g2;

        a = vertices[g1];
        vnext = gwy_xy_dist2(&a, vertices + g2);
        do {
            v = vnext;
            g2 = g2next;
            g2next = (g2next + 1) % nvertices;
            vnext = gwy_xy_dist2(&a, vertices + g2next);
        } while (vnext > v);

        if (v > vmax) {
            g1max = g1;
            g2max = g2;
            vmax = v;
        }
    }

    *cx = 0.5*(vertices[g2max].x + vertices[g1max].x);
    *cy = 0.5*(vertices[g2max].y + vertices[g1max].y);
    *vx = vertices[g2max].x - vertices[g1max].x;
    *vy = vertices[g2max].y - vertices[g1max].y;
}

static gdouble
line_point_dist2(const GwyXY *a, const GwyXY *ab, gdouble ab2, const GwyXY *c)
{
    GwyXY delta = { c->x - a->x, c->y - a->y };
    gdouble s = (delta.x*ab->x + delta.y*ab->y)/ab2;
    delta.x -= s*ab->x;
    delta.y -= s*ab->y;
    return delta.x*delta.x + delta.y*delta.y;
}

/**
 * grain_minimum_projection:
 * @vertices: Convex hull vertex list.
 * @nvertices: Number of vertices in @vertices.
 * @vx: Location to store vector x component to.
 * @vy: Location to store vector y component to.
 *
 * Given a list of integer convex hull vertices, return a vector corresponding to the minimum projection.
 *
 * The vector is always perpendicular to an edge of the convex hull. It may not be unique, for example for square
 * grains.
 *
 * The vector is always the vector between two vertices. It may not be unique, for example for square grains.
 **/
static void
grain_minimum_projection(const GwyXY *vertices, guint nvertices,
                         gdouble *vx, gdouble *vy)
{
    gdouble vprev = -G_MAXDOUBLE;
    GwyXY a = vertices[0];
    GwyXY ab = { vertices[1].x - a.x, vertices[1].y - a.y };
    gdouble ab2 = gwy_xy_len2(&ab);
    guint g2, g1min = 0, g2min = 0;

    // Find the most distant vertex to the first edge by brute force.
    for (g2 = 2; g2 < nvertices; g2++) {
        gdouble v = line_point_dist2(&a, &ab, ab2, vertices + g2);
        if (v < vprev)
            break;
        vprev = v;
    }
    g2min = g2 = g2-1;
    gdouble vmin = vprev;

    // Now move the edge along the perimeter, knowing the most distant point needs to move along the perimeter in the
    // same direction (or stay where it is).
    for (guint g1 = 1; g1 < nvertices; g1++) {
        gdouble vnext, v;
        guint g2next = g2, g1next = (g1 + 1) % nvertices;

        a = vertices[g1];
        ab.x = vertices[g1next].x - a.x;
        ab.y = vertices[g1next].y - a.y;
        ab2 = gwy_xy_len2(&ab);
        vnext = line_point_dist2(&a, &ab, ab2, vertices + g2);
        do {
            v = vnext;
            g2 = g2next;
            g2next = (g2next + 1) % nvertices;
            vnext = line_point_dist2(&a, &ab, ab2, vertices + g2next);
        } while (vnext > v);

        // Find the most distant point to each edge, but then minimise among these.
        if (v < vmin) {
            g1min = g1;
            g2min = g2;
            vmin = v;
        }
    }

    a = vertices[g1min];
    guint g1next = (g1min + 1) % nvertices;
    ab.x = vertices[g1next].x - a.x;
    ab.y = vertices[g1next].y - a.y;
    ab2 = gwy_xy_len2(&ab);

    GwyXY delta = { vertices[g2min].x - a.x, vertices[g2min].y - a.y };
    gdouble s = (delta.x*ab.x + delta.y*ab.y)/ab2;
    *vx = delta.x - s*ab.x;
    *vy = delta.y - s*ab.y;
}

static gdouble
grain_projection_length(const GwyXY *xy, guint nvertices,
                        gdouble cosa, gdouble sina)
{
    gdouble lower = G_MAXDOUBLE, upper = -G_MAXDOUBLE;

    for (guint i = 0; i < nvertices; i++) {
        gdouble proj = xy[i].x*cosa + xy[i].y*sina;
        lower = fmin(lower, proj);
        upper = fmax(upper, proj);
    }
    return upper - lower;
}

static gdouble
grain_convex_hull_area(const gint *vertices, guint nvertices, gdouble dA)
{
    gint acol = vertices[0], arow = vertices[1];
    gint bcol = vertices[2], brow = vertices[3];

    gint s = 0;
    for (guint i = 2; i < nvertices; i++) {
        gint ccol = vertices[2*i];
        gint crow = vertices[2*i + 1];
        gint bx = bcol - acol, by = brow - arow, cx = ccol - acol, cy = crow - arow;
        s += by*cx - bx*cy;
        bcol = ccol;
        brow = crow;
    }

    return 0.5*dA*s;
}

static gboolean
all_points_in_circle(const GrainDisc *circle, gdouble eps,
                     const GwyXY *xy, guint n)
{
    gdouble cx = circle->x, cy = circle->y, r2 = circle->R2 + eps;
    for (guint i = 0; i < n; i++) {
        const GwyXY *pt = xy + i;
        gdouble d2 = (pt->x - cx)*(pt->x - cx) + (pt->y - cy)*(pt->y - cy);
        if (d2 > r2)
            return FALSE;
    }
    return TRUE;
}

static GrainDisc
make_circle(GwyXY *Rset, guint Rsize)
{
    GrainDisc disc;

    if (Rsize == 3) {
        GwyXY a = Rset[0];
        GwyXY b_a = { Rset[1].x - a.x, Rset[1].y - a.y };
        gdouble b2_a = gwy_xy_len2(&b_a);
        GwyXY c_a = { Rset[2].x - a.x, Rset[2].y - a.y };
        gdouble c2_a = gwy_xy_len2(&c_a);
        gdouble D = 2*gwy_xy_crossprodz(&b_a, &c_a);

        disc.x = (c_a.y*b2_a - b_a.y*c2_a)/D;
        disc.y = (b_a.x*c2_a - c_a.x*b2_a)/D;
        disc.R2 = disc.x*disc.x + disc.y*disc.y;
        disc.x += a.x;
        disc.y += a.y;
        return disc;
    }
    else if (Rsize == 2) {
        disc.x = 0.5*(Rset[0].x + Rset[1].x);
        disc.y = 0.5*(Rset[0].y + Rset[1].y);
        disc.R2 = 0.25*gwy_xy_dist2(Rset, Rset+1);
    }
    else if (Rsize == 1) {
        disc.x = Rset[0].x;
        disc.y = Rset[0].y;
        disc.R2 = 0.0;
    }
    else {
        // Empty circle.
        disc.x = disc.y = 0.0;
        disc.R2 = -G_MAXDOUBLE;
    }
    return disc;
}

static GrainDisc
recurse_welzl(GwyXY *Pset, guint Psize, GwyXY *Rset, guint Rsize, gdouble eps)
{
    if (!Psize || Rsize == 3)
        return make_circle(Rset, Rsize);

    GrainDisc disc = recurse_welzl(Pset, Psize-1, Rset, Rsize, eps);
    gdouble deltax = Pset[Psize-1].x - disc.x, deltay = Pset[Psize-1].y - disc.y;
    if (deltax*deltax + deltay*deltay <= disc.R2 + eps)
        return disc;

    Rset[Rsize] = Pset[Psize-1];
    return recurse_welzl(Pset, Psize-1, Rset, Rsize+1, eps);
}

/**
 * circumcircle_welzl:
 * @vertices: Vertex list. Usually we already pass the convex hull.
 * @nvertices: Number of vertices in @vertices.
 * @circle: Location where to store circumcircle.
 * @eps: Radius comparison tolerance.
 *
 * Finds the circumcircle of a set of points using the recursive Welzl method.
 *
 * The function permutes the @vertices array.
 **/
static void
circumcircle_welzl(GwyXY *vertices, guint nvertices,
                   GrainDisc *circle, gdouble eps,
                   GRand *rng)
{
    for (guint i = 0; i < nvertices-1; i++) {
        guint k = g_rand_int_range(rng, i, nvertices);
        GWY_SWAP(GwyXY, vertices[k], vertices[i]);
    }

    GwyXY Rset[3];
    guint Rsize = 0;
    *circle = recurse_welzl(vertices, nvertices, Rset, Rsize, eps);
}

static guint*
realloc_if_needed(GwyNield *nield, guint w, guint h, guint *allocated_size)
{
    if (w*h > *allocated_size) {
        g_free(nield->priv->data);
        *allocated_size = w*h;
        nield->priv->data = g_new(gint, *allocated_size);
    }
    nield->xres = w;
    nield->yres = h;
    return (guint*)nield->priv->data;
}

static void
extract_upsampled_square_pixel_grain(GwyNield *grains, guint gno,
                                     const gint *bbox,
                                     GwyNield *upgrain, guint *allocated_size,
                                     gdouble dx, gdouble dy)
{
    gint col = bbox[0], row = bbox[1], w = bbox[2], h = bbox[3];
    guint w2 = 2*w, h2 = 2*h, xres = grains->xres;
    // Limit the larger upsampling to 5 as a stopgap measure. If the pixel aspect ratio is 1:1000, this does not find
    // the correct inscribed disc centres. On the other hand, the grains are probably not reasonably represented
    // geometrically in such case. See the comments in upsample_keeping_gran_numbers() for related ramblings.
    gint w2ex = GWY_ROUND(w2*fmin(dx/dy, 5.0)), h2ex = GWY_ROUND(h2*fmin(dy/dx, 5.0));
    const guint *gdata = (const guint*)grains->priv->data;
    guint i, j;

    /* Upsample sqaure (or nearly-square) pixels 2× isotroptically. */
    if (w2ex == w2 && h2ex == h2) {
        guint *ugdata = realloc_if_needed(upgrain, w2, h2, allocated_size);
        for (i = 0; i < h; i++) {
            guint k2 = w2*(2*i);
            guint k = (i + row)*xres + col;
            for (j = 0; j < w; j++, k++, k2 += 2) {
                guint v = (gdata[k] == gno) ? 1 : 0;
                ugdata[k2] = v;
                ugdata[k2+1] = v;
                ugdata[k2 + w2] = v;
                ugdata[k2 + w2+1] = v;
            }
        }
    }
    else if (dy < dx) {
        /* Horizontal upsampling > 2× (uneven), vertical 2×, precalculate index map to use in each row. */
        w2 = w2ex;
        guint *ugdata = realloc_if_needed(upgrain, w2, h2, allocated_size);
        guint *indices = g_new(guint, w2);
        for (j = 0; j < w2; j++) {
            gint jj = (gint)floor((gdouble)j/w2*w);
            indices[j] = CLAMP(jj, 0, (gint)w-1);
        }
        for (i = 0; i < h; i++) {
            guint k = (i + row)*xres + col;
            guint k2 = w2*(2*i);
            for (j = 0; j < w2; j++) {
                guint v = (gdata[k + indices[j]] == gno) ? 1 : 0;
                ugdata[k2 + j] = v;
                ugdata[k2 + w2 + j] = v;
            }
        }
        g_free(indices);
    }
    else {
        /* Vertical upsampling > 2×, rows are uniformly 2× scaled, just different numbers of them */
        h2 = h2ex;
        guint *ugdata = realloc_if_needed(upgrain, w2, h2, allocated_size);
        for (i = 0; i < h2; i++) {
            guint k, k2 = i*w2;
            gint ii = (gint)floor((gdouble)i/h2*h);
            ii = CLAMP(ii, 0, (gint)h-1);
            k = (ii + row)*xres + col;
            for (j = 0; j < w; j++) {
                guint v = (gdata[k + j] == gno) ? 1 : 0;
                ugdata[k2 + 2*j] = v;
                ugdata[k2 + 2*j + 1] = v;
            }
        }
    }
}

static gint
compare_candidates(gconstpointer a,
                   gconstpointer b)
{
    const GrainDisc *da = (const GrainDisc*)a;
    const GrainDisc *db = (const GrainDisc*)b;

    if (da->size > db->size)
        return -1;
    if (da->size < db->size)
        return 1;

    if (da->R2 < db->R2)
        return -1;
    if (da->R2 > db->R2)
        return 1;

    return 0;
}

#if 0
// Score how good a pixel is for disc centre candidate. More consecutive neighbours (angularly) with the same maximum
// value mean nicer candidate because clearly there is more wiggle room for the disc. If the neighbours are not
// consecutive, which is what we currently test, it may not mean a better candidate.
static guint
neighbour_pattern_disc_niceness(guint neighbour_bits)
{
    static const guint8 niceness[0x100] = {
        0, 1, 1, 3, 1, 2, 3, 4, 1, 1, 2, 3, 3, 3, 4, 5,
        1, 1, 1, 3, 2, 2, 3, 4, 3, 3, 3, 3, 4, 4, 5, 6,
        1, 1, 1, 3, 1, 2, 3, 4, 2, 2, 2, 3, 3, 3, 4, 5,
        3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 5, 5, 6, 7,
        1, 2, 1, 3, 1, 2, 3, 4, 1, 2, 2, 3, 3, 3, 4, 5,
        2, 2, 2, 3, 2, 2, 3, 4, 3, 3, 3, 3, 4, 4, 5, 6,
        3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 3, 3, 3, 3, 4, 5,
        4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 6, 6, 7, 8,
        1, 3, 2, 4, 1, 3, 3, 5, 1, 3, 2, 4, 3, 3, 4, 6,
        1, 3, 2, 4, 2, 3, 3, 5, 3, 3, 3, 4, 4, 4, 5, 7,
        2, 3, 2, 4, 2, 3, 3, 5, 2, 3, 2, 4, 3, 3, 4, 6,
        3, 3, 3, 4, 3, 3, 3, 5, 4, 4, 4, 4, 5, 5, 6, 8,
        3, 4, 3, 5, 3, 4, 3, 6, 3, 4, 3, 5, 3, 4, 4, 7,
        3, 4, 3, 5, 3, 4, 3, 6, 3, 4, 3, 5, 4, 4, 5, 8,
        4, 5, 4, 6, 4, 5, 4, 7, 4, 5, 4, 6, 4, 5, 4, 8,
        5, 6, 5, 7, 5, 6, 5, 8, 6, 7, 6, 8, 7, 8, 8, 9,
    };

    g_return_val_if_fail(neighbour_bits > 0xff, 0);
    return niceness[neighbour_bits];
}
#endif

static void
find_disc_centre_candidates(GArray *candidates,
                            PixelQueue *inqueue,
                            GwyNield *grain,
                            gdouble dx, gdouble dy,
                            gdouble centrex, gdouble centrey)
{
    const guint *gdata = (const guint*)grain->priv->data;
    guint width = grain->xres, height = grain->yres;
    guint m;

    g_array_set_size(candidates, 0);
    for (m = 0; m < inqueue->len; m++) {
        GridPoint *mpt = inqueue->points + m;
        guint i = mpt->i, j = mpt->j, k = i*width + j, size = 8*gdata[k], w;
        GrainDisc cand;

        if (i && j && (w = gdata[k - width-1]) != G_MAXUINT)
            size += w;
        if (i && (w = gdata[k - width]) != G_MAXUINT)
            size += 2*w;
        if (i && j < width-1 && (w = gdata[k - width+1]) != G_MAXUINT)
            size += w;
        if (j && (w = gdata[k-1]) != G_MAXUINT)
            size += 2*w;
        if (j < width-1 && (w = gdata[k+1]) != G_MAXUINT)
            size += 2*w;
        if (i < height-1 && j && (w = gdata[k + width-1]) != G_MAXUINT)
            size += w;
        if (i < height-1 && (w = gdata[k + width]) != G_MAXUINT)
            size += 2*w;
        if (i < height-1 && j < width-1 && (w = gdata[k + width+1]) != G_MAXUINT)
            size += w;

        cand.x = (mpt->j + 0.5)*dx;
        cand.y = (mpt->i + 0.5)*dy;
        cand.size = size;
        /* Use R2 temporarily for distance from the entire gdata centre; this is only for sorting below. */
        cand.R2 = ((cand.x - centrex)*(cand.x - centrex) + (cand.y - centrey)*(cand.y - centrey));
        g_array_append_val(candidates, cand);
    }
    g_array_sort(candidates, &compare_candidates);
}

static inline void
edge_list_add(EdgeQueue *queue,
              gdouble xa, gdouble ya,
              gdouble xb, gdouble yb)
{
    if (G_UNLIKELY(queue->len == queue->size)) {
        queue->size = MAX(2*queue->size, 16);
        queue->edges = g_renew(Edge, queue->edges, queue->size);
    }

    queue->edges[queue->len].xa = xa;
    queue->edges[queue->len].ya = ya;
    queue->edges[queue->len].xb = xb;
    queue->edges[queue->len].yb = yb;
    queue->len++;
}

static void
find_all_edges(EdgeQueue *edges,
               GwyNield *grains,
               guint gno, const gint *bbox,
               gdouble dx, gdouble dy)
{
    guint col = bbox[0], row = bbox[1], w = bbox[2], h = bbox[3];
    const guint *gdata = (const guint*)grains->priv->data;
    guint xres = grains->xres;
    guint i, j;
    guint *vertices;

    edges->len = 0;

    vertices = g_new(guint, w + 1);
    for (j = 0; j <= w; j++)
        vertices[j] = G_MAXUINT;

    for (i = 0; i <= h; i++) {
        guint k = (i + row)*xres + col;
        guint vertex = G_MAXUINT;

        for (j = 0; j <= w; j++, k++) {
            /*
             * 1 2
             * 3 4
             */
            guint g0 = i && j && gdata[k - xres - 1] == gno;
            guint g1 = i && j < w && gdata[k - xres] == gno;
            guint g2 = i < h && j && gdata[k - 1] == gno;
            guint g3 = i < h && j < w && gdata[k] == gno;
            guint g = g0 | (g1 << 1) | (g2 << 2) | (g3 << 3);

            if (g == 8 || g == 7) {
                vertex = j;
                vertices[j] = i;
            }
            else if (g == 2 || g == 13) {
                edge_list_add(edges, dx*j, dy*vertices[j], dx*j, dy*i);
                vertex = j;
                vertices[j] = G_MAXUINT;
            }
            else if (g == 4 || g == 11) {
                edge_list_add(edges, dx*vertex, dy*i, dx*j, dy*i);
                vertex = G_MAXUINT;
                vertices[j] = i;
            }
            else if (g == 1 || g == 14) {
                edge_list_add(edges, dx*vertex, dy*i, dx*j, dy*i);
                edge_list_add(edges, dx*j, dy*vertices[j], dx*j, dy*i);
                vertex = G_MAXUINT;
                vertices[j] = G_MAXUINT;
            }
            else if (g == 6 || g == 9) {
                edge_list_add(edges, dx*vertex, dy*i, dx*j, dy*i);
                edge_list_add(edges, dx*j, dy*vertices[j], dx*j, dy*i);
                vertex = j;
                vertices[j] = i;
            }
        }
    }

    g_free(vertices);
}

static gdouble
maximize_disc_radius(GrainDisc *disc, Edge *edges, guint n)
{
    gdouble x = disc->x, y = disc->y, r2best = G_MAXDOUBLE;

    while (n--) {
        gdouble rax = edges->xa - x, ray = edges->ya - y,
                rbx = edges->xb - x, rby = edges->yb - y,
                deltax = edges->xb - edges->xa, deltay = edges->yb - edges->ya;
        gdouble ca = -(deltax*rax + deltay*ray),
                cb = deltax*rbx + deltay*rby;

        if (ca <= 0.0)
            edges->r2 = rax*rax + ray*ray;
        else if (cb <= 0.0)
            edges->r2 = rbx*rbx + rby*rby;
        else {
            gdouble tx = cb*rax + ca*rbx, ty = cb*ray + ca*rby, D = ca + cb;
            edges->r2 = (tx*tx + ty*ty)/(D*D);
        }

        if (edges->r2 < r2best)
            r2best = edges->r2;
        edges++;
    }

    return r2best;
}

static guint
filter_relevant_edges(EdgeQueue *edges, gdouble r2, gdouble eps)
{
    Edge *edge = edges->edges, *enear = edges->edges;
    gdouble limit = sqrt(r2) + 4.0*eps + 0.5;
    guint i;

    limit *= limit;
    for (i = edges->len; i; i--, edge++) {
        if (edge->r2 <= limit) {
            if (edge != enear)
                GWY_SWAP(Edge, *edge, *enear);
            enear++;
        }
    }

    return enear - edges->edges;
}

static void
improve_inscribed_disc(GrainDisc *disc, EdgeQueue *edges, guint dist)
{
    gdouble eps = 0.5 + 0.25*(dist > 4) + 0.25*(dist > 16), improvement;
    guint nsuccessiveimprovements = 0;

    do {
        GrainDisc best;
        guint i, nr;

        disc->R2 = maximize_disc_radius(disc, edges->edges, edges->len);
        eps = MIN(eps, 0.5*sqrt(disc->R2));
        best = *disc;
        nr = filter_relevant_edges(edges, best.R2, eps);

        improvement = 0.0;
        for (i = 0; i < NDIRECTIONS; i++) {
            GrainDisc cand;
            gdouble sx = eps*shift_directions[2*i], sy = eps*shift_directions[2*i + 1];

            cand.size = disc->size;

            cand.x = disc->x + sx;
            cand.y = disc->y + sy;
            if ((cand.R2 = maximize_disc_radius(&cand, edges->edges, nr)) > best.R2)
                best = cand;

            cand.x = disc->x - sy;
            cand.y = disc->y + sx;
            if ((cand.R2 = maximize_disc_radius(&cand, edges->edges, nr)) > best.R2)
                best = cand;

            cand.x = disc->x - sx;
            cand.y = disc->y - sy;
            if ((cand.R2 = maximize_disc_radius(&cand, edges->edges, nr)) > best.R2)
                best = cand;

            cand.x = disc->x + sy;
            cand.y = disc->y - sx;
            if ((cand.R2 = maximize_disc_radius(&cand, edges->edges, nr)) > best.R2)
                best = cand;
        }

        if (best.R2 > disc->R2) {
            improvement = sqrt(best.R2) - sqrt(disc->R2);
            *disc = best;
            /* This scales up *each* successive improvement after 3 so eps can grow very quickly. */
            if (nsuccessiveimprovements++ > 2)
                eps *= 1.5;
        }
        else {
            eps *= 0.5;
            nsuccessiveimprovements = 0;
        }
    } while (eps > 1e-4 || improvement > 1e-4);
}

static void
calculate_inscribed_discs_contiguous(GwyNield *grains,
                                     gdouble dx, gdouble dy,
                                     gdouble field_xoff, gdouble field_yoff,
                                     const gdouble *xcentre_px, const gdouble *ycentre_px,
                                     const guint *sizes, const gint *bbox,
                                     gdouble *discx, gdouble *discy, gdouble *radii)
{
    guint ngrains = gwy_nield_max(grains);
    GwyNield *upgrain = gwy_nield_new(1, 1);
    guint allocated_size = 1;
    GArray *candidates = g_array_new(FALSE, FALSE, sizeof(GrainDisc));
    EdgeQueue edges = { 0, 0, NULL };
    GrainDisc *cand;

    PixelQueue inqueue, outqueue;
    gwy_clear1(inqueue);
    gwy_clear1(outqueue);

    /*
     * For each grain:
     *    Extract it, find all boundary pixels.
     *    Use (octagnoal) erosion to find disc centre candidate(s).
     *    For each candidate:
     *       Find maximum disc that fits with this centre.
     *       By expanding/moving try to find a larger disc until we cannot improve it.
     */
    for (gint gno = 1; gno <= ngrains; gno++) {
        guint w = bbox[4*gno + 2], h = bbox[4*gno + 3];
        gdouble xoff = dx*bbox[4*gno] + field_xoff, yoff = dy*bbox[4*gno + 1] + field_yoff;

        if (!sizes[gno])
            continue;

        /* If the grain is rectangular, calculate the disc directly. Large rectangular grains are rare but the
         * point is to catch grains with width or height of 1 here. */
        if (sizes[gno] == w*h) {
            gdouble sizex = 0.5*w*dx;
            gdouble sizey = 0.5*h*dy;
            if (radii)
                radii[gno] = (1.0 - 1e-14)*MIN(sizex, sizey);
            if (discx)
                discx[gno] = sizex + xoff;
            if (discy)
                discy[gno] = sizey + yoff;
            continue;
        }

        /* Upsampling twice combined with octagonal48 erosion has the nice property that we get candidate pixels in
         * places such as corners or junctions of one-pixel thin lines. However, this only works nicely for 1:1
         * pixels. For other aspect ratios, even reasonable, all kinds of weird things can happen. */
        extract_upsampled_square_pixel_grain(grains, gno, bbox + 4*gno, upgrain, &allocated_size, dx, dy);
        /* Size of upsampled pixel in real coodinates. */
        gdouble udx = w*dx/upgrain->xres;
        gdouble udy = h*dy/upgrain->yres;
        /* Grain centre in real coordinates within the bbox. */
        gdouble centrex = (xcentre_px[gno] + 0.5)*dx;
        gdouble centrey = (ycentre_px[gno] + 0.5)*dy;

        guint dist = _gwy_simple_dist_trans(upgrain, NULL, TRUE, TRUE, GWY_DISTANCE_TRANSFORM_OCTAGONAL48,
                                            &inqueue, &outqueue);

        if (dist % 2 == 0)
            GWY_SWAP(PixelQueue, inqueue, outqueue);

#if 0
        for (gint i = 0; i < upgrain->yres; i++) {
            gint uxres = upgrain->xres;
            for (gint j = 0; j < uxres; j++) {
                if (!upgrain->priv->data[i*uxres + j])
                    g_printerr("..");
                else
                    g_printerr("%02u", upgrain->priv->data[i*uxres + j]);
                g_printerr("%c", j == uxres-1 ? '\n' : ' ');
            }
        }
        g_printerr("when finished queue lengths: %u %u (dist = %u)\n", inqueue.len, outqueue.len, dist);
#endif

        /* Now inqueue is always non-empty and contains max-distance pixels of the upscaled grain. */
        find_disc_centre_candidates(candidates, &inqueue, upgrain, udx, udy, centrex, centrey);
        if (!candidates->len) {
            g_warning("No inscribed disc candidates.\n");
            if (radii)
                radii[gno] = 0.0;
            if (discx)
                discx[gno] = 0.5*w*dx + xoff;
            if (discy)
                discy[gno] = 0.5*h*dy + yoff;
            continue;
        }
        // This requires contiguous grains. For adversarial grain configurations (concentric rings) we can pay up to
        // O(Npixels²) this way. It would be more worst-case efficient to upsample the entire grain nield and find all
        // edges at once. We would also need an extra distance transform maxima location step which we now get for
        // free because it is just the last queue obtained from _gwy_simple_dist_trans().
        find_all_edges(&edges, grains, gno, bbox + 4*gno, dx, dy);

        /* Try a few first candidates for the inscribed disc centre. */
        guint ncand = MIN(15, candidates->len);
        for (guint i = 0; i < ncand; i++) {
            cand = &g_array_index(candidates, GrainDisc, i);
            improve_inscribed_disc(cand, &edges, dist);
        }

        cand = &g_array_index(candidates, GrainDisc, 0);
        for (guint i = 1; i < ncand; i++) {
            if (g_array_index(candidates, GrainDisc, i).R2 > cand->R2)
                cand = &g_array_index(candidates, GrainDisc, i);
        }

        // FIXME: There used to be multiplication by qgeom, but we removed the scaling.
        if (radii)
            radii[gno] = sqrt(cand->R2);
        if (discx)
            discx[gno] = cand->x + xoff;
        if (discy)
            discy[gno] = cand->y + yoff;
    }

    g_object_unref(upgrain);
    g_free(inqueue.points);
    g_free(outqueue.points);
    g_free(edges.edges);
    g_array_free(candidates, TRUE);
}

static void
calculate_inscribed_discs(GwyNield *nield,
                          GwyField *field,
                          const gdouble *xcentre_px, const gdouble *ycentre_px,
                          const guint *sizes, const gint *bbox,
                          gdouble *discx, gdouble *discy, gdouble *radii)
{
    gdouble dx = gwy_field_get_dx(field), dy = gwy_field_get_dy(field);
    gdouble xoff = field->xoff, yoff = field->yoff;
    gboolean is_contiguous = NCTEST(nield, IS_NUM_CONTIGUOUS);

    if (is_contiguous) {
        calculate_inscribed_discs_contiguous(nield, dx, dy, xoff, yoff,
                                             xcentre_px, ycentre_px, sizes, bbox,
                                             discx, discy, radii);
        return;
    }

    guint ngrains = gwy_nield_max(nield);
    GPtrArray *buffers = g_ptr_array_new_full(0, g_free);
    GwyNield *split = gwy_nield_copy(nield);
    // NB: Getting split_ngrains = ngrains can also mean the original grain numbering have holes. Cannot collapse back
    // to the simple case just based on the counts.
    guint split_ngrains = gwy_nield_split_noncontiguous(split);
    guint *grain_number_map = g_new(guint, split_ngrains+1);
    gsize n = (gsize)split->xres * (gsize)split->yres;
    const gint *split_grains = split->priv->data, *grains = nield->priv->data;
    // We can assign to the same target many times, but always the same value. Except for the zeroth, which we do not
    // care about.
    for (gsize i = 0; i < n; i++)
        grain_number_map[MAX(split_grains[i], 0)] = grains[i];

    gdouble *split_xcentre_px = ensure_buffer(buffers, 0, NULL, split_ngrains, 2, 0.0);
    gdouble *split_ycentre_px = split_xcentre_px + split_ngrains+1;
    const guint *split_sizes = _gwy_nield_ensure_sizes(split);
    const gint *split_bbox = _gwy_nield_ensure_bboxes(split);
    gdouble *split_discx = discx ? ensure_buffer(buffers, 0, NULL, split_ngrains, 1, 0.0) : NULL;
    gdouble *split_discy = discy ? ensure_buffer(buffers, 0, NULL, split_ngrains, 1, 0.0) : NULL;
    // We must always keep track of the radius because we select the largest inscribed disc from the split subgrains.
    // If the caller does not request radii, create a temporary buffer for the unsplit radii.
    gdouble *split_radii = ensure_buffer(buffers, 0, NULL, split_ngrains, 1, 0.0);
    gdouble *unsplit_quantities[1] = { radii };
    gdouble *max_radii = ensure_buffer(buffers, 0, unsplit_quantities, ngrains, 1, 0.0);

    calculate_grain_aux(split, field, sizes, NULL, NULL, split_xcentre_px, split_ycentre_px, NULL, NULL, NULL);
    calculate_inscribed_discs_contiguous(split, dx, dy, xoff, yoff,
                                         split_xcentre_px, split_ycentre_px, split_sizes, split_bbox,
                                         split_discx, split_discy, split_radii);

    // Merge back. For mean edge distance, reconstruct the total mean distance and area, so that the final computation
    // is done the same.
    for (guint i = 1; i <= split_ngrains; i++) {
        guint iorig = grain_number_map[i];
        g_assert(iorig);
        if (max_radii[iorig] < split_radii[i]) {
            max_radii[iorig] = split_radii[i];
            if (discx)
                discx[iorig] = split_discx[i];
            if (discy)
                discy[iorig] = split_discy[i];
        }
    }

    g_ptr_array_free(buffers, TRUE);
    g_free(grain_number_map);
    g_object_unref(split);
}

static GwyNield*
upsample_keeping_gran_numbers(GwyNield *nield, gdouble dx, gdouble dy)
{
    GwyNield *upsampled;
    gint xres = nield->xres, yres = nield->yres;
    gint w2 = 2*xres, h2 = 2*yres;
    gint w2ex = GWY_ROUND(w2*fmin(dx/dy, 5.0)), h2ex = GWY_ROUND(h2*fmin(dy/dx, 5.0));
    const gint *src = nield->priv->data;

    /* Upsample sqaure (or nearly-square) pixels 2× isotroptically. */
    if (w2ex == w2 && h2ex == h2) {
        upsampled = gwy_nield_new(w2, h2);
        guint *updata = upsampled->priv->data;

        for (gint i = 0; i < yres; i++) {
            const gint *srow = src + i*xres;
            gint *uprow = updata + (2*i)*w2;
            for (gint j = 0; j < xres; j++)
                uprow[2*j] = uprow[2*j + 1] = srow[j];
            gwy_assign(uprow + w2, uprow, w2);
        }
    }
    else if (dy < dx) {
        /* Horizontal upsampling > 2× (uneven), vertical 2×, precalculate index map to use in each row. */
        upsampled = gwy_nield_new(w2ex, h2);
        guint *updata = upsampled->priv->data;

        gint *indices = g_new(gint, w2ex);
        for (gint j = 0; j < w2ex; j++) {
            gint jj = (gint)floor((gdouble)j/w2ex*xres);
            indices[j] = CLAMP(jj, 0, (gint)xres-1);
        }
        for (gint i = 0; i < yres; i++) {
            const gint *srow = src + i*xres;
            gint *uprow = updata + (2*i)*w2ex;
            for (gint j = 0; j < w2ex; j++)
                uprow[j] = srow[indices[j]];
            gwy_assign(uprow + w2ex, uprow, w2ex);
        }
        g_free(indices);
    }
    else {
        /* Vertical upsampling > 2×, rows are uniformly 2× scaled, just different numbers of them */
        upsampled = gwy_nield_new(w2, h2ex);
        guint *updata = upsampled->priv->data;
        gint iiprev = -1;

        for (gint i = 0; i < h2ex; i++) {
            gint *uprow = updata + i*w2;
            gint ii = (gint)floor((gdouble)i/h2ex*yres);
            ii = CLAMP(ii, 0, (gint)yres-1);
            const gint *srow = src + ii*xres;
            if (ii == iiprev)
                gwy_assign(uprow, uprow - w2, w2);
            else {
                for (gint j = 0; j < xres; j++)
                    uprow[2*j] = uprow[2*j + 1] = srow[j];
            }
            iiprev = ii;
        }
    }

    return upsampled;
}

static void
calculate_mean_edge_distance(GwyNield *nield, gdouble dx, gdouble dy,
                             gdouble *meanedist, gdouble *shapeno)
{
    g_assert(meanedist);

    gdouble xres = nield->xres, yres = nield->yres;
    GwyNield *upsampled = upsample_keeping_gran_numbers(nield, dx, dy);
    gint uxres = upsampled->xres, uyres = upsampled->yres;
    // Favour slightly the smaller sampling step for the ‘effective’ size as the distance transform is isotropic.
    gdouble udx = xres*dx/uxres, udy = yres*dy/uyres, udeff = 2*udx*udy/(udx + udy);

    GwyField *distances = gwy_field_new(uxres, uyres, dx*xres, dy*yres, FALSE);
    gwy_nield_distance_transform(upsampled, distances, GWY_DISTANCE_TRANSFORM_EUCLIDEAN, FALSE, TRUE);
    guint ngrains = gwy_nield_max(nield);
    guint *sizes = g_new0(guint, ngrains+1);

    gsize n = (gsize)uxres * (gsize)uyres;
    const gdouble *d = distances->priv->data;
    const gint *g = upsampled->priv->data;
    for (gsize k = 0; k < n; k++) {
        gint gno = g[k];
        if (gno > 0) {
            meanedist[gno] += d[k];
            sizes[gno]++;
        }
    }
    g_object_unref(distances);
    g_object_unref(upsampled);

    for (guint gno = 1; gno <= ngrains; gno++) {
        if (sizes[gno]) {
            gdouble ed = (meanedist[gno]/sizes[gno] - 0.4) * udeff;
            meanedist[gno] = ed;
            if (shapeno) {
                gdouble A = sizes[gno]*udx*udy;
                shapeno[gno] = A/(ed*ed)/(9.0*G_PI);
            }
        }
    }

    g_free(sizes);
}

/**
 * gwy_nield_grain_quantity:
 * @nield: A number field.
 * @field: Data field used for height data. For some quantities its values are not used, but its dimensions determine
 *         the real dimensions of a pixel.
 * @quantity: The quantity to calculate.
 * @maxgno: (out) (nullable):
 *          Location to store the maximum grain number. Possibly %NULL if you know the maximum grain number.
 *
 * Calculates a single characteristics of grains in a number field.
 *
 * Some quantities are currently calculated incorrectly for non-contiguous grains. In particular those related to the
 * grain boundary. Others may be technically correct for non-contiguous grains, but not really meaningful.
 *
 * This function can be inefficient. When multiple related quantities are evaluated, gwy_nield_grain_quantities()
 * avoids doing the same work twice. See also gwy_nield_grain_values() for the evaluation of non-builtin grain
 * values.
 *
 * The zeroth item in the returned array does not correspond to any grain and does not generally contain any useful
 * information. Although the function fills some placeholder values for empty grains, it is recommended to apply
 * gwy_nield_compactify().
 *
 * Returns: (transfer full):
 *          A newly allocated array of size @maxgno+1 with grain values.
 **/
gdouble*
gwy_nield_grain_quantity(GwyNield *nield,
                         GwyField *field,
                         GwyGrainQuantity quantity,
                         gint *maxgno)
{
    g_return_val_if_fail(GWY_IS_NIELD(nield), NULL);
    gint ngrains = gwy_nield_max(nield);
    gdouble *values = g_new(gdouble, ngrains+1);
    gwy_nield_grain_quantities(nield, field, &quantity, 1, &values);
    if (maxgno)
        *maxgno = ngrains;
    return values;
}

static void
divide_by_size(gdouble *values, const gint *sizes, gint ngrains)
{
    for (gint gno = 0; gno <= ngrains; gno++) {
        if (sizes[gno] > 0)
            values[gno] /= sizes[gno];
    }
}

/* Note all coordinates are pixel-wise, not real.  For linear and quadratic, the origin is always the grain centre. */
static void
calculate_grain_aux(GwyNield *nield,
                    GwyField *field,
                    const guint *sizes,
                    gdouble *min, gdouble *max,
                    gdouble *xvalue, gdouble *yvalue, gdouble *zvalue,
                    gdouble *linear, gdouble *quadratic)
{
    guint xres = nield->xres, yres = nield->yres;
    gsize nn = (gsize)xres * (gsize)yres;
    const gint *grains = nield->priv->data;
    const gdouble *d = field->priv->data;
    gint ngrains = gwy_nield_max(nield);

    /* TODO: Individual loops may not be easy to paralelise, but we can execute the auxiliary calculations in
     * parallel (baring some dependencies). */
    if (min) {
        for (gsize k = 0; k < nn; k++) {
            if (grains[k] > 0)
                min[grains[k]] = fmin(d[k], min[grains[k]]);
        }
    }
    if (max) {
        for (gsize k = 0; k < nn; k++) {
            if (grains[k] > 0)
                max[grains[k]] = fmax(d[k], max[grains[k]]);
        }
    }
    if (zvalue) {
        g_assert(sizes);
        for (gsize k = 0; k < nn; k++) {
            if (grains[k] > 0)
                zvalue[grains[k]] += d[k];
        }
        divide_by_size(zvalue, sizes, ngrains);
    }
    if (xvalue) {
        g_assert(sizes);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j;
                if (grains[k] > 0)
                    xvalue[grains[k]] += j;
            }
        }
        divide_by_size(xvalue, sizes, ngrains);
    }
    if (yvalue) {
        g_assert(sizes);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j;
                if (grains[k] > 0)
                    yvalue[grains[k]] += i;
            }
        }
        divide_by_size(yvalue, sizes, ngrains);
    }
    if (linear) {
        g_assert(xvalue && yvalue);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j, gno;
                if ((gno = grains[k]) > 0) {
                    gdouble x = j - xvalue[gno];
                    gdouble y = i - yvalue[gno];
                    gdouble z = d[k];
                    gdouble *t = linear + 5*gno;

                    t[0] += x*x;
                    t[1] += x*y;
                    t[2] += y*y;
                    t[3] += x*z;
                    t[4] += y*z;
                }
            }
        }
    }
    if (quadratic) {
        g_assert(xvalue && yvalue);
        for (gint i = 0; i < yres; i++) {
            for (gint j = 0; j < xres; j++) {
                gint k = i*xres + j;
                if (grains[k] > 0) {
                    gdouble x = j - xvalue[grains[k]];
                    gdouble y = i - yvalue[grains[k]];
                    gdouble xx = x*x, yy = y*y, xy = x*y;
                    gdouble z = d[k];
                    gdouble *t = quadratic + 12*grains[k];

                    t[0] += xx*x;
                    t[1] += xx*y;
                    t[2] += x*yy;
                    t[3] += y*yy;
                    t[4] += xx*xx;
                    t[5] += xx*xy;
                    t[6] += xx*yy;
                    t[7] += xy*yy;
                    t[8] += yy*yy;
                    t[9] += xx*z;
                    t[10] += xy*z;
                    t[11] += yy*z;
                }
            }
        }
    }
}

static void
calculate_rms(const gint *grains, gint ngrains, const gdouble *z, gsize n,
              const guint *sizes, const gdouble *zmean,
              gdouble *rms)
{
    for (gsize k = 0; k < n; k++) {
        gint gno = grains[k];
        if (gno > 0)
            rms[gno] += (z[k] - zmean[gno])*(z[k] - zmean[gno]);
    }
    for (gint gno = 0; gno <= ngrains; gno++) {
        if (sizes[gno] > 1)
            rms[gno] = sqrt(rms[gno]/(sizes[gno] - 1));
    }
}

static void
integrate_grain_volume0(const gdouble *d, const gint *grains,
                        gint xres, gint yres,
                        gdouble *volume, guint ngrains,
                        gdouble pixelarea)
{
    gint i, j, gno;

    gwy_clear(volume, ngrains + 1);
    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++) {
            gint ix, ipx, imx, jp, jm;
            gdouble v;

            ix = i*xres;
            if ((gno = grains[ix + j]) <= 0)
                continue;

            imx = (i > 0) ? ix-xres : ix;
            ipx = (i < yres-1) ? ix+xres : ix;
            jm = (j > 0) ? j-1 : j;
            jp = (j < xres-1) ? j+1 : j;

            // Use weights corresponding to GWY_FIELD_VOLUME_DEFAULT.
            v = (484.0*d[ix + j] + 22.0*(d[imx + j] + d[ix + jm] + d[ix + jp] + d[ipx + j])
                 + (d[imx + jm] + d[imx + jp] + d[ipx + jm] + d[ipx + jp]));

            volume[gno] += v;
        }
    }
    for (gno = 0; gno <= ngrains; gno++)
        volume[gno] *= pixelarea/576.0;
}

/* What does this do?
 *
 * We want to integrate grain volume with Laplace background as if each grain existed in isolation, i.e. it was the
 * only marked region.
 *
 * GWY_LAPLACE_ALL_GRAINS is a start. It gives the correct interpolated pixel values. However, the volume computation
 * also uses values from grain exterior (data from a 1-pixel border around the grain have non-zero weight). When
 * grains touch, these values are the result of interpolation. Meaning, they are not the same as if each grain existed
 * in isolation.
 *
 * Therefore, this function exists to apply the correct exterior. We do this by noting that if a grain existed in
 * isolation, the result of the subtraction of Laplacian background would be zero everywhere outside the grain.
 * Therefore, we replace all d[not-inside-the-same-grains] with zeros. In a general case the replacement would require
 * the original data or something, but knowing it is zero makes the replacement quite simple. */
static void
integrate_grain_volume0_in_isolation(const gdouble *d, const gint *grains,
                                     gint xres, gint yres,
                                     gdouble *volume, guint ngrains,
                                     gdouble pixelarea)
{
    gint i, j, gno;

    gwy_clear(volume, ngrains + 1);
    for (i = 0; i < yres; i++) {
        for (j = 0; j < xres; j++) {
            gint ix, ipx, imx, jp, jm;
            gdouble v;

            ix = i*xres;
            if ((gno = grains[ix + j]) <= 0)
                continue;

            imx = (i > 0) ? ix-xres : ix;
            ipx = (i < yres-1) ? ix+xres : ix;
            jm = (j > 0) ? j-1 : j;
            jp = (j < xres-1) ? j+1 : j;

            // Use weights corresponding to GWY_FIELD_VOLUME_DEFAULT.
            v = (484.0*d[ix + j]
                 + 22.0*(d[imx + j]*(grains[imx + j] == gno)
                         + d[ix + jm]*(grains[ix + jm] == gno)
                         + d[ix + jp]*(grains[ix + jp] == gno)
                         + d[ipx + j]*(grains[ipx + j] == gno))
                 + (d[imx + jm]*(grains[imx + jm] == gno)
                    + d[imx + jp]*(grains[imx + jp] == gno)
                    + d[ipx + jm]*(grains[ipx + jm] == gno)
                    + d[ipx + jp]*(grains[ipx + jp] == gno)));

            volume[gno] += v;
        }
    }
    for (gno = 0; gno <= ngrains; gno++)
        volume[gno] *= pixelarea/576.0;
}

/* This returns the signed distance of the median line (in coordinate system rotate to idir direction). */
static gdouble
median_cut_for_direction(guint idir,
                         guint gno, const guint *csizes, const GwyXY *coords,
                         gdouble *rotcoords)
{
    gdouble ca, sa;
    guint k, kfrom, kto;
    guint ranks[2];
    gdouble mm[2];

    if (idir < NDIRECTIONS) {
        ca = shift_directions[2*idir];
        sa = shift_directions[2*idir + 1];
    }
    else {
        sa = -shift_directions[2*(idir - NDIRECTIONS)];
        ca = shift_directions[2*(idir - NDIRECTIONS) + 1];
    }

    kfrom = 4*csizes[gno - 1];
    kto = 4*csizes[gno];
    for (k = kfrom; k < kto; k++)
        rotcoords[k - kfrom] = sa*coords[k].x - ca*coords[k].y;

    /* The length is a multiple of 4, i.e. even.  So we calculate the average of the two median candidates. */
    ranks[0] = (kto - kfrom)/2 - 1;
    ranks[1] = (kto - kfrom)/2;
    gwy_math_kth_ranks(rotcoords, kto - kfrom, ranks, 2, mm);

    return 0.5*(mm[0] + mm[1]);
}

static gdouble
martin_intersection_length(guint idir, gdouble med,
                           gdouble qh, gdouble qv, gdouble qdiag,
                           guint gno, const gint *bbox,
                           const gdouble *xvalue, const gdouble *yvalue,
                           gint xres, const gint *grains)
{
    gint jmin, jmax, imin, imax, i, j, len;
    gdouble ca, sa, xc, yc, deltax, deltay, x, y;

    bbox += 4*gno;
    jmin = bbox[0];
    imin = bbox[1];
    jmax = bbox[0] + bbox[2]-1;
    imax = bbox[1] + bbox[3]-1;

    if (idir < NDIRECTIONS) {
        ca = shift_directions[2*idir];
        sa = shift_directions[2*idir + 1];
    }
    else {
        sa = -shift_directions[2*(idir - NDIRECTIONS)];
        ca = shift_directions[2*(idir - NDIRECTIONS) + 1];
    }

    /* Start from a point on the median line and go along it to either direction until we get outside of the bounding
     * box.  The +0.5 actually belongs to the floor() function but we can do it here.  */
    xc = xvalue[gno] + med*sa/qh + 0.5;
    yc = yvalue[gno] - med*ca/qv + 0.5;
    /* The step in real coordinates is qdiag. */
    deltax = 0.061803398875*qdiag * ca/qh;
    deltay = 0.061803398875*qdiag * sa/qv;
    len = 0;

    x = xc;
    y = yc;
    while (TRUE) {
        j = (gint)floor(x);
        if (j < jmin || j > jmax)
            break;

        i = (gint)floor(y);
        if (i < imin || i > imax)
            break;

        if (grains[i*xres + j] == gno)
            len++;

        x += deltax;
        y += deltay;
    }

    x = xc - deltax;
    y = yc - deltay;
    while (TRUE) {
        j = (gint)floor(x);
        if (j < jmin || j > jmax)
            break;

        i = (gint)floor(y);
        if (i < imin || i > imax)
            break;

        if (grains[i*xres + j] == gno)
            len++;

        x -= deltax;
        y -= deltay;
    }

    return len*0.061803398875*qdiag;
}

static gdouble
refine_diameter_direction(const gdouble *diams, guint k, gboolean is_max)
{
    gint sign = is_max ? 1 : -1;
    gdouble mvals[3];
    gdouble x, phi;

    mvals[0] = sign*diams[(k + 2*NDIRECTIONS-1) % (2*NDIRECTIONS)];
    mvals[1] = sign*diams[k];
    mvals[2] = sign*diams[(k + 1) % (2*NDIRECTIONS)];
    gwy_math_refine_maximum_1d(mvals, &x);
    phi = gwy_canonicalize_angle(G_PI - 0.5*G_PI*(k + x)/NDIRECTIONS, FALSE, FALSE);

    return phi;
}

static inline guint
start_new_grain_corner(gint *grains, guint *wherebits, guint ndifferent, guint bit, gint gno)
{
    grains[ndifferent] = gno;
    wherebits[ndifferent] = bit;
    return ndifferent+1;
}

// Calculate contributions to grain boundary lengths from a grain conrer (four quarters) with possibly touching
// grains.
static void
add_to_boundary_lengths(gint gul, gint gur, gint glr, gint gll,
                        const gdouble *length_contributions, gdouble *lengths)
{
    gint ndifferent = 0, grains[4];
    guint wherebits[4] = { 0, 0, 0, 0 };

    if (gul > 0)
        ndifferent = start_new_grain_corner(grains, wherebits, ndifferent, 1, gul);
    if (gur > 0) {
        if (ndifferent && gur == grains[0])
            wherebits[0] |= 2;
        else
            ndifferent = start_new_grain_corner(grains, wherebits, ndifferent, 2, gur);
    }
    if (glr > 0) {
        if (ndifferent == 2 && glr == grains[1])
            wherebits[1] |= 4;
        else if (ndifferent && glr == grains[0])
            wherebits[0] |= 4;
        else
            ndifferent = start_new_grain_corner(grains, wherebits, ndifferent, 4, glr);
    }
    if (gll > 0) {
        if (ndifferent == 3 && gll == grains[2])
            wherebits[2] |= 8;
        else if (ndifferent >= 2 && gll == grains[1])
            wherebits[1] |= 8;
        else if (ndifferent && gll == grains[0])
            wherebits[0] |= 8;
        else
            ndifferent = start_new_grain_corner(grains, wherebits, ndifferent, 8, gll);
    }

    for (guint i = 0; i < ndifferent; i++)
        lengths[grains[i]] += length_contributions[wherebits[i]];
}

static void
calculate_flat_boundary_lengths(const gint *grains, gint xres, gint yres,
                                gdouble dx, gdouble dy, gdouble dh,
                                gdouble *lengths)
{
    // Which combination of corners (as given by wherebits) gives what contribution to the boundary length.
    const double lcontribs[16] = {
        0, dh/2, dh/2, dx, dh/2, dh, dy, dh/2, dh/2, dy, dh, dh/2, dx, dh/2, dh/2, 0
    };
    gint k;

    k = 0;
    add_to_boundary_lengths(0, 0, grains[k], 0, lcontribs, lengths);
    for (gint j = 0; j < xres-1; j++, k++)
        add_to_boundary_lengths(0, 0, grains[k+1], grains[k], lcontribs, lengths);
    add_to_boundary_lengths(0, 0, 0, grains[k], lcontribs, lengths);

    for (gint i = 0; i < yres-1; i++) {
        k = i*xres;
        add_to_boundary_lengths(0, grains[k], grains[k+xres], 0, lcontribs, lengths);
        for (gint j = 0; j < xres-1; j++, k++)
            add_to_boundary_lengths(grains[k], grains[k+1], grains[k+xres+1], grains[k+xres], lcontribs, lengths);
        add_to_boundary_lengths(grains[k], 0, 0, grains[k+xres], lcontribs, lengths);
    }

    k = (yres-1)*xres;
    add_to_boundary_lengths(0, grains[k], 0, 0, lcontribs, lengths);
    for (gint j = 0; j < xres-1; j++, k++)
        add_to_boundary_lengths(0, 0, grains[k+1], grains[k], lcontribs, lengths);
    add_to_boundary_lengths(grains[k], 0, 0, 0, lcontribs, lengths);
}

static void
calculate_mean_radii(const gint *grains, guint ngrains, gint xres, gint yres,
                     gdouble dx, gdouble dy,
                     const gdouble *xcentre_px, const gdouble *ycentre_px,
                     gdouble *radii)
{
    guint *blen = g_new0(guint, ngrains + 1);

    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint k = i*xres + j;
            gint gno;

            if ((gno = grains[k]) <= 0)
                continue;

            gdouble xc = xcentre_px[gno];
            gdouble yc = ycentre_px[gno];
            if (!i || grains[k - xres] <= 0) {
                radii[gno] += hypot(dx*(j+0.5 - xc), dy*(i - yc));
                radii[gno] += hypot(dx*(j+1 - xc), dy*(i - yc));
                blen[gno] += 2;
            }
            if (!j || grains[k-1] <= 0) {
                radii[gno] += hypot(dx*(j - xc), dy*(i - yc));
                radii[gno] += hypot(dx*(j - xc), dy*(i+0.5 - yc));
                blen[gno] += 2;
            }
            if (j == xres-1 || grains[k+1] <= 0) {
                radii[gno] += hypot(dx*(j+1 - xc), dy*(i+0.5 - yc));
                radii[gno] += hypot(dx*(j+1 - xc), dy*(i+1 - yc));
                blen[gno] += 2;
            }
            if (i == yres-1 || grains[k + xres] <= 0) {
                radii[gno] += hypot(dx*(j - xc), dy*(i+1 - yc));
                radii[gno] += hypot(dx*(j+0.5 - xc), dy*(i+1 - yc));
                blen[gno] += 2;
            }
        }
    }

    divide_by_size(radii, blen, ngrains);
    g_free(blen);
}

static void
calculate_half_height_area(const gint *grains, guint ngrains, const gdouble *z, gsize n,
                           gdouble dA,
                           const gdouble *min, const gdouble *max,
                           gdouble *areas)
{
    /* Find the grain half-heights, i.e. (z_min + z_max)/2, first */
    gdouble *zhalf = g_new(gdouble, ngrains + 1);
    for (gint gno = 0; gno <= ngrains; gno++) {
        if (min[gno] >= max[gno])
            zhalf[gno] = G_MAXDOUBLE;
        else
            zhalf[gno] = (min[gno] + max[gno])/2.0;
    }
    /* Calculate the area of pixels above the half-heights. The description says strictly above, so produce zero
     * reliably for flat grains. */
    guint *zhsizes = g_new0(gint, ngrains + 1);
    for (gsize k = 0; k < n; k++) {
        gint gno = grains[k];
        if (gno > 0 && z[k] > zhalf[gno])
            zhsizes[gno]++;
    }
    for (gint gno = 0; gno <= ngrains; gno++)
        areas[gno] = dA*zhsizes[gno];

    g_free(zhalf);
    g_free(zhsizes);
}

static void
calculate_grain_medians(const gint *grains, guint ngrains, const gdouble *z, gsize n,
                        const guint *sizes,
                        gdouble *medians)
{
    guint *csizes = g_new0(guint, ngrains + 1);
    guint *pos = g_new0(guint, ngrains + 1);

    /* Find cumulative sizes (we care only about grains, ignore the outside-grains area) */
    csizes[0] = 0;
    csizes[1] = sizes[1];
    for (gint gno = 2; gno <= ngrains; gno++)
        csizes[gno] = sizes[gno] + csizes[gno-1];

    gdouble *tmp = g_new(gdouble, csizes[ngrains]);
    /* Find where each grain starts in tmp sorted by grain # */
    for (gint gno = 1; gno <= ngrains; gno++)
        pos[gno] = csizes[gno-1];
    /* Sort values by grain # to tmp */
    for (gsize k = 0; k < n; k++) {
        gint gno;
        if ((gno = grains[k]) > 0) {
            tmp[pos[gno]] = z[k];
            pos[gno]++;
        }
    }
    /* Find medians of each block */
    for (gint gno = 1; gno <= ngrains; gno++) {
        if (csizes[gno] > csizes[gno-1])
            medians[gno] = gwy_math_median(tmp + csizes[gno-1], csizes[gno] - csizes[gno-1]);
        else
            medians[gno] = 0.0;
    }

    g_free(csizes);
    g_free(pos);
    g_free(tmp);
}

static void
calculate_boundary_min_max(const gint *grains, guint ngrains, const gdouble *z, gint xres, gint yres,
                           gdouble *bmin, gdouble *bmax)
{
    if (bmin)
        gwy_math_fill(bmin, ngrains+1, G_MAXDOUBLE);
    if (bmax)
        gwy_math_fill(bmax, ngrains+1, -G_MAXDOUBLE);

    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint gno;

            /* Processing of the none-grain boundary is a waste of time. */
            if ((gno = grains[i*xres + j]) <= 0)
                continue;

            if (i && j && i < yres-1 && j < xres - 1
                && grains[(i - 1)*xres + j] == gno
                && grains[i*xres + j - 1] == gno
                && grains[i*xres + j + 1] == gno
                && grains[(i + 1)*xres + j] == gno)
                continue;

            gdouble v = z[i*xres + j];
            if (bmin && v < bmin[gno])
                bmin[gno] = v;
            if (bmax && v > bmax[gno])
                bmax[gno] = v;
        }
    }
}

/* See rectangular_area_corner() in stats--sum.c for description, this function calculates twice `contribution of one
 * corner', i.e. with only one pixel weight non-zero (the twice is to move multiplications from inner loops) */
static inline gdouble
rectangular_area_quarter_corner(gdouble z1, gdouble z2, gdouble z4, gdouble c,
                                gdouble dx2, gdouble dy2)
{
    return sqrt(1.0 + (z1 - z2)*(z1 - z2)/dx2 + (z1 + z2 - c)*(z1 + z2 - c)/dy2)
           + sqrt(1.0 + (z1 - z4)*(z1 - z4)/dy2 + (z1 + z4 - c)*(z1 + z4 - c)/dx2);
}

static void
calculate_surface_area(const gint *grains, guint ngrains, const gdouble *z, gint xres, gint yres,
                       gdouble dx, gdouble dy,
                       gdouble *areas)
{
    gdouble dx2 = dx*dx, dy2 = dy*dy, dA = dx*dy;

    /* Every contribution is calculated twice -- for each pixel (vertex) participating to a particular triangle */
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint gno, ix = i*xres;
            if ((gno = grains[ix + j]) <= 0)
                continue;

            gint imx = (i > 0) ? ix-xres : ix;
            gint ipx = (i < yres-1) ? ix+xres : ix;
            gint jm = (j > 0) ? j-1 : j;
            gint jp = (j < xres-1) ? j+1 : j;
            gdouble c;

            c = (z[ix + j] + z[ix + jm] + z[imx + jm] + z[imx + j])/2.0;
            areas[gno] += rectangular_area_quarter_corner(z[ix + j], z[ix + jm], z[imx + j], c, dx2, dy2);

            c = (z[ix + j] + z[ix + jp] + z[imx + jp] + z[imx + j])/2.0;
            areas[gno] += rectangular_area_quarter_corner(z[ix + j], z[ix + jp], z[imx + j], c, dx2, dy2);

            c = (z[ix + j] + z[ix + jm] + z[ipx + jm] + z[ipx + j])/2.0;
            areas[gno] += rectangular_area_quarter_corner(z[ix + j], z[ix + jm], z[ipx + j], c, dx2, dy2);

            c = (z[ix + j] + z[ix + jp] + z[ipx + jp] + z[ipx + j])/2.0;
            areas[gno] += rectangular_area_quarter_corner(z[ix + j], z[ix + jp], z[ipx + j], c, dx2, dy2);
        }
    }
    for (gint gno = 0; gno <= ngrains; gno++)
        areas[gno] *= dA/8.0;
}

// See A-guide-to-particle-size-and-shape-parameters-by-image-analysis.pdf
static void
calculate_martin_diameter(const gint *grains, guint ngrains, gint xres, gint yres,
                          gdouble dx, gdouble dy,
                          const guint *sizes, const gint *bboxes,
                          const gdouble *xcentre, const gdouble *ycentre,
                          gdouble *mmin, gdouble *mmax, gdouble *phimin, gdouble *phimax)
{
    /*
     * For each grain:
     *    Extract all coordinates, real and grain-centered.  Extract four points from each pixel (this gives good
     *    median lines even for small L-shaped grains).
     *
     *    For each selected direction:
     *      Rotate coordinates, find the ortohognal median line (using the ortogonal coordinate).
     *      Go along supersampled median line, count how many times we hit the grain (again, must use real-space
     *      angles).
     *
     *    Find minimum and/or maximum.
     */
    guint *csizes = g_new0(guint, ngrains + 1);
    guint maxsize;
    csizes[0] = 0;
    csizes[1] = maxsize = sizes[1];
    for (gint gno = 2; gno <= ngrains; gno++) {
        csizes[gno] = sizes[gno] + csizes[gno-1];
        if (sizes[gno] > maxsize)
            maxsize = sizes[gno];
    }

    guint *pos = g_new0(guint, ngrains + 1);
    for (gint gno = 1; gno <= ngrains; gno++)
        pos[gno] = 4*csizes[gno-1];

    /* Extract coordinates */
    GwyXY *coords = g_new(GwyXY, 4*csizes[ngrains]);
    for (gint i = 0; i < yres; i++) {
        for (gint j = 0; j < xres; j++) {
            gint gno, k = i*xres + j;
            if ((gno = grains[k]) > 0) {
                gdouble x = dx*(j - xcentre[gno]);
                gdouble y = dy*(i - ycentre[gno]);
                gint t = pos[gno];
                coords[t].x = x - 0.25*dx;
                coords[t].y = y - 0.25*dy;
                t++;
                coords[t].x = x + 0.25*dx;
                coords[t].y = y - 0.25*dy;
                t++;
                coords[t].x = x - 0.25*dx;
                coords[t].y = y + 0.25*dy;
                t++;
                coords[t].x = x + 0.25*dx;
                coords[t].y = y + 0.25*dy;
                pos[gno] = t+1;
            }
        }
    }
    g_free(pos);

    /* Find median lines by direction of each block */
    gdouble *rotcoords = g_new(gdouble, 4*maxsize);
    gdouble *diams = g_new(gdouble, 2*NDIRECTIONS);
    gdouble qdiag = hypot(dx, dy);
    for (gint gno = 1; gno <= ngrains; gno++) {
        if (!sizes[gno])
            continue;

        for (gint t = 0; t < 2*NDIRECTIONS; t++) {
            gdouble med = median_cut_for_direction(t, gno, csizes, coords, rotcoords);
            diams[t] = martin_intersection_length(t, med, dx, dy, qdiag, gno, bboxes, xcentre, ycentre, xres, grains);
        }

        /* Find the minima/maxima. */
        if (mmin || phimin) {
            gint k = 0;
            for (gint t = 1; t < 2*NDIRECTIONS; t++) {
                if (diams[t] < diams[k])
                    k = t;
            }
            if (mmin)
                mmin[gno] = diams[k];
            if (phimin)
                phimin[gno] = refine_diameter_direction(diams, k, FALSE);
        }
        if (mmax || phimax) {
            gint k = 0;
            for (gint t = 1; t < 2*NDIRECTIONS; t++) {
                if (diams[t] > diams[k])
                    k = t;
            }
            if (mmax)
                mmax[gno] = diams[k];
            if (phimax)
                phimax[gno] = refine_diameter_direction(diams, k, TRUE);
        }
    }

    /* Finalize */
    g_free(csizes);
    g_free(coords);
    g_free(rotcoords);
    g_free(diams);
}

static void
calculate_equiv_ellipse(guint ngrains,
                        gdouble dx, gdouble dy,
                        const guint *sizes,
                        const gdouble *linear,
                        gdouble *amaj, gdouble *amin, gdouble *phi)
{
    gdouble dx2 = dx*dx, dy2 = dy*dy, dA = dx*dy;

    for (gint gno = 1; gno <= ngrains; gno++) {
        guint n = sizes[gno];
        if (!n)
            continue;

        const gdouble *lin = linear + 5*gno;
        // The extra constant 1/12 comes from actually integrating over the pixel area instead of just summing
        // coordinates of pixel centres (it does not matter for (bi)linear terms, but appears in higher powers).
        // It automatically and correctly makes the two moments non-zero even for single-pixel grains.
        gdouble Jxx = dx2*(lin[0] + n/12.0)*dA;
        gdouble Jxy = dA*lin[1]*dA;
        gdouble Jyy = dy2*(lin[2] + n/12.0)*dA;

        if (phi) {
            gdouble Jeps = 1e-9*MAX(Jxx, Jyy);

            if (fabs(Jxx - Jyy) > Jeps || fabs(Jxy) > Jeps)
                phi[gno] = 0.5*atan2(-2.0*Jxy, Jxx - Jyy);
            else
                phi[gno] = 0.0;
        }

        if (amaj || amin) {
            gdouble u = Jxx + Jyy, v = hypot(2.0*Jxy, Jxx - Jyy), w = sqrt(G_PI*sqrt(Jxx*Jyy - Jxy*Jxy));

            if (amaj)
                amaj[gno] = sqrt((u + v)/w);
            if (amin)
                amin[gno] = sqrt((u - v)/w);
        }
    }
}

static void
calculate_slope(guint ngrains,
                gdouble dx, gdouble dy,
                const guint *sizes,
                const gdouble *linear,
                gdouble *thetas, gdouble *phis)
{
    for (gint gno = 1; gno <= ngrains; gno++) {
        if (sizes[gno] < 2)
            continue;

        const gdouble *lin = linear + 5*gno;
        gdouble bx, by;
        // The function returns the rank, but we already exclude empty and single-pixel grains, so we accept
        // anything else it calculates as the slope.
        gwy_math_plane_coefficients(lin[0], lin[1], lin[2], lin[3], lin[4], sizes[gno], &bx, &by);
        bx /= dx;
        by /= dy;

        if (thetas)
            thetas[gno] = atan(hypot(bx, by));
        if (phis)
            phis[gno] = atan2(by, -bx);
    }
}

static void
calculate_curvature(guint ngrains,
                    gdouble dx, gdouble dy, gdouble xoff, gdouble yoff,
                    const guint *sizes,
                    const gdouble *xcentre, const gdouble *ycentre, const gdouble *zmean,
                    const gdouble *linear, const gdouble *quadratic,
                    gdouble *xapex, gdouble *yapex, gdouble *zapex,
                    gdouble *kappa1, gdouble *kappa2,
                    gdouble *phi1, gdouble *phi2)
{
    gdouble mx = sqrt(dx/dy), my = sqrt(dy/dx);
    gdouble dgeom = sqrt(dx*dy);

    for (gint gno = 1; gno <= ngrains; gno++) {
        guint n = sizes[gno];
        if (!n)
            continue;

        /* a:
         *  0 [<1>
         *  1  <x>   <x²>
         *  3  <y>   <xy>   <y²>
         *  6  <x²>  <x³>   <x²y>  <x⁴>
         * 10  <xy>  <x²y>  <xy²>  <x³y>   <x²y²>
         * 15  <y²>  <xy²>  <y³>   <x²y²>  <xy³>   <y⁴>]
         *
         * b: [<z>  <xz>  <yz>  <x²z>  <xyz>  <y²z>]
         */
        const gdouble *lin = linear + 5*gno, *quad = quadratic + 12*gno;
        gdouble a[21], b[6];

        if (n >= 6) {
            a[0] = n;
            a[1] = a[3] = 0.0;
            a[2] = a[6] = lin[0];
            a[4] = a[10] = lin[1];
            a[5] = a[15] = lin[2];
            a[7] = quad[0];
            a[8] = a[11] = quad[1];
            a[9] = quad[4];
            a[12] = a[16] = quad[2];
            a[13] = quad[5];
            a[14] = a[18] = quad[6];
            a[17] = quad[3];
            a[19] = quad[7];
            a[20] = quad[8];
            if (gwy_math_choleski_decompose(6, a)) {
                b[0] = n*zmean[gno];
                b[1] = lin[3];
                b[2] = lin[4];
                b[3] = quad[9];
                b[4] = quad[10];
                b[5] = quad[11];
                gwy_math_choleski_solve(6, a, b);
                /* Get pixel aspect ratio right while keeping pixel size around 1. */
                b[1] /= mx;
                b[2] /= my;
                b[3] /= mx*mx;
                b[5] /= my*my;
            }
            else
                n = 0;
        }

        /* Recycle a[] for the curvature parameters. */
        if (n >= 6)
            gwy_math_curvature_at_apex(b, a+0, a+1, a+2, a+3, a+4, a+5, a+6);
        else {
            a[0] = a[1] = a[2] = a[4] = a[5] = 0.0;
            a[3] = G_PI/2.0;
            a[6] = zmean[gno];
        }
        if (kappa1)
            kappa1[gno] = a[0]/(dgeom*dgeom);
        if (kappa2)
            kappa2[gno] = a[1]/(dgeom*dgeom);
        if (phi1)
            phi1[gno] = a[2];
        if (phi2)
            phi2[gno] = a[3];
        if (xapex)
            xapex[gno] = dgeom*a[4] + dx*(xcentre[gno] + 0.5) + xoff;
        if (yapex)
            yapex[gno] = dgeom*a[5] + dy*(ycentre[gno] + 0.5) + yoff;
        if (zapex)
            zapex[gno] = a[6];
    }
}

static void
calculate_convex_hull_quantities(const gint *grains, gint xres, guint ngrains,
                                 gdouble dx, gdouble dy, gdouble xoff, gdouble yoff,
                                 const gint *bboxes, const gint *chull, const guint *cindex,
                                 gdouble *psmin, gdouble *psmax, gdouble *pamin, gdouble *pamax, gdouble *plength,
                                 gdouble *achull,
                                 gdouble *circcr, gdouble *circcx, gdouble *circcy)
{
    gboolean need_circumcircle = (circcr || circcx || circcy);
    gboolean need_polygon = (need_circumcircle || psmin || psmax || pamin || pamax);
    GRand *rng = need_circumcircle ? g_rand_new() : NULL;
    GArray *polygon = need_polygon ? g_array_new(FALSE, FALSE, sizeof(GwyXY)) : NULL;
    gdouble dA = dx*dy;
    GwyXY *xy = NULL;

    for (gint gno = 1; gno <= ngrains; gno++) {
        const gint *bbox = bboxes + 4*gno;
        gint col = bbox[0], row = bbox[1], w = bbox[2], h = bbox[3];
        if (w < 0)
            continue;

        guint bcorners = bbox_grain_corners(grains, xres, gno, bbox);
        gdouble dxmin = G_MAXDOUBLE, dymin = G_MAXDOUBLE;
        gdouble dxmax = 0.0, dymax = 0.0;
        gdouble cxmax = 0.0, cymax = 0.0;
        const gint *vertices = chull + 2*cindex[gno];
        guint nvertices = cindex[gno+1] - cindex[gno];
        // When all four corners of the bounding box are occupied by the grain we never need polygon. So do not bother.
        if (polygon && bcorners != 4*16 + 15) {
            g_array_set_size(polygon, nvertices);
            xy = &g_array_index(polygon, GwyXY, 0);
            for (guint i = 0; i < nvertices; i++) {
                xy[i].x = dx*vertices[2*i];
                xy[i].y = dy*vertices[2*i + 1];
            }
        }

        if (psmin || pamin || plength) {
            // If all four corners of the bounding box are occupied by the grain, the minimum projection is the
            // smaller side of the bounding box.
            if (bcorners == 4*16 + 15) {
                dxmin = dymin = 0.0;
                if (dx*w <= dy*h)
                    dxmin = dx*w;
                else
                    dymin = dy*h;
            }
            else
                grain_minimum_projection(xy, nvertices, &dxmin, &dymin);

            if (psmin)
                psmin[gno] = hypot(dxmin, dymin);
            if (pamin)
                pamin[gno] = gwy_canonicalize_angle(atan2(-dymin, dxmin), FALSE, FALSE);
            if (plength)
                plength[gno] = grain_projection_length(xy, nvertices, -dymin, dxmin)/hypot(dxmin, dymin);
        }
        if (psmax || pamax || need_circumcircle) {
            // If any two opposite corners of the bounding box are occupied by the grain, the maximum projection is
            // the bounding box diagonal.
            if ((bcorners & 5) == 5 || (bcorners & 10) == 10) {
                dxmax = dx*w;
                dymax = (bcorners & 10) == 10 ? -dy*h : dy*h;
                cxmax = dx*(col + 0.5*w);
                cymax = dy*(row + 0.5*h);
            }
            else
                grain_maximum_projection(xy, nvertices, &dxmax, &dymax, &cxmax, &cymax);

            if (psmax)
                psmax[gno] = hypot(dxmax, dymax);
            if (pamax)
                pamax[gno] = gwy_canonicalize_angle(atan2(-dymax, dxmax), FALSE, FALSE);
        }
        if (achull)
            achull[gno] = grain_convex_hull_area(vertices, nvertices, dA);

        // Run the circumcircle last because it reorders the contents of @polygon randomly.
        if (!need_circumcircle)
            continue;

        // If at least three corners of the bounding box are occupied by the grain, the circumcircle of the
        // corresponding triangle (which is then also the circle which has the two farthest points as the diameter)
        // must be the sought circle and we are done.
        GrainDisc circle = { .x = cxmax, .y = cymax, .R2 = 0.25*(dxmax*dxmax + dymax*dymax) };
        if (bcorners/16 < 3) {
            // Otherwise still check the circle which has the two farthest points as the diameter. When it contains
            // all the points it is the sought circle (no smaller circle can then contain the two farthest points).
            gdouble eps = 1e-12*(dx + dy);
            if (!all_points_in_circle(&circle, eps, xy, nvertices))
                circumcircle_welzl(xy, nvertices, &circle, eps, rng);
        }

        if (circcr)
            circcr[gno] = sqrt(circle.R2);
        if (circcx)
            circcx[gno] = circle.x + xoff;
        if (circcy)
            circcy[gno] = circle.y + yoff;
    }

    if (rng)
        g_rand_free(rng);
    GWY_FREE_ARRAY(polygon);
}

/* FIXME: A few may still be broken for non-contiguous grains. */
/**
 * gwy_nield_grain_quantities:
 * @nield: A number field.
 * @field: Data field used for height data. For some quantities its values are not used, but its dimensions determine
 *         the real dimensions of a pixel.
 * @quantities: (array length=nquantities) (transfer none):
 *              Array of @nquantities items that specify the requested #GwyGrainQuantity to put to corresponding items
 *              in @values. Quantities can repeat.
 * @nquantities: The number of requested different grain values.
 * @results: Array of length @nquantities of arrays of length @ngrains+1 of doubles to put the calculated values to.
 *
 * Calculates multiple characteristics of grains in a number field simultaneously.
 *
 * See gwy_nield_grain_quantity() for some discussion. This function can a lot more efficient than repeated
 * gwy_nield_grain_quantity() if several related grain quantities need to be calculated since
 * gwy_nield_grain_values() must do lot of repeated work in such case.
 *
 * Some quantities are meaningful for marked regions of any shape, others really only for comtiguous grains, although
 * the functions always computes something even for non-contiguous grains. Grain boundaries and borders are lines
 * separating a grain from anything that is not this grain, whether it is the unmarked area or a different grain (when
 * grains are touching).
 **/
void
gwy_nield_grain_quantities(GwyNield *nield,
                           GwyField *field,
                           const GwyGrainQuantity *quantities,
                           guint nquantities,
                           gdouble **results)
{
    enum {
        NEED_SIZES = 1 << 0,
        NEED_BBOX = 1 << 1,
        NEED_MIN = 1 << 2,
        NEED_MAX = 1 << 3,
        NEED_XVALUE = (1 << 4) | NEED_SIZES,
        NEED_YVALUE = (1 << 5) | NEED_SIZES,
        NEED_CENTRE = NEED_XVALUE | NEED_YVALUE,
        NEED_ZVALUE = (1 << 6) | NEED_SIZES,
        NEED_LINEAR = (1 << 7) | NEED_ZVALUE | NEED_CENTRE,
        NEED_QUADRATIC = (1 << 8) | NEED_LINEAR,
        NEED_CHULL = (1 << 9) | NEED_BBOX,
        INVALID = G_MAXUINT
    };
    static const guint need_aux[N_GRAIN_QUANTITIES] = {
        NEED_SIZES,                   /* projected area */
        0,                            /* id */
        NEED_SIZES,                   /* equiv disc radius */
        0,                            /* surface area */
        NEED_MAX,                     /* maximum */
        NEED_MIN,                     /* minimum */
        NEED_ZVALUE,                  /* mean */
        NEED_SIZES,                   /* median */
        NEED_SIZES,                   /* pixel area */
        NEED_MIN | NEED_MAX,          /* half-height area */
        0,                            /* flat boundary length */
        NEED_ZVALUE,                  /* rms */
        NEED_CHULL,                   /* min bounding size */
        NEED_CHULL,                   /* min bounding direction */
        NEED_CHULL,                   /* max bounding size */
        NEED_CHULL,                   /* max bounding direction */
        NEED_XVALUE,                  /* centre x */
        NEED_YVALUE,                  /* centre y */
        0,                            /* volume, 0-based */
        NEED_MIN | NEED_SIZES,        /* volume, min-based */
        NEED_BBOX | NEED_SIZES,       /* volume, Laplace-based */
        NEED_CHULL,                   /* Fered length */
        INVALID,
        NEED_LINEAR,                  /* slope theta */
        NEED_LINEAR,                  /* slope phi */
        0,                            /* boundary minimum */
        0,                            /* boundary maximum */
        NEED_QUADRATIC,               /* curvature centre x */
        NEED_QUADRATIC,               /* curvature centre y */
        NEED_QUADRATIC,               /* curvature centre z */
        NEED_QUADRATIC,               /* curvature invrad 1 */
        NEED_QUADRATIC,               /* curvature invrad 2 */
        NEED_QUADRATIC,               /* curvature direction 1 */
        NEED_QUADRATIC,               /* curvature direction 2 */
        NEED_CENTRE | NEED_BBOX,      /* inscribed disc radius */
        NEED_CENTRE | NEED_BBOX,      /* inscribed disc centre x */
        NEED_CENTRE | NEED_BBOX,      /* inscribed disc centre y */
        NEED_CHULL,                   /* convex hull area */
        NEED_CHULL,                   /* circumcircle radius */
        NEED_CHULL,                   /* circumcircle centre x */
        NEED_CHULL,                   /* circumcircle centre y */
        NEED_CENTRE,                  /* mean radius */
        NEED_LINEAR,                  /* equiv ellipse major axis */
        NEED_LINEAR,                  /* equiv ellipse minor axis */
        NEED_LINEAR,                  /* equiv ellipse major axis angle */
        NEED_CENTRE | NEED_BBOX,      /* minimum Martin diameter */
        NEED_CENTRE | NEED_BBOX,      /* minimum Martin diameter angle */
        NEED_CENTRE | NEED_BBOX,      /* maximum Martin diameter */
        NEED_CENTRE | NEED_BBOX,      /* maximum Martin diameter angle */
        0,                            /* mean edge distance */
        0,                            /* shape number */
    };

    if (!_gwy_nield_check_field(nield, field, FALSE))
        return;
    if (!nquantities)
        return;

    gint ngrains = gwy_nield_max(nield);
    ngrains = MAX(ngrains, 0);
    g_return_if_fail(results);
    for (guint i = 0; i < nquantities; i++) {
        g_return_if_fail(results[i]);
        gwy_clear(results[i], ngrains + 1);
    }

    /* Figure out which quantities are requested. */
    gdouble *quantity_data[N_GRAIN_QUANTITIES];
    gwy_clear(quantity_data, N_GRAIN_QUANTITIES);
    for (guint i = 0; i < nquantities; i++) {
        GwyGrainQuantity quantity = quantities[i];

        if ((guint)quantity >= N_GRAIN_QUANTITIES || need_aux[quantity] == INVALID) {
            g_warning("Invalid built-in grain quantity number %u.", quantity);
            continue;
        }
        /* Take the first if the same quantity is requested multiple times. We will deal with this later. */
        if (!quantity_data[quantity])
            quantity_data[quantity] = results[i];
    }

    /* Figure out the auxiliary data to calculate.  Do this after we gathered all quantities as some auxiliary data
     * are in fact quantities too. */
    GPtrArray *buffers = g_ptr_array_new_full(0, g_free);
    const guint *sizes = NULL;
    const gint *bboxes = NULL;
    gint *chull = NULL;
    guint *cindex = NULL;
    gdouble *xcentre = NULL, *ycentre = NULL, *zmean = NULL, *linear = NULL, *quadratic = NULL;
    gdouble *min = NULL, *max = NULL;
    for (guint i = 0; i < nquantities; i++) {
        GwyGrainQuantity quantity = quantities[i];

        if ((guint)quantity >= N_GRAIN_QUANTITIES || need_aux[quantity] == INVALID)
            continue;

        guint need = need_aux[quantity];
        /* Integer data which also have functions that calculate them for us. */
        if ((need & NEED_SIZES) && !sizes)
            sizes = _gwy_nield_ensure_sizes(nield);
        if ((need & NEED_BBOX) && !bboxes)
            bboxes = _gwy_nield_ensure_bboxes(nield);
        if ((need & NEED_CHULL) && !chull) {
            cindex = g_new(guint, ngrains+2);
            chull = gwy_nield_convex_hulls(nield, cindex);
            g_ptr_array_add(buffers, cindex);
            g_ptr_array_add(buffers, chull);
        }
        /* Floating point data that coincide with some quantity.  An array is allocated only if the corresponding
         * quantity is not requested. Otherwise we use the supplied array. */
        if (need & NEED_MIN)
            min = ensure_buffer(buffers, GWY_GRAIN_MINIMUM, quantity_data, ngrains, 1, G_MAXDOUBLE);
        if (need & NEED_MAX)
            max = ensure_buffer(buffers, GWY_GRAIN_MAXIMUM, quantity_data, ngrains, 1, -G_MAXDOUBLE);
        if (need & NEED_XVALUE)
            xcentre = ensure_buffer(buffers, GWY_GRAIN_CENTER_X, quantity_data, ngrains, 1, 0.0);
        if (need & NEED_YVALUE)
            ycentre = ensure_buffer(buffers, GWY_GRAIN_CENTER_Y, quantity_data, ngrains, 1, 0.0);
        if (need & NEED_ZVALUE)
            zmean = ensure_buffer(buffers, GWY_GRAIN_MEAN, quantity_data, ngrains, 1, 0.0);
        /* Complex floating point data */
        if ((need & NEED_LINEAR) && !linear)
            linear = ensure_buffer(buffers, 0, NULL, ngrains, 5, 0.0);
        if ((need & NEED_QUADRATIC) && !quadratic)
            quadratic = ensure_buffer(buffers, 0, NULL, ngrains, 12, 0.0);
    }

    /* Calculate auxiliary quantities (in pixel lateral coordinates) */
    calculate_grain_aux(nield, field, sizes, min, max, xcentre, ycentre, zmean, linear, quadratic);

    gint xres = nield->xres, yres = nield->yres;
    gsize nn = (gsize)xres * (gsize)yres;
    const gint *grains = nield->priv->data;
    const gdouble *d = field->priv->data;
    gdouble dx = gwy_field_get_dx(field);
    gdouble dy = gwy_field_get_dy(field);
    gdouble qdiag = hypot(dx, dy);
    gdouble qarea = dx*dy;
    gdouble *p;

    /* Calculate specific requested quantities */
    if ((p = quantity_data[GWY_GRAIN_ID])) {
        for (gint gno = 0; gno <= ngrains; gno++)
            p[gno] = gno;
    }
    if ((p = quantity_data[GWY_GRAIN_PIXEL_AREA])) {
        for (gint gno = 0; gno <= ngrains; gno++)
            p[gno] = sizes[gno];
    }
    if ((p = quantity_data[GWY_GRAIN_PROJECTED_AREA])) {
        for (gint gno = 0; gno <= ngrains; gno++)
            p[gno] = qarea*sizes[gno];
    }
    if ((p = quantity_data[GWY_GRAIN_EQUIV_DISC_RADIUS])) {
        for (gint gno = 0; gno <= ngrains; gno++)
            p[gno] = sqrt(qarea/G_PI*sizes[gno]);
    }
    if ((p = quantity_data[GWY_GRAIN_SURFACE_AREA]))
        calculate_surface_area(grains, ngrains, d, xres, yres, dx, dy, p);
    /* GWY_GRAIN_MINIMUM is calculated directly as an auxiliary quantity. */
    /* GWY_GRAIN_MAXIMUM is calculated directly as an auxiliary quantity. */
    /* GWY_GRAIN_MEAN is calculated directly as an auxiliary quantity. */
    if ((p = quantity_data[GWY_GRAIN_MEDIAN]))
        calculate_grain_medians(grains, ngrains, d, nn, sizes, p);
    if ((p = quantity_data[GWY_GRAIN_HALF_HEIGHT_AREA]))
        calculate_half_height_area(grains, ngrains, d, nn, qarea, min, max, p);
    if ((p = quantity_data[GWY_GRAIN_RMS]))
        calculate_rms(grains, ngrains, d, nn, sizes, zmean, p);
    if ((p = quantity_data[GWY_GRAIN_PERIMETER]))
        calculate_flat_boundary_lengths(grains, xres, yres, dx, dy, qdiag, p);
    if (quantity_data[GWY_GRAIN_BOUNDARY_MINIMUM] || quantity_data[GWY_GRAIN_BOUNDARY_MAXIMUM]) {
        gdouble *bmin = quantity_data[GWY_GRAIN_BOUNDARY_MINIMUM];
        gdouble *bmax = quantity_data[GWY_GRAIN_BOUNDARY_MAXIMUM];
        calculate_boundary_min_max(grains, ngrains, d, xres, yres, bmin, bmax);
    }
    if (quantity_data[GWY_GRAIN_MINIMUM_PROJECT_SIZE]
        || quantity_data[GWY_GRAIN_MINIMUM_PROJECT_ANGLE]
        || quantity_data[GWY_GRAIN_MAXIMUM_PROJECT_SIZE]
        || quantity_data[GWY_GRAIN_MAXIMUM_PROJECT_ANGLE]
        || quantity_data[GWY_GRAIN_CONVEX_HULL_AREA]
        || quantity_data[GWY_GRAIN_CIRCUMCIRCLE_R]
        || quantity_data[GWY_GRAIN_CIRCUMCIRCLE_X]
        || quantity_data[GWY_GRAIN_CIRCUMCIRCLE_Y]) {
        gdouble *psmin = quantity_data[GWY_GRAIN_MINIMUM_PROJECT_SIZE];
        gdouble *psmax = quantity_data[GWY_GRAIN_MAXIMUM_PROJECT_SIZE];
        gdouble *pamin = quantity_data[GWY_GRAIN_MINIMUM_PROJECT_ANGLE];
        gdouble *pamax = quantity_data[GWY_GRAIN_MAXIMUM_PROJECT_ANGLE];
        gdouble *plength = quantity_data[GWY_GRAIN_FERET_LENGTH];
        gdouble *achull = quantity_data[GWY_GRAIN_CONVEX_HULL_AREA];
        gdouble *circcr = quantity_data[GWY_GRAIN_CIRCUMCIRCLE_R];
        gdouble *circcx = quantity_data[GWY_GRAIN_CIRCUMCIRCLE_X];
        gdouble *circcy = quantity_data[GWY_GRAIN_CIRCUMCIRCLE_Y];
        calculate_convex_hull_quantities(grains, xres, ngrains,
                                         dx, dy, field->xoff, field->yoff, bboxes, chull, cindex,
                                         psmin, psmax, pamin, pamax, plength, achull, circcr, circcx, circcy);
    }
    /* NB: Curvature, inscribed disc, mean radius and Martin diameter must be calculate before conversion of grain
     * centre from pixel to real coordinates! */
    if (quantity_data[GWY_GRAIN_CURVATURE_CENTER_X]
        || quantity_data[GWY_GRAIN_CURVATURE_CENTER_Y]
        || quantity_data[GWY_GRAIN_CURVATURE_CENTER_Z]
        || quantity_data[GWY_GRAIN_CURVATURE1]
        || quantity_data[GWY_GRAIN_CURVATURE2]
        || quantity_data[GWY_GRAIN_CURVATURE_ANGLE1]
        || quantity_data[GWY_GRAIN_CURVATURE_ANGLE2]) {
        gdouble *xapex = quantity_data[GWY_GRAIN_CURVATURE_CENTER_X];
        gdouble *yapex = quantity_data[GWY_GRAIN_CURVATURE_CENTER_Y];
        gdouble *zapex = quantity_data[GWY_GRAIN_CURVATURE_CENTER_Z];
        gdouble *kappa1 = quantity_data[GWY_GRAIN_CURVATURE1];
        gdouble *kappa2 = quantity_data[GWY_GRAIN_CURVATURE2];
        gdouble *phi1 = quantity_data[GWY_GRAIN_CURVATURE_ANGLE1];
        gdouble *phi2 = quantity_data[GWY_GRAIN_CURVATURE_ANGLE2];
        calculate_curvature(ngrains, dx, dy, field->xoff, field->yoff,
                            sizes, xcentre, ycentre, zmean, linear, quadratic,
                            xapex, yapex, zapex, kappa1, kappa2, phi1, phi2);
    }
    if (quantity_data[GWY_GRAIN_INSCRIBED_DISC_R]
        || quantity_data[GWY_GRAIN_INSCRIBED_DISC_X]
        || quantity_data[GWY_GRAIN_INSCRIBED_DISC_Y]) {
        gdouble *radii = quantity_data[GWY_GRAIN_INSCRIBED_DISC_R];
        gdouble *discx = quantity_data[GWY_GRAIN_INSCRIBED_DISC_X];
        gdouble *discy = quantity_data[GWY_GRAIN_INSCRIBED_DISC_Y];
        calculate_inscribed_discs(nield, field, xcentre, ycentre, sizes, bboxes, discx, discy, radii);
    }
    if (quantity_data[GWY_GRAIN_MEAN_BOUNDARY_DISTANCE] || quantity_data[GWY_GRAIN_SHAPE_NUMBER]) {
        gdouble *meanedist = quantity_data[GWY_GRAIN_MEAN_BOUNDARY_DISTANCE];
        gdouble *shapeno = quantity_data[GWY_GRAIN_SHAPE_NUMBER];
        // Shape number needs the mean distance as an intermediate calculation, but calculating only the mean distance
        // is fine.
        if (!meanedist)
            meanedist = ensure_buffer(buffers, 0, NULL, ngrains, 1, 0.0);
        calculate_mean_edge_distance(nield, dx, dy, meanedist, shapeno);
    }
    if ((p = quantity_data[GWY_GRAIN_MEAN_RADIUS]))
        calculate_mean_radii(grains, ngrains, xres, yres, dx, dy, xcentre, ycentre, p);
    if (quantity_data[GWY_GRAIN_MINIMUM_MARTIN_DIAMETER]
        || quantity_data[GWY_GRAIN_MINIMUM_MARTIN_ANGLE]
        || quantity_data[GWY_GRAIN_MAXIMUM_MARTIN_DIAMETER]
        || quantity_data[GWY_GRAIN_MAXIMUM_MARTIN_ANGLE]) {
        gdouble *mmin = quantity_data[GWY_GRAIN_MINIMUM_MARTIN_DIAMETER];
        gdouble *mmax = quantity_data[GWY_GRAIN_MAXIMUM_MARTIN_DIAMETER];
        gdouble *phimin = quantity_data[GWY_GRAIN_MINIMUM_MARTIN_ANGLE];
        gdouble *phimax = quantity_data[GWY_GRAIN_MAXIMUM_MARTIN_ANGLE];
        calculate_martin_diameter(grains, ngrains, xres, yres, dx, dy, sizes, bboxes, xcentre, ycentre,
                                  mmin, mmax, phimin, phimax);
    }
    if (quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_MAJOR]
        || quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_MINOR]
        || quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_ANGLE]) {
        gdouble *amaj = quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_MAJOR];
        gdouble *amin = quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_MINOR];
        gdouble *phi = quantity_data[GWY_GRAIN_EQUIV_ELLIPSE_ANGLE];
        calculate_equiv_ellipse(ngrains, dx, dy, sizes, linear, amaj, amin, phi);
    }
    if (quantity_data[GWY_GRAIN_VOLUME_0]
        || quantity_data[GWY_GRAIN_VOLUME_MIN]) {
        gdouble *pv0 = quantity_data[GWY_GRAIN_VOLUME_0];
        gdouble *pvm = quantity_data[GWY_GRAIN_VOLUME_MIN];

        if (pv0) {
            integrate_grain_volume0(d, grains, xres, yres, pv0, ngrains, qarea);
            if (pvm) {
                for (gint gno = 0; gno <= ngrains; gno++) {
                    if (sizes[gno])
                        pvm[gno] = pv0[gno] - qarea*min[gno]*sizes[gno];
                }
            }
        }
        else {
            g_assert(pvm);
            integrate_grain_volume0(d, grains, xres, yres, pvm, ngrains, qarea);
            for (gint gno = 0; gno <= ngrains; gno++) {
                if (sizes[gno])
                    pvm[gno] -= qarea*min[gno]*sizes[gno];
            }
        }
    }
    if ((p = quantity_data[GWY_GRAIN_VOLUME_LAPLACE])) {
        /* Fail gracefully when there is one big `grain' over all data. */
        if (ngrains == 1 && sizes[1] == xres*yres)
            p[1] = 0.0;
        else {
            GwyField *difference = gwy_field_copy(field);
            gwy_field_laplace_solve(difference, nield, GWY_LAPLACE_ALL_GRAINS, 0.4);
            gwy_field_subtract_fields(difference, field, difference);
            integrate_grain_volume0_in_isolation(difference->priv->data, grains, xres, yres, p, ngrains, qarea);
            g_object_unref(difference);
        }
    }
    if (quantity_data[GWY_GRAIN_SLOPE_THETA] || quantity_data[GWY_GRAIN_SLOPE_PHI]) {
        gdouble *ptheta = quantity_data[GWY_GRAIN_SLOPE_THETA];
        gdouble *pphi = quantity_data[GWY_GRAIN_SLOPE_PHI];
        calculate_slope(ngrains, dx, dy, sizes, linear, ptheta, pphi);
    }
    // Finally convert grain centres from pixel coordinates to real.
    if ((p = quantity_data[GWY_GRAIN_CENTER_X])) {
        for (gint gno = 0; gno <= ngrains; gno++) {
            if (sizes[gno])
                p[gno] = dx*(p[gno] + 0.5) + field->xoff;
        }
    }
    if ((p = quantity_data[GWY_GRAIN_CENTER_Y])) {
        for (gint gno = 0; gno <= ngrains; gno++) {
            if (sizes[gno])
                p[gno] = dy*(p[gno] + 0.5) + field->yoff;
        }
    }

    /* Copy quantity values to all other instances of the same quantity in @values. */
    gboolean seen[N_GRAIN_QUANTITIES];
    gwy_clear(seen, N_GRAIN_QUANTITIES);
    for (guint i = 0; i < nquantities; i++) {
        GwyGrainQuantity quantity = quantities[i];

        if ((guint)quantity >= N_GRAIN_QUANTITIES || need_aux[quantity] == INVALID)
            continue;

        if (seen[quantity])
            gwy_assign(results[i], quantity_data[quantity], ngrains + 1);
        seen[quantity] = TRUE;
    }

    /* Finalize */
    g_ptr_array_free(buffers, TRUE);
}

static gdouble*
ensure_buffer(GPtrArray *buffers,
              GwyGrainQuantity quantity,
              gdouble **quantity_data,
              guint ngrains,
              guint multiplicity,
              gdouble fillvalue)
{
    gdouble *buf;
    gsize size;

    if (quantity_data && quantity_data[quantity]) {
        g_assert(multiplicity == 1);
        size = ngrains + 1;
        buf = quantity_data[quantity];
        if (!fillvalue)
            gwy_clear(buf, size);
    }
    else {
        size = (ngrains + 1)*multiplicity;
        buf = fillvalue ? g_new(gdouble, size) : g_new0(gdouble, size);
        g_ptr_array_add(buffers, buf);
    }
    if (fillvalue)
        gwy_math_fill(buf, size, fillvalue);

    return buf;
}

/**
 * gwy_grain_quantity_needs_same_units:
 * @quantity: A grain quantity.
 *
 * Tests whether a grain quantity is defined only when lateral and value units match.
 *
 * Returns: %TRUE if @quantity is meaningless when lateral and value units differ, %FALSE if it is always defined.
 **/
gboolean
gwy_grain_quantity_needs_same_units(GwyGrainQuantity quantity)
{
    const GwyGrainQuantity need_same_units[] = {
        GWY_GRAIN_SLOPE_THETA,
        GWY_GRAIN_SURFACE_AREA,
        GWY_GRAIN_CURVATURE1,
        GWY_GRAIN_CURVATURE2,
    };
    for (guint i = 0; i < G_N_ELEMENTS(need_same_units); i++) {
        if (quantity == need_same_units[i])
            return TRUE;
    }
    return FALSE;
}

/**
 * gwy_grain_quantity_get_units:
 * @quantity: A grain quantity.
 * @unitxy: Lateral SI unit of data.
 * @unitz: Value SI unit of data.
 * @result: (nullable) (transfer full):
 *          An SI unit to set to the units of @quantity. It can be %NULL, a new SI unit is created then and returned.
 *
 * Calculates the units of a grain quantity.
 *
 * Returns: (transfer full):
 *          When @result is %NULL, a newly creates SI unit that has to be dereferenced when no longer used later.
 *          Otherwise @result itself is simply returned, its reference count is NOT increased.
 **/
GwyUnit*
gwy_grain_quantity_get_units(GwyGrainQuantity quantity,
                             GwyUnit *unitxy,
                             GwyUnit *unitz,
                             GwyUnit *result)
{
    static const struct {
        gint powerxy;
        gint powerz;
    }
    unit_powers[] = {
        {  2,  0 }, // GWY_GRAIN_PROJECTED_AREA
        {  0,  0 }, // GWY_GRAIN_ID
        {  1,  0 }, // GWY_GRAIN_EQUIV_DISC_RADIUS
        {  2,  0 }, // GWY_GRAIN_SURFACE_AREA
        {  0,  1 }, // GWY_GRAIN_MAXIMUM
        {  0,  1 }, // GWY_GRAIN_MINIMUM
        {  0,  1 }, // GWY_GRAIN_MEAN
        {  0,  1 }, // GWY_GRAIN_MEDIAN
        {  0,  0 }, // GWY_GRAIN_PIXEL_AREA
        {  2,  0 }, // GWY_GRAIN_HALF_HEIGHT_AREA
        {  1,  0 }, // GWY_GRAIN_PERIMETER
        {  0,  1 }, // GWY_GRAIN_RMS
        {  1,  0 }, // GWY_GRAIN_MINIMUM_PROJECT_SIZE
        {  0,  0 }, // GWY_GRAIN_MINIMUM_PROJECT_ANGLE
        {  1,  0 }, // GWY_GRAIN_MAXIMUM_PROJECT_SIZE
        {  0,  0 }, // GWY_GRAIN_MAXIMUM_PROJECT_ANGLE
        {  1,  0 }, // GWY_GRAIN_CENTER_X
        {  1,  0 }, // GWY_GRAIN_CENTER_Y
        {  2,  1 }, // GWY_GRAIN_VOLUME_0
        {  2,  1 }, // GWY_GRAIN_VOLUME_MIN
        {  2,  1 }, // GWY_GRAIN_VOLUME_LAPLACE
        {  1,  0 }, // GWY_GRAIN_FERET_LENGTH
        {  0,  0 }, // UNUSED
        {  0,  0 }, // GWY_GRAIN_SLOPE_THETA
        {  0,  0 }, // GWY_GRAIN_SLOPE_PHI
        {  0,  1 }, // GWY_GRAIN_BOUNDARY_MAXIMUM
        {  0,  1 }, // GWY_GRAIN_BOUNDARY_MINIMUM
        {  1,  0 }, // GWY_GRAIN_CURVATURE_CENTER_X
        {  1,  0 }, // GWY_GRAIN_CURVATURE_CENTER_Y
        {  0,  1 }, // GWY_GRAIN_CURVATURE_CENTER_Z
        { -1,  0 }, // GWY_GRAIN_CURVATURE1
        { -1,  0 }, // GWY_GRAIN_CURVATURE2
        {  0,  0 }, // GWY_GRAIN_CURVATURE_ANGLE1
        {  0,  0 }, // GWY_GRAIN_CURVATURE_ANGLE2
        {  1,  0 }, // GWY_GRAIN_INSCRIBED_DISC_R
        {  1,  0 }, // GWY_GRAIN_INSCRIBED_DISC_X
        {  1,  0 }, // GWY_GRAIN_INSCRIBED_DISC_Y
        {  2,  0 }, // GWY_GRAIN_CONVEX_HULL_AREA
        {  1,  0 }, // GWY_GRAIN_CIRCUMCIRCLE_R
        {  1,  0 }, // GWY_GRAIN_CIRCUMCIRCLE_X
        {  1,  0 }, // GWY_GRAIN_CIRCUMCIRCLE_Y
        {  1,  0 }, // GWY_GRAIN_MEAN_RADIUS
        {  1,  0 }, // GWY_GRAIN_EQUIV_ELLIPSE_MAJOR
        {  1,  0 }, // GWY_GRAIN_EQUIV_ELLIPSE_MINOR
        {  0,  0 }, // GWY_GRAIN_EQUIV_ELLIPSE_ANGLE
        {  1,  0 }, // GWY_GRAIN_MINIMUM_MARTIN_DIAMETER
        {  0,  0 }, // GWY_GRAIN_MINIMUM_MARTIN_ANGLE
        {  1,  0 }, // GWY_GRAIN_MAXIMUM_MARTIN_DIAMETER
        {  0,  0 }, // GWY_GRAIN_MAXIMUM_MARTIN_ANGLE
        {  1,  0 }, // GWY_GRAIN_MEAN_BOUNDARY_DISTANCE
        {  0,  0 }, // GWY_GRAIN_SHAPE_NUMBER
    };

    g_return_val_if_fail(quantity < G_N_ELEMENTS(unit_powers), result);
    return gwy_unit_power_multiply(result,
                                   unitxy, unit_powers[quantity].powerxy,
                                   unitz, unit_powers[quantity].powerz);
}

/**
 * SECTION: grain-quantities
 * @title: Grain quantities
 * @short_description: Evaluation of grain parameters
 **/

/**
 * GwyGrainQuantity:
 * @GWY_GRAIN_ID: Grain number, always a whole number. It is not particlarly useful, except when you want to handle
 *                the grain number generically as a grain quantity.
 * @GWY_GRAIN_PROJECTED_AREA: Projected (flat) grain area.
 * @GWY_GRAIN_EQUIV_DISC_RADIUS: Radius of a disc with the same area as the grain.
 * @GWY_GRAIN_SURFACE_AREA: Surface area.
 * @GWY_GRAIN_MAXIMUM: Minimum value.
 * @GWY_GRAIN_MINIMUM: Maximum value.
 * @GWY_GRAIN_MEAN: Mean value.
 * @GWY_GRAIN_MEDIAN: Median value.
 * @GWY_GRAIN_PIXEL_AREA: Flat grain area measured in pixels.  This value is redundant but it is useful for filtering.
 *                        It is always a whole number.
 * @GWY_GRAIN_HALF_HEIGHT_AREA: Projected area of the part of grain that is strictly above the half-height, i.e. the
 *                              height between the minimum and maximum.
 * @GWY_GRAIN_RMS: Standard deviation of grain values.
 * @GWY_GRAIN_PERIMETER: Length of grain boundary projected to the horizontal plane.
 * @GWY_GRAIN_MINIMUM_PROJECT_SIZE: The minimum size of grain projection to any line in the horizontal plane.
 *                                  It is also known as the minimum Feret diameter.
 * @GWY_GRAIN_MINIMUM_PROJECT_ANGLE: Direction of the minimum projection (any of them if the minimum is not unique).
 * @GWY_GRAIN_MAXIMUM_PROJECT_SIZE: The maximum size of grain projection to any line in the horizontal plane.
 *                                  It is also known as the maximum Feret diameter.
 * @GWY_GRAIN_MAXIMUM_PROJECT_ANGLE: Direction of the maximum projection (any of them if the maximum is not unique).
 * @GWY_GRAIN_CENTER_X: Grain centre horizontal position, i.e. the mean value of its physical x-coordinates.
 * @GWY_GRAIN_CENTER_Y: Grain centre vertical position, i.e. the mean value of its physical y-coordinates.
 * @GWY_GRAIN_VOLUME_0: Grain volume calculated with grain basis at @z=0 (therefore it is just an integral it can be
 *                      negative).
 * @GWY_GRAIN_VOLUME_MIN: Grain volume calculated with grain basis at grain minimum value.  This value is a lower
 *                        bound of the volume.
 * @GWY_GRAIN_VOLUME_LAPLACE: Grain volume calculated with grain basis calculated by Laplacian interpolation of
 *                            surrounding values (as with gwy_field_laplace_solve()).
 * @GWY_GRAIN_FERET_LENGTH: Size of particle projection to the direction perpendicular to the minimum Feret diameter,
 *                          i.e. perpendicular to @GWY_GRAIN_MINIMUM_PROJECT_ANGLE.
 * @GWY_GRAIN_SLOPE_THETA: Spherical angle theta of grain normal (0 is upwards).
 * @GWY_GRAIN_SLOPE_PHI: Spherical angle phi of grain normal (0 is in positive x direction).
 * @GWY_GRAIN_BOUNDARY_MINIMUM: Minimum value on the grain inner boundary (image edges are not considered grain
 *                              boundaries).
 * @GWY_GRAIN_BOUNDARY_MAXIMUM: Maximum value in the grain inner boundary (image edges are not considered grain
 *                              boundaries).
 * @GWY_GRAIN_CURVATURE_CENTER_X: Grain curvature centre horizontal position.  For too small or flat grains it reduces
 *                                to the horizontal position of geometrical centre.
 * @GWY_GRAIN_CURVATURE_CENTER_Y: Grain curvature centre vertical position.  For too small or flat grains it reduces
 *                                to the vertical position of geometrical centre.
 * @GWY_GRAIN_CURVATURE_CENTER_Z: The value at curvature centre.  Note this is the value in the origin of the fitted
 *                                quadratic surface, not at the real surface.
 * @GWY_GRAIN_CURVATURE1: Smaller grain curvature.
 * @GWY_GRAIN_CURVATURE2: Larger grain curvature.
 * @GWY_GRAIN_CURVATURE_ANGLE1: Direction of the smaller grain curvature radius.  If the grain is flat or too small
 *                              the angle is reported as 0.
 * @GWY_GRAIN_CURVATURE_ANGLE2: Direction of the larger grain curvature radius.  If the grain is flat or too small the
 *                              angle is reported as π/2.
 * @GWY_GRAIN_INSCRIBED_DISC_R: Radius of maximum disc that fits inside the grain
 * @GWY_GRAIN_INSCRIBED_DISC_X: Real X-coordinate of the centre of the maximum inscribed disc.
 * @GWY_GRAIN_INSCRIBED_DISC_Y: Real Y-coordinate of the centre of the maximum inscribed disc.
 * @GWY_GRAIN_CONVEX_HULL_AREA: Projected (flat) area of grain convex hull.
 * @GWY_GRAIN_CIRCUMCIRCLE_R: Radius of minimum circle containing the grain.
 * @GWY_GRAIN_CIRCUMCIRCLE_X: Real X-coordinate of the centre of the minimum circumcircle.
 * @GWY_GRAIN_CIRCUMCIRCLE_Y: Real Y-coordinate of the centre of the minimum circumcircle.
 * @GWY_GRAIN_MEAN_RADIUS: Mean distance from boundary to the grain centre as defined by @GWY_GRAIN_CENTER_X and
 *                         @GWY_GRAIN_CENTER_Y. All boundaries are included (even internal).
 * @GWY_GRAIN_EQUIV_ELLIPSE_MAJOR: Length of major semiaxis of equivalent ellipse.
 * @GWY_GRAIN_EQUIV_ELLIPSE_MINOR: Length of minor semiaxis of equivalent ellipse.
 * @GWY_GRAIN_EQUIV_ELLIPSE_ANGLE: Orientation of the major axis of equivalent ellipse.
 * @GWY_GRAIN_MINIMUM_MARTIN_DIAMETER: Minimum value of Martin diameter.
 * @GWY_GRAIN_MINIMUM_MARTIN_ANGLE: Direction corresponding to minimum Martin diameter.
 * @GWY_GRAIN_MAXIMUM_MARTIN_DIAMETER: Maximum value of Martin diameter.
 * @GWY_GRAIN_MAXIMUM_MARTIN_ANGLE: Direction corresponding to maximum Martin diameter.
 * @GWY_GRAIN_MEAN_BOUNDARY_DISTANCE: Mean distance from a grain point to the closest boundary point.
 * @GWY_GRAIN_SHAPE_NUMBER: Circularity measure based on the mean edge distance.
 *
 * Grain quantity to request from gwy_nield_grain_quantity() and similar functions.
 **/

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