/*
 *  $Id: stats--minkowski.c 29550 2026-02-27 09:08:17Z yeti-dn $
 *  Copyright (C) 2009-2026 David Nečas (Yeti).
 *  E-mail: yeti@gwyddion.net.
 *
 *  This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public
 *  License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any
 *  later version.
 *
 *  This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
 *  warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
 *  details.
 *
 *  You should have received a copy of the GNU General Public License along with this program; if not, write to the
 *  Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */

#include <string.h>
#include <string.h>

#include "libgwyddion/macros.h"
#include "libgwyddion/field.h"
#include "libgwyddion/arithmetic.h"
#include "libgwyddion/stats.h"
#include "libgwyddion/threads.h"

#include "libgwyddion/internal.h"

static void
sanitise_range(gdouble *min,
               gdouble *max)
{
    if (*max > *min)
        return;
    if (*max) {
        gdouble d = fabs(*max);
        // Break the symmetry in whatever way, but consistently. Otherwise we would be comparing the same numbers
        // to themselves, just obtained differently, and be completely at mercy of rounding errors.
        *max += 0.11*d;
        *min -= 0.1*d;
        return;
    }
    *min = -1.0;
    *max = 1.0;
}

static inline void
add_to_dist(GwyLine *dist, gdouble z)
{
    gdouble x = (z - dist->off)/dist->real*dist->res - 0.5;
    gdouble *data = dist->priv->data;
    if (x <= 0.0)
        data[0] += 1.0;
    else if (x <= dist->res-1)
        data[(guint)ceil(x)] += 1.0;
}

static GwyLine*
minkowski_volume(GwyField *field,
                 GwyNield *mask,
                 GwyMaskingType masking,
                 guint col, guint row,
                 guint width, guint height,
                 guint npoints,
                 gdouble min, gdouble max)
{
    gsize n = width*height;
    if (masking != GWY_MASK_IGNORE) {
        gsize nmasked = gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);
        n = (masking == GWY_MASK_INCLUDE ? nmasked : n - nmasked);
    }

    if (!n)
        return NULL;

    if (!(min < max))
        gwy_field_area_min_max(field, mask, masking, col, row, width, height, &min, &max);
    sanitise_range(&min, &max);

    if (!npoints)
        npoints = dist_points_for_n_points(n);

    GwyLine *dist = gwy_line_new(npoints, max-min, TRUE);
    dist->off = min;

    gint xres = field->xres;
    const gdouble *datapos = field->priv->data + xres*row + col;
    const gint *maskpos = (mask ? mask->priv->data + xres*row + col : NULL);

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

    // The non-cumulative distributions are already prepared pixel-centered so use plain summing here.
    gwy_line_cumulate(dist, FALSE);
    gdouble *tdata = dist->priv->data;
    for (guint k = 0; k < dist->res; k++)
        tdata[k] = 1.0 - tdata[k]/n;

    return dist;
}

/**
 * count_edges:
 * @mask: A mask field.
 * @masking: Masking mode.
 * @col: Column index.
 * @row: Row index.
 * @width: Part width (number of column).
 * @height: Part height (number of rows).
 * @min: Location to store the minimum.
 * @max: Location to store the maximum.
 *
 * Counts the number of edges between two pixels.
 *
 * An edge is counted if both pixels are counted according to the masking mode @masking.
 *
 * Since only edges that lie between two counted pixels contribute the minimum and maximum is also calculated
 * edge-wise.  This essentially means that they are calculated ignoring single-pixel grains as all other masked values
 * have some neighbour.
 *
 * Returns: The number of edges.
 **/
