diff mupdf-source/thirdparty/leptonica/src/pix4.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/leptonica/src/pix4.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,3486 @@
+/*====================================================================*
+ -  Copyright (C) 2001 Leptonica.  All rights reserved.
+ -
+ -  Redistribution and use in source and binary forms, with or without
+ -  modification, are permitted provided that the following conditions
+ -  are met:
+ -  1. Redistributions of source code must retain the above copyright
+ -     notice, this list of conditions and the following disclaimer.
+ -  2. Redistributions in binary form must reproduce the above
+ -     copyright notice, this list of conditions and the following
+ -     disclaimer in the documentation and/or other materials
+ -     provided with the distribution.
+ -
+ -  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ -  ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ -  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ -  A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL ANY
+ -  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ -  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ -  PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ -  PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
+ -  OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ -  NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ -  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ *====================================================================*/
+
+/*!
+ * \file pix4.c
+ * <pre>
+ *
+ *    This file has these operations:
+ *
+ *      (1) Pixel histograms
+ *      (2) Pixel row/column statistics
+ *      (3) Foreground/background estimation
+ *
+ *    Pixel histogram, rank val, averaging and min/max
+ *           NUMA       *pixGetGrayHistogram()
+ *           NUMA       *pixGetGrayHistogramMasked()
+ *           NUMA       *pixGetGrayHistogramInRect()
+ *           NUMAA      *pixGetGrayHistogramTiled()
+ *           l_int32     pixGetColorHistogram()
+ *           l_int32     pixGetColorHistogramMasked()
+ *           NUMA       *pixGetCmapHistogram()
+ *           NUMA       *pixGetCmapHistogramMasked()
+ *           NUMA       *pixGetCmapHistogramInRect()
+ *           l_int32     pixCountRGBColorsByHash()
+ *           l_int32     pixCountRGBColors()
+ *           L_AMAP     *pixGetColorAmapHistogram()
+ *           l_int32     amapGetCountForColor()
+ *           l_int32     pixGetRankValue()
+ *           l_int32     pixGetRankValueMaskedRGB()
+ *           l_int32     pixGetRankValueMasked()
+ *           l_int32     pixGetPixelAverage()
+ *           l_int32     pixGetPixelStats()
+ *           l_int32     pixGetAverageMaskedRGB()
+ *           l_int32     pixGetAverageMasked()
+ *           l_int32     pixGetAverageTiledRGB()
+ *           PIX        *pixGetAverageTiled()
+ *           NUMA       *pixRowStats()
+ *           NUMA       *pixColumnStats()
+ *           l_int32     pixGetRangeValues()
+ *           l_int32     pixGetExtremeValue()
+ *           l_int32     pixGetMaxValueInRect()
+ *           l_int32     pixGetMaxColorIndex()
+ *           l_int32     pixGetBinnedComponentRange()
+ *           l_int32     pixGetRankColorArray()
+ *           l_int32     pixGetBinnedColor()
+ *           PIX        *pixDisplayColorArray()
+ *           PIX        *pixRankBinByStrip()
+ *
+ *    Pixelwise aligned statistics
+ *           PIX        *pixaGetAlignedStats()
+ *           l_int32     pixaExtractColumnFromEachPix()
+ *           l_int32     pixGetRowStats()
+ *           l_int32     pixGetColumnStats()
+ *           l_int32     pixSetPixelColumn()
+ *
+ *    Foreground/background estimation
+ *           l_int32     pixThresholdForFgBg()
+ *           l_int32     pixSplitDistributionFgBg()
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include <math.h>
+#include "allheaders.h"
+
+
+/*------------------------------------------------------------------*
+ *                  Pixel histogram and averaging                   *
+ *------------------------------------------------------------------*/
+/*!
+ * \brief   pixGetGrayHistogram()
+ *
+ * \param[in]   pixs     1, 2, 4, 8, 16 bpp; can be colormapped
+ * \param[in]   factor   subsampling factor; integer >= 1
+ * \return  na histogram, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs has a colormap, it is converted to 8 bpp gray.
+ *          If you want a histogram of the colormap indices, use
+ *          pixGetCmapHistogram().
+ *      (2) If pixs does not have a colormap, the output histogram is
+ *          of size 2^d, where d is the depth of pixs.
+ *      (3) Set the subsampling factor > 1 to reduce the amount of computation.
+ * </pre>
+ */
+NUMA *
+pixGetGrayHistogram(PIX     *pixs,
+                    l_int32  factor)
+{
+l_int32     i, j, w, h, d, wpl, val, size, count;
+l_uint32   *data, *line;
+l_float32  *array;
+NUMA       *na;
+PIX        *pixg;
+
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    d = pixGetDepth(pixs);
+    if (d > 16)
+        return (NUMA *)ERROR_PTR("depth not in {1,2,4,8,16}", __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+
+    if (pixGetColormap(pixs))
+        pixg = pixRemoveColormap(pixs, REMOVE_CMAP_TO_GRAYSCALE);
+    else
+        pixg = pixClone(pixs);
+
+    pixGetDimensions(pixg, &w, &h, &d);
+    size = 1 << d;
+    if ((na = numaCreate(size)) == NULL) {
+        pixDestroy(&pixg);
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    }
+    numaSetCount(na, size);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    if (d == 1) {  /* special case */
+        pixCountPixels(pixg, &count, NULL);
+        array[0] = w * h - count;
+        array[1] = count;
+        pixDestroy(&pixg);
+        return na;
+    }
+
+    wpl = pixGetWpl(pixg);
+    data = pixGetData(pixg);
+    for (i = 0; i < h; i += factor) {
+        line = data + i * wpl;
+        if (d == 2) {
+            for (j = 0; j < w; j += factor) {
+                val = GET_DATA_DIBIT(line, j);
+                array[val] += 1.0;
+            }
+        } else if (d == 4) {
+            for (j = 0; j < w; j += factor) {
+                val = GET_DATA_QBIT(line, j);
+                array[val] += 1.0;
+            }
+        } else if (d == 8) {
+            for (j = 0; j < w; j += factor) {
+                val = GET_DATA_BYTE(line, j);
+                array[val] += 1.0;
+            }
+        } else {  /* d == 16 */
+            for (j = 0; j < w; j += factor) {
+                val = GET_DATA_TWO_BYTES(line, j);
+                array[val] += 1.0;
+            }
+        }
+    }
+
+    pixDestroy(&pixg);
+    return na;
+}
+
+
+/*!
+ * \brief   pixGetGrayHistogramMasked()
+ *
+ * \param[in]   pixs     8 bpp, or colormapped
+ * \param[in]   pixm     [optional] 1 bpp mask over which histogram is
+ *                       to be computed; use all pixels if null
+ * \param[in]   x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                       can be < 0; these values are ignored if pixm is null
+ * \param[in]   factor   subsampling factor; integer >= 1
+ * \return  na histogram, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs is cmapped, it is converted to 8 bpp gray.
+ *          If you want a histogram of the colormap indices, use
+ *          pixGetCmapHistogramMasked().
+ *      (2) This always returns a 256-value histogram of pixel values.
+ *      (3) Set the subsampling factor > 1 to reduce the amount of computation.
+ *      (4) Clipping of pixm (if it exists) to pixs is done in the inner loop.
+ *      (5) Input x,y are ignored unless pixm exists.
+ * </pre>
+ */
+NUMA *
+pixGetGrayHistogramMasked(PIX        *pixs,
+                          PIX        *pixm,
+                          l_int32     x,
+                          l_int32     y,
+                          l_int32     factor)
+{
+l_int32     i, j, w, h, wm, hm, dm, wplg, wplm, val;
+l_uint32   *datag, *datam, *lineg, *linem;
+l_float32  *array;
+NUMA       *na;
+PIX        *pixg;
+
+    if (!pixm)
+        return pixGetGrayHistogram(pixs, factor);
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 8 && !pixGetColormap(pixs))
+        return (NUMA *)ERROR_PTR("pixs neither 8 bpp nor colormapped",
+                                 __func__, NULL);
+    pixGetDimensions(pixm, &wm, &hm, &dm);
+    if (dm != 1)
+        return (NUMA *)ERROR_PTR("pixm not 1 bpp", __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+
+    if ((na = numaCreate(256)) == NULL)
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    numaSetCount(na, 256);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    if (pixGetColormap(pixs))
+        pixg = pixRemoveColormap(pixs, REMOVE_CMAP_TO_GRAYSCALE);
+    else
+        pixg = pixClone(pixs);
+    pixGetDimensions(pixg, &w, &h, NULL);
+    datag = pixGetData(pixg);
+    wplg = pixGetWpl(pixg);
+    datam = pixGetData(pixm);
+    wplm = pixGetWpl(pixm);
+
+        /* Generate the histogram */
+    for (i = 0; i < hm; i += factor) {
+        if (y + i < 0 || y + i >= h) continue;
+        lineg = datag + (y + i) * wplg;
+        linem = datam + i * wplm;
+        for (j = 0; j < wm; j += factor) {
+            if (x + j < 0 || x + j >= w) continue;
+            if (GET_DATA_BIT(linem, j)) {
+                val = GET_DATA_BYTE(lineg, x + j);
+                array[val] += 1.0;
+            }
+        }
+    }
+
+    pixDestroy(&pixg);
+    return na;
+}
+
+
+/*!
+ * \brief   pixGetGrayHistogramInRect()
+ *
+ * \param[in]   pixs    8 bpp, or colormapped
+ * \param[in]   box     [optional] over which histogram is to be computed;
+ *                      use full image if NULL
+ * \param[in]   factor  subsampling factor; integer >= 1
+ * \return  na histogram, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs is cmapped, it is converted to 8 bpp gray.
+ *          If you want a histogram of the colormap indices, use
+ *          pixGetCmapHistogramInRect().
+ *      (2) This always returns a 256-value histogram of pixel values.
+ *      (3) Set the subsampling %factor > 1 to reduce the amount of computation.
+ * </pre>
+ */
+NUMA *
+pixGetGrayHistogramInRect(PIX     *pixs,
+                          BOX     *box,
+                          l_int32  factor)
+{
+l_int32     i, j, bx, by, bw, bh, w, h, wplg, val;
+l_uint32   *datag, *lineg;
+l_float32  *array;
+NUMA       *na;
+PIX        *pixg;
+
+    if (!box)
+        return pixGetGrayHistogram(pixs, factor);
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 8 && !pixGetColormap(pixs))
+        return (NUMA *)ERROR_PTR("pixs neither 8 bpp nor colormapped",
+                                 __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+
+    if ((na = numaCreate(256)) == NULL)
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    numaSetCount(na, 256);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    if (pixGetColormap(pixs))
+        pixg = pixRemoveColormap(pixs, REMOVE_CMAP_TO_GRAYSCALE);
+    else
+        pixg = pixClone(pixs);
+    pixGetDimensions(pixg, &w, &h, NULL);
+    datag = pixGetData(pixg);
+    wplg = pixGetWpl(pixg);
+    boxGetGeometry(box, &bx, &by, &bw, &bh);
+
+        /* Generate the histogram */
+    for (i = 0; i < bh; i += factor) {
+        if (by + i < 0 || by + i >= h) continue;
+        lineg = datag + (by + i) * wplg;
+        for (j = 0; j < bw; j += factor) {
+            if (bx + j < 0 || bx + j >= w) continue;
+            val = GET_DATA_BYTE(lineg, bx + j);
+            array[val] += 1.0;
+        }
+    }
+
+    pixDestroy(&pixg);
+    return na;
+}
+
+
+/*!
+ * \brief   pixGetGrayHistogramTiled()
+ *
+ * \param[in]   pixs     any depth, colormap OK
+ * \param[in]   factor   subsampling factor; integer >= 1
+ * \param[in]   nx, ny   tiling; >= 1; typically small
+ * \return  naa set of histograms, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs is cmapped, it is converted to 8 bpp gray.
+ *      (2) This returns a set of 256-value histograms of pixel values.
+ *      (3) Set the subsampling factor > 1 to reduce the amount of computation.
+ * </pre>
+ */
+NUMAA *
+pixGetGrayHistogramTiled(PIX     *pixs,
+                         l_int32  factor,
+                         l_int32  nx,
+                         l_int32  ny)
+{
+l_int32  i, n;
+NUMA    *na;
+NUMAA   *naa;
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+
+    if (!pixs)
+        return (NUMAA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (factor < 1)
+        return (NUMAA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+    if (nx < 1 || ny < 1)
+        return (NUMAA *)ERROR_PTR("nx and ny must both be > 0", __func__, NULL);
+
+    n = nx * ny;
+    if ((naa = numaaCreate(n)) == NULL)
+        return (NUMAA *)ERROR_PTR("naa not made", __func__, NULL);
+
+    pix1 = pixConvertTo8(pixs, FALSE);
+    pixa = pixaSplitPix(pix1, nx, ny, 0, 0);
+    for (i = 0; i < n; i++) {
+        pix2 = pixaGetPix(pixa, i, L_CLONE);
+        na = pixGetGrayHistogram(pix2, factor);
+        numaaAddNuma(naa, na, L_INSERT);
+        pixDestroy(&pix2);
+    }
+
+    pixDestroy(&pix1);
+    pixaDestroy(&pixa);
+    return naa;
+}
+
+
+/*!
+ * \brief   pixGetColorHistogram()
+ *
+ * \param[in]    pixs     rgb or colormapped
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[out]   pnar     red histogram
+ * \param[out]   pnag     green histogram
+ * \param[out]   pnab     blue histogram
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a set of three 256 entry histograms,
+ *          one for each color component (r,g,b).
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of computation.
+ * </pre>
+ */
+l_ok
+pixGetColorHistogram(PIX     *pixs,
+                     l_int32  factor,
+                     NUMA   **pnar,
+                     NUMA   **pnag,
+                     NUMA   **pnab)
+{
+l_int32     i, j, w, h, d, wpl, index, rval, gval, bval;
+l_uint32   *data, *line;
+l_float32  *rarray, *garray, *barray;
+NUMA       *nar, *nag, *nab;
+PIXCMAP    *cmap;
+
+    if (pnar) *pnar = NULL;
+    if (pnag) *pnag = NULL;
+    if (pnab) *pnab = NULL;
+    if (!pnar || !pnag || !pnab)
+        return ERROR_INT("&nar, &nag, &nab not all defined", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, &d);
+    cmap = pixGetColormap(pixs);
+    if (cmap && (d != 2 && d != 4 && d != 8))
+        return ERROR_INT("colormap and not 2, 4, or 8 bpp", __func__, 1);
+    if (!cmap && d != 32)
+        return ERROR_INT("no colormap and not rgb", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+
+        /* Set up the histogram arrays */
+    nar = numaCreate(256);
+    nag = numaCreate(256);
+    nab = numaCreate(256);
+    numaSetCount(nar, 256);
+    numaSetCount(nag, 256);
+    numaSetCount(nab, 256);
+    rarray = numaGetFArray(nar, L_NOCOPY);
+    garray = numaGetFArray(nag, L_NOCOPY);
+    barray = numaGetFArray(nab, L_NOCOPY);
+    *pnar = nar;
+    *pnag = nag;
+    *pnab = nab;
+
+        /* Generate the color histograms */
+    data = pixGetData(pixs);
+    wpl = pixGetWpl(pixs);
+    if (cmap) {
+        for (i = 0; i < h; i += factor) {
+            line = data + i * wpl;
+            for (j = 0; j < w; j += factor) {
+                if (d == 8)
+                    index = GET_DATA_BYTE(line, j);
+                else if (d == 4)
+                    index = GET_DATA_QBIT(line, j);
+                else   /* 2 bpp */
+                    index = GET_DATA_DIBIT(line, j);
+                pixcmapGetColor(cmap, index, &rval, &gval, &bval);
+                rarray[rval] += 1.0;
+                garray[gval] += 1.0;
+                barray[bval] += 1.0;
+            }
+        }
+    } else {  /* 32 bpp rgb */
+        for (i = 0; i < h; i += factor) {
+            line = data + i * wpl;
+            for (j = 0; j < w; j += factor) {
+                extractRGBValues(line[j], &rval, &gval, &bval);
+                rarray[rval] += 1.0;
+                garray[gval] += 1.0;
+                barray[bval] += 1.0;
+            }
+        }
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetColorHistogramMasked()
+ *
+ * \param[in]    pixs     32 bpp rgb, or colormapped
+ * \param[in]    pixm     [optional] 1 bpp mask over which histogram is
+ *                        to be computed; use all pixels if null
+ * \param[in]    x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                        can be < 0; these values are ignored if pixm is null
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[out]   pnar     red histogram
+ * \param[out]   pnag     green histogram
+ * \param[out]   pnab     blue histogram
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a set of three 256 entry histograms,
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of computation.
+ *      (3) Clipping of pixm (if it exists) to pixs is done in the inner loop.
+ *      (4) Input x,y are ignored unless pixm exists.
+ * </pre>
+ */
+l_ok
+pixGetColorHistogramMasked(PIX        *pixs,
+                           PIX        *pixm,
+                           l_int32     x,
+                           l_int32     y,
+                           l_int32     factor,
+                           NUMA      **pnar,
+                           NUMA      **pnag,
+                           NUMA      **pnab)
+{
+l_int32     i, j, w, h, d, wm, hm, dm, wpls, wplm, index, rval, gval, bval;
+l_uint32   *datas, *datam, *lines, *linem;
+l_float32  *rarray, *garray, *barray;
+NUMA       *nar, *nag, *nab;
+PIXCMAP    *cmap;
+
+    if (!pixm)
+        return pixGetColorHistogram(pixs, factor, pnar, pnag, pnab);
+
+    if (pnar) *pnar = NULL;
+    if (pnag) *pnag = NULL;
+    if (pnab) *pnab = NULL;
+    if (!pnar || !pnag || !pnab)
+        return ERROR_INT("&nar, &nag, &nab not all defined", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, &d);
+    cmap = pixGetColormap(pixs);
+    if (cmap && (d != 2 && d != 4 && d != 8))
+        return ERROR_INT("colormap and not 2, 4, or 8 bpp", __func__, 1);
+    if (!cmap && d != 32)
+        return ERROR_INT("no colormap and not rgb", __func__, 1);
+    pixGetDimensions(pixm, &wm, &hm, &dm);
+    if (dm != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+
+        /* Set up the histogram arrays */
+    nar = numaCreate(256);
+    nag = numaCreate(256);
+    nab = numaCreate(256);
+    numaSetCount(nar, 256);
+    numaSetCount(nag, 256);
+    numaSetCount(nab, 256);
+    rarray = numaGetFArray(nar, L_NOCOPY);
+    garray = numaGetFArray(nag, L_NOCOPY);
+    barray = numaGetFArray(nab, L_NOCOPY);
+    *pnar = nar;
+    *pnag = nag;
+    *pnab = nab;
+
+        /* Generate the color histograms */
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    datam = pixGetData(pixm);
+    wplm = pixGetWpl(pixm);
+    if (cmap) {
+        for (i = 0; i < hm; i += factor) {
+            if (y + i < 0 || y + i >= h) continue;
+            lines = datas + (y + i) * wpls;
+            linem = datam + i * wplm;
+            for (j = 0; j < wm; j += factor) {
+                if (x + j < 0 || x + j >= w) continue;
+                if (GET_DATA_BIT(linem, j)) {
+                    if (d == 8)
+                        index = GET_DATA_BYTE(lines, x + j);
+                    else if (d == 4)
+                        index = GET_DATA_QBIT(lines, x + j);
+                    else   /* 2 bpp */
+                        index = GET_DATA_DIBIT(lines, x + j);
+                    pixcmapGetColor(cmap, index, &rval, &gval, &bval);
+                    rarray[rval] += 1.0;
+                    garray[gval] += 1.0;
+                    barray[bval] += 1.0;
+                }
+            }
+        }
+    } else {  /* 32 bpp rgb */
+        for (i = 0; i < hm; i += factor) {
+            if (y + i < 0 || y + i >= h) continue;
+            lines = datas + (y + i) * wpls;
+            linem = datam + i * wplm;
+            for (j = 0; j < wm; j += factor) {
+                if (x + j < 0 || x + j >= w) continue;
+                if (GET_DATA_BIT(linem, j)) {
+                    extractRGBValues(lines[x + j], &rval, &gval, &bval);
+                    rarray[rval] += 1.0;
+                    garray[gval] += 1.0;
+                    barray[bval] += 1.0;
+                }
+            }
+        }
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetCmapHistogram()
+ *
+ * \param[in]   pixs    colormapped: d = 2, 4 or 8
+ * \param[in]   factor  subsampling factor; integer >= 1
+ * \return  na histogram of cmap indices, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a histogram of colormap pixel indices,
+ *          and is of size 2^d.
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of computation.
+ * </pre>
+ */
+NUMA *
+pixGetCmapHistogram(PIX     *pixs,
+                    l_int32  factor)
+{
+l_int32     i, j, w, h, d, wpl, val, size;
+l_uint32   *data, *line;
+l_float32  *array;
+NUMA       *na;
+
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetColormap(pixs) == NULL)
+        return (NUMA *)ERROR_PTR("pixs not cmapped", __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 2 && d != 4 && d != 8)
+        return (NUMA *)ERROR_PTR("d not 2, 4 or 8", __func__, NULL);
+
+    size = 1 << d;
+    if ((na = numaCreate(size)) == NULL)
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    numaSetCount(na, size);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    wpl = pixGetWpl(pixs);
+    data = pixGetData(pixs);
+    for (i = 0; i < h; i += factor) {
+        line = data + i * wpl;
+        for (j = 0; j < w; j += factor) {
+            if (d == 8)
+                val = GET_DATA_BYTE(line, j);
+            else if (d == 4)
+                val = GET_DATA_QBIT(line, j);
+            else  /* d == 2 */
+                val = GET_DATA_DIBIT(line, j);
+            array[val] += 1.0;
+        }
+    }
+
+    return na;
+}
+
+
+/*!
+ * \brief   pixGetCmapHistogramMasked()
+ *
+ * \param[in]   pixs     colormapped: d = 2, 4 or 8
+ * \param[in]   pixm     [optional] 1 bpp mask over which histogram is
+ *                       to be computed; use all pixels if null
+ * \param[in]   x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                       can be < 0; these values are ignored if pixm is null
+ * \param[in]   factor   subsampling factor; integer >= 1
+ * \return  na histogram, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a histogram of colormap pixel indices,
+ *          and is of size 2^d.
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of computation.
+ *      (3) Clipping of pixm to pixs is done in the inner loop.
+ * </pre>
+ */
+NUMA *
+pixGetCmapHistogramMasked(PIX     *pixs,
+                          PIX     *pixm,
+                          l_int32  x,
+                          l_int32  y,
+                          l_int32  factor)
+{
+l_int32     i, j, w, h, d, wm, hm, dm, wpls, wplm, val, size;
+l_uint32   *datas, *datam, *lines, *linem;
+l_float32  *array;
+NUMA       *na;
+
+    if (!pixm)
+        return pixGetCmapHistogram(pixs, factor);
+
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetColormap(pixs) == NULL)
+        return (NUMA *)ERROR_PTR("pixs not cmapped", __func__, NULL);
+    pixGetDimensions(pixm, &wm, &hm, &dm);
+    if (dm != 1)
+        return (NUMA *)ERROR_PTR("pixm not 1 bpp", __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 2 && d != 4 && d != 8)
+        return (NUMA *)ERROR_PTR("d not 2, 4 or 8", __func__, NULL);
+
+    size = 1 << d;
+    if ((na = numaCreate(size)) == NULL)
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    numaSetCount(na, size);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    datam = pixGetData(pixm);
+    wplm = pixGetWpl(pixm);
+
+    for (i = 0; i < hm; i += factor) {
+        if (y + i < 0 || y + i >= h) continue;
+        lines = datas + (y + i) * wpls;
+        linem = datam + i * wplm;
+        for (j = 0; j < wm; j += factor) {
+            if (x + j < 0 || x + j >= w) continue;
+            if (GET_DATA_BIT(linem, j)) {
+                if (d == 8)
+                    val = GET_DATA_BYTE(lines, x + j);
+                else if (d == 4)
+                    val = GET_DATA_QBIT(lines, x + j);
+                else  /* d == 2 */
+                    val = GET_DATA_DIBIT(lines, x + j);
+                array[val] += 1.0;
+            }
+        }
+    }
+
+    return na;
+}
+
+
+/*!
+ * \brief   pixGetCmapHistogramInRect()
+ *
+ * \param[in]   pixs     colormapped: d = 2, 4 or 8
+ * \param[in]   box      [optional] over which histogram is to be computed;
+ *                       use full image if NULL
+ * \param[in]   factor   subsampling factor; integer >= 1
+ * \return  na histogram, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a histogram of colormap pixel indices,
+ *          and is of size 2^d.
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of computation.
+ *      (3) Clipping to the box is done in the inner loop.
+ * </pre>
+ */
+NUMA *
+pixGetCmapHistogramInRect(PIX     *pixs,
+                          BOX     *box,
+                          l_int32  factor)
+{
+l_int32     i, j, bx, by, bw, bh, w, h, d, wpls, val, size;
+l_uint32   *datas, *lines;
+l_float32  *array;
+NUMA       *na;
+
+    if (!box)
+        return pixGetCmapHistogram(pixs, factor);
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetColormap(pixs) == NULL)
+        return (NUMA *)ERROR_PTR("pixs not cmapped", __func__, NULL);
+    if (factor < 1)
+        return (NUMA *)ERROR_PTR("sampling must be >= 1", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 2 && d != 4 && d != 8)
+        return (NUMA *)ERROR_PTR("d not 2, 4 or 8", __func__, NULL);
+
+    size = 1 << d;
+    if ((na = numaCreate(size)) == NULL)
+        return (NUMA *)ERROR_PTR("na not made", __func__, NULL);
+    numaSetCount(na, size);  /* all initialized to 0.0 */
+    array = numaGetFArray(na, L_NOCOPY);
+
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    boxGetGeometry(box, &bx, &by, &bw, &bh);
+
+    for (i = 0; i < bh; i += factor) {
+        if (by + i < 0 || by + i >= h) continue;
+        lines = datas + (by + i) * wpls;
+        for (j = 0; j < bw; j += factor) {
+            if (bx + j < 0 || bx + j >= w) continue;
+            if (d == 8)
+                val = GET_DATA_BYTE(lines, bx + j);
+            else if (d == 4)
+                val = GET_DATA_QBIT(lines, bx + j);
+            else  /* d == 2 */
+                val = GET_DATA_DIBIT(lines, bx + j);
+            array[val] += 1.0;
+        }
+    }
+
+    return na;
+}
+
+
+/*!
+ * \brief   pixCountRGBColorsByHash()
+ *
+ * \param[in]    pixs       rgb or rgba
+ * \param[out]   pncolors   number of colors found
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This is about 4x faster than pixCountRGBColors(),
+ *          which uses an ordered map.
+ * </pre>
+ */
+l_ok
+pixCountRGBColorsByHash(PIX      *pixs,
+                        l_int32  *pncolors)
+{
+L_DNA  *da1, *da2;
+
+    if (!pncolors)
+        return ERROR_INT("&ncolors not defined", __func__, 1);
+    *pncolors = 0;
+    if (!pixs || pixGetDepth(pixs) != 32)
+        return ERROR_INT("pixs not defined or not 32 bpp", __func__, 1);
+    da1 = pixConvertDataToDna(pixs);
+    l_dnaRemoveDupsByHmap(da1, &da2, NULL);
+    *pncolors = l_dnaGetCount(da2);
+    l_dnaDestroy(&da1);
+    l_dnaDestroy(&da2);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixCountRGBColors()
+ *
+ * \param[in]    pixs       rgb or rgba
+ * \param[in]    factor     subsampling factor; integer >= 1
+ * \param[out]   pncolors   number of colors found
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If %factor == 1, this gives the exact number of colors.
+ *      (2) This is about 4x slower than pixCountRGBColorsByHash().
+ * </pre>
+ */
+l_ok
+pixCountRGBColors(PIX      *pixs,
+                  l_int32   factor,
+                  l_int32  *pncolors)
+{
+L_AMAP  *amap;
+
+    if (!pncolors)
+        return ERROR_INT("&ncolors not defined", __func__, 1);
+    *pncolors = 0;
+    if (!pixs || pixGetDepth(pixs) != 32)
+        return ERROR_INT("pixs not defined or not 32 bpp", __func__, 1);
+    if (factor <= 0)
+        return ERROR_INT("factor must be > 0", __func__, 1);
+    amap = pixGetColorAmapHistogram(pixs, factor);
+    *pncolors = l_amapSize(amap);
+    l_amapDestroy(&amap);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetColorAmapHistogram()
+ *
+ * \param[in]   pixs    rgb or rgba
+ * \param[in]   factor  subsampling factor; integer >= 1
+ * \return  amap, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates an ordered map from pixel value to histogram count.
+ *      (2) Use amapGetCountForColor() to use the map to look up a count.
+ * </pre>
+ */
+L_AMAP  *
+pixGetColorAmapHistogram(PIX     *pixs,
+                         l_int32  factor)
+{
+l_int32    i, j, w, h, wpl;
+l_uint32  *data, *line;
+L_AMAP    *amap;
+RB_TYPE    key, value;
+RB_TYPE   *pval;
+
+    if (!pixs)
+        return (L_AMAP *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 32)
+        return (L_AMAP *)ERROR_PTR("pixs not 32 bpp", __func__, NULL);
+    if (factor <= 0)
+        return (L_AMAP *)ERROR_PTR("factor must be > 0", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, NULL);
+    data = pixGetData(pixs);
+    wpl = pixGetWpl(pixs);
+    amap = l_amapCreate(L_UINT_TYPE);
+    for (i = 0; i < h; i += factor) {
+        line = data + i * wpl;
+        for (j = 0; j < w; j += factor) {
+            key.utype = line[j];
+            pval = l_amapFind(amap, key);
+            if (!pval)
+                value.itype = 1;
+            else
+                value.itype = 1 + pval->itype;
+            l_amapInsert(amap, key, value);
+        }
+    }
+
+    return amap;
+}
+
+
+/*!
+ * \brief   amapGetCountForColor()
+ *
+ * \param[in]   amap   map from pixel value to count
+ * \param[in]   val    rgb or rgba pixel value
+ * \return  count, or -1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The ordered map is made by pixGetColorAmapHistogram().
+ * </pre>
+ */
+l_int32
+amapGetCountForColor(L_AMAP   *amap,
+                     l_uint32  val)
+{
+RB_TYPE   key;
+RB_TYPE  *pval;
+
+    if (!amap)
+        return ERROR_INT("amap not defined", __func__, -1);
+
+    key.utype = val;
+    pval = l_amapFind(amap, key);
+    return (pval) ? pval->itype : 0;
+}
+
+
+/*!
+ * \brief   pixGetRankValue()
+ *
+ * \param[in]    pixs     8 bpp, 32 bpp or colormapped
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[in]    rank     between 0.0 and 1.0; 1.0 is brightest, 0.0 is darkest
+ * \param[out]   pvalue   pixel value corresponding to input rank
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Simple function to get a rank value (color) of an image.
+ *          For a color image, the median value (rank = 0.5) can be
+ *          used to linearly remap the colors based on the median
+ *          of a target image, using pixLinearMapToTargetColor().
+ *      (2) For RGB, this treats each color component independently.
+ *          It calls pixGetGrayHistogramMasked() on each component, and
+ *          uses the returned gray histogram to get the rank value.
+ *          It then combines the 3 rank values into a color pixel.
+ * </pre>
+ */
+l_ok
+pixGetRankValue(PIX       *pixs,
+                l_int32    factor,
+                l_float32  rank,
+                l_uint32  *pvalue)
+{
+l_int32    d;
+l_float32  val, rval, gval, bval;
+PIX       *pixt;
+PIXCMAP   *cmap;
+
+    if (!pvalue)
+        return ERROR_INT("&value not defined", __func__, 1);
+    *pvalue = 0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    d = pixGetDepth(pixs);
+    cmap = pixGetColormap(pixs);
+    if (d != 8 && d != 32 && !cmap)
+        return ERROR_INT("pixs not 8 or 32 bpp, or cmapped", __func__, 1);
+    if (cmap)
+        pixt = pixRemoveColormap(pixs, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pixt = pixClone(pixs);
+    d = pixGetDepth(pixt);
+
+    if (d == 8) {
+        pixGetRankValueMasked(pixt, NULL, 0, 0, factor, rank, &val, NULL);
+        *pvalue = lept_roundftoi(val);
+    } else {
+        pixGetRankValueMaskedRGB(pixt, NULL, 0, 0, factor, rank,
+                                 &rval, &gval, &bval);
+        composeRGBPixel(lept_roundftoi(rval), lept_roundftoi(gval),
+                        lept_roundftoi(bval), pvalue);
+    }
+
+    pixDestroy(&pixt);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetRankValueMaskedRGB()
+ *
+ * \param[in]    pixs     32 bpp
+ * \param[in]    pixm     [optional] 1 bpp mask over which rank val is to be taken;
+ *                        use all pixels if null
+ * \param[in]    x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                        can be < 0; these values are ignored if pixm is null
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[in]    rank     between 0.0 and 1.0; 1.0 is brightest, 0.0 is darkest
+ * \param[out]   prval    [optional] red component val for input rank
+ * \param[out]   pgval    [optional] green component val for input rank
+ * \param[out]   pbval    [optional] blue component val for input rank
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Computes the rank component values of pixels in pixs that
+ *          are under the fg of the optional mask.  If the mask is null, it
+ *          computes the average of the pixels in pixs.
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of
+ *          computation.
+ *      (4) Input x,y are ignored unless pixm exists.
+ *      (5) The rank must be in [0.0 ... 1.0], where the brightest pixel
+ *          has rank 1.0.  For the median pixel value, use 0.5.
+ * </pre>
+ */
+l_ok
+pixGetRankValueMaskedRGB(PIX        *pixs,
+                         PIX        *pixm,
+                         l_int32     x,
+                         l_int32     y,
+                         l_int32     factor,
+                         l_float32   rank,
+                         l_float32  *prval,
+                         l_float32  *pgval,
+                         l_float32  *pbval)
+{
+l_float32  scale;
+PIX       *pixmt, *pixt;
+
+    if (prval) *prval = 0.0;
+    if (pgval) *pgval = 0.0;
+    if (pbval) *pbval = 0.0;
+    if (!prval && !pgval && !pbval)
+        return ERROR_INT("no results requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    if (pixGetDepth(pixs) != 32)
+        return ERROR_INT("pixs not 32 bpp", __func__, 1);
+    if (pixm && pixGetDepth(pixm) != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (rank < 0.0 || rank > 1.0)
+        return ERROR_INT("rank not in [0.0 ... 1.0]", __func__, 1);
+
+    pixmt = NULL;
+    if (pixm) {
+        scale = 1.0f / (l_float32)factor;
+        pixmt = pixScale(pixm, scale, scale);
+    }
+    if (prval) {
+        pixt = pixScaleRGBToGrayFast(pixs, factor, COLOR_RED);
+        pixGetRankValueMasked(pixt, pixmt, x / factor, y / factor,
+                              factor, rank, prval, NULL);
+        pixDestroy(&pixt);
+    }
+    if (pgval) {
+        pixt = pixScaleRGBToGrayFast(pixs, factor, COLOR_GREEN);
+        pixGetRankValueMasked(pixt, pixmt, x / factor, y / factor,
+                              factor, rank, pgval, NULL);
+        pixDestroy(&pixt);
+    }
+    if (pbval) {
+        pixt = pixScaleRGBToGrayFast(pixs, factor, COLOR_BLUE);
+        pixGetRankValueMasked(pixt, pixmt, x / factor, y / factor,
+                              factor, rank, pbval, NULL);
+        pixDestroy(&pixt);
+    }
+    pixDestroy(&pixmt);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetRankValueMasked()
+ *
+ * \param[in]    pixs     8 bpp, or colormapped
+ * \param[in]    pixm     [optional] 1 bpp mask, over which the rank val
+ *                        is to be taken; use all pixels if null
+ * \param[in]    x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                        can be < 0; these values are ignored if pixm is null
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[in]    rank     between 0.0 and 1.0; 1.0 is brightest, 0.0 is darkest
+ * \param[out]   pval     pixel value corresponding to input rank
+ * \param[out]   pna     [optional] of histogram
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Computes the rank value of pixels in pixs that are under
+ *          the fg of the optional mask.  If the mask is null, it
+ *          computes the average of the pixels in pixs.
+ *      (2) Set the subsampling %factor > 1 to reduce the amount of
+ *          computation.
+ *      (3) Clipping of pixm (if it exists) to pixs is done in the inner loop.
+ *      (4) Input x,y are ignored unless pixm exists.
+ *      (5) The rank must be in [0.0 ... 1.0], where the brightest pixel
+ *          has rank 1.0.  For the median pixel value, use 0.5.
+ *      (6) The histogram can optionally be returned, so that other rank
+ *          values can be extracted without recomputing the histogram.
+ *          In that case, just use
+ *              numaHistogramGetValFromRank(na, rank, &val);
+ *          on the returned Numa for additional rank values.
+ * </pre>
+ */
+l_ok
+pixGetRankValueMasked(PIX        *pixs,
+                      PIX        *pixm,
+                      l_int32     x,
+                      l_int32     y,
+                      l_int32     factor,
+                      l_float32   rank,
+                      l_float32  *pval,
+                      NUMA      **pna)
+{
+NUMA  *na;
+
+    if (pna) *pna = NULL;
+    if (!pval)
+        return ERROR_INT("&val not defined", __func__, 1);
+    *pval = 0.0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    if (pixGetDepth(pixs) != 8 && !pixGetColormap(pixs))
+        return ERROR_INT("pixs neither 8 bpp nor colormapped", __func__, 1);
+    if (pixm && pixGetDepth(pixm) != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (rank < 0.0 || rank > 1.0)
+        return ERROR_INT("rank not in [0.0 ... 1.0]", __func__, 1);
+
+    if ((na = pixGetGrayHistogramMasked(pixs, pixm, x, y, factor)) == NULL)
+        return ERROR_INT("na not made", __func__, 1);
+    numaHistogramGetValFromRank(na, rank, pval);
+    if (pna)
+        *pna = na;
+    else
+        numaDestroy(&na);
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetPixelAverage()
+ *
+ * \param[in]    pixs     8 or 32 bpp, or colormapped
+ * \param[in]    pixm     [optional] 1 bpp mask over which average is
+ *                        to be taken; use all pixels if null
+ * \param[in]    x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                        can be < 0
+ * \param[in]    factor   subsampling factor; >= 1
+ * \param[out]   pval     average pixel value
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For rgb pix, this is a more direct computation of the
+ *          average value of the pixels in %pixs that are under the
+ *          mask %pixm. It is faster than pixGetPixelStats(), which
+ *          calls pixGetAverageMaskedRGB() and has the overhead of
+ *          generating a temporary pix of each of the three components;
+ *          this can take most of the time if %factor > 1.
+ *      (2) If %pixm is null, this gives the average value of all
+ *          pixels in %pixs.  The returned value is an integer.
+ *      (3) For color %pixs, the returned pixel value is in the standard
+ *          uint32 RGBA packing.
+ *      (4) Clipping of pixm (if it exists) to pixs is done in the inner loop.
+ *      (5) Input x,y are ignored if %pixm does not exist.
+ *      (6) For general averaging of 1, 2, 4 or 8 bpp grayscale, use
+ *          pixAverageInRect().
+ * </pre>
+ */
+l_ok
+pixGetPixelAverage(PIX       *pixs,
+                   PIX       *pixm,
+                   l_int32    x,
+                   l_int32    y,
+                   l_int32    factor,
+                   l_uint32  *pval)
+{
+l_int32    i, j, w, h, d, wm, hm, wpl1, wplm, val, rval, gval, bval, count;
+l_uint32  *data1, *datam, *line1, *linem;
+l_float64  sum, rsum, gsum, bsum;
+PIX       *pix1;
+
+    if (!pval)
+        return ERROR_INT("&val not defined", __func__, 1);
+    *pval = 0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    d = pixGetDepth(pixs);
+    if (d != 32 && !pixGetColormap(pixs))
+        return ERROR_INT("pixs not rgb or colormapped", __func__, 1);
+    if (pixm && pixGetDepth(pixm) != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+
+    if (pixGetColormap(pixs))
+        pix1 = pixRemoveColormap(pixs, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pix1 = pixClone(pixs);
+    pixGetDimensions(pix1, &w, &h, &d);
+    if (d == 1) {
+        pixDestroy(&pix1);
+        return ERROR_INT("pix1 is just 1 bpp", __func__, 1);
+    }
+    data1 = pixGetData(pix1);
+    wpl1 = pixGetWpl(pix1);
+
+    sum = rsum = gsum = bsum = 0.0;
+    count = 0;
+    if (!pixm) {
+        for (i = 0; i < h; i += factor) {
+            line1 = data1 + i * wpl1;
+            for (j = 0; j < w; j += factor) {
+                if (d == 8) {
+                    val = GET_DATA_BYTE(line1, j);
+                    sum += val;
+                } else {  /* rgb */
+                    extractRGBValues(*(line1 + j), &rval, &gval, &bval);
+                    rsum += rval;
+                    gsum += gval;
+                    bsum += bval;
+                }
+                count++;
+            }
+        }
+    } else {  /* masked */
+        pixGetDimensions(pixm, &wm, &hm, NULL);
+        datam = pixGetData(pixm);
+        wplm = pixGetWpl(pixm);
+        for (i = 0; i < hm; i += factor) {
+            if (y + i < 0 || y + i >= h) continue;
+            line1 = data1 + (y + i) * wpl1;
+            linem = datam + i * wplm;
+            for (j = 0; j < wm; j += factor) {
+                if (x + j < 0 || x + j >= w) continue;
+                if (GET_DATA_BIT(linem, j)) {
+                    if (d == 8) {
+                        val = GET_DATA_BYTE(line1, x + j);
+                        sum += val;
+                    } else {  /* rgb */
+                        extractRGBValues(*(line1 + x + j), &rval, &gval, &bval);
+                        rsum += rval;
+                        gsum += gval;
+                        bsum += bval;
+                    }
+                    count++;
+                }
+            }
+        }
+    }
+
+    pixDestroy(&pix1);
+    if (count == 0)
+        return ERROR_INT("no pixels sampled", __func__, 1);
+    if (d == 8) {
+        *pval = (l_uint32)(sum / (l_float64)count);
+    } else {  /* d == 32 */
+        rval = (l_uint32)(rsum / (l_float64)count);
+        gval = (l_uint32)(gsum / (l_float64)count);
+        bval = (l_uint32)(bsum / (l_float64)count);
+        composeRGBPixel(rval, gval, bval, pval);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetPixelStats()
+ *
+ * \param[in]    pixs     8 bpp, 32 bpp or colormapped
+ * \param[in]    factor   subsampling factor; integer >= 1
+ * \param[in]    type     L_MEAN_ABSVAL, L_ROOT_MEAN_SQUARE,
+ *                        L_STANDARD_DEVIATION, L_VARIANCE
+ * \param[out]   pvalue   pixel value corresponding to input type
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Simple function to get one of four statistical values of an image.
+ *      (2) It does not take a mask: it uses the entire image.
+ *      (3) To get the average pixel value of an RGB image, suggest using
+ *          pixGetPixelAverage(), which is considerably faster.
+ * </pre>
+ */
+l_ok
+pixGetPixelStats(PIX       *pixs,
+                 l_int32    factor,
+                 l_int32    type,
+                 l_uint32  *pvalue)
+{
+l_int32    d;
+l_float32  val, rval, gval, bval;
+PIX       *pixt;
+PIXCMAP   *cmap;
+
+    if (!pvalue)
+        return ERROR_INT("&value not defined", __func__, 1);
+    *pvalue = 0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    d = pixGetDepth(pixs);
+    cmap = pixGetColormap(pixs);
+    if (d != 8 && d != 32 && !cmap)
+        return ERROR_INT("pixs not 8 or 32 bpp, or cmapped", __func__, 1);
+    if (cmap)
+        pixt = pixRemoveColormap(pixs, REMOVE_CMAP_BASED_ON_SRC);
+    else
+        pixt = pixClone(pixs);
+    d = pixGetDepth(pixt);
+
+    if (d == 8) {
+        pixGetAverageMasked(pixt, NULL, 0, 0, factor, type, &val);
+        *pvalue = lept_roundftoi(val);
+    } else {
+        pixGetAverageMaskedRGB(pixt, NULL, 0, 0, factor, type,
+                               &rval, &gval, &bval);
+        composeRGBPixel(lept_roundftoi(rval), lept_roundftoi(gval),
+                        lept_roundftoi(bval), pvalue);
+    }
+
+    pixDestroy(&pixt);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetAverageMaskedRGB()
+ *
+ * \param[in]    pixs     32 bpp, or colormapped
+ * \param[in]    pixm     [optional] 1 bpp mask over which average is
+ *                        to be taken; use all pixels if null
+ * \param[in]    x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                        can be < 0
+ * \param[in]    factor   subsampling factor; >= 1
+ * \param[in]    type     L_MEAN_ABSVAL, L_ROOT_MEAN_SQUARE,
+ *                        L_STANDARD_DEVIATION, L_VARIANCE
+ * \param[out]   prval    [optional] measured red value of given 'type'
+ * \param[out]   pgval    [optional] measured green value of given 'type'
+ * \param[out]   pbval    [optional] measured blue value of given 'type'
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For usage, see pixGetAverageMasked().
+ *      (2) If there is a colormap, it is removed before the 8 bpp
+ *          component images are extracted.
+ *      (3) A better name for this would be: pixGetPixelStatsRGB()
+ * </pre>
+ */
+l_ok
+pixGetAverageMaskedRGB(PIX        *pixs,
+                       PIX        *pixm,
+                       l_int32     x,
+                       l_int32     y,
+                       l_int32     factor,
+                       l_int32     type,
+                       l_float32  *prval,
+                       l_float32  *pgval,
+                       l_float32  *pbval)
+{
+l_int32   empty;
+PIX      *pixt;
+PIXCMAP  *cmap;
+
+    if (prval) *prval = 0.0;
+    if (pgval) *pgval = 0.0;
+    if (pbval) *pbval = 0.0;
+    if (!prval && !pgval && !pbval)
+        return ERROR_INT("no values requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    cmap = pixGetColormap(pixs);
+    if (pixGetDepth(pixs) != 32 && !cmap)
+        return ERROR_INT("pixs neither 32 bpp nor colormapped", __func__, 1);
+    if (pixm && pixGetDepth(pixm) != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (type != L_MEAN_ABSVAL && type != L_ROOT_MEAN_SQUARE &&
+        type != L_STANDARD_DEVIATION && type != L_VARIANCE)
+        return ERROR_INT("invalid measure type", __func__, 1);
+    if (pixm) {
+        pixZero(pixm, &empty);
+        if (empty)
+            return ERROR_INT("empty mask", __func__, 1);
+    }
+
+    if (prval) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_RED);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_RED);
+        pixGetAverageMasked(pixt, pixm, x, y, factor, type, prval);
+        pixDestroy(&pixt);
+    }
+    if (pgval) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_GREEN);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_GREEN);
+        pixGetAverageMasked(pixt, pixm, x, y, factor, type, pgval);
+        pixDestroy(&pixt);
+    }
+    if (pbval) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_BLUE);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_BLUE);
+        pixGetAverageMasked(pixt, pixm, x, y, factor, type, pbval);
+        pixDestroy(&pixt);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetAverageMasked()
+ *
+ * \param[in]   pixs     8 or 16 bpp, or colormapped
+ * \param[in]   pixm     [optional] 1 bpp mask over which average is
+ *                       to be taken; use all pixels if null
+ * \param[in]   x, y     UL corner of pixm relative to the UL corner of pixs;
+ *                       can be < 0
+ * \param[in]   factor   subsampling factor; >= 1
+ * \param[in]   type     L_MEAN_ABSVAL, L_ROOT_MEAN_SQUARE,
+ *                       L_STANDARD_DEVIATION, L_VARIANCE
+ * \param[out]  pval     measured value of given 'type'
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Use L_MEAN_ABSVAL to get the average value of pixels in pixs
+ *          that are under the fg of the optional mask.  If the mask
+ *          is null, it finds the average of the pixels in pixs.
+ *      (2) Likewise, use L_ROOT_MEAN_SQUARE to get the rms value of
+ *          pixels in pixs, either masked or not; L_STANDARD_DEVIATION
+ *          to get the standard deviation from the mean of the pixels;
+ *          L_VARIANCE to get the average squared difference from the
+ *          expected value.  The variance is the square of the stdev.
+ *          For the standard deviation, we use
+ *              sqrt([([x] - x)]^2) = sqrt([x^2] - [x]^2)
+ *      (3) Set the subsampling %factor > 1 to reduce the amount of
+ *          computation.
+ *      (4) Clipping of pixm (if it exists) to pixs is done in the inner loop.
+ *      (5) Input x,y are ignored unless pixm exists.
+ *      (6) A better name for this would be: pixGetPixelStatsGray()
+ * </pre>
+ */
+l_ok
+pixGetAverageMasked(PIX        *pixs,
+                    PIX        *pixm,
+                    l_int32     x,
+                    l_int32     y,
+                    l_int32     factor,
+                    l_int32     type,
+                    l_float32  *pval)
+{
+l_int32    i, j, w, h, d, wm, hm, wplg, wplm, val, count, empty;
+l_uint32  *datag, *datam, *lineg, *linem;
+l_float64  sumave, summs, ave, meansq, var;
+PIX       *pixg;
+
+    if (!pval)
+        return ERROR_INT("&val not defined", __func__, 1);
+    *pval = 0.0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    d = pixGetDepth(pixs);
+    if (d != 8 && d != 16 && !pixGetColormap(pixs))
+        return ERROR_INT("pixs not 8 or 16 bpp or colormapped", __func__, 1);
+    if (pixm && pixGetDepth(pixm) != 1)
+        return ERROR_INT("pixm not 1 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (type != L_MEAN_ABSVAL && type != L_ROOT_MEAN_SQUARE &&
+        type != L_STANDARD_DEVIATION && type != L_VARIANCE)
+        return ERROR_INT("invalid measure type", __func__, 1);
+    if (pixm) {
+        pixZero(pixm, &empty);
+        if (empty)
+            return ERROR_INT("empty mask", __func__, 1);
+    }
+
+    if (pixGetColormap(pixs))
+        pixg = pixRemoveColormap(pixs, REMOVE_CMAP_TO_GRAYSCALE);
+    else
+        pixg = pixClone(pixs);
+    pixGetDimensions(pixg, &w, &h, &d);
+    datag = pixGetData(pixg);
+    wplg = pixGetWpl(pixg);
+
+    sumave = summs = 0.0;
+    count = 0;
+    if (!pixm) {
+        for (i = 0; i < h; i += factor) {
+            lineg = datag + i * wplg;
+            for (j = 0; j < w; j += factor) {
+                if (d == 8)
+                    val = GET_DATA_BYTE(lineg, j);
+                else  /* d == 16 */
+                    val = GET_DATA_TWO_BYTES(lineg, j);
+                if (type != L_ROOT_MEAN_SQUARE)
+                    sumave += val;
+                if (type != L_MEAN_ABSVAL)
+                    summs += (l_float64)(val) * val;
+                count++;
+            }
+        }
+    } else {
+        pixGetDimensions(pixm, &wm, &hm, NULL);
+        datam = pixGetData(pixm);
+        wplm = pixGetWpl(pixm);
+        for (i = 0; i < hm; i += factor) {
+            if (y + i < 0 || y + i >= h) continue;
+            lineg = datag + (y + i) * wplg;
+            linem = datam + i * wplm;
+            for (j = 0; j < wm; j += factor) {
+                if (x + j < 0 || x + j >= w) continue;
+                if (GET_DATA_BIT(linem, j)) {
+                    if (d == 8)
+                        val = GET_DATA_BYTE(lineg, x + j);
+                    else  /* d == 16 */
+                        val = GET_DATA_TWO_BYTES(lineg, x + j);
+                    if (type != L_ROOT_MEAN_SQUARE)
+                        sumave += val;
+                    if (type != L_MEAN_ABSVAL)
+                        summs += (l_float64)(val) * val;
+                    count++;
+                }
+            }
+        }
+    }
+
+    pixDestroy(&pixg);
+    if (count == 0)
+        return ERROR_INT("no pixels sampled", __func__, 1);
+    ave = sumave / (l_float64)count;
+    meansq = summs / (l_float64)count;
+    var = meansq - ave * ave;
+    if (type == L_MEAN_ABSVAL)
+        *pval = (l_float32)ave;
+    else if (type == L_ROOT_MEAN_SQUARE)
+        *pval = (l_float32)sqrt(meansq);
+    else if (type == L_STANDARD_DEVIATION)
+        *pval = (l_float32)sqrt(var);
+    else  /* type == L_VARIANCE */
+        *pval = (l_float32)var;
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetAverageTiledRGB()
+ *
+ * \param[in]   pixs     32 bpp, or colormapped
+ * \param[in]   sx, sy   tile size; must be at least 2 x 2
+ * \param[in]   type     L_MEAN_ABSVAL, L_ROOT_MEAN_SQUARE, L_STANDARD_DEVIATION
+ * \param[out]  ppixr    [optional] tiled 'average' of red component
+ * \param[out]  ppixg    [optional] tiled 'average' of green component
+ * \param[out]  ppixb    [optional] tiled 'average' of blue component
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For usage, see pixGetAverageTiled().
+ *      (2) If there is a colormap, it is removed before the 8 bpp
+ *          component images are extracted.
+ * </pre>
+ */
+l_ok
+pixGetAverageTiledRGB(PIX     *pixs,
+                      l_int32  sx,
+                      l_int32  sy,
+                      l_int32  type,
+                      PIX    **ppixr,
+                      PIX    **ppixg,
+                      PIX    **ppixb)
+{
+PIX      *pixt;
+PIXCMAP  *cmap;
+
+    if (ppixr) *ppixr = NULL;
+    if (ppixg) *ppixg = NULL;
+    if (ppixb) *ppixb = NULL;
+    if (!ppixr && !ppixg && !ppixb)
+        return ERROR_INT("no data requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    cmap = pixGetColormap(pixs);
+    if (pixGetDepth(pixs) != 32 && !cmap)
+        return ERROR_INT("pixs neither 32 bpp nor colormapped", __func__, 1);
+    if (sx < 2 || sy < 2)
+        return ERROR_INT("sx and sy not both > 1", __func__, 1);
+    if (type != L_MEAN_ABSVAL && type != L_ROOT_MEAN_SQUARE &&
+        type != L_STANDARD_DEVIATION)
+        return ERROR_INT("invalid measure type", __func__, 1);
+
+    if (ppixr) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_RED);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_RED);
+        *ppixr = pixGetAverageTiled(pixt, sx, sy, type);
+        pixDestroy(&pixt);
+    }
+    if (ppixg) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_GREEN);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_GREEN);
+        *ppixg = pixGetAverageTiled(pixt, sx, sy, type);
+        pixDestroy(&pixt);
+    }
+    if (ppixb) {
+        if (cmap)
+            pixt = pixGetRGBComponentCmap(pixs, COLOR_BLUE);
+        else
+            pixt = pixGetRGBComponent(pixs, COLOR_BLUE);
+        *ppixb = pixGetAverageTiled(pixt, sx, sy, type);
+        pixDestroy(&pixt);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetAverageTiled()
+ *
+ * \param[in]   pixs    8 bpp, or colormapped
+ * \param[in]   sx, sy  tile size; must be at least 2 x 2
+ * \param[in]   type    L_MEAN_ABSVAL, L_ROOT_MEAN_SQUARE, L_STANDARD_DEVIATION
+ * \return  pixd average values in each tile, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Only computes for tiles that are entirely contained in pixs.
+ *      (2) Use L_MEAN_ABSVAL to get the average abs value within the tile;
+ *          L_ROOT_MEAN_SQUARE to get the rms value within each tile;
+ *          L_STANDARD_DEVIATION to get the standard dev. from the average
+ *          within each tile.
+ *      (3) If colormapped, converts to 8 bpp gray.
+ * </pre>
+ */
+PIX *
+pixGetAverageTiled(PIX     *pixs,
+                   l_int32  sx,
+                   l_int32  sy,
+                   l_int32  type)
+{
+l_int32    i, j, k, m, w, h, wd, hd, d, pos, wplt, wpld, valt;
+l_uint32  *datat, *datad, *linet, *lined, *startt;
+l_float64  sumave, summs, ave, meansq, normfact;
+PIX       *pixt, *pixd;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 8 && !pixGetColormap(pixs))
+        return (PIX *)ERROR_PTR("pixs not 8 bpp or cmapped", __func__, NULL);
+    if (sx < 2 || sy < 2)
+        return (PIX *)ERROR_PTR("sx and sy not both > 1", __func__, NULL);
+    wd = w / sx;
+    hd = h / sy;
+    if (wd < 1 || hd < 1)
+        return (PIX *)ERROR_PTR("wd or hd == 0", __func__, NULL);
+    if (type != L_MEAN_ABSVAL && type != L_ROOT_MEAN_SQUARE &&
+        type != L_STANDARD_DEVIATION)
+        return (PIX *)ERROR_PTR("invalid measure type", __func__, NULL);
+
+    pixt = pixRemoveColormap(pixs, REMOVE_CMAP_TO_GRAYSCALE);
+    pixd = pixCreate(wd, hd, 8);
+    datat = pixGetData(pixt);
+    wplt = pixGetWpl(pixt);
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    normfact = 1. / (l_float64)(sx * sy);
+    for (i = 0; i < hd; i++) {
+        lined = datad + i * wpld;
+        linet = datat + i * sy * wplt;
+        for (j = 0; j < wd; j++) {
+            if (type == L_MEAN_ABSVAL || type == L_STANDARD_DEVIATION) {
+                sumave = 0.0;
+                for (k = 0; k < sy; k++) {
+                    startt = linet + k * wplt;
+                    for (m = 0; m < sx; m++) {
+                        pos = j * sx + m;
+                        valt = GET_DATA_BYTE(startt, pos);
+                        sumave += valt;
+                    }
+                }
+                ave = normfact * sumave;
+            }
+            if (type == L_ROOT_MEAN_SQUARE || type == L_STANDARD_DEVIATION) {
+                summs = 0.0;
+                for (k = 0; k < sy; k++) {
+                    startt = linet + k * wplt;
+                    for (m = 0; m < sx; m++) {
+                        pos = j * sx + m;
+                        valt = GET_DATA_BYTE(startt, pos);
+                        summs += (l_float64)(valt) * valt;
+                    }
+                }
+                meansq = normfact * summs;
+            }
+            if (type == L_MEAN_ABSVAL)
+                valt = (l_int32)(ave + 0.5);
+            else if (type == L_ROOT_MEAN_SQUARE)
+                valt = (l_int32)(sqrt(meansq) + 0.5);
+            else  /* type == L_STANDARD_DEVIATION */
+                valt = (l_int32)(sqrt(meansq - ave * ave) + 0.5);
+            SET_DATA_BYTE(lined, j, valt);
+        }
+    }
+
+    pixDestroy(&pixt);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixRowStats()
+ *
+ * \param[in]    pixs          8 bpp; not cmapped
+ * \param[in]    box           [optional] clipping box; can be null
+ * \param[out]   pnamean       [optional] numa of mean values
+ * \param[out]   pnamedian     [optional] numa of median values
+ * \param[out]   pnamode       [optional] numa of mode intensity values
+ * \param[out]   pnamodecount  [optional] numa of mode counts
+ * \param[out]   pnavar        [optional] numa of variance
+ * \param[out]   pnarootvar    [optional] numa of square root of variance
+ * \return  na numa of requested statistic for each row, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes numas that represent column vectors of statistics,
+ *          with each of its values derived from the corresponding row of a Pix.
+ *      (2) Use NULL on input to prevent computation of any of the 5 numas.
+ *      (3) Other functions that compute pixel row statistics are:
+ *             pixCountPixelsByRow()
+ *             pixAverageByRow()
+ *             pixVarianceByRow()
+ *             pixGetRowStats()
+ * </pre>
+ */
+l_int32
+pixRowStats(PIX    *pixs,
+            BOX    *box,
+            NUMA  **pnamean,
+            NUMA  **pnamedian,
+            NUMA  **pnamode,
+            NUMA  **pnamodecount,
+            NUMA  **pnavar,
+            NUMA  **pnarootvar)
+{
+l_int32     i, j, k, w, h, val, wpls, sum, sumsq, target, max, modeval;
+l_int32     xstart, xend, ystart, yend, bw, bh;
+l_int32    *histo;
+l_uint32   *lines, *datas;
+l_float32   norm;
+l_float32  *famean, *fameansq, *favar, *farootvar;
+l_float32  *famedian, *famode, *famodecount;
+
+    if (pnamean) *pnamean = NULL;
+    if (pnamedian) *pnamedian = NULL;
+    if (pnamode) *pnamode = NULL;
+    if (pnamodecount) *pnamodecount = NULL;
+    if (pnavar) *pnavar = NULL;
+    if (pnarootvar) *pnarootvar = NULL;
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return ERROR_INT("pixs undefined or not 8 bpp", __func__, 1);
+    famean = fameansq = favar = farootvar = NULL;
+    famedian = famode = famodecount = NULL;
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (boxClipToRectangleParams(box, w, h, &xstart, &ystart, &xend, &yend,
+                                 &bw, &bh) == 1)
+        return ERROR_INT("invalid clipping box", __func__, 1);
+
+        /* We need the mean for variance and root variance */
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    if (pnamean || pnavar || pnarootvar) {
+        norm = 1.f / (l_float32)bw;
+        famean = (l_float32 *)LEPT_CALLOC(bh, sizeof(l_float32));
+        fameansq = (l_float32 *)LEPT_CALLOC(bh, sizeof(l_float32));
+        if (pnavar || pnarootvar) {
+            favar = (l_float32 *)LEPT_CALLOC(bh, sizeof(l_float32));
+            if (pnarootvar)
+                farootvar = (l_float32 *)LEPT_CALLOC(bh, sizeof(l_float32));
+        }
+        for (i = ystart; i < yend; i++) {
+            sum = sumsq = 0;
+            lines = datas + i * wpls;
+            for (j = xstart; j < xend; j++) {
+                val = GET_DATA_BYTE(lines, j);
+                sum += val;
+                sumsq += val * val;
+            }
+            famean[i] = norm * sum;
+            fameansq[i] = norm * sumsq;
+            if (pnavar || pnarootvar) {
+                favar[i] = fameansq[i] - famean[i] * famean[i];
+                if (pnarootvar)
+                    farootvar[i] = sqrtf(favar[i]);
+            }
+        }
+        LEPT_FREE(fameansq);
+        if (pnamean)
+            *pnamean = numaCreateFromFArray(famean, bh, L_INSERT);
+        else
+            LEPT_FREE(famean);
+        if (pnavar)
+            *pnavar = numaCreateFromFArray(favar, bh, L_INSERT);
+        else
+            LEPT_FREE(favar);
+        if (pnarootvar)
+            *pnarootvar = numaCreateFromFArray(farootvar, bh, L_INSERT);
+    }
+
+        /* We need a histogram to find the median and/or mode values */
+    if (pnamedian || pnamode || pnamodecount) {
+        histo = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+        if (pnamedian) {
+            *pnamedian = numaMakeConstant(0, bh);
+            famedian = numaGetFArray(*pnamedian, L_NOCOPY);
+        }
+        if (pnamode) {
+            *pnamode = numaMakeConstant(0, bh);
+            famode = numaGetFArray(*pnamode, L_NOCOPY);
+        }
+        if (pnamodecount) {
+            *pnamodecount = numaMakeConstant(0, bh);
+            famodecount = numaGetFArray(*pnamodecount, L_NOCOPY);
+        }
+        for (i = ystart; i < yend; i++) {
+            lines = datas + i * wpls;
+            memset(histo, 0, 1024);
+            for (j = xstart; j < xend; j++) {
+                val = GET_DATA_BYTE(lines, j);
+                histo[val]++;
+            }
+
+            if (pnamedian) {
+                sum = 0;
+                target = (bw + 1) / 2;
+                for (k = 0; k < 256; k++) {
+                    sum += histo[k];
+                    if (sum >= target) {
+                        famedian[i] = k;
+                        break;
+                    }
+                }
+            }
+
+            if (pnamode || pnamodecount) {
+                max = 0;
+                modeval = 0;
+                for (k = 0; k < 256; k++) {
+                    if (histo[k] > max) {
+                        max = histo[k];
+                        modeval = k;
+                    }
+                }
+                if (pnamode)
+                    famode[i] = modeval;
+                if (pnamodecount)
+                    famodecount[i] = max;
+            }
+        }
+        LEPT_FREE(histo);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixColumnStats()
+ *
+ * \param[in]    pixs          8 bpp; not cmapped
+ * \param[in]    box           [optional] clipping box; can be null
+ * \param[out]   pnamean       [optional] numa of mean values
+ * \param[out]   pnamedian     [optional] numa of median values
+ * \param[out]   pnamode       [optional] numa of mode intensity values
+ * \param[out]   pnamodecount  [optional] numa of mode counts
+ * \param[out]   pnavar        [optional] numa of variance
+ * \param[out]   pnarootvar    [optional] numa of square root of variance
+ * \return  na numa of requested statistic for each column,
+ *                  or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes numas that represent row vectors of statistics,
+ *          with each of its values derived from the corresponding col of a Pix.
+ *      (2) Use NULL on input to prevent computation of any of the 5 numas.
+ *      (3) Other functions that compute pixel column statistics are:
+ *             pixCountPixelsByColumn()
+ *             pixAverageByColumn()
+ *             pixVarianceByColumn()
+ *             pixGetColumnStats()
+ * </pre>
+ */
+l_int32
+pixColumnStats(PIX    *pixs,
+               BOX    *box,
+               NUMA  **pnamean,
+               NUMA  **pnamedian,
+               NUMA  **pnamode,
+               NUMA  **pnamodecount,
+               NUMA  **pnavar,
+               NUMA  **pnarootvar)
+{
+l_int32     i, j, k, w, h, val, wpls, sum, sumsq, target, max, modeval;
+l_int32     xstart, xend, ystart, yend, bw, bh;
+l_int32    *histo;
+l_uint32   *lines, *datas;
+l_float32   norm;
+l_float32  *famean, *fameansq, *favar, *farootvar;
+l_float32  *famedian, *famode, *famodecount;
+
+    if (pnamean) *pnamean = NULL;
+    if (pnamedian) *pnamedian = NULL;
+    if (pnamode) *pnamode = NULL;
+    if (pnamodecount) *pnamodecount = NULL;
+    if (pnavar) *pnavar = NULL;
+    if (pnarootvar) *pnarootvar = NULL;
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return ERROR_INT("pixs undefined or not 8 bpp", __func__, 1);
+    famean = fameansq = favar = farootvar = NULL;
+    famedian = famode = famodecount = NULL;
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (boxClipToRectangleParams(box, w, h, &xstart, &ystart, &xend, &yend,
+                                 &bw, &bh) == 1)
+        return ERROR_INT("invalid clipping box", __func__, 1);
+
+        /* We need the mean for variance and root variance */
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    if (pnamean || pnavar || pnarootvar) {
+        norm = 1.f / (l_float32)bh;
+        famean = (l_float32 *)LEPT_CALLOC(bw, sizeof(l_float32));
+        fameansq = (l_float32 *)LEPT_CALLOC(bw, sizeof(l_float32));
+        if (pnavar || pnarootvar) {
+            favar = (l_float32 *)LEPT_CALLOC(bw, sizeof(l_float32));
+            if (pnarootvar)
+                farootvar = (l_float32 *)LEPT_CALLOC(bw, sizeof(l_float32));
+        }
+        for (j = xstart; j < xend; j++) {
+            sum = sumsq = 0;
+            for (i = ystart, lines = datas; i < yend; lines += wpls, i++) {
+                val = GET_DATA_BYTE(lines, j);
+                sum += val;
+                sumsq += val * val;
+            }
+            famean[j] = norm * sum;
+            fameansq[j] = norm * sumsq;
+            if (pnavar || pnarootvar) {
+                favar[j] = fameansq[j] - famean[j] * famean[j];
+                if (pnarootvar)
+                    farootvar[j] = sqrtf(favar[j]);
+            }
+        }
+        LEPT_FREE(fameansq);
+        if (pnamean)
+            *pnamean = numaCreateFromFArray(famean, bw, L_INSERT);
+        else
+            LEPT_FREE(famean);
+        if (pnavar)
+            *pnavar = numaCreateFromFArray(favar, bw, L_INSERT);
+        else
+            LEPT_FREE(favar);
+        if (pnarootvar)
+            *pnarootvar = numaCreateFromFArray(farootvar, bw, L_INSERT);
+    }
+
+        /* We need a histogram to find the median and/or mode values */
+    if (pnamedian || pnamode || pnamodecount) {
+        histo = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+        if (pnamedian) {
+            *pnamedian = numaMakeConstant(0, bw);
+            famedian = numaGetFArray(*pnamedian, L_NOCOPY);
+        }
+        if (pnamode) {
+            *pnamode = numaMakeConstant(0, bw);
+            famode = numaGetFArray(*pnamode, L_NOCOPY);
+        }
+        if (pnamodecount) {
+            *pnamodecount = numaMakeConstant(0, bw);
+            famodecount = numaGetFArray(*pnamodecount, L_NOCOPY);
+        }
+        for (j = xstart; j < xend; j++) {
+            memset(histo, 0, 1024);
+            for (i = ystart, lines = datas; i < yend; lines += wpls, i++) {
+                val = GET_DATA_BYTE(lines, j);
+                histo[val]++;
+            }
+
+            if (pnamedian) {
+                sum = 0;
+                target = (bh + 1) / 2;
+                for (k = 0; k < 256; k++) {
+                    sum += histo[k];
+                    if (sum >= target) {
+                        famedian[j] = k;
+                        break;
+                    }
+                }
+            }
+
+            if (pnamode || pnamodecount) {
+                max = 0;
+                modeval = 0;
+                for (k = 0; k < 256; k++) {
+                    if (histo[k] > max) {
+                        max = histo[k];
+                        modeval = k;
+                    }
+                }
+                if (pnamode)
+                    famode[j] = modeval;
+                if (pnamodecount)
+                    famodecount[j] = max;
+            }
+        }
+        LEPT_FREE(histo);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetRangeValues()
+ *
+ * \param[in]    pixs     8 bpp grayscale, 32 bpp rgb, or colormapped
+ * \param[in]    factor   subsampling factor; >= 1; ignored if colormapped
+ * \param[in]    color    L_SELECT_RED, L_SELECT_GREEN or L_SELECT_BLUE
+ * \param[out]   pminval  [optional] minimum value of component
+ * \param[out]   pmaxval  [optional] maximum value of component
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs is 8 bpp grayscale, the color selection type is ignored.
+ * </pre>
+ */
+l_ok
+pixGetRangeValues(PIX      *pixs,
+                  l_int32   factor,
+                  l_int32   color,
+                  l_int32  *pminval,
+                  l_int32  *pmaxval)
+{
+l_int32   d;
+PIXCMAP  *cmap;
+
+    if (pminval) *pminval = 0;
+    if (pmaxval) *pmaxval = 0;
+    if (!pminval && !pmaxval)
+        return ERROR_INT("no result requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+
+    cmap = pixGetColormap(pixs);
+    if (cmap)
+        return pixcmapGetRangeValues(cmap, color, pminval, pmaxval,
+                                     NULL, NULL);
+
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    d = pixGetDepth(pixs);
+    if (d != 8 && d != 32)
+        return ERROR_INT("pixs not 8 or 32 bpp", __func__, 1);
+
+    if (d == 8) {
+        pixGetExtremeValue(pixs, factor, L_SELECT_MIN,
+                           NULL, NULL, NULL, pminval);
+        pixGetExtremeValue(pixs, factor, L_SELECT_MAX,
+                           NULL, NULL, NULL, pmaxval);
+    } else if (color == L_SELECT_RED) {
+        pixGetExtremeValue(pixs, factor, L_SELECT_MIN,
+                           pminval, NULL, NULL, NULL);
+        pixGetExtremeValue(pixs, factor, L_SELECT_MAX,
+                           pmaxval, NULL, NULL, NULL);
+    } else if (color == L_SELECT_GREEN) {
+        pixGetExtremeValue(pixs, factor, L_SELECT_MIN,
+                           NULL, pminval, NULL, NULL);
+        pixGetExtremeValue(pixs, factor, L_SELECT_MAX,
+                           NULL, pmaxval, NULL, NULL);
+    } else if (color == L_SELECT_BLUE) {
+        pixGetExtremeValue(pixs, factor, L_SELECT_MIN,
+                           NULL, NULL, pminval, NULL);
+        pixGetExtremeValue(pixs, factor, L_SELECT_MAX,
+                           NULL, NULL, pmaxval, NULL);
+    } else {
+        return ERROR_INT("invalid color", __func__, 1);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetExtremeValue()
+ *
+ * \param[in]    pixs      8 bpp grayscale, 32 bpp rgb, or colormapped
+ * \param[in]    factor    subsampling factor; >= 1; ignored if colormapped
+ * \param[in]    type      L_SELECT_MIN or L_SELECT_MAX
+ * \param[out]   prval     [optional] red component
+ * \param[out]   pgval     [optional] green component
+ * \param[out]   pbval     [optional] blue component
+ * \param[out]   pgrayval  [optional] min or max gray value
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) If pixs is grayscale, the result is returned in &grayval.
+ *          Otherwise, if there is a colormap or d == 32,
+ *          each requested color component is returned.  At least
+ *          one color component (address) must be input.
+ * </pre>
+ */
+l_ok
+pixGetExtremeValue(PIX      *pixs,
+                   l_int32   factor,
+                   l_int32   type,
+                   l_int32  *prval,
+                   l_int32  *pgval,
+                   l_int32  *pbval,
+                   l_int32  *pgrayval)
+{
+l_int32    i, j, w, h, d, wpl;
+l_int32    val, extval, rval, gval, bval, extrval, extgval, extbval;
+l_uint32   pixel;
+l_uint32  *data, *line;
+PIXCMAP   *cmap;
+
+    if (prval) *prval = -1;
+    if (pgval) *pgval = -1;
+    if (pbval) *pbval = -1;
+    if (pgrayval) *pgrayval = -1;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    if (type != L_SELECT_MIN && type != L_SELECT_MAX)
+        return ERROR_INT("invalid type", __func__, 1);
+
+    cmap = pixGetColormap(pixs);
+    if (cmap) {
+        if (type == L_SELECT_MIN) {
+            if (prval) pixcmapGetRangeValues(cmap, L_SELECT_RED, prval, NULL,
+                                             NULL, NULL);
+            if (pgval) pixcmapGetRangeValues(cmap, L_SELECT_GREEN, pgval, NULL,
+                                             NULL, NULL);
+            if (pbval) pixcmapGetRangeValues(cmap, L_SELECT_BLUE, pbval, NULL,
+                                             NULL, NULL);
+        } else {  /* type == L_SELECT_MAX */
+            if (prval) pixcmapGetRangeValues(cmap, L_SELECT_RED, NULL, prval,
+                                             NULL, NULL);
+            if (pgval) pixcmapGetRangeValues(cmap, L_SELECT_GREEN, NULL, pgval,
+                                             NULL, NULL);
+            if (pbval) pixcmapGetRangeValues(cmap, L_SELECT_BLUE, NULL, pbval,
+                                             NULL, NULL);
+        }
+        return 0;
+    }
+
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (d != 8 && d != 32)
+        return ERROR_INT("pixs not 8 or 32 bpp", __func__, 1);
+    if (d == 8 && !pgrayval)
+        return ERROR_INT("can't return result in grayval", __func__, 1);
+    if (d == 32 && !prval && !pgval && !pbval)
+        return ERROR_INT("can't return result in r/g/b-val", __func__, 1);
+
+    data = pixGetData(pixs);
+    wpl = pixGetWpl(pixs);
+    if (d == 8) {
+        if (type == L_SELECT_MIN)
+            extval = 100000;
+        else  /* get max */
+            extval = -1;
+
+        for (i = 0; i < h; i += factor) {
+            line = data + i * wpl;
+            for (j = 0; j < w; j += factor) {
+                val = GET_DATA_BYTE(line, j);
+                if ((type == L_SELECT_MIN && val < extval) ||
+                    (type == L_SELECT_MAX && val > extval))
+                    extval = val;
+            }
+        }
+        *pgrayval = extval;
+        return 0;
+    }
+
+        /* 32 bpp rgb */
+    if (type == L_SELECT_MIN) {
+        extrval = 100000;
+        extgval = 100000;
+        extbval = 100000;
+    } else {
+        extrval = -1;
+        extgval = -1;
+        extbval = -1;
+    }
+    for (i = 0; i < h; i += factor) {
+        line = data + i * wpl;
+        for (j = 0; j < w; j += factor) {
+            pixel = line[j];
+            if (prval) {
+                rval = (pixel >> L_RED_SHIFT) & 0xff;
+                if ((type == L_SELECT_MIN && rval < extrval) ||
+                    (type == L_SELECT_MAX && rval > extrval))
+                    extrval = rval;
+            }
+            if (pgval) {
+                gval = (pixel >> L_GREEN_SHIFT) & 0xff;
+                if ((type == L_SELECT_MIN && gval < extgval) ||
+                    (type == L_SELECT_MAX && gval > extgval))
+                    extgval = gval;
+            }
+            if (pbval) {
+                bval = (pixel >> L_BLUE_SHIFT) & 0xff;
+                if ((type == L_SELECT_MIN && bval < extbval) ||
+                    (type == L_SELECT_MAX && bval > extbval))
+                    extbval = bval;
+            }
+        }
+    }
+    if (prval) *prval = extrval;
+    if (pgval) *pgval = extgval;
+    if (pbval) *pbval = extbval;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetMaxValueInRect()
+ *
+ * \param[in]    pixs     8, 16 or 32 bpp grayscale; no color space components
+ * \param[in]    box      [optional] region; set box = NULL to use entire pixs
+ * \param[out]   pmaxval  [optional] max value in region
+ * \param[out]   pxmax    [optional] x location of max value
+ * \param[out]   pymax    [optional] y location of max value
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This can be used to find the maximum and its location
+ *          in a 2-dimensional histogram, where the x and y directions
+ *          represent two color components (e.g., saturation and hue).
+ *      (2) Note that here a 32 bpp pixs has pixel values that are simply
+ *          numbers.  They are not 8 bpp components in a colorspace.
+ * </pre>
+ */
+l_ok
+pixGetMaxValueInRect(PIX       *pixs,
+                     BOX       *box,
+                     l_uint32  *pmaxval,
+                     l_int32   *pxmax,
+                     l_int32   *pymax)
+{
+l_int32    i, j, w, h, d, wpl, bw, bh;
+l_int32    xstart, ystart, xend, yend, xmax, ymax;
+l_uint32   val, maxval;
+l_uint32  *data, *line;
+
+    if (pmaxval) *pmaxval = 0;
+    if (pxmax) *pxmax = 0;
+    if (pymax) *pymax = 0;
+    if (!pmaxval && !pxmax && !pymax)
+        return ERROR_INT("no data requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    if (pixGetColormap(pixs) != NULL)
+        return ERROR_INT("pixs has colormap", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 8 && d != 16 && d != 32)
+        return ERROR_INT("pixs not 8, 16 or 32 bpp", __func__, 1);
+
+    xstart = ystart = 0;
+    xend = w - 1;
+    yend = h - 1;
+    if (box) {
+        boxGetGeometry(box, &xstart, &ystart, &bw, &bh);
+        xend = xstart + bw - 1;
+        yend = ystart + bh - 1;
+    }
+
+    data = pixGetData(pixs);
+    wpl = pixGetWpl(pixs);
+    maxval = 0;
+    xmax = ymax = 0;
+    for (i = ystart; i <= yend; i++) {
+        line = data + i * wpl;
+        for (j = xstart; j <= xend; j++) {
+            if (d == 8)
+                val = GET_DATA_BYTE(line, j);
+            else if (d == 16)
+                val = GET_DATA_TWO_BYTES(line, j);
+            else  /* d == 32 */
+                val = line[j];
+            if (val > maxval) {
+                maxval = val;
+                xmax = j;
+                ymax = i;
+            }
+        }
+    }
+    if (maxval == 0) {  /* no counts; pick the center of the rectangle */
+        xmax = (xstart + xend) / 2;
+        ymax = (ystart + yend) / 2;
+    }
+
+    if (pmaxval) *pmaxval = maxval;
+    if (pxmax) *pxmax = xmax;
+    if (pymax) *pymax = ymax;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetMaxColorIndex()
+ *
+ * \param[in]    pixs          1, 2, 4 or 8 bpp colormapped
+ * \param[out]   pmaxindex     max colormap index value
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+pixGetMaxColorIndex(PIX      *pixs,
+                    l_int32  *pmaxindex)
+{
+l_int32    i, j, w, h, d, wpl, val, max, maxval, empty;
+l_uint32  *data, *line;
+
+    if (!pmaxindex)
+        return ERROR_INT("&maxindex not defined", __func__, 1);
+    *pmaxindex = 0;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, &d);
+    if (d != 1 && d != 2 && d != 4 && d != 8)
+        return ERROR_INT("invalid pixs depth; not in (1,2,4,8}", __func__, 1);
+
+    wpl = pixGetWpl(pixs);
+    data = pixGetData(pixs);
+    max = 0;
+    maxval = (1 << d) - 1;
+    if (d == 1) {
+        pixZero(pixs, &empty);
+        if (!empty) max = 1;
+        *pmaxindex = max;
+        return 0;
+    }
+    for (i = 0; i < h; i++) {
+        line = data + i * wpl;
+        if (d == 2) {
+            for (j = 0; j < w; j++) {
+                val = GET_DATA_DIBIT(line, j);
+                if (val > max) max = val;
+            }
+        } else if (d == 4) {
+            for (j = 0; j < w; j++) {
+                val = GET_DATA_QBIT(line, j);
+                if (val > max) max = val;
+            }
+        } else if (d == 8) {
+            for (j = 0; j < w; j++) {
+                val = GET_DATA_BYTE(line, j);
+                if (val > max) max = val;
+            }
+        }
+        if (max == maxval) break;
+    }
+    *pmaxindex = max;
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetBinnedComponentRange()
+ *
+ * \param[in]    pixs      32 bpp rgb
+ * \param[in]    nbins     number of equal population bins; must be > 1
+ * \param[in]    factor    subsampling factor; >= 1
+ * \param[in]    color     L_SELECT_RED, L_SELECT_GREEN or L_SELECT_BLUE
+ * \param[out]   pminval   [optional] minimum value of component
+ * \param[out]   pmaxval   [optional] maximum value of component
+ * \param[out]   pcarray   [optional] color array of bins
+ * \param[in]    fontsize  [optional] 0 for no debug; for debug, valid set
+ *                         is {4,6,8,10,12,14,16,18,20}.
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This returns the min and max average values of the
+ *          selected color component in the set of rank bins,
+ *          where the ranking is done using the specified component.
+ * </pre>
+ */
+l_ok
+pixGetBinnedComponentRange(PIX        *pixs,
+                           l_int32     nbins,
+                           l_int32     factor,
+                           l_int32     color,
+                           l_int32    *pminval,
+                           l_int32    *pmaxval,
+                           l_uint32  **pcarray,
+                           l_int32     fontsize)
+{
+l_int32    i, minval, maxval, rval, gval, bval;
+l_uint32  *carray;
+PIX       *pixt;
+
+    if (pminval) *pminval = 0;
+    if (pmaxval) *pmaxval = 0;
+    if (pcarray) *pcarray = NULL;
+    if (!pminval && !pmaxval)
+        return ERROR_INT("no result requested", __func__, 1);
+    if (!pixs || pixGetDepth(pixs) != 32)
+        return ERROR_INT("pixs not defined or not 32 bpp", __func__, 1);
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (color != L_SELECT_RED && color != L_SELECT_GREEN &&
+        color != L_SELECT_BLUE)
+        return ERROR_INT("invalid color", __func__, 1);
+    if (fontsize < 0 || fontsize > 20 || fontsize & 1 || fontsize == 2)
+        return ERROR_INT("invalid fontsize", __func__, 1);
+
+    pixGetRankColorArray(pixs, nbins, color, factor, &carray, NULL, 0);
+    if (!carray)
+        return ERROR_INT("carray not made", __func__, 1);
+
+    if (fontsize > 0) {
+        for (i = 0; i < nbins; i++)
+            L_INFO("c[%d] = %x\n", __func__, i, carray[i]);
+        pixt = pixDisplayColorArray(carray, nbins, 200, 5, fontsize);
+        pixDisplay(pixt, 100, 100);
+        pixDestroy(&pixt);
+    }
+
+    extractRGBValues(carray[0], &rval, &gval, &bval);
+    minval = rval;
+    if (color == L_SELECT_GREEN)
+        minval = gval;
+    else if (color == L_SELECT_BLUE)
+        minval = bval;
+    extractRGBValues(carray[nbins - 1], &rval, &gval, &bval);
+    maxval = rval;
+    if (color == L_SELECT_GREEN)
+        maxval = gval;
+    else if (color == L_SELECT_BLUE)
+        maxval = bval;
+
+    if (pminval) *pminval = minval;
+    if (pmaxval) *pmaxval = maxval;
+    if (pcarray)
+        *pcarray = carray;
+    else
+        LEPT_FREE(carray);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetRankColorArray()
+ *
+ * \param[in]    pixs       32 bpp or cmapped
+ * \param[in]    nbins      number of equal population bins; must be > 1
+ * \param[in]    type       color selection flag
+ * \param[in]    factor     subsampling factor; integer >= 1
+ * \param[out]   pcarray    array of colors, ranked by intensity
+ * \param[in]    pixadb     [optional] debug: caller passes this in.
+ *                          Use to display color squares and to
+ *                          capture plots of color components
+ * \param[in]    fontsize   [optional] debug: only used if pixadb exists.
+ *                          Valid set is {4,6,8,10,12,14,16,18,20}.
+ *                          fontsize == 6 is typical.
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The color selection flag is one of: L_SELECT_RED, L_SELECT_GREEN,
+ *          L_SELECT_BLUE, L_SELECT_MIN, L_SELECT_MAX, L_SELECT_AVERAGE,
+ *          L_SELECT_HUE, L_SELECT_SATURATION.
+ *      (2) The pixels are ordered by the value of the selected color
+            value, and an equal number are placed in %nbins.  The average
+ *          color in each bin is returned in a color array with %nbins colors.
+ *      (3) Set the subsampling factor > 1 to reduce the amount of
+ *          computation.  Typically you want at least 10,000 pixels
+ *          for reasonable statistics.  Must be at least 10 samples/bin.
+ *      (4) A crude "rank color" as a function of rank can be found from
+ *             rankint = (l_int32)(rank * (nbins - 1) + 0.5);
+ *             extractRGBValues(array[rankint], &rval, &gval, &bval);
+ *          where the rank is in [0.0 ... 1.0].
+ * </pre>
+ */
+l_ok
+pixGetRankColorArray(PIX        *pixs,
+                     l_int32     nbins,
+                     l_int32     type,
+                     l_int32     factor,
+                     l_uint32  **pcarray,
+                     PIXA       *pixadb,
+                     l_int32     fontsize)
+{
+l_int32    ret, w, h, samplesperbin;
+l_uint32  *array;
+PIX       *pix1, *pixc, *pixg, *pixd;
+PIXCMAP   *cmap;
+
+    if (!pcarray)
+        return ERROR_INT("&carray not defined", __func__, 1);
+    *pcarray = NULL;
+    if (factor < 1)
+        return ERROR_INT("sampling factor must be >= 1", __func__, 1);
+    if (nbins < 2)
+        return ERROR_INT("nbins must be at least 2", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    cmap = pixGetColormap(pixs);
+    if (pixGetDepth(pixs) != 32 && !cmap)
+        return ERROR_INT("pixs neither 32 bpp nor cmapped", __func__, 1);
+    if (type != L_SELECT_RED && type != L_SELECT_GREEN &&
+        type != L_SELECT_BLUE && type != L_SELECT_MIN &&
+        type != L_SELECT_MAX && type != L_SELECT_AVERAGE &&
+        type != L_SELECT_HUE && type != L_SELECT_SATURATION)
+        return ERROR_INT("invalid type", __func__, 1);
+    if (pixadb) {
+        if (fontsize < 0 || fontsize > 20 || fontsize & 1 || fontsize == 2) {
+            L_WARNING("invalid fontsize %d; setting to 6\n", __func__,
+                      fontsize);
+            fontsize = 6;
+        }
+    }
+    pixGetDimensions(pixs, &w, &h, NULL);
+    samplesperbin = (w * h) / (factor * factor * nbins);
+    if (samplesperbin < 10) {
+        L_ERROR("samplesperbin = %d < 10\n", __func__, samplesperbin);
+        return 1;
+    }
+
+        /* Downscale by factor and remove colormap if it exists */
+    pix1 = pixScaleByIntSampling(pixs, factor);
+    if (cmap)
+        pixc = pixRemoveColormap(pix1, REMOVE_CMAP_TO_FULL_COLOR);
+    else
+        pixc = pixClone(pix1);
+    pixDestroy(&pix1);
+
+        /* Convert to an 8 bit version for ordering the pixels */
+    pixg = pixConvertRGBToGrayGeneral(pixc, type, 0.0, 0.0, 0.0);
+
+        /* Get the average color in each bin for pixels whose grayscale
+         * values are in the range for that bin. */
+    pixGetBinnedColor(pixc, pixg, 1, nbins, pcarray, pixadb);
+    ret = 0;
+    if ((array = *pcarray) == NULL) {
+        L_ERROR("color array not returned\n", __func__);
+        ret = 1;
+    }
+    if (array && pixadb) {
+        pixd = pixDisplayColorArray(array, nbins, 200, 5, fontsize);
+        pixWriteDebug("/tmp/lept/regout/rankhisto.png", pixd, IFF_PNG);
+        pixDestroy(&pixd);
+    }
+
+    pixDestroy(&pixc);
+    pixDestroy(&pixg);
+    return ret;
+}
+
+
+/*!
+ * \brief   pixGetBinnedColor()
+ *
+ * \param[in]    pixs       32 bpp
+ * \param[in]    pixg       8 bpp grayscale version of pixs
+ * \param[in]    factor     sampling factor along pixel counting direction
+ * \param[in]    nbins      number of bins based on grayscale value {1,...,100}
+ * \param[out]   pcarray    array of average color values in each bin
+ * \param[in]    pixadb     [optional] debug: caller passes this in.
+ *                          Use to display output color squares and plots of
+ *                          color components.
+ * \return  0 if OK; 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This takes a color image, a grayscale version, and the number
+ *          of requested bins.  The pixels are ordered by the corresponding
+ *          gray value and an equal number of pixels are put in each bin.
+ *          The average color for each bin is returned as an array
+ *          of l_uint32 colors in our standard RGBA ordering.  We require
+ *          at least 5 pixels in each bin.
+ *      (2) This is used by pixGetRankColorArray(), which generates the
+ *          grayscale image %pixg from the color image %pixs.
+ *      (3) Arrays of float64 are used for intermediate storage, without
+ *          loss of precision, of the sampled uint32 pixel values.
+ * </pre>
+ */
+l_ok
+pixGetBinnedColor(PIX        *pixs,
+                  PIX        *pixg,
+                  l_int32     factor,
+                  l_int32     nbins,
+                  l_uint32  **pcarray,
+                  PIXA       *pixadb)
+{
+l_int32     i, j, w, h, wpls, wplg;
+l_int32     count, bincount, binindex, binsize, npts, avepts, ntot;
+l_int32     rval, gval, bval, grayval, rave, gave, bave;
+l_uint32   *datas, *datag, *lines, *lineg, *carray;
+l_float64   val64, rsum, gsum, bsum;
+L_DNAA     *daa;
+NUMA       *naeach;
+PIX        *pix1;
+
+    if (!pcarray)
+        return ERROR_INT("&carray not defined", __func__, 1);
+    *pcarray = NULL;
+    if (!pixs || pixGetDepth(pixs) != 32)
+        return ERROR_INT("pixs undefined or not 32 bpp", __func__, 1);
+    if (!pixg || pixGetDepth(pixg) != 8)
+        return ERROR_INT("pixg undefined or not 8 bpp", __func__, 1);
+    if (factor < 1) {
+        L_WARNING("sampling factor less than 1; setting to 1\n", __func__);
+        factor = 1;
+    }
+    if (nbins < 1 || nbins > 100)
+        return ERROR_INT("nbins not in [1,100]", __func__, 1);
+
+        /* Require that each bin has at least 5 pixels. */
+    pixGetDimensions(pixs, &w, &h, NULL);
+    npts = (w + factor - 1) * (h + factor - 1) / (factor * factor);
+    avepts = (npts + nbins - 1) / nbins;  /* average number of pts in a bin */
+    if (avepts < 5) {
+        L_ERROR("avepts = %d; must be >= 5\n", __func__, avepts);
+        return 1;
+    }
+
+    /* ------------------------------------------------------------ *
+     * Find the average color for each bin.  The colors are ordered *
+     * by the gray value in the corresponding pixel in %pixg.       *
+     * The bins have equal numbers of pixels (within 1).            *
+     * ------------------------------------------------------------ */
+
+        /* Generate a dnaa, where each dna has the colors corresponding
+         * to the grayscale value given by the index of the dna in the dnaa */
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    datag = pixGetData(pixg);
+    wplg = pixGetWpl(pixg);
+    daa = l_dnaaCreateFull(256, 0);
+    for (i = 0; i < h; i += factor) {
+        lines = datas + i * wpls;
+        lineg = datag + i * wplg;
+        for (j = 0; j < w; j += factor) {
+            grayval = GET_DATA_BYTE(lineg, j);
+            l_dnaaAddNumber(daa, grayval, lines[j]);
+        }
+    }
+
+    if (pixadb) {
+        NUMA  *na, *nabinval, *narank;
+        na = numaCreate(256);  /* grayscale histogram */
+        for (i = 0; i < 256; i++)
+            numaAddNumber(na, l_dnaaGetDnaCount(daa, i));
+
+            /* Plot the gray bin value and the rank(gray) values */
+        numaDiscretizeHistoInBins(na, nbins, &nabinval, &narank);
+        pix1 = gplotSimplePix1(nabinval, "Gray value in each bin");
+        pixaAddPix(pixadb, pix1, L_INSERT);
+        pix1 = gplotSimplePix1(narank, "rank as function of gray value");
+        pixaAddPix(pixadb, pix1, L_INSERT);
+        numaDestroy(&na);
+        numaDestroy(&nabinval);
+        numaDestroy(&narank);
+    }
+
+        /* Get the number of items in each bin */
+    ntot = l_dnaaGetNumberCount(daa);
+    if ((naeach = numaGetUniformBinSizes(ntot, nbins)) == NULL) {
+        l_dnaaDestroy(&daa);
+        return ERROR_INT("naeach not made", __func__, 1);
+    }
+
+        /* Get the average color in each bin.  This algorithm is
+         * esssentially the same as in numaDiscretizeHistoInBins() */
+    carray = (l_uint32 *)LEPT_CALLOC(nbins, sizeof(l_uint32));
+    rsum = gsum = bsum = 0.0;
+    bincount = 0;
+    binindex = 0;
+    numaGetIValue(naeach, 0, &binsize);
+    for (i = 0; i < 256; i++) {
+        count = l_dnaaGetDnaCount(daa, i);
+        for (j = 0; j < count; j++) {
+            bincount++;
+            l_dnaaGetValue(daa, i, j, &val64);
+            extractRGBValues((l_uint32)val64, &rval, &gval, &bval);
+            rsum += rval;
+            gsum += gval;
+            bsum += bval;
+            if (bincount == binsize) {  /* add bin entry */
+                rave = (l_int32)(rsum / binsize + 0.5);
+                gave = (l_int32)(gsum / binsize + 0.5);
+                bave = (l_int32)(bsum / binsize + 0.5);
+                composeRGBPixel(rave, gave, bave, carray + binindex);
+                rsum = gsum = bsum = 0.0;
+                bincount = 0;
+                binindex++;
+                if (binindex == nbins) break;
+                numaGetIValue(naeach, binindex, &binsize);
+            }
+        }
+        if (binindex == nbins) break;
+    }
+    if (binindex != nbins)
+        L_ERROR("binindex = %d != nbins = %d\n", __func__, binindex, nbins);
+
+    if (pixadb) {
+        NUMA  *nared, *nagreen, *nablue;
+        nared = numaCreate(nbins);
+        nagreen = numaCreate(nbins);
+        nablue = numaCreate(nbins);
+        for (i = 0; i < nbins; i++) {
+            extractRGBValues(carray[i], &rval, &gval, &bval);
+            numaAddNumber(nared, rval);
+            numaAddNumber(nagreen, gval);
+            numaAddNumber(nablue, bval);
+        }
+        lept_mkdir("lept/regout");
+        pix1 = gplotSimplePix1(nared, "Average red val vs. rank bin");
+        pixaAddPix(pixadb, pix1, L_INSERT);
+        pix1 = gplotSimplePix1(nagreen, "Average green val vs. rank bin");
+        pixaAddPix(pixadb, pix1, L_INSERT);
+        pix1 = gplotSimplePix1(nablue, "Average blue val vs. rank bin");
+        pixaAddPix(pixadb, pix1, L_INSERT);
+        numaDestroy(&nared);
+        numaDestroy(&nagreen);
+        numaDestroy(&nablue);
+    }
+
+    *pcarray = carray;
+    numaDestroy(&naeach);
+    l_dnaaDestroy(&daa);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixDisplayColorArray()
+ *
+ * \param[in]   carray    array of colors: 0xrrggbb00
+ * \param[in]   ncolors   size of array
+ * \param[in]   side      size of each color square; suggest 200
+ * \param[in]   ncols     number of columns in output color matrix
+ * \param[in]   fontsize  to label each square with text.
+ *                        Valid set is {4,6,8,10,12,14,16,18,20}.
+ *                        Suggest 6 for 200x200 square. Use 0 to disable.
+ * \return  pixd color array, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates an array of labeled color squares from an
+ *          array of color values.
+ *      (2) To make a single color square, use pixMakeColorSquare().
+ * </pre>
+ */
+PIX *
+pixDisplayColorArray(l_uint32  *carray,
+                     l_int32    ncolors,
+                     l_int32    side,
+                     l_int32    ncols,
+                     l_int32    fontsize)
+{
+char     textstr[256];
+l_int32  i, rval, gval, bval;
+L_BMF   *bmf;
+PIX     *pix1, *pix2, *pix3, *pix4;
+PIXA    *pixa;
+
+    if (!carray)
+        return (PIX *)ERROR_PTR("carray not defined", __func__, NULL);
+    if (fontsize < 0 || fontsize > 20 || fontsize & 1 || fontsize == 2)
+        return (PIX *)ERROR_PTR("invalid fontsize", __func__, NULL);
+
+    bmf = (fontsize == 0) ? NULL : bmfCreate(NULL, fontsize);
+    pixa = pixaCreate(ncolors);
+    for (i = 0; i < ncolors; i++) {
+        pix1 = pixCreate(side, side, 32);
+        pixSetAllArbitrary(pix1, carray[i]);
+        pix2 = pixAddBorder(pix1, 2, 1);
+        if (bmf) {
+            extractRGBValues(carray[i], &rval, &gval, &bval);
+            snprintf(textstr, sizeof(textstr),
+                     "%d: (%d %d %d)", i, rval, gval, bval);
+            pix3 = pixAddSingleTextblock(pix2, bmf, textstr, 0xff000000,
+                                         L_ADD_BELOW, NULL);
+        } else {
+            pix3 = pixClone(pix2);
+        }
+        pixaAddPix(pixa, pix3, L_INSERT);
+        pixDestroy(&pix1);
+        pixDestroy(&pix2);
+    }
+    pix4 = pixaDisplayTiledInColumns(pixa, ncols, 1.0, 20, 2);
+    pixaDestroy(&pixa);
+    bmfDestroy(&bmf);
+    return pix4;
+}
+
+
+/*!
+ * \brief   pixRankBinByStrip()
+ *
+ * \param[in]   pixs       32 bpp or cmapped
+ * \param[in]   direction  L_SCAN_HORIZONTAL or L_SCAN_VERTICAL
+ * \param[in]   size       of strips in scan direction
+ * \param[in]   nbins      number of equal population bins; must be > 1
+ * \param[in]   type       color selection flag
+ * \return  pixd result, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a pix of height %nbins, where each column
+ *          represents a horizontal or vertical strip of the input image.
+ *          If %direction == L_SCAN_HORIZONTAL, the input image is
+ *          tiled into vertical strips of width %size, where %size is
+ *          chosen as a compromise between getting better spatial
+ *          columnwise resolution (small %size) and getting better
+ *          columnwise statistical information (larger %size).  Likewise
+ *          with rows of the image if %direction == L_SCAN_VERTICAL.
+ *      (2) For L_HORIZONTAL_SCAN, the output pix contains rank binned
+ *          median colors in each column that correspond to a vertical
+ *          strip of width %size in the input image.
+ *      (3) The color selection flag is one of: L_SELECT_RED, L_SELECT_GREEN,
+ *          L_SELECT_BLUE, L_SELECT_MIN, L_SELECT_MAX, L_SELECT_AVERAGE,
+ *          L_SELECT_HUE, L_SELECT_SATURATION.
+ *          It determines how the rank ordering is done.
+ *      (4) Typical input values might be %size = 5, %nbins = 10.
+ * </pre>
+ */
+PIX *
+pixRankBinByStrip(PIX     *pixs,
+                  l_int32  direction,
+                  l_int32  size,
+                  l_int32  nbins,
+                  l_int32  type)
+{
+l_int32    i, j, w, h, mindim, nstrips;
+l_uint32  *array;
+BOXA      *boxa;
+PIX       *pix1, *pix2, *pixd;
+PIXA      *pixa;
+PIXCMAP   *cmap;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    cmap = pixGetColormap(pixs);
+    if (pixGetDepth(pixs) != 32 && !cmap)
+        return (PIX *)ERROR_PTR("pixs neither 32 bpp nor cmapped",
+                                __func__, NULL);
+    if (direction != L_SCAN_HORIZONTAL && direction != L_SCAN_VERTICAL)
+        return (PIX *)ERROR_PTR("invalid direction", __func__, NULL);
+    if (size < 1)
+        return (PIX *)ERROR_PTR("size < 1", __func__, NULL);
+    if (nbins < 2)
+        return (PIX *)ERROR_PTR("nbins must be at least 2", __func__, NULL);
+    if (type != L_SELECT_RED && type != L_SELECT_GREEN &&
+        type != L_SELECT_BLUE && type != L_SELECT_MIN &&
+        type != L_SELECT_MAX && type != L_SELECT_AVERAGE &&
+        type != L_SELECT_HUE && type != L_SELECT_SATURATION)
+        return (PIX *)ERROR_PTR("invalid type", __func__, NULL);
+    pixGetDimensions(pixs, &w, &h, NULL);
+    mindim = L_MIN(w, h);
+    if (mindim < 20 || nbins > mindim)
+        return (PIX *)ERROR_PTR("pix too small and/or too many bins",
+                                __func__, NULL);
+
+        /* Remove colormap if it exists */
+    if (cmap)
+        pix1 = pixRemoveColormap(pixs, REMOVE_CMAP_TO_FULL_COLOR);
+    else
+        pix1 = pixClone(pixs);
+    pixGetDimensions(pixs, &w, &h, NULL);
+
+    pixd = NULL;
+    boxa = makeMosaicStrips(w, h, direction, size);
+    pixa = pixClipRectangles(pix1, boxa);
+    nstrips = pixaGetCount(pixa);
+    if (direction == L_SCAN_HORIZONTAL) {
+        pixd = pixCreate(nstrips, nbins, 32);
+        for (i = 0; i < nstrips; i++) {
+            pix2 = pixaGetPix(pixa, i, L_CLONE);
+            pixGetRankColorArray(pix2, nbins, type, 1, &array, NULL, 0);
+            if (array) {
+                for (j = 0; j < nbins; j++)
+                    pixSetPixel(pixd, i, j, array[j]);
+                LEPT_FREE(array);
+            }
+            pixDestroy(&pix2);
+        }
+    } else {  /* L_SCAN_VERTICAL */
+        pixd = pixCreate(nbins, nstrips, 32);
+        for (i = 0; i < nstrips; i++) {
+            pix2 = pixaGetPix(pixa, i, L_CLONE);
+            pixGetRankColorArray(pix2, nbins, type, 1, &array, NULL, 0);
+            if (array) {
+                for (j = 0; j < nbins; j++)
+                    pixSetPixel(pixd, j, i, array[j]);
+                LEPT_FREE(array);
+            }
+            pixDestroy(&pix2);
+        }
+    }
+    pixDestroy(&pix1);
+    boxaDestroy(&boxa);
+    pixaDestroy(&pixa);
+    return pixd;
+}
+
+
+
+/*-------------------------------------------------------------*
+ *                 Pixelwise aligned statistics                *
+ *-------------------------------------------------------------*/
+/*!
+ * \brief   pixaGetAlignedStats()
+ *
+ * \param[in]   pixa    of identically sized, 8 bpp pix; not cmapped
+ * \param[in]   type    L_MEAN_ABSVAL, L_MEDIAN_VAL, L_MODE_VAL, L_MODE_COUNT
+ * \param[in]   nbins   of histogram for median and mode; ignored for mean
+ * \param[in]   thresh  on histogram for mode val; ignored for all other types
+ * \return  pix with pixelwise aligned stats, or NULL on error.
+ *
+ * <pre>
+ * Notes:
+ *      (1) Each pixel in the returned pix represents an average
+ *          (or median, or mode) over the corresponding pixels in each
+ *          pix in the pixa.
+ *      (2) The %thresh parameter works with L_MODE_VAL only, and
+ *          sets a minimum occupancy of the mode bin.
+ *          If the occupancy of the mode bin is less than %thresh, the
+ *          mode value is returned as 0.  To always return the actual
+ *          mode value, set %thresh = 0.  See pixGetRowStats().
+ * </pre>
+ */
+PIX *
+pixaGetAlignedStats(PIXA     *pixa,
+                    l_int32   type,
+                    l_int32   nbins,
+                    l_int32   thresh)
+{
+l_int32     j, n, w, h, d;
+l_float32  *colvect;
+PIX        *pixt, *pixd;
+
+    if (!pixa)
+        return (PIX *)ERROR_PTR("pixa not defined", __func__, NULL);
+    if (type != L_MEAN_ABSVAL && type != L_MEDIAN_VAL &&
+        type != L_MODE_VAL && type != L_MODE_COUNT)
+        return (PIX *)ERROR_PTR("invalid type", __func__, NULL);
+    n = pixaGetCount(pixa);
+    if (n == 0)
+        return (PIX *)ERROR_PTR("no pix in pixa", __func__, NULL);
+    pixaGetPixDimensions(pixa, 0, &w, &h, &d);
+    if (d != 8)
+        return (PIX *)ERROR_PTR("pix not 8 bpp", __func__, NULL);
+
+    pixd = pixCreate(w, h, 8);
+    pixt = pixCreate(n, h, 8);
+    colvect = (l_float32 *)LEPT_CALLOC(h, sizeof(l_float32));
+    for (j = 0; j < w; j++) {
+        pixaExtractColumnFromEachPix(pixa, j, pixt);
+        pixGetRowStats(pixt, type, nbins, thresh, colvect);
+        pixSetPixelColumn(pixd, j, colvect);
+    }
+
+    LEPT_FREE(colvect);
+    pixDestroy(&pixt);
+    return pixd;
+}
+
+
+/*!
+ * \brief   pixaExtractColumnFromEachPix()
+ *
+ * \param[in]   pixa   of identically sized, 8 bpp; not cmapped
+ * \param[in]   col    column index
+ * \param[in]   pixd   pix into which each column is inserted
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+pixaExtractColumnFromEachPix(PIXA    *pixa,
+                             l_int32  col,
+                             PIX     *pixd)
+{
+l_int32    i, k, n, w, h, ht, val, wplt, wpld;
+l_uint32  *datad, *datat;
+PIX       *pixt;
+
+    if (!pixa)
+        return ERROR_INT("pixa not defined", __func__, 1);
+    if (!pixd || pixGetDepth(pixd) != 8)
+        return ERROR_INT("pixd not defined or not 8 bpp", __func__, 1);
+    n = pixaGetCount(pixa);
+    pixGetDimensions(pixd, &w, &h, NULL);
+    if (n != w)
+        return ERROR_INT("pix width != n", __func__, 1);
+    pixt = pixaGetPix(pixa, 0, L_CLONE);
+    wplt = pixGetWpl(pixt);
+    pixGetDimensions(pixt, NULL, &ht, NULL);
+    pixDestroy(&pixt);
+    if (h != ht)
+        return ERROR_INT("pixd height != column height", __func__, 1);
+
+    datad = pixGetData(pixd);
+    wpld = pixGetWpl(pixd);
+    for (k = 0; k < n; k++) {
+        pixt = pixaGetPix(pixa, k, L_CLONE);
+        datat = pixGetData(pixt);
+        for (i = 0; i < h; i++) {
+            val = GET_DATA_BYTE(datat, col);
+            SET_DATA_BYTE(datad + i * wpld, k, val);
+            datat += wplt;
+        }
+        pixDestroy(&pixt);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetRowStats()
+ *
+ * \param[in]   pixs     8 bpp; not cmapped
+ * \param[in]   type     L_MEAN_ABSVAL, L_MEDIAN_VAL, L_MODE_VAL, L_MODE_COUNT
+ * \param[in]   nbins    of histogram for median and mode; ignored for mean
+ * \param[in]   thresh   on histogram for mode; ignored for mean and median
+ * \param[in]   colvect  vector of results gathered across the rows of pixs
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes a column vector of statistics using each
+ *          row of a Pix.  The result is put in %colvect.
+ *      (2) The %thresh parameter works with L_MODE_VAL only, and
+ *          sets a minimum occupancy of the mode bin.
+ *          If the occupancy of the mode bin is less than %thresh, the
+ *          mode value is returned as 0.  To always return the actual
+ *          mode value, set %thresh = 0.
+ *      (3) What is the meaning of this %thresh parameter?
+ *          For each row, the total count in the histogram is w, the
+ *          image width.  So %thresh, relative to w, gives a measure
+ *          of the ratio of the bin width to the width of the distribution.
+ *          The larger %thresh, the narrower the distribution must be
+ *          for the mode value to be returned (instead of returning 0).
+ *      (4) If the Pix consists of a set of corresponding columns,
+ *          one for each Pix in a Pixa, the width of the Pix is the
+ *          number of Pix in the Pixa and the column vector can
+ *          be stored as a column in a Pix of the same size as
+ *          each Pix in the Pixa.
+ * </pre>
+ */
+l_ok
+pixGetRowStats(PIX        *pixs,
+               l_int32     type,
+               l_int32     nbins,
+               l_int32     thresh,
+               l_float32  *colvect)
+{
+l_int32    i, j, k, w, h, val, wpls, sum, target, max, modeval;
+l_int32   *histo, *gray2bin, *bin2gray;
+l_uint32  *lines, *datas;
+
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return ERROR_INT("pixs not defined or not 8 bpp", __func__, 1);
+    if (!colvect)
+        return ERROR_INT("colvect not defined", __func__, 1);
+    if (type != L_MEAN_ABSVAL && type != L_MEDIAN_VAL &&
+        type != L_MODE_VAL && type != L_MODE_COUNT)
+        return ERROR_INT("invalid type", __func__, 1);
+    if (type != L_MEAN_ABSVAL && (nbins < 1 || nbins > 256))
+        return ERROR_INT("invalid nbins", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, NULL);
+
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    if (type == L_MEAN_ABSVAL) {
+        for (i = 0; i < h; i++) {
+            sum = 0;
+            lines = datas + i * wpls;
+            for (j = 0; j < w; j++)
+                sum += GET_DATA_BYTE(lines, j);
+            colvect[i] = (l_float32)sum / (l_float32)w;
+        }
+        return 0;
+    }
+
+        /* We need a histogram; binwidth ~ 256 / nbins */
+    histo = (l_int32 *)LEPT_CALLOC(nbins, sizeof(l_int32));
+    gray2bin = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+    bin2gray = (l_int32 *)LEPT_CALLOC(nbins, sizeof(l_int32));
+    for (i = 0; i < 256; i++)  /* gray value --> histo bin */
+        gray2bin[i] = (i * nbins) / 256;
+    for (i = 0; i < nbins; i++)  /* histo bin --> gray value */
+        bin2gray[i] = (i * 256 + 128) / nbins;
+
+    for (i = 0; i < h; i++) {
+        lines = datas + i * wpls;
+        for (k = 0; k < nbins; k++)
+            histo[k] = 0;
+        for (j = 0; j < w; j++) {
+            val = GET_DATA_BYTE(lines, j);
+            histo[gray2bin[val]]++;
+        }
+
+        if (type == L_MEDIAN_VAL) {
+            sum = 0;
+            target = (w + 1) / 2;
+            for (k = 0; k < nbins; k++) {
+                sum += histo[k];
+                if (sum >= target) {
+                    colvect[i] = bin2gray[k];
+                    break;
+                }
+            }
+        } else if (type == L_MODE_VAL) {
+            max = 0;
+            modeval = 0;
+            for (k = 0; k < nbins; k++) {
+                if (histo[k] > max) {
+                    max = histo[k];
+                    modeval = k;
+                }
+            }
+            if (max < thresh)
+                colvect[i] = 0;
+            else
+                colvect[i] = bin2gray[modeval];
+        } else {  /* type == L_MODE_COUNT */
+            max = 0;
+            for (k = 0; k < nbins; k++) {
+                if (histo[k] > max)
+                    max = histo[k];
+            }
+            colvect[i] = max;
+        }
+    }
+
+    LEPT_FREE(histo);
+    LEPT_FREE(gray2bin);
+    LEPT_FREE(bin2gray);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixGetColumnStats()
+ *
+ * \param[in]   pixs     8 bpp; not cmapped
+ * \param[in]   type     L_MEAN_ABSVAL, L_MEDIAN_VAL, L_MODE_VAL, L_MODE_COUNT
+ * \param[in]   nbins    of histogram for median and mode; ignored for mean
+ * \param[in]   thresh   on histogram for mode val; ignored for all other types
+ * \param[in]   rowvect  vector of results gathered down the columns of pixs
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This computes a row vector of statistics using each
+ *          column of a Pix.  The result is put in %rowvect.
+ *      (2) The %thresh parameter works with L_MODE_VAL only, and
+ *          sets a minimum occupancy of the mode bin.
+ *          If the occupancy of the mode bin is less than %thresh, the
+ *          mode value is returned as 0.  To always return the actual
+ *          mode value, set %thresh = 0.
+ *      (3) What is the meaning of this %thresh parameter?
+ *          For each column, the total count in the histogram is h, the
+ *          image height.  So %thresh, relative to h, gives a measure
+ *          of the ratio of the bin width to the width of the distribution.
+ *          The larger %thresh, the narrower the distribution must be
+ *          for the mode value to be returned (instead of returning 0).
+ * </pre>
+ */
+l_ok
+pixGetColumnStats(PIX        *pixs,
+                  l_int32     type,
+                  l_int32     nbins,
+                  l_int32     thresh,
+                  l_float32  *rowvect)
+{
+l_int32    i, j, k, w, h, val, wpls, sum, target, max, modeval;
+l_int32   *histo, *gray2bin, *bin2gray;
+l_uint32  *datas;
+
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return ERROR_INT("pixs not defined or not 8 bpp", __func__, 1);
+    if (!rowvect)
+        return ERROR_INT("rowvect not defined", __func__, 1);
+    if (type != L_MEAN_ABSVAL && type != L_MEDIAN_VAL &&
+        type != L_MODE_VAL && type != L_MODE_COUNT)
+        return ERROR_INT("invalid type", __func__, 1);
+    if (type != L_MEAN_ABSVAL && (nbins < 1 || nbins > 256))
+        return ERROR_INT("invalid nbins", __func__, 1);
+    pixGetDimensions(pixs, &w, &h, NULL);
+
+    datas = pixGetData(pixs);
+    wpls = pixGetWpl(pixs);
+    if (type == L_MEAN_ABSVAL) {
+        for (j = 0; j < w; j++) {
+            sum = 0;
+            for (i = 0; i < h; i++)
+                sum += GET_DATA_BYTE(datas + i * wpls, j);
+            rowvect[j] = (l_float32)sum / (l_float32)h;
+        }
+        return 0;
+    }
+
+        /* We need a histogram; binwidth ~ 256 / nbins */
+    histo = (l_int32 *)LEPT_CALLOC(nbins, sizeof(l_int32));
+    gray2bin = (l_int32 *)LEPT_CALLOC(256, sizeof(l_int32));
+    bin2gray = (l_int32 *)LEPT_CALLOC(nbins, sizeof(l_int32));
+    for (i = 0; i < 256; i++)  /* gray value --> histo bin */
+        gray2bin[i] = (i * nbins) / 256;
+    for (i = 0; i < nbins; i++)  /* histo bin --> gray value */
+        bin2gray[i] = (i * 256 + 128) / nbins;
+
+    for (j = 0; j < w; j++) {
+        for (i = 0; i < h; i++) {
+            val = GET_DATA_BYTE(datas + i * wpls, j);
+            histo[gray2bin[val]]++;
+        }
+
+        if (type == L_MEDIAN_VAL) {
+            sum = 0;
+            target = (h + 1) / 2;
+            for (k = 0; k < nbins; k++) {
+                sum += histo[k];
+                if (sum >= target) {
+                    rowvect[j] = bin2gray[k];
+                    break;
+                }
+            }
+        } else if (type == L_MODE_VAL) {
+            max = 0;
+            modeval = 0;
+            for (k = 0; k < nbins; k++) {
+                if (histo[k] > max) {
+                    max = histo[k];
+                    modeval = k;
+                }
+            }
+            if (max < thresh)
+                rowvect[j] = 0;
+            else
+                rowvect[j] = bin2gray[modeval];
+        } else {  /* type == L_MODE_COUNT */
+            max = 0;
+            for (k = 0; k < nbins; k++) {
+                if (histo[k] > max)
+                    max = histo[k];
+            }
+            rowvect[j] = max;
+        }
+        for (k = 0; k < nbins; k++)
+            histo[k] = 0;
+    }
+
+    LEPT_FREE(histo);
+    LEPT_FREE(gray2bin);
+    LEPT_FREE(bin2gray);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixSetPixelColumn()
+ *
+ * \param[in]   pix      8 bpp; not cmapped
+ * \param[in]   col      column index
+ * \param[in]   colvect  vector of floats
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+pixSetPixelColumn(PIX        *pix,
+                  l_int32     col,
+                  l_float32  *colvect)
+{
+l_int32    i, w, h, wpl;
+l_uint32  *data;
+
+    if (!pix || pixGetDepth(pix) != 8)
+        return ERROR_INT("pix not defined or not 8 bpp", __func__, 1);
+    if (!colvect)
+        return ERROR_INT("colvect not defined", __func__, 1);
+    pixGetDimensions(pix, &w, &h, NULL);
+    if (col < 0 || col > w)
+        return ERROR_INT("invalid col", __func__, 1);
+
+    data = pixGetData(pix);
+    wpl = pixGetWpl(pix);
+    for (i = 0; i < h; i++)
+        SET_DATA_BYTE(data + i * wpl, col, (l_int32)colvect[i]);
+
+    return 0;
+}
+
+
+/*-------------------------------------------------------------*
+ *              Foreground/background estimation               *
+ *-------------------------------------------------------------*/
+/*!
+ * \brief   pixThresholdForFgBg()
+ *
+ * \param[in]    pixs    any depth; cmapped ok
+ * \param[in]    factor  subsampling factor; integer >= 1
+ * \param[in]    thresh  threshold for generating foreground mask
+ * \param[out]   pfgval  [optional] average foreground value
+ * \param[out]   pbgval  [optional] average background value
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+pixThresholdForFgBg(PIX      *pixs,
+                    l_int32   factor,
+                    l_int32   thresh,
+                    l_int32  *pfgval,
+                    l_int32  *pbgval)
+{
+l_float32  fval;
+PIX       *pixg, *pixm;
+
+    if (pfgval) *pfgval = 0;
+    if (pbgval) *pbgval = 0;
+    if (!pfgval && !pbgval)
+        return ERROR_INT("no data requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+
+        /* Generate a subsampled 8 bpp version and a mask over the fg */
+    pixg = pixConvertTo8BySampling(pixs, factor, 0);
+    pixm = pixThresholdToBinary(pixg, thresh);
+
+    if (pfgval) {
+        pixGetAverageMasked(pixg, pixm, 0, 0, 1, L_MEAN_ABSVAL, &fval);
+        *pfgval = (l_int32)(fval + 0.5);
+    }
+
+    if (pbgval) {
+        pixInvert(pixm, pixm);
+        pixGetAverageMasked(pixg, pixm, 0, 0, 1, L_MEAN_ABSVAL, &fval);
+        *pbgval = (l_int32)(fval + 0.5);
+    }
+
+    pixDestroy(&pixg);
+    pixDestroy(&pixm);
+    return 0;
+}
+
+
+/*!
+ * \brief   pixSplitDistributionFgBg()
+ *
+ * \param[in]    pixs        any depth; cmapped ok
+ * \param[in]    scorefract  fraction of the max score, used to determine
+ *                           the range over which the histogram min is searched
+ * \param[in]    factor      subsampling factor; integer >= 1
+ * \param[out]   pthresh     [optional] best threshold for separating
+ * \param[out]   pfgval      [optional] average foreground value
+ * \param[out]   pbgval      [optional] average background value
+ * \param[out]   ppixdb      [optional] plot of distribution and split point
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) See numaSplitDistribution() for details on the underlying
+ *          method of choosing a threshold.
+ * </pre>
+ */
+l_ok
+pixSplitDistributionFgBg(PIX       *pixs,
+                         l_float32  scorefract,
+                         l_int32    factor,
+                         l_int32   *pthresh,
+                         l_int32   *pfgval,
+                         l_int32   *pbgval,
+                         PIX      **ppixdb)
+{
+char       buf[256];
+l_int32    thresh;
+l_float32  avefg, avebg, maxnum;
+GPLOT     *gplot;
+NUMA      *na, *nascore, *nax, *nay;
+PIX       *pixg;
+
+    if (pthresh) *pthresh = 0;
+    if (pfgval) *pfgval = 0;
+    if (pbgval) *pbgval = 0;
+    if (ppixdb) *ppixdb = NULL;
+    if (!pthresh && !pfgval && !pbgval)
+        return ERROR_INT("no data requested", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+
+        /* Generate a subsampled 8 bpp version */
+    pixg = pixConvertTo8BySampling(pixs, factor, 0);
+
+        /* Make the fg/bg estimates */
+    na = pixGetGrayHistogram(pixg, 1);
+    if (ppixdb) {
+        numaSplitDistribution(na, scorefract, &thresh, &avefg, &avebg,
+                              NULL, NULL, &nascore);
+        numaDestroy(&nascore);
+    } else {
+        numaSplitDistribution(na, scorefract, &thresh, &avefg, &avebg,
+                              NULL, NULL, NULL);
+    }
+
+    if (pthresh) *pthresh = thresh;
+    if (pfgval) *pfgval = (l_int32)(avefg + 0.5);
+    if (pbgval) *pbgval = (l_int32)(avebg + 0.5);
+
+    if (ppixdb) {
+        lept_mkdir("lept/redout");
+        gplot = gplotCreate("/tmp/lept/redout/histplot", GPLOT_PNG, "Histogram",
+                            "Grayscale value", "Number of pixels");
+        gplotAddPlot(gplot, NULL, na, GPLOT_LINES, NULL);
+        nax = numaMakeConstant(thresh, 2);
+        numaGetMax(na, &maxnum, NULL);
+        nay = numaMakeConstant(0, 2);
+        numaReplaceNumber(nay, 1, (l_int32)(0.5 * maxnum));
+        snprintf(buf, sizeof(buf), "score fract = %3.1f", scorefract);
+        gplotAddPlot(gplot, nax, nay, GPLOT_LINES, buf);
+        *ppixdb = gplotMakeOutputPix(gplot);
+        gplotDestroy(&gplot);
+        numaDestroy(&nax);
+        numaDestroy(&nay);
+    }
+
+    pixDestroy(&pixg);
+    numaDestroy(&na);
+    return 0;
+}