diff mupdf-source/thirdparty/leptonica/src/readbarcode.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/readbarcode.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1500 @@
+/*====================================================================*
+ -  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 readbarcode.c
+ * <pre>
+ *
+ *      Basic operations to locate and identify the line widths
+ *      in 1D barcodes.
+ *
+ *      Top level
+ *          SARRAY          *pixProcessBarcodes()
+ *
+ *      Next levels
+ *          PIXA            *pixExtractBarcodes()
+ *          SARRAY          *pixReadBarcodes()
+ *          l_int32          pixReadBarcodeWidths()
+ *
+ *      Location
+ *          BOXA            *pixLocateBarcodes()
+ *          static PIX      *pixGenerateBarcodeMask()
+ *
+ *      Extraction and deskew
+ *          PIXA            *pixDeskewBarcodes()
+ *
+ *      Process to get line widths
+ *          NUMA            *pixExtractBarcodeWidths1()
+ *          NUMA            *pixExtractBarcodeWidths2()
+ *          NUMA            *pixExtractBarcodeCrossings()
+ *
+ *      Average adjacent rasters
+ *          static NUMA     *pixAverageRasterScans()
+ *
+ *      Signal processing for barcode widths
+ *          NUMA            *numaQuantizeCrossingsByWidth()
+ *          static l_int32   numaGetCrossingDistances()
+ *          static NUMA     *numaLocatePeakRanges()
+ *          static NUMA     *numaGetPeakCentroids()
+ *          static NUMA     *numaGetPeakWidthLUT()
+ *          NUMA            *numaQuantizeCrossingsByWindow()
+ *          static l_int32   numaEvalBestWidthAndShift()
+ *          static l_int32   numaEvalSyncError()
+ *
+ *
+ *  NOTE CAREFULLY: This is "early beta" code.  It has not been tuned
+ *  to work robustly on a large database of barcode images.  I'm putting
+ *  it out so that people can play with it, find out how it breaks, and
+ *  contribute decoders for other barcode formats.  Both the functional
+ *  interfaces and ABI will almost certainly change in the coming
+ *  few months.  The actual decoder, in bardecode.c, at present only
+ *  works on the following codes: Code I2of5, Code 2of5, Code 39, Code 93
+ *  Codabar and UPCA.  To add another barcode format, it is necessary
+ *  to make changes in readbarcode.h and bardecode.c.
+ *  The program prog/barcodetest shows how to run from the top level
+ *  (image --> decoded data).
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include "allheaders.h"
+#include "readbarcode.h"
+
+    /* Parameters for pixGenerateBarcodeMask() */
+static const l_int32  MAX_SPACE_WIDTH = 19;  /* was 15 */
+static const l_int32  MAX_NOISE_WIDTH = 50;  /* smaller than barcode width */
+static const l_int32  MAX_NOISE_HEIGHT = 30;  /* smaller than barcode height */
+
+    /* Minimum barcode image size */
+static const l_int32  MIN_BC_WIDTH = 50;
+static const l_int32  MIN_BC_HEIGHT = 50;
+
+    /* Static functions */
+static PIX *pixGenerateBarcodeMask(PIX *pixs, l_int32 maxspace,
+                                   l_int32 nwidth, l_int32 nheight);
+static NUMA *pixAverageRasterScans(PIX *pixs, l_int32 nscans);
+static l_int32 numaGetCrossingDistances(NUMA *nas, NUMA **pnaedist,
+                                        NUMA **pnaodist, l_float32 *pmindist,
+                                        l_float32 *pmaxdist);
+static NUMA *numaLocatePeakRanges(NUMA *nas, l_float32 minfirst,
+                                  l_float32 minsep, l_float32 maxmin);
+static NUMA *numaGetPeakCentroids(NUMA *nahist, NUMA *narange);
+static NUMA *numaGetPeakWidthLUT(NUMA *narange, NUMA *nacent);
+static l_int32 numaEvalBestWidthAndShift(NUMA *nas, l_int32 nwidth,
+                                         l_int32 nshift, l_float32 minwidth,
+                                         l_float32 maxwidth,
+                                         l_float32 *pbestwidth,
+                                         l_float32 *pbestshift,
+                                         l_float32 *pbestscore);
+static l_int32 numaEvalSyncError(NUMA *nas, l_int32 ifirst, l_int32 ilast,
+                                 l_float32 width, l_float32 shift,
+                                 l_float32 *pscore, NUMA **pnad);
+
+
+#ifndef  NO_CONSOLE_IO
+#define  DEBUG_DESKEW     0
+#define  DEBUG_WIDTHS     0
+#endif  /* ~NO_CONSOLE_IO */
+
+
+/*------------------------------------------------------------------------*
+ *                               Top level                                *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   pixProcessBarcodes()
+ *
+ * \param[in]    pixs        any depth
+ * \param[in]    format      L_BF_ANY, L_BF_CODEI2OF5, L_BF_CODE93, ...
+ * \param[in]    method      L_USE_WIDTHS, L_USE_WINDOWS
+ * \param[out]   psaw        [optional] sarray of bar widths
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  sarray text of barcodes, or NULL if none found or on error
+ */
+SARRAY *
+pixProcessBarcodes(PIX      *pixs,
+                   l_int32   format,
+                   l_int32   method,
+                   SARRAY  **psaw,
+                   l_int32   debugflag)
+{
+PIX     *pixg;
+PIXA    *pixa;
+SARRAY  *sad;
+
+    if (psaw) *psaw = NULL;
+    if (!pixs)
+        return (SARRAY *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (format != L_BF_ANY && !barcodeFormatIsSupported(format))
+        return (SARRAY *)ERROR_PTR("unsupported format", __func__, NULL);
+    if (method != L_USE_WIDTHS && method != L_USE_WINDOWS)
+        return (SARRAY *)ERROR_PTR("invalid method", __func__, NULL);
+
+        /* Get an 8 bpp image, no cmap */
+    if (pixGetDepth(pixs) == 8 && !pixGetColormap(pixs))
+        pixg = pixClone(pixs);
+    else
+        pixg = pixConvertTo8(pixs, 0);
+
+    pixa = pixExtractBarcodes(pixg, debugflag);
+    pixDestroy(&pixg);
+    if (!pixa)
+        return (SARRAY *)ERROR_PTR("no barcode(s) found", __func__, NULL);
+
+    sad = pixReadBarcodes(pixa, format, method, psaw, debugflag);
+    pixaDestroy(&pixa);
+    return sad;
+}
+
+
+/*!
+ * \brief   pixExtractBarcodes()
+ *
+ * \param[in]    pixs        8 bpp, no colormap
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  pixa  deskewed and cropped barcodes, or NULL if none found
+ *                or on error
+ */
+PIXA *
+pixExtractBarcodes(PIX     *pixs,
+                   l_int32  debugflag)
+{
+l_int32    i, n;
+l_float32  angle, conf;
+BOX       *box;
+BOXA      *boxa;
+PIX       *pix1, *pix2, *pix3;
+PIXA      *pixa;
+
+    if (!pixs || pixGetDepth(pixs) != 8 || pixGetColormap(pixs))
+        return (PIXA *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+
+        /* Locate them; use small threshold for edges. */
+    boxa = pixLocateBarcodes(pixs, 20, &pix2, &pix1);
+    n = boxaGetCount(boxa);
+    L_INFO("%d possible barcode(s) found\n", __func__, n);
+    if (n == 0) {
+        boxaDestroy(&boxa);
+        pixDestroy(&pix2);
+        pixDestroy(&pix1);
+        return NULL;
+    }
+
+    if (debugflag) {
+        boxaWriteStderr(boxa);
+        pixDisplay(pix2, 100, 100);
+        pixDisplay(pix1, 800, 100);
+    }
+    pixDestroy(&pix1);
+
+        /* Deskew each barcode individually */
+    pixa = pixaCreate(n);
+    for (i = 0; i < n; i++) {
+        box = boxaGetBox(boxa, i, L_CLONE);
+        pix3 = pixDeskewBarcode(pixs, pix2, box, 15, 20, &angle, &conf);
+        if (!pix3) conf = 0.0;  /* don't use */
+        L_INFO("angle = %6.2f, conf = %6.2f\n", __func__, angle, conf);
+        if (conf > 5.0) {
+            pixaAddPix(pixa, pix3, L_INSERT);
+            pixaAddBox(pixa, box, L_INSERT);
+        } else {
+            pixDestroy(&pix3);
+            boxDestroy(&box);
+        }
+    }
+    pixDestroy(&pix2);
+    boxaDestroy(&boxa);
+
+#if  DEBUG_DESKEW
+    pix3 = pixaDisplayTiledInRows(pixa, 8, 1000, 1.0, 0, 30, 2);
+    pixWrite("/tmp/lept/pix3.png", pix3, IFF_PNG);
+    pixDestroy(&pix3);
+#endif  /* DEBUG_DESKEW */
+
+    return pixa;
+}
+
+
+/*!
+ * \brief   pixReadBarcodes()
+ *
+ * \param[in]    pixa        of 8 bpp deskewed and cropped barcodes
+ * \param[in]    format      L_BF_ANY, L_BF_CODEI2OF5, L_BF_CODE93, ...
+ * \param[in]    method      L_USE_WIDTHS, L_USE_WINDOWS;
+ * \param[out]   psaw        [optional] sarray of bar widths
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  sa   sarray of widths, one string for each barcode found,
+ *               or NULL on error
+ */
+SARRAY *
+pixReadBarcodes(PIXA     *pixa,
+                l_int32   format,
+                l_int32   method,
+                SARRAY  **psaw,
+                l_int32   debugflag)
+{
+char      *barstr, *data;
+char       emptystring[] = "";
+l_int32    w, h, i, j, n, nbars, ival;
+NUMA      *na;
+PIX       *pix1;
+SARRAY    *saw, *sad;
+
+    if (psaw) *psaw = NULL;
+    if (!pixa)
+        return (SARRAY *)ERROR_PTR("pixa not defined", __func__, NULL);
+    if (format != L_BF_ANY && !barcodeFormatIsSupported(format))
+        return (SARRAY *)ERROR_PTR("unsupported format", __func__, NULL);
+    if (method != L_USE_WIDTHS && method != L_USE_WINDOWS)
+        return (SARRAY *)ERROR_PTR("invalid method", __func__, NULL);
+
+    n = pixaGetCount(pixa);
+    saw = sarrayCreate(n);
+    sad = sarrayCreate(n);
+    for (i = 0; i < n; i++) {
+            /* Extract the widths of the lines in each barcode */
+        pix1 = pixaGetPix(pixa, i, L_CLONE);
+        pixGetDimensions(pix1, &w, &h, NULL);
+        if (w < MIN_BC_WIDTH || h < MIN_BC_HEIGHT) {
+            L_ERROR("pix is too small: w = %d, h = %d\n", __func__, w, h);
+            pixDestroy(&pix1);
+            continue;
+        }
+        na = pixReadBarcodeWidths(pix1, method, debugflag);
+        pixDestroy(&pix1);
+        if (!na) {
+            ERROR_INT("valid barcode widths not returned", __func__, 1);
+            continue;
+        }
+
+            /* Save the widths as a string */
+        nbars = numaGetCount(na);
+        barstr = (char *)LEPT_CALLOC(nbars + 1, sizeof(char));
+        for (j = 0; j < nbars; j++) {
+            numaGetIValue(na, j, &ival);
+            barstr[j] = 0x30 + ival;
+        }
+        sarrayAddString(saw, barstr, L_INSERT);
+        numaDestroy(&na);
+
+            /* Decode the width strings */
+        data = barcodeDispatchDecoder(barstr, format, debugflag);
+        if (!data) {
+            ERROR_INT("barcode not decoded", __func__, 1);
+            sarrayAddString(sad, emptystring, L_COPY);
+            continue;
+        }
+        sarrayAddString(sad, data, L_INSERT);
+    }
+
+        /* If nothing found, clean up */
+    if (sarrayGetCount(saw) == 0) {
+        sarrayDestroy(&saw);
+        sarrayDestroy(&sad);
+        return (SARRAY *)ERROR_PTR("no valid barcode data", __func__, NULL);
+    }
+
+    if (psaw)
+        *psaw = saw;
+    else
+        sarrayDestroy(&saw);
+    return sad;
+}
+
+
+/*!
+ * \brief   pixReadBarcodeWidths()
+ *
+ * \param[in]    pixs        of 8 bpp deskewed and cropped barcode
+ * \param[in]    method      L_USE_WIDTHS, L_USE_WINDOWS;
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  na   numa of widths (each in set {1,2,3,4}, or NULL on error
+ */
+NUMA *
+pixReadBarcodeWidths(PIX     *pixs,
+                     l_int32  method,
+                     l_int32  debugflag)
+{
+l_float32  winwidth;
+NUMA      *na;
+
+    if (!pixs)
+        return (NUMA *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (pixGetDepth(pixs) != 8)
+        return (NUMA *)ERROR_PTR("pixs not 8 bpp", __func__, NULL);
+    if (method != L_USE_WIDTHS && method != L_USE_WINDOWS)
+        return (NUMA *)ERROR_PTR("invalid method", __func__, NULL);
+
+        /* Extract the widths of the lines in each barcode */
+    if (method == L_USE_WIDTHS)
+        na = pixExtractBarcodeWidths1(pixs, 120, 0.25, NULL, NULL,
+                                      debugflag);
+    else  /* method == L_USE_WINDOWS */
+        na = pixExtractBarcodeWidths2(pixs, 120, &winwidth,
+                                      NULL, debugflag);
+#if  DEBUG_WIDTHS
+        if (method == L_USE_WINDOWS)
+            lept_stderr("Window width for barcode: %7.3f\n", winwidth);
+        numaWriteStderr(na);
+#endif  /* DEBUG_WIDTHS */
+
+    if (!na)
+        return (NUMA *)ERROR_PTR("barcode widths invalid", __func__, NULL);
+
+    return na;
+}
+
+
+/*------------------------------------------------------------------------*
+ *                        Locate barcode in image                         *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   pixLocateBarcodes()
+ *
+ * \param[in]    pixs     any depth
+ * \param[in]    thresh   for binarization of edge filter output; typ. 20
+ * \param[out]   ppixb    [optional] binarized edge filtered input image
+ * \param[out]   ppixm    [optional] mask over barcodes
+ * \return  boxa   location of barcodes, or NULL if none found or on error
+ */
+BOXA *
+pixLocateBarcodes(PIX     *pixs,
+                  l_int32  thresh,
+                  PIX    **ppixb,
+                  PIX    **ppixm)
+{
+BOXA  *boxa;
+PIX   *pix8, *pixe, *pixb, *pixm;
+
+    if (!pixs)
+        return (BOXA *)ERROR_PTR("pixs not defined", __func__, NULL);
+
+        /* Get an 8 bpp image, no cmap */
+    if (pixGetDepth(pixs) == 8 && !pixGetColormap(pixs))
+        pix8 = pixClone(pixs);
+    else
+        pix8 = pixConvertTo8(pixs, 0);
+
+        /* Get a 1 bpp image of the edges */
+    pixe = pixSobelEdgeFilter(pix8, L_ALL_EDGES);
+    pixb = pixThresholdToBinary(pixe, thresh);
+    pixInvert(pixb, pixb);
+    pixDestroy(&pix8);
+    pixDestroy(&pixe);
+
+    pixm = pixGenerateBarcodeMask(pixb, MAX_SPACE_WIDTH, MAX_NOISE_WIDTH,
+                                  MAX_NOISE_HEIGHT);
+    boxa = pixConnComp(pixm, NULL, 8);
+
+    if (ppixb)
+        *ppixb = pixb;
+    else
+        pixDestroy(&pixb);
+    if (ppixm)
+        *ppixm = pixm;
+    else
+        pixDestroy(&pixm);
+
+    return boxa;
+}
+
+
+/*!
+ * \brief   pixGenerateBarcodeMask()
+ *
+ * \param[in]    pixs       1 bpp
+ * \param[in]    maxspace   largest space in the barcode, in pixels
+ * \param[in]    nwidth     opening 'width' to remove noise
+ * \param[in]    nheight    opening 'height' to remove noise
+ * \return  pixm   mask over barcodes, or NULL if none found or on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) For noise removal, 'width' and 'height' are referred to the
+ *          barcode orientation.
+ *      (2) If there is skew, the mask will not cover the barcode corners.
+ * </pre>
+ */
+static PIX *
+pixGenerateBarcodeMask(PIX     *pixs,
+                       l_int32  maxspace,
+                       l_int32  nwidth,
+                       l_int32  nheight)
+{
+PIX  *pixt1, *pixt2, *pixd;
+
+    if (!pixs || pixGetDepth(pixs) != 1)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+
+        /* Identify horizontal barcodes */
+    pixt1 = pixCloseBrick(NULL, pixs, maxspace + 1, 1);
+    pixt2 = pixOpenBrick(NULL, pixs, maxspace + 1, 1);
+    pixXor(pixt2, pixt2, pixt1);
+    pixOpenBrick(pixt2, pixt2, nwidth, nheight);
+    pixDestroy(&pixt1);
+
+        /* Identify vertical barcodes */
+    pixt1 = pixCloseBrick(NULL, pixs, 1, maxspace + 1);
+    pixd = pixOpenBrick(NULL, pixs, 1, maxspace + 1);
+    pixXor(pixd, pixd, pixt1);
+    pixOpenBrick(pixd, pixd, nheight, nwidth);
+    pixDestroy(&pixt1);
+
+        /* Combine to get all barcodes */
+    pixOr(pixd, pixd, pixt2);
+    pixDestroy(&pixt2);
+
+    return pixd;
+}
+
+
+/*------------------------------------------------------------------------*
+ *                        Extract and deskew barcode                      *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   pixDeskewBarcode()
+ *
+ * \param[in]    pixs        input image; 8 bpp
+ * \param[in]    pixb        binarized edge-filtered input image
+ * \param[in]    box         identified region containing barcode
+ * \param[in]    margin      of extra pixels around box to extract
+ * \param[in]    threshold   for binarization; ~20
+ * \param[out]   pangle      [optional] in degrees, clockwise is positive
+ * \param[out]   pconf       [optional] confidence
+ * \return  pixd   deskewed barcode, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *     (1) The (optional) angle returned is the angle in degrees (cw positive)
+ *         necessary to rotate the image so that it is deskewed.
+ * </pre>
+ */
+PIX *
+pixDeskewBarcode(PIX        *pixs,
+                 PIX        *pixb,
+                 BOX        *box,
+                 l_int32     margin,
+                 l_int32     threshold,
+                 l_float32  *pangle,
+                 l_float32  *pconf)
+{
+l_int32    x, y, w, h, n;
+l_float32  angle, angle1, angle2, conf, conf1, conf2, score1, score2, deg2rad;
+BOX       *box1, *box2;
+BOXA      *boxa1, *boxa2;
+PIX       *pix1, *pix2, *pix3, *pix4, *pix5, *pix6, *pixd;
+
+    if (pangle) *pangle = 0.0;
+    if (pconf) *pconf = 0.0;
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return (PIX *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+    if (!pixb || pixGetDepth(pixb) != 1)
+        return (PIX *)ERROR_PTR("pixb undefined or not 1 bpp", __func__, NULL);
+    if (!box)
+        return (PIX *)ERROR_PTR("box not defined or 1 bpp", __func__, NULL);
+
+        /* Clip out */
+    deg2rad = 3.1415926535f / 180.f;
+    boxGetGeometry(box, &x, &y, &w, &h);
+    box2 = boxCreate(x - 25, y - 25, w + 51, h + 51);
+    pix1 = pixClipRectangle(pixb, box2, NULL);
+    pix2 = pixClipRectangle(pixs, box2, NULL);
+    boxDestroy(&box2);
+
+        /* Deskew, looking at all possible orientations over 180 degrees */
+    pix3 = pixRotateOrth(pix1, 1);  /* look for vertical bar lines */
+    pix4 = pixClone(pix1);   /* look for horizontal bar lines */
+    pixFindSkewSweepAndSearchScore(pix3, &angle1, &conf1, &score1,
+                                   1, 1, 0.0f, 45.0f, 2.5f, 0.01f);
+    pixFindSkewSweepAndSearchScore(pix4, &angle2, &conf2, &score2,
+                                   1, 1, 0.0f, 45.0f, 2.5f, 0.01f);
+    pixDestroy(&pix1);
+    pixDestroy(&pix3);
+    pixDestroy(&pix4);
+
+        /* Because we're using the boundary pixels of the barcodes,
+         * the peak can be sharper (and the confidence ratio higher)
+         * from the signal across the top and bottom of the barcode.
+         * However, the max score, which is the magnitude of the signal
+         * at the optimum skew angle, will be smaller, so we use the
+         * max score as the primary indicator of orientation. */
+    if (score1 >= score2) {
+        conf = conf1;
+        if (conf1 > 6.0 && L_ABS(angle1) > 0.1) {
+            angle = angle1;
+            pix5 = pixRotate(pix2, deg2rad * angle1, L_ROTATE_AREA_MAP,
+                              L_BRING_IN_WHITE, 0, 0);
+        } else {
+            angle = 0.0;
+            pix5 = pixClone(pix2);
+        }
+    } else {  /* score2 > score1 */
+        conf = conf2;
+        pix6 = pixRotateOrth(pix2, 1);
+        if (conf2 > 6.0 && L_ABS(angle2) > 0.1) {
+            angle = 90.0 + angle2;
+            pix5 = pixRotate(pix6, deg2rad * angle2, L_ROTATE_AREA_MAP,
+                              L_BRING_IN_WHITE, 0, 0);
+        } else {
+            angle = 90.0;
+            pix5 = pixClone(pix6);
+        }
+        pixDestroy(&pix6);
+    }
+    pixDestroy(&pix2);
+
+        /* Extract barcode plus a margin around it */
+    boxa1 = pixLocateBarcodes(pix5, threshold, 0, 0);
+    if ((n = boxaGetCount(boxa1)) != 1) {
+        L_WARNING("barcode mask in %d components\n", __func__, n);
+        boxa2 = boxaSort(boxa1, L_SORT_BY_AREA, L_SORT_DECREASING, NULL);
+    } else {
+        boxa2 = boxaCopy(boxa1, L_CLONE);
+    }
+    box1 = boxaGetBox(boxa2, 0, L_CLONE);
+    boxGetGeometry(box1, &x, &y, &w, &h);
+    box2 = boxCreate(x - margin, y - margin, w + 2 * margin,
+                     h + 2 * margin);
+    pixd = pixClipRectangle(pix5, box2, NULL);
+    boxDestroy(&box1);
+    boxDestroy(&box2);
+    boxaDestroy(&boxa1);
+    boxaDestroy(&boxa2);
+    pixDestroy(&pix5);
+
+    if (pangle) *pangle = angle;
+    if (pconf) *pconf = conf;
+
+    if (!pixd)
+        L_ERROR("pixd not made\n", __func__);
+    return pixd;
+}
+
+
+/*------------------------------------------------------------------------*
+ *                        Process to get line widths                      *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   pixExtractBarcodeWidths1()
+ *
+ * \param[in]    pixs        input image; 8 bpp
+ * \param[in]    thresh      estimated pixel threshold for crossing
+ *                           white <--> black; typ. ~120
+ * \param[in]    binfract    histo binsize as a fraction of minsize; e.g., 0.25
+ * \param[out]   pnaehist    [optional] histogram of black widths; NULL ok
+ * \param[out]   pnaohist    [optional] histogram of white widths; NULL ok
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  nad   numa of barcode widths in encoded integer units,
+ *                or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *     (1) The widths are alternating black/white, starting with black
+ *         and ending with black.
+ *     (2) This method uses the widths of the bars directly, in terms
+ *         of the (float) number of pixels between transitions.
+ *         The histograms of these widths for black and white bars is
+ *         generated and interpreted.
+ * </pre>
+ */
+NUMA *
+pixExtractBarcodeWidths1(PIX      *pixs,
+                        l_float32  thresh,
+                        l_float32  binfract,
+                        NUMA     **pnaehist,
+                        NUMA     **pnaohist,
+                        l_int32    debugflag)
+{
+NUMA  *nac, *nad;
+
+    if (pnaehist) *pnaehist = NULL;
+    if (pnaohist) *pnaohist = NULL;
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return (NUMA *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+
+        /* Get the best estimate of the crossings, in pixel units */
+    if ((nac = pixExtractBarcodeCrossings(pixs, thresh, debugflag)) == NULL)
+        return (NUMA *)ERROR_PTR("nac not made", __func__, NULL);
+
+        /* Get the array of bar widths, starting with a black bar  */
+    nad = numaQuantizeCrossingsByWidth(nac, binfract, pnaehist,
+                                       pnaohist, debugflag);
+
+    numaDestroy(&nac);
+    return nad;
+}
+
+
+/*!
+ * \brief   pixExtractBarcodeWidths2()
+ *
+ * \param[in]    pixs        input image; 8 bpp
+ * \param[in]    thresh      estimated pixel threshold for crossing
+ *                           white <--> black; typ. ~120
+ * \param[out]   pwidth      [optional] best decoding window width, in pixels
+ * \param[out]   pnac        [optional] number of transitions in each window
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  nad   numa of barcode widths in encoded integer units,
+ *                or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The widths are alternating black/white, starting with black
+ *          and ending with black.
+ *      (2) The optional best decoding window width is the width of the window
+ *          that is used to make a decision about whether a transition occurs.
+ *          It is approximately the average width in pixels of the narrowest
+ *          white and black bars (i.e., those corresponding to unit width).
+ *      (3) The optional return signal %nac is a sequence of 0s, 1s,
+ *          and perhaps a few 2s, giving the number of crossings in each window.
+ *          On the occasion where there is a '2', it is interpreted as
+ *          as ending two runs: the previous one and another one that has length 1.
+ * </pre>
+ */
+NUMA *
+pixExtractBarcodeWidths2(PIX        *pixs,
+                         l_float32   thresh,
+                         l_float32  *pwidth,
+                         NUMA      **pnac,
+                         l_int32     debugflag)
+{
+NUMA  *nacp, *nad;
+
+    if (pwidth) *pwidth = 0;
+    if (pnac) *pnac = NULL;
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return (NUMA *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+
+        /* Get the best estimate of the crossings, in pixel units */
+    if ((nacp = pixExtractBarcodeCrossings(pixs, thresh, debugflag)) == NULL)
+        return (NUMA *)ERROR_PTR("nacp not made", __func__, NULL);
+
+        /* Quantize the crossings to get actual windowed data */
+    nad = numaQuantizeCrossingsByWindow(nacp, 2.0, pwidth, NULL,
+                                        pnac, debugflag);
+    numaDestroy(&nacp);
+    return nad;
+}
+
+
+/*!
+ * \brief   pixExtractBarcodeCrossings()
+ *
+ * \param[in]    pixs        input image; 8 bpp
+ * \param[in]    thresh      estimated pixel threshold for crossing
+ *                           white <--> black; typ. ~120
+ * \param[in]    debugflag   use 1 to generate debug output
+ * \return  numa   of crossings, in pixel units, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Require at least 10 crossings.
+ * </pre>
+ */
+NUMA *
+pixExtractBarcodeCrossings(PIX       *pixs,
+                           l_float32  thresh,
+                           l_int32    debugflag)
+{
+l_int32    w;
+l_float32  bestthresh;
+GPLOT     *gplot;
+NUMA      *nas, *nax, *nay, *nad;
+
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return (NUMA *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+
+        /* Scan pixels horizontally and average results */
+    if ((nas = pixAverageRasterScans(pixs, 50)) == NULL)
+        return (NUMA *)ERROR_PTR("nas not made", __func__, NULL);
+
+        /* Interpolate to get 4x the number of values */
+    w = pixGetWidth(pixs);
+    numaInterpolateEqxInterval(0.0, 1.0, nas, L_QUADRATIC_INTERP, 0.0,
+                               (l_float32)(w - 1), 4 * w + 1, &nax, &nay);
+
+    if (debugflag) {
+        lept_mkdir("lept/barcode");
+        gplot = gplotCreate("/tmp/lept/barcode/signal", GPLOT_PNG,
+                            "Pixel values", "dist in pixels", "value");
+        gplotAddPlot(gplot, nax, nay, GPLOT_LINES, "plot 1");
+        gplotMakeOutput(gplot);
+        gplotDestroy(&gplot);
+    }
+
+        /* Locate the crossings.  Run multiple times with different
+         * thresholds, and choose a threshold in the center of the
+         * run of thresholds that all give the maximum number of crossings. */
+    numaSelectCrossingThreshold(nax, nay, thresh, &bestthresh);
+
+        /* Get the crossings with the best threshold. */
+    nad = numaCrossingsByThreshold(nax, nay, bestthresh);
+    numaDestroy(&nas);
+    numaDestroy(&nax);
+    numaDestroy(&nay);
+
+    if (numaGetCount(nad) < 10) {
+        L_ERROR("Only %d crossings; failure\n", __func__, numaGetCount(nad));
+        numaDestroy(&nad);
+    }
+    return nad;
+}
+
+
+/*------------------------------------------------------------------------*
+ *                         Average adjacent rasters                       *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   pixAverageRasterScans()
+ *
+ * \param[in]    pixs     input image; 8 bpp
+ * \param[in]    nscans   number of adjacent scans, about the center vertically
+ * \return  numa of average pixel values across image, or NULL on error
+ */
+static NUMA *
+pixAverageRasterScans(PIX     *pixs,
+                      l_int32  nscans)
+{
+l_int32     w, h, first, last, i, j, wpl, val;
+l_uint32   *line, *data;
+l_float32  *array;
+NUMA       *nad;
+
+    if (!pixs || pixGetDepth(pixs) != 8)
+        return (NUMA *)ERROR_PTR("pixs undefined or not 8 bpp", __func__, NULL);
+
+    pixGetDimensions(pixs, &w, &h, NULL);
+    if (nscans > h) {
+        first = 0;
+        last = h - 1;
+        nscans = h;
+    } else {
+        first = (h - nscans) / 2;
+        last = first + nscans - 1;
+    }
+
+    nad = numaCreate(w);
+    numaSetCount(nad, w);
+    array = numaGetFArray(nad, L_NOCOPY);
+    wpl = pixGetWpl(pixs);
+    data = pixGetData(pixs);
+    for (j = 0; j < w; j++) {
+        for (i = first; i <= last; i++) {
+            line = data + i * wpl;
+            val = GET_DATA_BYTE(line, j);
+            array[j] += val;
+        }
+        array[j] = array[j] / (l_float32)nscans;
+    }
+
+    return nad;
+}
+
+
+/*------------------------------------------------------------------------*
+ *                   Signal processing for barcode widths                 *
+ *------------------------------------------------------------------------*/
+/*!
+ * \brief   numaQuantizeCrossingsByWidth()
+ *
+ * \param[in]    nas         numa of crossing locations, in pixel units
+ * \param[in]    binfract    histo binsize as a fraction of minsize; e.g., 0.25
+ * \param[out]   pnaehist    [optional] histo of even (black) bar widths
+ * \param[out]   pnaohist    [optional] histo of odd (white) bar widths
+ * \param[in]    debugflag   1 to generate plots of histograms of bar widths
+ * \return  nad   sequence of widths, in unit sizes, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This first computes the histogram of black and white bar widths,
+ *          binned in appropriate units.  There should be well-defined
+ *          peaks, each corresponding to a specific width.  The sequence
+ *          of barcode widths (namely, the integers from the set {1,2,3,4})
+ *          is returned.
+ *      (2) The optional returned histograms are binned in width units
+ *          that are inversely proportional to %binfract.  For example,
+ *          if %binfract = 0.25, there are 4.0 bins in the distance of
+ *          the width of the narrowest bar.
+ * </pre>
+ */
+NUMA *
+numaQuantizeCrossingsByWidth(NUMA       *nas,
+                             l_float32   binfract,
+                             NUMA      **pnaehist,
+                             NUMA      **pnaohist,
+                             l_int32     debugflag)
+{
+l_int32    i, n, ret, ned, nod, iw, width;
+l_float32  val, minsize, maxsize, factor;
+GPLOT     *gplot;
+NUMA      *naedist, *naodist, *naehist, *naohist, *naecent, *naocent;
+NUMA      *naerange, *naorange, *naelut, *naolut, *nad;
+
+    if (pnaehist) *pnaehist = NULL;
+    if (pnaohist) *pnaohist = NULL;
+    if (!nas)
+        return (NUMA *)ERROR_PTR("nas not defined", __func__, NULL);
+    n = numaGetCount(nas);
+    if (n < 10)
+        return (NUMA *)ERROR_PTR("n < 10", __func__, NULL);
+    if (binfract <= 0.0)
+        return (NUMA *)ERROR_PTR("binfract <= 0.0", __func__, NULL);
+
+        /* Get even and odd crossing distances, and determine the rank
+         * widths for rank 0.1 (minsize) and 0.9 (maxsize). */
+    ret = numaGetCrossingDistances(nas, &naedist, &naodist, &minsize, &maxsize);
+    if (ret || minsize < 1.0 || maxsize / minsize > 8.0) {
+        L_ERROR("bad data, or minsize = %5.2f < 1.0 or max/min = %f > 4.0\n",
+                __func__, minsize, maxsize / minsize);
+        numaDestroy(&naedist);
+        numaDestroy(&naodist);
+        return NULL;
+    }
+
+        /* Bin the spans in units of binfract * minsize.  These
+         * units are convenient because they scale to make at least
+         * 1/binfract bins in the smallest span (width).  We want this
+         * number to be large enough to clearly separate the
+         * widths, but small enough so that the histogram peaks
+         * have very few if any holes (zeroes) within them.  */
+    naehist = numaMakeHistogramClipped(naedist, binfract * minsize,
+                                       (1.25 / binfract) * maxsize);
+    naohist = numaMakeHistogramClipped(naodist, binfract * minsize,
+                                       (1.25 / binfract) * maxsize);
+
+    if (debugflag) {
+        lept_mkdir("lept/barcode");
+        gplot = gplotCreate("/tmp/lept/barcode/histw", GPLOT_PNG,
+                            "Raw width histogram", "Width", "Number");
+        gplotAddPlot(gplot, NULL, naehist, GPLOT_LINES, "plot black");
+        gplotAddPlot(gplot, NULL, naohist, GPLOT_LINES, "plot white");
+        gplotMakeOutput(gplot);
+        gplotDestroy(&gplot);
+    }
+
+        /* Compute the peak ranges, still in units of binfract * minsize.  */
+    naerange = numaLocatePeakRanges(naehist, 1.0 / binfract,
+                                    1.0 / binfract, 0.0);
+    naorange = numaLocatePeakRanges(naohist, 1.0 / binfract,
+                                    1.0 / binfract, 0.0);
+
+        /* Find the centroid values of each peak */
+    naecent = numaGetPeakCentroids(naehist, naerange);
+    naocent = numaGetPeakCentroids(naohist, naorange);
+
+        /* Generate the lookup tables that map from the bar width, in
+         * units of (binfract * minsize), to the integerized barcode
+         * units (1, 2, 3, 4), which are the output integer widths
+         * between transitions. */
+    naelut = numaGetPeakWidthLUT(naerange, naecent);
+    naolut = numaGetPeakWidthLUT(naorange, naocent);
+
+        /* Get the widths.  Because the LUT accepts our funny units,
+         * we first must convert the pixel widths to these units,
+         * which is what 'factor' does. */
+    nad = numaCreate(0);
+    ned = numaGetCount(naedist);
+    nod = numaGetCount(naodist);
+    if (nod != ned - 1)
+        L_WARNING("ned != nod + 1\n", __func__);
+    factor = 1.0 / (binfract * minsize);  /* for converting units */
+    for (i = 0; i < ned - 1; i++) {
+        numaGetFValue(naedist, i, &val);
+        width = (l_int32)(factor * val);
+        numaGetIValue(naelut, width, &iw);
+        numaAddNumber(nad, iw);
+/*        lept_stderr("even: val = %7.3f, width = %d, iw = %d\n",
+                      val, width, iw); */
+        numaGetFValue(naodist, i, &val);
+        width = (l_int32)(factor * val);
+        numaGetIValue(naolut, width, &iw);
+        numaAddNumber(nad, iw);
+/*        lept_stderr("odd: val = %7.3f, width = %d, iw = %d\n",
+                      val, width, iw); */
+    }
+    numaGetFValue(naedist, ned - 1, &val);
+    width = (l_int32)(factor * val);
+    numaGetIValue(naelut, width, &iw);
+    numaAddNumber(nad, iw);
+
+    if (debugflag) {
+        lept_stderr(" ---- Black bar widths (pixels) ------ \n");
+        numaWriteStderr(naedist);
+        lept_stderr(" ---- Histogram of black bar widths ------ \n");
+        numaWriteStderr(naehist);
+        lept_stderr(" ---- Peak ranges in black bar histogram bins --- \n");
+        numaWriteStderr(naerange);
+        lept_stderr(" ---- Peak black bar centroid width values ------ \n");
+        numaWriteStderr(naecent);
+        lept_stderr(" ---- Black bar lookup table ------ \n");
+        numaWriteStderr(naelut);
+        lept_stderr(" ---- White bar widths (pixels) ------ \n");
+        numaWriteStderr(naodist);
+        lept_stderr(" ---- Histogram of white bar widths ------ \n");
+        numaWriteStderr(naohist);
+        lept_stderr(" ---- Peak ranges in white bar histogram bins --- \n");
+        numaWriteStderr(naorange);
+        lept_stderr(" ---- Peak white bar centroid width values ------ \n");
+        numaWriteStderr(naocent);
+        lept_stderr(" ---- White bar lookup table ------ \n");
+        numaWriteStderr(naolut);
+    }
+
+    numaDestroy(&naedist);
+    numaDestroy(&naodist);
+    numaDestroy(&naerange);
+    numaDestroy(&naorange);
+    numaDestroy(&naecent);
+    numaDestroy(&naocent);
+    numaDestroy(&naelut);
+    numaDestroy(&naolut);
+    if (pnaehist)
+        *pnaehist = naehist;
+    else
+        numaDestroy(&naehist);
+    if (pnaohist)
+        *pnaohist = naohist;
+    else
+        numaDestroy(&naohist);
+    return nad;
+}
+
+
+/*!
+ * \brief   numaGetCrossingDistances()
+ *
+ * \param[in]    nas        numa of crossing locations
+ * \param[out]   pnaedist   [optional] even distances between crossings
+ * \param[out]   pnaodist   [optional] odd distances between crossings
+ * \param[out]   pmindist   [optional] min distance between crossings
+ * \param[out]   pmaxdist   [optional] max distance between crossings
+ * \return  0 if OK, 1 on error
+ */
+static l_int32
+numaGetCrossingDistances(NUMA       *nas,
+                         NUMA      **pnaedist,
+                         NUMA      **pnaodist,
+                         l_float32  *pmindist,
+                         l_float32  *pmaxdist)
+{
+l_int32    i, n;
+l_float32  val, newval, mindist, maxdist;
+NUMA      *na1, *na2, *naedist, *naodist;
+
+    if (pnaedist) *pnaedist = NULL;
+    if (pnaodist) *pnaodist = NULL;
+    if (pmindist) *pmindist = 0.0;
+    if (pmaxdist) *pmaxdist = 0.0;
+    if (!nas)
+        return ERROR_INT("nas not defined", __func__, 1);
+    if ((n = numaGetCount(nas)) < 2)
+        return ERROR_INT("n < 2", __func__, 1);
+
+        /* Get numas of distances between crossings.  Separate these
+         * into even (e.g., black) and odd (e.g., white) spans.
+         * For barcodes, the black spans are 0, 2, etc.  These
+         * distances are in pixel units.  */
+    naedist = numaCreate(n / 2 + 1);
+    naodist = numaCreate(n / 2);
+    numaGetFValue(nas, 0, &val);
+    for (i = 1; i < n; i++) {
+        numaGetFValue(nas, i, &newval);
+        if (i % 2)
+            numaAddNumber(naedist, newval - val);
+        else
+            numaAddNumber(naodist, newval - val);
+        val = newval;
+    }
+
+        /* The min and max rank distances of the spans are in pixel units. */
+    na1 = numaCopy(naedist);
+    numaJoin(na1, naodist, 0, -1);  /* use both bars and spaces */
+    na2 = numaMakeHistogram(na1, 100, NULL, NULL);
+    numaHistogramGetValFromRank(na2, 0.1f, &mindist);
+    numaHistogramGetValFromRank(na2, 0.9f, &maxdist);
+    numaDestroy(&na1);
+    numaDestroy(&na2);
+    L_INFO("mindist = %7.3f, maxdist = %7.3f\n", __func__, mindist, maxdist);
+
+    if (pnaedist)
+        *pnaedist = naedist;
+    else
+        numaDestroy(&naedist);
+    if (pnaodist)
+        *pnaodist = naodist;
+    else
+        numaDestroy(&naodist);
+    if (pmindist) *pmindist = mindist;
+    if (pmaxdist) *pmaxdist = maxdist;
+    return 0;
+}
+
+
+/*!
+ * \brief   numaLocatePeakRanges()
+ *
+ * \param[in]    nas        numa of histogram of crossing widths
+ * \param[in]    minfirst   min location of center of first peak
+ * \param[in]    minsep     min separation between peak range centers
+ * \param[in]    maxmin     max allowed value for min histo value between peaks
+ * \return  nad   ranges for each peak found, in pairs, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Units of %minsep are the index into nas.
+ *          This puts useful constraints on peak-finding.
+ *      (2) If maxmin == 0.0, the value of nas[i] must go to 0.0 (or less)
+ *          between peaks.
+ *      (3) All calculations are done in units of the index into nas.
+ *          The resulting ranges are therefore integers.
+ *      (4) The output nad gives pairs of range values for successive peaks.
+ *          Any location [i] for which maxmin = nas[i] = 0.0 will NOT be
+ *          included in a peak range.  This works fine for histograms where
+ *          if nas[i] == 0.0, it means that there are no samples at [i].
+ *      (5) For barcodes, when this is used on a histogram of barcode
+ *          widths, use maxmin = 0.0.  This requires that there is at
+ *          least one histogram bin corresponding to a width value between
+ *          adjacent peak ranges that is unpopulated, making the separation
+ *          of the histogram peaks unambiguous.
+ * </pre>
+ */
+static NUMA *
+numaLocatePeakRanges(NUMA      *nas,
+                     l_float32  minfirst,
+                     l_float32  minsep,
+                     l_float32  maxmin)
+{
+l_int32    i, n, inpeak, left;
+l_float32  center, prevcenter, val;
+NUMA      *nad;
+
+    if (!nas)
+        return (NUMA *)ERROR_PTR("nas not defined", __func__, NULL);
+    n = numaGetCount(nas);
+    nad = numaCreate(0);
+
+    inpeak = FALSE;
+    prevcenter = minfirst - minsep - 1.0;
+    for (i = 0; i < n; i++) {
+        numaGetFValue(nas, i, &val);
+        if (inpeak == FALSE && val > maxmin) {
+            inpeak = TRUE;
+            left = i;
+        } else if (inpeak == TRUE && val <= maxmin) {  /* end peak */
+            center = (left + i - 1.0) / 2.0;
+            if (center - prevcenter >= minsep) {  /* save new peak */
+                inpeak = FALSE;
+                numaAddNumber(nad, left);
+                numaAddNumber(nad, i - 1);
+                prevcenter = center;
+            } else {  /* attach to previous peak; revise the right edge */
+                numaSetValue(nad, numaGetCount(nad) - 1, i - 1);
+            }
+        }
+    }
+    if (inpeak == TRUE) {  /* save the last peak */
+        numaAddNumber(nad, left);
+        numaAddNumber(nad, n - 1);
+    }
+
+    return nad;
+}
+
+
+/*!
+ * \brief   numaGetPeakCentroids()
+ *
+ * \param[in]    nahist    numa of histogram of crossing widths
+ * \param[in]    narange   numa of ranges of x-values for the peaks in %nahist
+ * \return  nad   centroids for each peak found; max of 4, corresponding
+ *                to 4 different barcode line widths, or NULL on error
+ */
+static NUMA *
+numaGetPeakCentroids(NUMA  *nahist,
+                     NUMA  *narange)
+{
+l_int32    i, j, nr, low, high;
+l_float32  cent, sum, val;
+NUMA      *nad;
+
+    if (!nahist)
+        return (NUMA *)ERROR_PTR("nahist not defined", __func__, NULL);
+    if (!narange)
+        return (NUMA *)ERROR_PTR("narange not defined", __func__, NULL);
+    nr = numaGetCount(narange) / 2;
+
+    nad = numaCreate(4);
+    for (i = 0; i < nr; i++) {
+        numaGetIValue(narange, 2 * i, &low);
+        numaGetIValue(narange, 2 * i + 1, &high);
+        cent = 0.0;
+        sum = 0.0;
+        for (j = low; j <= high; j++) {
+            numaGetFValue(nahist, j, &val);
+            cent += j * val;
+            sum += val;
+        }
+        numaAddNumber(nad, cent / sum);
+    }
+
+    return nad;
+}
+
+
+/*!
+ * \brief   numaGetPeakWidthLUT()
+ *
+ * \param[in]    narange   numa of x-val ranges for the histogram width peaks
+ * \param[in]    nacent    numa of centroids of each peak -- up to 4
+ * \return  nalut  lookup table from the width of a bar to one of the four
+ *                 integerized barcode units, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates the lookup table that maps from a sequence of widths
+ *          (in some units) to the integerized barcode units (1, 2, 3, 4),
+ *          which are the output integer widths between transitions.
+ *      (2) The smallest width can be lost in float roundoff.  To avoid
+ *          losing it, we expand the peak range of the smallest width.
+ * </pre>
+ */
+static NUMA *
+numaGetPeakWidthLUT(NUMA  *narange,
+                    NUMA  *nacent)
+{
+l_int32     i, j, nc, low, high, imax;
+l_int32     assign[4];
+l_float32  *warray;
+l_float32   max, rat21, rat32, rat42;
+NUMA       *nalut;
+
+    if (!narange)
+        return (NUMA *)ERROR_PTR("narange not defined", __func__, NULL);
+    if (!nacent)
+        return (NUMA *)ERROR_PTR("nacent not defined", __func__, NULL);
+    nc = numaGetCount(nacent);  /* half the size of narange */
+    if (nc < 1 || nc > 4)
+        return (NUMA *)ERROR_PTR("nc must be 1, 2, 3, or 4", __func__, NULL);
+
+        /* Check the peak centroids for consistency with bar widths.
+         * The third peak can correspond to a width of either 3 or 4.
+         * Use ratios 3/2 and 4/2 instead of 3/1 and 4/1 because the
+         * former are more stable and closer to the expected ratio.  */
+    if (nc > 1) {
+        warray = numaGetFArray(nacent, L_NOCOPY);
+        if (warray[0] == 0)
+            return (NUMA *)ERROR_PTR("first peak has width 0.0",
+                                     __func__, NULL);
+        rat21 = warray[1] / warray[0];
+        if (rat21 < 1.5 || rat21 > 2.6)
+            L_WARNING("width ratio 2/1 = %f\n", __func__, rat21);
+        if (nc > 2) {
+            rat32 = warray[2] / warray[1];
+            if (rat32 < 1.3 || rat32 > 2.25)
+                L_WARNING("width ratio 3/2 = %f\n", __func__, rat32);
+        }
+        if (nc == 4) {
+            rat42 = warray[3] / warray[1];
+            if (rat42 < 1.7 || rat42 > 2.3)
+                L_WARNING("width ratio 4/2 = %f\n", __func__, rat42);
+        }
+    }
+
+        /* Set width assignments.
+         * The only possible ambiguity is with nc = 3 */
+    for (i = 0; i < 4; i++)
+        assign[i] = i + 1;
+    if (nc == 3) {
+        if (rat32 > 1.75)
+            assign[2] = 4;
+    }
+
+        /* Put widths into the LUT */
+    numaGetMax(narange, &max, NULL);
+    imax = (l_int32)max;
+    nalut = numaCreate(imax + 1);
+    numaSetCount(nalut, imax + 1);  /* fill the array with zeroes */
+    for (i = 0; i < nc; i++) {
+        numaGetIValue(narange, 2 * i, &low);
+        if (i == 0) low--;  /* catch smallest width */
+        numaGetIValue(narange, 2 * i + 1, &high);
+        for (j = low; j <= high; j++)
+            numaSetValue(nalut, j, assign[i]);
+    }
+
+    return nalut;
+}
+
+
+/*!
+ * \brief   numaQuantizeCrossingsByWindow()
+ *
+ * \param[in]    nas         numa of crossing locations
+ * \param[in]    ratio       of max window size over min window size in search;
+ *                           typ. 2.0
+ * \param[out]   pwidth      [optional] best window width
+ * \param[out]   pfirstloc   [optional] center of window for first xing
+ * \param[out]   pnac        [optional] array of window crossings (0, 1, 2)
+ * \param[in]    debugflag   1 to generate various plots of intermediate results
+ * \return  nad   sequence of widths, in unit sizes, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The minimum size of the window is set by the minimum
+ *          distance between zero crossings.
+ *      (2) The optional return signal %nac is a sequence of 0s, 1s,
+ *          and perhaps a few 2s, giving the number of crossings in each window.
+ *          On the occasion where there is a '2', it is interpreted as
+ *          ending two runs: the previous one and another one that has length 1.
+ * </pre>
+ */
+NUMA *
+numaQuantizeCrossingsByWindow(NUMA       *nas,
+                              l_float32   ratio,
+                              l_float32  *pwidth,
+                              l_float32  *pfirstloc,
+                              NUMA      **pnac,
+                              l_int32     debugflag)
+{
+l_int32    i, nw, started, count, trans;
+l_float32  minsize, minwidth, minshift, xfirst;
+NUMA      *nac, *nad;
+
+    if (!nas)
+        return (NUMA *)ERROR_PTR("nas not defined", __func__, NULL);
+    if (numaGetCount(nas) < 2)
+        return (NUMA *)ERROR_PTR("nas size < 2", __func__, NULL);
+
+        /* Get the minsize, which is needed for the search for
+         * the window width (ultimately found as 'minwidth') */
+    numaGetCrossingDistances(nas, NULL, NULL, &minsize, NULL);
+
+        /* Compute the width and shift increments; start at minsize
+         * and go up to ratio * minsize  */
+    numaEvalBestWidthAndShift(nas, 100, 10, minsize, ratio * minsize,
+                              &minwidth, &minshift, NULL);
+
+        /* Refine width and shift calculation */
+    numaEvalBestWidthAndShift(nas, 100, 10, 0.98 * minwidth, 1.02 * minwidth,
+                              &minwidth, &minshift, NULL);
+
+    L_INFO("best width = %7.3f, best shift = %7.3f\n",
+           __func__, minwidth, minshift);
+
+        /* Get the crossing array (0,1,2) for the best window width and shift */
+    numaEvalSyncError(nas, 0, 0, minwidth, minshift, NULL, &nac);
+    if (pwidth) *pwidth = minwidth;
+    if (pfirstloc) {
+        numaGetFValue(nas, 0, &xfirst);
+        *pfirstloc = xfirst + minshift;
+    }
+
+        /* Get the array of bar widths, starting with a black bar  */
+    nad = numaCreate(0);
+    nw = numaGetCount(nac);  /* number of window measurements */
+    started = FALSE;
+    count = 0;  /* unnecessary init */
+    for (i = 0; i < nw; i++) {
+        numaGetIValue(nac, i, &trans);
+        if (trans > 2)
+            L_WARNING("trans = %d > 2 !!!\n", __func__, trans);
+        if (started) {
+            if (trans > 1) {  /* i.e., when trans == 2 */
+                numaAddNumber(nad, count);
+                trans--;
+                count = 1;
+            }
+            if (trans == 1) {
+                numaAddNumber(nad, count);
+                count = 1;
+            } else {
+                count++;
+            }
+        }
+        if (!started && trans) {
+            started = TRUE;
+            if (trans == 2)  /* a whole bar in this window */
+                numaAddNumber(nad, 1);
+            count = 1;
+        }
+    }
+
+    if (pnac)
+        *pnac = nac;
+    else
+        numaDestroy(&nac);
+    return nad;
+}
+
+
+/*!
+ * \brief   numaEvalBestWidthAndShift()
+ *
+ * \param[in]    nas          numa of crossing locations
+ * \param[in]    nwidth       number of widths to consider
+ * \param[in]    nshift       number of shifts to consider for each width
+ * \param[in]    minwidth     smallest width to consider
+ * \param[in]    maxwidth     largest width to consider
+ * \param[out]   pbestwidth   best size of window
+ * \param[out]   pbestshift   best shift for the window
+ * \param[out]   pbestscore   [optional] average squared error of dist
+ *                            of crossing signal from the center of the window
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does a linear sweep of widths, evaluating at %nshift
+ *          shifts for each width, finding the (width, shift) pair that
+ *          gives the minimum score.
+ * </pre>
+ */
+static l_int32
+numaEvalBestWidthAndShift(NUMA       *nas,
+                          l_int32     nwidth,
+                          l_int32     nshift,
+                          l_float32   minwidth,
+                          l_float32   maxwidth,
+                          l_float32  *pbestwidth,
+                          l_float32  *pbestshift,
+                          l_float32  *pbestscore)
+{
+l_int32    i, j;
+l_float32  delwidth, delshift, width, shift, score;
+l_float32  bestwidth, bestshift, bestscore;
+
+    if (!nas)
+        return ERROR_INT("nas not defined", __func__, 1);
+    if (!pbestwidth || !pbestshift)
+        return ERROR_INT("&bestwidth and &bestshift not defined", __func__, 1);
+
+    bestwidth = 0.0f;
+    bestshift = 0.0f;
+    bestscore = 1.0;
+    delwidth = (maxwidth - minwidth) / (nwidth - 1.0);
+    for (i = 0; i < nwidth; i++) {
+        width = minwidth + delwidth * i;
+        delshift = width / (l_float32)(nshift);
+        for (j = 0; j < nshift; j++) {
+            shift = -0.5 * (width - delshift) + j * delshift;
+            numaEvalSyncError(nas, 0, 0, width, shift, &score, NULL);
+            if (score < bestscore) {
+                bestscore = score;
+                bestwidth = width;
+                bestshift = shift;
+#if  DEBUG_FREQUENCY
+                lept_stderr("width = %7.3f, shift = %7.3f, score = %7.3f\n",
+                            width, shift, score);
+#endif  /* DEBUG_FREQUENCY */
+            }
+        }
+    }
+
+    *pbestwidth = bestwidth;
+    *pbestshift = bestshift;
+    if (pbestscore)
+        *pbestscore = bestscore;
+    return 0;
+}
+
+
+/*!
+ * \brief   numaEvalSyncError()
+ *
+ * \param[in]    nas      numa of crossing locations
+ * \param[in]    ifirst   first crossing to use
+ * \param[in]    ilast    last crossing to use; use 0 for all crossings
+ * \param[in]    width    size of window
+ * \param[in]    shift    of center of window w/rt first crossing
+ * \param[out]   pscore   [optional] average squared error of dist
+ *                        of crossing signal from the center of the window
+ * \param[out]   pnad     [optional] numa of 1s and 0s for crossings
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) The score is computed only on the part of the signal from the
+ *          %ifirst to %ilast crossings.  Use 0 for both of these to
+ *          use all the crossings.  The score is normalized for
+ *          the number of crossings and with half-width of the window.
+ *      (2) The optional return %nad is a sequence of 0s and 1s, where a '1'
+ *          indicates a crossing in the window.
+ * </pre>
+ */
+static l_int32
+numaEvalSyncError(NUMA       *nas,
+                  l_int32     ifirst,
+                  l_int32     ilast,
+                  l_float32   width,
+                  l_float32   shift,
+                  l_float32  *pscore,
+                  NUMA      **pnad)
+{
+l_int32    i, n, nc, nw, ival;
+l_int32    iw;  /* cell in which transition occurs */
+l_float32  score, xfirst, xlast, xleft, xc, xwc;
+NUMA      *nad;
+
+    if (!nas)
+        return ERROR_INT("nas not defined", __func__, 1);
+    if ((n = numaGetCount(nas)) < 2)
+        return ERROR_INT("nas size < 2", __func__, 1);
+    if (ifirst < 0) ifirst = 0;
+    if (ilast <= 0) ilast = n - 1;
+    if (ifirst >= ilast)
+        return ERROR_INT("ifirst not < ilast", __func__, 1);
+    nc = ilast - ifirst + 1;
+
+        /* Set up an array corresponding to the (shifted) windows,
+         * and fill in the crossings. */
+    score = 0.0;
+    numaGetFValue(nas, ifirst, &xfirst);
+    numaGetFValue(nas, ilast, &xlast);
+    nw = (l_int32) ((xlast - xfirst + 2.0 * width) / width);
+    nad = numaCreate(nw);
+    numaSetCount(nad, nw);  /* init to all 0.0 */
+    xleft = xfirst - width / 2.0 + shift;  /* left edge of first window */
+    for (i = ifirst; i <= ilast; i++) {
+        numaGetFValue(nas, i, &xc);
+        iw = (l_int32)((xc - xleft) / width);
+        xwc = xleft + (iw + 0.5) * width;  /* center of cell iw */
+        score += (xwc - xc) * (xwc - xc);
+        numaGetIValue(nad, iw, &ival);
+        numaSetValue(nad, iw, ival + 1);
+    }
+
+    if (pscore)
+        *pscore = 4.0 * score / (width * width * (l_float32)nc);
+    if (pnad)
+        *pnad = nad;
+    else
+        numaDestroy(&nad);
+
+    return 0;
+}