static guint
count_edges(GwyField *field,
            GwyNield *mask,
            GwyMaskingType masking,
            guint col, guint row,
            guint width, guint height,
            gdouble *pmin, gdouble *pmax)
{
    if (masking == GWY_MASK_IGNORE) {
        gwy_field_area_min_max(field, NULL, GWY_MASK_IGNORE, col, row, width, height, pmin, pmax);
        return 2*width*height - width - height;
    }

    g_assert(mask);

    guint xres = field->xres;
    const gdouble *datapos = field->priv->data + xres*row + col;
    const gint *maskpos = mask->priv->data + xres*row + col;

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

        if (i == height-1) {
            for (guint j = 0; j < width-1; j++) {
                if (nielded_included(mrow + j, masking)) {
                    min = fmin(min, drow[j]);
                    max = fmin(max, drow[j]);
                    if (nielded_included(mrow + j+1, masking))
                        nedges++;
                }
            }
        }
        else {
            for (guint j = 0; j < width-1; j++) {
                if (nielded_included(mrow + j, masking)) {
                    min = fmin(min, drow[j]);
                    max = fmin(max, drow[j]);
                    if (nielded_included(mrow + j+1, masking))
                        nedges++;
                    if (nielded_included(mrow + j+xres, masking))
                        nedges++;
                }
            }

            guint j = width-1;
            if (nielded_included(mrow + j, masking)) {
                min = fmin(min, drow[j]);
                max = fmin(max, drow[j]);
                if (nielded_included(mrow + j+xres, masking))
                    nedges++;
            }
        }
    }

    *pmin = min;
    *pmax = max;

    return nedges;
}

static inline void
add_to_min_max_dist(GwyLine *mindist, GwyLine *maxdist,
                    gdouble z1, gdouble z2)
{
    GWY_ORDER(gdouble, z1, z2);
    add_to_dist(mindist, z1);
    add_to_dist(maxdist, z2);
}

static void
calculate_min_max_dist(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       guint col, guint row,
                       guint width, guint height,
                       GwyLine *mindist, GwyLine *maxdist)
{
    guint xres = field->xres;
    const gdouble *datapos = field->priv->data + xres*row + col;

    if (masking == GWY_MASK_IGNORE) {
        for (guint i = 0; i < height-1; i++) {
            const gdouble *drow = datapos + i*xres;
            for (guint j = 0; j < width-1; j++) {
                add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+1]);
                add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+xres]);
            }
            guint j = width-1;
            add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+xres]);
        }
        const gdouble *drow = datapos + (row + height-1)*xres + col;
        for (guint j = 0; j < width-1; j++)
            add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+1]);

        return;
    }

    g_assert(mask);
    const gint *maskpos = mask->priv->data + xres*row + col;

    for (guint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        const gint *mrow = maskpos + i*xres;

        if (i == height-1) {
            for (guint j = 0; j < width-1; j++) {
                if (nielded_included(mrow + j, masking) && nielded_included(mrow + j+1, masking))
                    add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+1]);
            }
        }
        else {
            for (guint j = 0; j < width-1; j++) {
                if (nielded_included(mrow + j, masking)) {
                    if (nielded_included(mrow + j+1, masking))
                        add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+1]);
                    if (nielded_included(mrow + j+xres, masking))
                        add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+xres]);
                }
            }

            guint j = width-1;
            if (nielded_included(mrow + j, masking) && nielded_included(mrow + j+xres, masking))
                    add_to_min_max_dist(mindist, maxdist, drow[j], drow[j+xres]);
        }
    }
}

/* The calculation is based on expressing the functional as the difference of
 * cumulative distributions of min(z1, z2) and max(z1, z2) where (z1, z2) are
 * couples of pixels with a common edge. */
static GwyLine*
minkowski_boundary(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   guint col, guint row,
                   guint width, guint height,
                   guint npoints,
                   gdouble min, gdouble max)
{
    gdouble min1, max1;
    guint nedges = count_edges(field, mask, masking, col, row, width, height, &min1, &max1);

    if (!nedges)
        return NULL;

    if (!npoints)
        npoints = dist_points_for_n_points(nedges);

    if (!(min < max)) {
        min = min1;
        max = max1;
    }
    sanitise_range(&min, &max);

    GwyLine *line = gwy_line_new(npoints, max-min, TRUE);
    line->off = min;

    GwyLine *maxdist = g_object_ref(line), *mindist = gwy_line_copy(line);
    calculate_min_max_dist(field, mask, masking, col, row, width, height, mindist, maxdist);
    // The non-cumulative distributions are already prepared pixel-centered so
    // use plain summing here.
    gwy_line_cumulate(mindist, FALSE);
    gwy_line_cumulate(maxdist, FALSE);
    gwy_line_subtract_lines(line, maxdist, mindist);
    g_object_unref(mindist);
    g_object_unref(maxdist);
    gwy_line_multiply(line, 1.0/nedges);

    return line;
}

/* Calculate discrete heights.
 *
 * There are @npoints+1 buckets. The 0th collects everything below 1/2, the rest is pixel-sized, the last collects
 * evrything above @npoints-1/2 . This makes the distribution pixel-centered and symmetrical for white and black
 * cases, as necessary.
 */
static void
discretise_heights(GwyField *field,
                   GwyNield *mask,
                   GwyMaskingType masking,
                   GwyNield *discretised,
                   guint col, guint row,
                   guint npoints,
                   gdouble min, gdouble max,
                   gboolean white)
{
    gdouble q = npoints/(max - min);
    guint xres = field->xres;
    guint width = discretised->xres, height = discretised->yres;
    const gdouble *datapos = field->priv->data + xres*row + col;
    const gint *maskpos = (mask ? mask->priv->data + xres*row + col : NULL);
    gint *target = discretised->priv->data;

    for (guint i = 0; i < height; i++) {
        const gdouble *drow = datapos + i*xres;
        gint *trow = target + i*width;

        if (masking == GWY_MASK_IGNORE) {
            for (guint j = 0; j < width; j++) {
                gdouble x = white ? (max - drow[j]) : (drow[j] - min);
                x = ceil(x*q - 0.5);
                x = CLAMP(x, 0.0, npoints);
                trow[j] = (gint)x;
            }
        }
        else {
            const gint *mrow = maskpos + i*xres;
            for (guint j = 0; j < width; j++) {
                if (nielded_included(mrow + j, masking)) {
                    gdouble x = white ? (max - drow[j]) : (drow[j] - min);
                    x = ceil(x*q - 0.5);
                    x = CLAMP(x, 0.0, npoints);
                    trow[j] = (gint)x;
                }
                else
                    trow[j] = -1;
            }
        }
    }
}

/*
 * Group pixels of the same discrete height.  Array nh then holds indices in hindex where each height starts.
 */
static void
group_by_height(GwyNield *dheights, guint npoints, guint *nh, guint *hindex)
{
    // Make nh[i] the start of the block of discrete height i in hindex[].
    gsize size = (gsize)dheights->xres * (gsize)dheights->yres;
    const gint *heights = dheights->priv->data;
    for (guint i = 0; i < size; i++) {
        gint h = heights[i];
        if (h >= 0)
            nh[h]++;
    }
    for (guint i = 1; i <= npoints; i++)
        nh[i] += nh[i-1];
    for (guint i = npoints; i; i--)
        nh[i] = nh[i-1];
    nh[0] = 0;

    // Fill the blocks in hindex[] with indices of points with the corresponding discrete height.
    for (guint i = 0; i < size; i++) {
        gint h = heights[i];
        if (h >= 0)
            hindex[nh[h]++] = i;
    }
    for (guint i = npoints+1; i; i--)
        nh[i] = nh[i-1];
    nh[0] = 0;
}

static inline guint
uniq_array(gint *x, guint n)
{
    guint i = 1;
    while (i < n) {
        guint j;
        for (j = 0; j < i; j++) {
            if (x[i] == x[j])
                break;
        }

        if (j < i) {
            x[i] = x[--n];
        }
        else
            i++;
    }
    return n;
}

static guint
compress_grain_numbers(gint *grains, gint n, gssize *m, gint ngrains)
{
    // Build a fully resolved renumbering.
    gint count = 1;
    for (gint i = 1; i < ngrains; i++) {
        if (m[i] == i)
            m[i] = count++;
        else
            m[i] = m[m[i]];
    }

    // Apply the renumbering to grains.
    for (gint k = 0; k < n; k++)
        grains[k] = m[grains[k]];

    // Apply the renumbering to the map.  This is trivial because the result must be the sequence 1..count-1.
    for (gint i = 1; i < count; i++)
        m[i] = i;

    return count;
}

/**
 * grain_number_dist:
 * @data_field: A data field.
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @min: Minimum threshold value.
 * @max: Maximum threshold value.
 * @white: If %TRUE, hills are marked, otherwise valleys are marked.
 * @nstats: The number of samples to take on the distribution function.  If nonpositive, a suitable resolution is
 *          determined automatically.
 *
 * Calculates threshold grain number distribution in given height range.
 *
 * This is the number of grains for each of @nstats equidistant height threshold levels.  For large @nstats this
 * function is much faster than the equivalent number of gwy_data_field_grains_mark_height() calls.
 *
 * Returns: A new data line with the distribution.
 **/
static GwyLine*
grain_number_dist(GwyField *field,
                  GwyNield *mask,
                  GwyMaskingType masking,
                  guint col, guint row,
                  guint width, guint height,
                  gboolean white,
                  guint npoints,
                  gdouble min, gdouble max)
{
    gsize n = width*height;
    if (masking != GWY_MASK_IGNORE) {
        gsize nmasked = gwy_nield_area_count(mask, NULL, GWY_MASK_IGNORE, col, row, width, height);
        n = (masking == GWY_MASK_INCLUDE ? nmasked : n - nmasked);
    }

    if (!n)
        return NULL;

    if (!npoints)
        npoints = dist_points_for_n_points(n);

    GwyLine *line = gwy_line_new(npoints, max - min, FALSE);
    line->off = min;

    GwyNield *nield = gwy_nield_new(width, height);
    discretise_heights(field, mask, masking, nield, col, row, npoints, min, max, white);
    guint *nh = g_new0(guint, npoints+2);
    guint *hindex = g_new(guint, n);
    group_by_height(nield, npoints, nh, hindex);

    guint *grains = nield->priv->data;     // No longer needed. Recycle.
    gwy_clear(grains, width*height);
    gssize *m = g_new(gssize, n+1);
    m[0] = 0;

    // Main iteration
    guint ngrains = 1;
    gdouble *ldata = line->priv->data;
    for (guint h = 0; h < npoints; h++) {
        if (h && nh[h] == nh[h+1]) {
            ldata[h] = ldata[h-1];
            continue;
        }

        for (guint l = nh[h]; l < nh[h+1]; l++) {
            guint k = hindex[l], i = k/width, j = k % width;
            g_assert(!grains[k]);

            // Find grain numbers of neighbours, if any.
            guint neigh[4], nn = 0;
            if (i && grains[k-width])
                neigh[nn++] = grains[k-width];
            if (j && grains[k-1])
                neigh[nn++] = grains[k-1];
            if (j < width-1 && grains[k+1])
                neigh[nn++] = grains[k+1];
            if (i < height-1 && grains[k+width])
                neigh[nn++] = grains[k+width];

            if (nn) {
                // Merge all grains that touch this pixel to one.
                nn = uniq_array(neigh, nn);
                for (guint p = 1; p < nn; p++)
                    resolve_grain_map(m, neigh[p-1], neigh[p]);
                guint ming = m[neigh[0]];
                for (guint p = 1; p < nn; p++) {
                    if (m[neigh[p]] < ming)
                        ming = m[neigh[p]];
                }
                // And this is also the number the new pixel gets.
                grains[k] = ming;
            }
            else {
                // A new grain not touching anything gets a new number.
                g_assert(ngrains <= n);
                m[ngrains] = ngrains;
                grains[k] = ngrains++;
                continue;
            }
        }

        // Resolve remaining grain number links in the map.  This nicely works because we resolve downwards and go
        // from the lowest number.
        guint count = 0;
        for (guint i = 1; i < ngrains; i++) {
            m[i] = m[m[i]];
            if (m[i] == i)
                count++;
        }

        ldata[h] = (gdouble)count/n;

        if (7*count/5 + 4 < ngrains)
            ngrains = compress_grain_numbers(grains, width*height, m, ngrains);

#if 0
        g_printerr("GRAINS %u :: %u\n", h, count);
        for (guint i = 0; i < height; i++) {
            for (guint j = 0; j < width; j++) {
                if (grains[i*width + j])
                    g_printerr("%02u", grains[i*width + j]);
                else
                    g_printerr("..");
                g_printerr("%c", j == width-1 ? '\n' : ' ');
            }
        }
        g_printerr("MAPPED %u\n", h);
        for (guint i = 0; i < height; i++) {
            for (guint j = 0; j < width; j++) {
                if (grains[i*width + j])
                    g_printerr("%02u", m[grains[i*width + j]]);
                else
                    g_printerr("..");
                g_printerr("%c", j == width-1 ? '\n' : ' ');
            }
        }
#endif
    }

    if (white)
        invert_array_in_place(ldata, npoints);

    g_free(m);
    g_free(hindex);
    g_free(nh);
    g_object_unref(nield);

    return line;
}

static GwyLine*
minkowski_ngrains(GwyField *field,
                  GwyNield *mask,
                  GwyMaskingType masking,
                  guint col, guint row,
                  guint width, guint height,
                  gboolean white,
                  guint npoints,
                  gdouble min, gdouble max)
{
    if (!(min < max))
        gwy_field_area_min_max(field, mask, masking, col, row, width, height, &min, &max);
    sanitise_range(&min, &max);

    return grain_number_dist(field, mask, masking, col, row, width, height, white, npoints, min, max);
}

static GwyLine*
minkowski_connectivity(GwyField *field,
                       GwyNield *mask,
                       GwyMaskingType masking,
                       guint col, guint row,
                       guint width, guint height,
                       guint npoints,
                       gdouble min, gdouble max)
{
    if (!(min < max))
        gwy_field_area_min_max(field, mask, masking, col, row, width, height, &min, &max);
    sanitise_range(&min, &max);

    GwyLine *whitedist = grain_number_dist(field, mask, masking, col, row, width, height, TRUE, npoints, min, max);
    if (!whitedist)
        return NULL;

    GwyLine *blackdist = grain_number_dist(field, mask, masking, col, row, width, height, FALSE, npoints, min, max);
    g_assert(blackdist);
    gwy_line_subtract_lines(whitedist, whitedist, blackdist);
    g_object_unref(blackdist);

    return whitedist;
}

/**
 * gwy_field_minkowski:
 * @field: A two-dimensional data field.
 * @type: Type of functional to calculate.
 * @nstats: Resolution, i.e. the number of returned line points. Pass zero to choose a suitable resolution
 *          automatically.
 *
 * Calculates a Minkowski functional of a data field.
 *
 * Returns: (transfer full):
 *          A new one-dimensional data line with the requested functional.
 **/
GwyLine*
gwy_field_minkowski(GwyField *field,
                    GwyMinkowskiFunctional type,
                    gint nstats)
{
    g_return_val_if_fail(GWY_IS_FIELD(field), NULL);
    return gwy_field_area_minkowski(field, NULL, GWY_MASK_IGNORE, 0, 0, field->xres, field->yres,
                                    type, nstats, G_MAXDOUBLE, -G_MAXDOUBLE);
}

/**
 * gwy_field_area_minkowski:
 * @field: A two-dimensional data field.
 * @mask: (nullable): Mask specifying which values to take into account/exclude, or %NULL.
 * @masking: Masking mode to use (has any effect only with non-%NULL @mask).
 * @col: Upper-left column coordinate.
 * @row: Upper-left row coordinate.
 * @width: Area width (number of columns).
 * @height: Area height (number of rows).
 * @type: Type of functional to calculate.
 * @nstats: Resolution, i.e. the number of returned line points. Pass zero to choose a suitable resolution
 *          automatically.
 * @min: Minimum value of the range to calculate the distribution in.
 * @max: Maximum value of the range to calculate the distribution in.
 *
 * Calculates a Minkowski functional of a part of a data field.
 *
 * Pass @max <= @min to calculate the functional in the full data range (with masking still considered).
 *
 * If the area contains no pixels to consider due to masking, a valid but useless #GwyLine is returned. It is best to
 * check the number of pixels with gwy_nield_area_count().
 *
 * Returns: (transfer full):
 *          A new one-dimensional data line with the requested functional.
 **/
GwyLine*
gwy_field_area_minkowski(GwyField *field,
                         GwyNield *mask,
                         GwyMaskingType masking,
                         gint col, gint row,
                         gint width, gint height,
                         GwyMinkowskiFunctional type,
                         gint nstats,
                         gdouble min, gdouble max)
{
    if (!_gwy_field_check_area(field, col, row, width, height, TRUE)
        || !_gwy_field_check_mask(field, &mask, &masking))
        return NULL;

    // Cannot determine nstats here, it depends on the functional. But pass 0 in place of negative values since the
    // functions can take nstats as unsigned.
    nstats = MAX(nstats, 0);

    GwyLine *line;
    if (type == GWY_MINKOWSKI_VOLUME)
        line = minkowski_volume(field, mask, masking, col, row, width, height, nstats, min, max);
    else if (type == GWY_MINKOWSKI_BOUNDARY)
        line = minkowski_boundary(field, mask, masking, col, row, width, height, nstats, min, max);
    else if (type == GWY_MINKOWSKI_BLACK)
        line = minkowski_ngrains(field, mask, masking, col, row, width, height, FALSE, nstats, min, max);
    else if (type == GWY_MINKOWSKI_WHITE)
        line = minkowski_ngrains(field, mask, masking, col, row, width, height, TRUE, nstats, min, max);
    else if (type == GWY_MINKOWSKI_CONNECTIVITY)
        line = minkowski_connectivity(field, mask, masking, col, row, width, height, nstats, min, max);
    else {
        g_critical("Unknown Minkowski functional type %u.", type);
        return NULL;
    }

    if (!line) {
        sanitise_range(&min, &max);
        line = gwy_line_new(1, max-min, TRUE);
        line->off = min;
    }

    gwy_unit_assign(gwy_line_get_unit_x(line), gwy_field_get_unit_z(field));

    return line;
}

/**
 * GwyMinkowskiFunctional:
 * @GWY_MINKOWSKI_VOLUME: Fraction of ‘white’ pixels from the total number of pixels.
 * @GWY_MINKOWSKI_BOUNDARY: Fraction of ‘black–white’ pixel edges from the total number of edges.
 * @GWY_MINKOWSKI_BLACK: The number of ‘black’ connected areas (grains) divided by the total number of pixels.
 * @GWY_MINKOWSKI_WHITE: The number of ‘white’ connected areas (grains) divided by the total number of pixels.
 * @GWY_MINKOWSKI_CONNECTIVITY: Difference between the numbers of ‘white’ and ‘black’ connected areas (grains) divided
 *                              by the total number of pixels.
 *
 * Types of Minkowski functionals and related quantities.
 *
 * Each quantity is a function of threshold; pixels above this threshold are considered ‘white’, pixels below ‘black’.
 * The normalisation is always to the total number of things that could contribute, making the maximum possible value
 * one. This can differ from some traditional definitions that normalise edges to pixels, for example, which is not
 * applicable to irregular regions.
 **/

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