diff mupdf-source/thirdparty/leptonica/src/morphseq.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/morphseq.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1229 @@
+/*====================================================================*
+ -  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 morphseq.c
+ * <pre>
+ *
+ *      Run a sequence of binary rasterop morphological operations
+ *            PIX     *pixMorphSequence()
+ *
+ *      Run a sequence of binary composite rasterop morphological operations
+ *            PIX     *pixMorphCompSequence()
+ *
+ *      Run a sequence of binary dwa morphological operations
+ *            PIX     *pixMorphSequenceDwa()
+ *
+ *      Run a sequence of binary composite dwa morphological operations
+ *            PIX     *pixMorphCompSequenceDwa()
+ *
+ *      Parser verifier for binary morphological operations
+ *            l_int32  morphSequenceVerify()
+ *
+ *      Run a sequence of grayscale morphological operations
+ *            PIX     *pixGrayMorphSequence()
+ *
+ *      Run a sequence of color morphological operations
+ *            PIX     *pixColorMorphSequence()
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <string.h>
+#include "allheaders.h"
+
+/*-------------------------------------------------------------------------*
+ *         Run a sequence of binary rasterop morphological operations      *
+ *-------------------------------------------------------------------------*/
+/*!
+ * \brief   pixMorphSequence()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does rasterop morphology on binary images.
+ *      (2) This runs a pipeline of operations; no branching is allowed.
+ *      (3) This only uses brick Sels, which are created on the fly.
+ *          In the future this will be generalized to extract Sels from
+ *          a Sela by name.
+ *      (4) A new image is always produced; the input image is not changed.
+ *      (5) This contains an interpreter, allowing sequences to be
+ *          generated and run.
+ *      (6) The format of the sequence string is defined below.
+ *      (7) In addition to morphological operations, rank order reduction
+ *          and replicated expansion allow operations to take place
+ *          downscaled by a power of 2.
+ *      (8) Intermediate results can optionally be displayed.
+ *      (9) Thanks to Dar-Shyang Lee, who had the idea for this and
+ *          built the first implementation.
+ *      (10) The sequence string is formatted as follows:
+ *            ~ An arbitrary number of operations,  each separated
+ *              by a '+' character.  White space is ignored.
+ *            ~ Each operation begins with a case-independent character
+ *              specifying the operation:
+ *                 d or D  (dilation)
+ *                 e or E  (erosion)
+ *                 o or O  (opening)
+ *                 c or C  (closing)
+ *                 r or R  (rank binary reduction)
+ *                 x or X  (replicative binary expansion)
+ *                 b or B  (add a border of 0 pixels of this size)
+ *            ~ The args to the morphological operations are bricks of hits,
+ *              and are formatted as a.b, where a and b are horizontal and
+ *              vertical dimensions, rsp.
+ *            ~ The args to the reduction are a sequence of up to 4 integers,
+ *              each from 1 to 4.
+ *            ~ The arg to the expansion is a power of two, in the set
+ *              {2, 4, 8, 16}.
+ *      (11) An example valid sequence is:
+ *               "b32 + o1.3 + C3.1 + r23 + e2.2 + D3.2 + X4"
+ *           In this example, the following operation sequence is carried out:
+ *             * b32: Add a 32 pixel border around the input image
+ *             * o1.3: Opening with vert sel of length 3 (e.g., 1 x 3)
+ *             * C3.1: Closing with horiz sel of length 3  (e.g., 3 x 1)
+ *             * r23: Two successive 2x2 reductions with rank 2 in the first
+ *                    and rank 3 in the second.  The result is a 4x reduced pix.
+ *             * e2.2: Erosion with a 2x2 sel (origin will be at x,y: 0,0)
+ *             * d3.2: Dilation with a 3x2 sel (origin will be at x,y: 1,0)
+ *             * X4: 4x replicative expansion, back to original resolution
+ *      (12) The safe closing is used.  However, if you implement a
+ *           closing as separable dilations followed by separable erosions,
+ *           it will not be safe.  For that situation, you need to add
+ *           a sufficiently large border as the first operation in
+ *           the sequence.  This will be removed automatically at the
+ *           end.  There are two cautions:
+ *              ~ When computing what is sufficient, remember that if
+ *                reductions are carried out, the border is also reduced.
+ *              ~ The border is removed at the end, so if a border is
+ *                added at the beginning, the result must be at the
+ *                same resolution as the input!
+ * </pre>
+ */
+PIX *
+pixMorphSequence(PIX         *pixs,
+                 const char  *sequence,
+                 l_int32      dispsep)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, j, nred, fact, w, h, x, border, pdfout;
+l_int32  level[4];
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+    if (!morphSequenceVerify(sa)) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence not valid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    border = 0;
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixDilateBrick(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixErodeBrick(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixOpenBrick(pix1, pix1, w, h);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixCloseSafeBrick(pix1, pix1, w, h);
+            break;
+        case 'r':
+        case 'R':
+            nred = strlen(op) - 1;
+            for (j = 0; j < nred; j++)
+                level[j] = op[j + 1] - '0';
+            for (j = nred; j < 4; j++)
+                level[j] = 0;
+            pix2 = pixReduceRankBinaryCascade(pix1, level[0], level[1],
+                                               level[2], level[3]);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'x':
+        case 'X':
+            sscanf(&op[1], "%d", &fact);
+            pix2 = pixExpandReplicate(pix1, fact);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'b':
+        case 'B':
+            sscanf(&op[1], "%d", &border);
+            pix2 = pixAddBorder(pix1, border, 0);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, 0);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+    if (border > 0) {
+        pix2 = pixRemoveBorder(pix1, border);
+        pixSwapAndDestroy(&pix1, &pix2);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}
+
+
+/*-------------------------------------------------------------------------*
+ *   Run a sequence of binary composite rasterop morphological operations  *
+ *-------------------------------------------------------------------------*/
+/*!
+ * \brief   pixMorphCompSequence()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display of results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does rasterop morphology on binary images, using composite
+ *          operations for extra speed on large Sels.
+ *      (2) Safe closing is used atomically.  However, if you implement a
+ *          closing as a sequence with a dilation followed by an
+ *          erosion, it will not be safe, and to ensure that you have
+ *          no boundary effects you must add a border in advance and
+ *          remove it at the end.
+ *      (3) For other usage details, see the notes for pixMorphSequence().
+ *      (4) The sequence string is formatted as follows:
+ *            ~ An arbitrary number of operations,  each separated
+ *              by a '+' character.  White space is ignored.
+ *            ~ Each operation begins with a case-independent character
+ *              specifying the operation:
+ *                 d or D  (dilation)
+ *                 e or E  (erosion)
+ *                 o or O  (opening)
+ *                 c or C  (closing)
+ *                 r or R  (rank binary reduction)
+ *                 x or X  (replicative binary expansion)
+ *                 b or B  (add a border of 0 pixels of this size)
+ *            ~ The args to the morphological operations are bricks of hits,
+ *              and are formatted as a.b, where a and b are horizontal and
+ *              vertical dimensions, rsp.
+ *            ~ The args to the reduction are a sequence of up to 4 integers,
+ *              each from 1 to 4.
+ *            ~ The arg to the expansion is a power of two, in the set
+ *              {2, 4, 8, 16}.
+ * </pre>
+ */
+PIX *
+pixMorphCompSequence(PIX         *pixs,
+                     const char  *sequence,
+                     l_int32      dispsep)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, j, nred, fact, w, h, x, border, pdfout;
+l_int32  level[4];
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+
+    if (!morphSequenceVerify(sa)) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence not valid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    border = 0;
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixDilateCompBrick(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixErodeCompBrick(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixOpenCompBrick(pix1, pix1, w, h);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixCloseSafeCompBrick(pix1, pix1, w, h);
+            break;
+        case 'r':
+        case 'R':
+            nred = strlen(op) - 1;
+            for (j = 0; j < nred; j++)
+                level[j] = op[j + 1] - '0';
+            for (j = nred; j < 4; j++)
+                level[j] = 0;
+            pix2 = pixReduceRankBinaryCascade(pix1, level[0], level[1],
+                                               level[2], level[3]);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'x':
+        case 'X':
+            sscanf(&op[1], "%d", &fact);
+            pix2 = pixExpandReplicate(pix1, fact);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'b':
+        case 'B':
+            sscanf(&op[1], "%d", &border);
+            pix2 = pixAddBorder(pix1, border, 0);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, 0);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+    if (border > 0) {
+        pix2 = pixRemoveBorder(pix1, border);
+        pixSwapAndDestroy(&pix1, &pix2);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}
+
+
+/*-------------------------------------------------------------------------*
+ *           Run a sequence of binary dwa morphological operations         *
+ *-------------------------------------------------------------------------*/
+/*!
+ * \brief   pixMorphSequenceDwa()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display of results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does dwa morphology on binary images.
+ *      (2) This runs a pipeline of operations; no branching is allowed.
+ *      (3) This only uses brick Sels that have been pre-compiled with
+ *          dwa code.
+ *      (4) A new image is always produced; the input image is not changed.
+ *      (5) This contains an interpreter, allowing sequences to be
+ *          generated and run.
+ *      (6) See pixMorphSequence() for further information about usage.
+ * </pre>
+ */
+PIX *
+pixMorphSequenceDwa(PIX         *pixs,
+                    const char  *sequence,
+                    l_int32      dispsep)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, j, nred, fact, w, h, x, border, pdfout;
+l_int32  level[4];
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+
+    if (!morphSequenceVerify(sa)) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence not valid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    border = 0;
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixDilateBrickDwa(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixErodeBrickDwa(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixOpenBrickDwa(pix1, pix1, w, h);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixCloseBrickDwa(pix1, pix1, w, h);
+            break;
+        case 'r':
+        case 'R':
+            nred = strlen(op) - 1;
+            for (j = 0; j < nred; j++)
+                level[j] = op[j + 1] - '0';
+            for (j = nred; j < 4; j++)
+                level[j] = 0;
+            pix2 = pixReduceRankBinaryCascade(pix1, level[0], level[1],
+                                               level[2], level[3]);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'x':
+        case 'X':
+            sscanf(&op[1], "%d", &fact);
+            pix2 = pixExpandReplicate(pix1, fact);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'b':
+        case 'B':
+            sscanf(&op[1], "%d", &border);
+            pix2 = pixAddBorder(pix1, border, 0);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, 0);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+    if (border > 0) {
+        pix2 = pixRemoveBorder(pix1, border);
+        pixSwapAndDestroy(&pix1, &pix2);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}
+
+
+/*-------------------------------------------------------------------------*
+ *      Run a sequence of binary composite dwa morphological operations    *
+ *-------------------------------------------------------------------------*/
+/*!
+ * \brief   pixMorphCompSequenceDwa()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display of results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does dwa morphology on binary images, using brick Sels.
+ *      (2) This runs a pipeline of operations; no branching is allowed.
+ *      (3) It implements all brick Sels that have dimensions up to 63
+ *          on each side, using a composite (linear + comb) when useful.
+ *      (4) A new image is always produced; the input image is not changed.
+ *      (5) This contains an interpreter, allowing sequences to be
+ *          generated and run.
+ *      (6) See pixMorphSequence() for further information about usage.
+ * </pre>
+ */
+PIX *
+pixMorphCompSequenceDwa(PIX         *pixs,
+                        const char  *sequence,
+                        l_int32      dispsep)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, j, nred, fact, w, h, x, border, pdfout;
+l_int32  level[4];
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+
+    if (!morphSequenceVerify(sa)) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence not valid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    border = 0;
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixDilateCompBrickDwa(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixErodeCompBrickDwa(NULL, pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixOpenCompBrickDwa(pix1, pix1, w, h);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pixCloseCompBrickDwa(pix1, pix1, w, h);
+            break;
+        case 'r':
+        case 'R':
+            nred = strlen(op) - 1;
+            for (j = 0; j < nred; j++)
+                level[j] = op[j + 1] - '0';
+            for (j = nred; j < 4; j++)
+                level[j] = 0;
+            pix2 = pixReduceRankBinaryCascade(pix1, level[0], level[1],
+                                               level[2], level[3]);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'x':
+        case 'X':
+            sscanf(&op[1], "%d", &fact);
+            pix2 = pixExpandReplicate(pix1, fact);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'b':
+        case 'B':
+            sscanf(&op[1], "%d", &border);
+            pix2 = pixAddBorder(pix1, border, 0);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, 0);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+    if (border > 0) {
+        pix2 = pixRemoveBorder(pix1, border);
+        pixSwapAndDestroy(&pix1, &pix2);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}
+
+
+/*-------------------------------------------------------------------------*
+ *            Parser verifier for binary morphological operations          *
+ *-------------------------------------------------------------------------*/
+/*!
+ * \brief   morphSequenceVerify()
+ *
+ * \param[in]    sa    string array of operation sequence
+ * \return  TRUE if valid; FALSE otherwise or on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This does verification of valid binary morphological
+ *          operation sequences.
+ *      (2) See pixMorphSequence() for notes on valid operations
+ *          in the sequence.
+ * </pre>
+ */
+l_int32
+morphSequenceVerify(SARRAY  *sa)
+{
+char    *rawop, *op = NULL;
+l_int32  nops, i, j, nred, fact, valid, w, h, netred, border;
+l_int32  level[4];
+l_int32  intlogbase2[5] = {1, 2, 3, 0, 4};  /* of arg/4 */
+
+    if (!sa)
+        return ERROR_INT("sa not defined", __func__, FALSE);
+
+    nops = sarrayGetCount(sa);
+    valid = TRUE;
+    netred = 0;
+    border = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+        case 'e':
+        case 'E':
+        case 'o':
+        case 'O':
+        case 'c':
+        case 'C':
+            if (sscanf(&op[1], "%d.%d", &w, &h) != 2) {
+                lept_stderr("*** op: %s invalid\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (w <= 0 || h <= 0) {
+                lept_stderr("*** op: %s; w = %d, h = %d; must both be > 0\n",
+                            op, w, h);
+                valid = FALSE;
+                break;
+            }
+/*            lept_stderr("op = %s; w = %d, h = %d\n", op, w, h); */
+            break;
+        case 'r':
+        case 'R':
+            nred = strlen(op) - 1;
+            netred += nred;
+            if (nred < 1 || nred > 4) {
+                lept_stderr(
+                        "*** op = %s; num reduct = %d; must be in {1,2,3,4}\n",
+                        op, nred);
+                valid = FALSE;
+                break;
+            }
+            for (j = 0; j < nred; j++) {
+                level[j] = op[j + 1] - '0';
+                if (level[j] < 1 || level[j] > 4) {
+                    lept_stderr("*** op = %s; level[%d] = %d is invalid\n",
+                                op, j, level[j]);
+                    valid = FALSE;
+                    break;
+                }
+            }
+            if (!valid)
+                break;
+/*            lept_stderr("op = %s", op); */
+            for (j = 0; j < nred; j++) {
+                level[j] = op[j + 1] - '0';
+/*                lept_stderr(", level[%d] = %d", j, level[j]); */
+            }
+/*            lept_stderr("\n"); */
+            break;
+        case 'x':
+        case 'X':
+            if (sscanf(&op[1], "%d", &fact) != 1) {
+                lept_stderr("*** op: %s; fact invalid\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (fact != 2 && fact != 4 && fact != 8 && fact != 16) {
+                lept_stderr("*** op = %s; invalid fact = %d\n", op, fact);
+                valid = FALSE;
+                break;
+            }
+            netred -= intlogbase2[fact / 4];
+/*            lept_stderr("op = %s; fact = %d\n", op, fact); */
+            break;
+        case 'b':
+        case 'B':
+            if (sscanf(&op[1], "%d", &fact) != 1) {
+                lept_stderr("*** op: %s; fact invalid\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (i > 0) {
+                lept_stderr("*** op = %s; must be first op\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (fact < 1) {
+                lept_stderr("*** op = %s; invalid fact = %d\n", op, fact);
+                valid = FALSE;
+                break;
+            }
+            border = fact;
+/*            lept_stderr("op = %s; fact = %d\n", op, fact); */
+            break;
+        default:
+            lept_stderr("*** nonexistent op = %s\n", op);
+            valid = FALSE;
+        }
+        LEPT_FREE(op);
+    }
+
+    if (border != 0 && netred != 0) {
+        lept_stderr("*** op = %s; border added but net reduction not 0\n", op);
+        valid = FALSE;
+    }
+    return valid;
+}
+
+
+/*-----------------------------------------------------------------*
+ *       Run a sequence of grayscale morphological operations      *
+ *-----------------------------------------------------------------*/
+/*!
+ * \brief   pixGrayMorphSequence()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display of results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \param[in]    dispy      if dispsep > 0, this gives the y-value of the
+ *                          UL corner for display; otherwise it is ignored
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This works on 8 bpp grayscale images.
+ *      (2) This runs a pipeline of operations; no branching is allowed.
+ *      (3) This only uses brick SELs.
+ *      (4) A new image is always produced; the input image is not changed.
+ *      (5) This contains an interpreter, allowing sequences to be
+ *          generated and run.
+ *      (6) The format of the sequence string is defined below.
+ *      (7) In addition to morphological operations, the composite
+ *          morph/subtract tophat can be performed.
+ *      (8) Sel sizes (width, height) must each be odd numbers.
+ *      (9) Intermediate results can optionally be displayed
+ *      (10) The sequence string is formatted as follows:
+ *            ~ An arbitrary number of operations,  each separated
+ *              by a '+' character.  White space is ignored.
+ *            ~ Each operation begins with a case-independent character
+ *              specifying the operation:
+ *                 d or D  (dilation)
+ *                 e or E  (erosion)
+ *                 o or O  (opening)
+ *                 c or C  (closing)
+ *                 t or T  (tophat)
+ *            ~ The args to the morphological operations are bricks of hits,
+ *              and are formatted as a.b, where a and b are horizontal and
+ *              vertical dimensions, rsp. (each must be an odd number)
+ *            ~ The args to the tophat are w or W (for white tophat)
+ *              or b or B (for black tophat), followed by a.b as for
+ *              the dilation, erosion, opening and closing.
+ *           Example valid sequences are:
+ *             "c5.3 + o7.5"
+ *             "c9.9 + tw9.9"
+ * </pre>
+ */
+PIX *
+pixGrayMorphSequence(PIX         *pixs,
+                     const char  *sequence,
+                     l_int32      dispsep,
+                     l_int32      dispy)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, valid, w, h, x, pdfout;
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+
+        /* Verify that the operation sequence is valid */
+    valid = TRUE;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+        case 'e':
+        case 'E':
+        case 'o':
+        case 'O':
+        case 'c':
+        case 'C':
+            if (sscanf(&op[1], "%d.%d", &w, &h) != 2) {
+                lept_stderr("*** op: %s invalid\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (w < 1 || (w & 1) == 0 || h < 1 || (h & 1) == 0 ) {
+                lept_stderr("*** op: %s; w = %d, h = %d; must both be odd\n",
+                            op, w, h);
+                valid = FALSE;
+                break;
+            }
+/*            lept_stderr("op = %s; w = %d, h = %d\n", op, w, h); */
+            break;
+        case 't':
+        case 'T':
+            if (op[1] != 'w' && op[1] != 'W' &&
+                op[1] != 'b' && op[1] != 'B') {
+                lept_stderr(
+                        "*** op = %s; arg %c must be 'w' or 'b'\n", op, op[1]);
+                valid = FALSE;
+                break;
+            }
+            sscanf(&op[2], "%d.%d", &w, &h);
+            if (w < 1 || (w & 1) == 0 || h < 1 || (h & 1) == 0 ) {
+                lept_stderr("*** op: %s; w = %d, h = %d; must both be odd\n",
+                            op, w, h);
+                valid = FALSE;
+                break;
+            }
+/*            lept_stderr("op = %s", op); */
+            break;
+        default:
+            lept_stderr("*** nonexistent op = %s\n", op);
+            valid = FALSE;
+        }
+        LEPT_FREE(op);
+    }
+    if (!valid) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence invalid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixDilateGray(pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixErodeGray(pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixOpenGray(pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixCloseGray(pix1, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 't':
+        case 'T':
+            sscanf(&op[2], "%d.%d", &w, &h);
+            if (op[1] == 'w' || op[1] == 'W')
+                pix2 = pixTophat(pix1, w, h, L_TOPHAT_WHITE);
+            else   /* 'b' or 'B' */
+                pix2 = pixTophat(pix1, w, h, L_TOPHAT_BLACK);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, dispy);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}
+
+
+/*-----------------------------------------------------------------*
+ *         Run a sequence of color morphological operations        *
+ *-----------------------------------------------------------------*/
+/*!
+ * \brief   pixColorMorphSequence()
+ *
+ * \param[in]    pixs
+ * \param[in]    sequence   string specifying sequence
+ * \param[in]    dispsep    controls debug display of results in the sequence:
+ *                          0: no output
+ *                          > 0: gives horizontal separation in pixels between
+ *                               successive displays
+ *                          < 0: pdf output; abs(dispsep) is used for naming
+ * \param[in]    dispy      if dispsep > 0, this gives the y-value of the
+ *                          UL corner for display; otherwise it is ignored
+ * \return  pixd, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This works on 32 bpp rgb images.
+ *      (2) Each component is processed separately.
+ *      (3) This runs a pipeline of operations; no branching is allowed.
+ *      (4) This only uses brick SELs.
+ *      (5) A new image is always produced; the input image is not changed.
+ *      (6) This contains an interpreter, allowing sequences to be
+ *          generated and run.
+ *      (7) Sel sizes (width, height) must each be odd numbers.
+ *      (8) The format of the sequence string is defined below.
+ *      (9) Intermediate results can optionally be displayed.
+ *      (10) The sequence string is formatted as follows:
+ *            ~ An arbitrary number of operations,  each separated
+ *              by a '+' character.  White space is ignored.
+ *            ~ Each operation begins with a case-independent character
+ *              specifying the operation:
+ *                 d or D  (dilation)
+ *                 e or E  (erosion)
+ *                 o or O  (opening)
+ *                 c or C  (closing)
+ *            ~ The args to the morphological operations are bricks of hits,
+ *              and are formatted as a.b, where a and b are horizontal and
+ *              vertical dimensions, rsp. (each must be an odd number)
+ *           Example valid sequences are:
+ *             "c5.3 + o7.5"
+ *             "D9.1"
+ * </pre>
+ */
+PIX *
+pixColorMorphSequence(PIX         *pixs,
+                      const char  *sequence,
+                      l_int32      dispsep,
+                      l_int32      dispy)
+{
+char    *rawop, *op;
+char     fname[256];
+l_int32  nops, i, valid, w, h, x, pdfout;
+PIX     *pix1, *pix2;
+PIXA    *pixa;
+SARRAY  *sa;
+
+    if (!pixs)
+        return (PIX *)ERROR_PTR("pixs not defined", __func__, NULL);
+    if (!sequence)
+        return (PIX *)ERROR_PTR("sequence not defined", __func__, NULL);
+
+        /* Split sequence into individual operations */
+    sa = sarrayCreate(0);
+    sarraySplitString(sa, sequence, "+");
+    nops = sarrayGetCount(sa);
+    pdfout = (dispsep < 0) ? 1 : 0;
+
+        /* Verify that the operation sequence is valid */
+    valid = TRUE;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+        case 'e':
+        case 'E':
+        case 'o':
+        case 'O':
+        case 'c':
+        case 'C':
+            if (sscanf(&op[1], "%d.%d", &w, &h) != 2) {
+                lept_stderr("*** op: %s invalid\n", op);
+                valid = FALSE;
+                break;
+            }
+            if (w < 1 || (w & 1) == 0 || h < 1 || (h & 1) == 0 ) {
+                lept_stderr("*** op: %s; w = %d, h = %d; must both be odd\n",
+                            op, w, h);
+                valid = FALSE;
+                break;
+            }
+/*            lept_stderr("op = %s; w = %d, h = %d\n", op, w, h); */
+            break;
+        default:
+            lept_stderr("*** nonexistent op = %s\n", op);
+            valid = FALSE;
+        }
+        LEPT_FREE(op);
+    }
+    if (!valid) {
+        sarrayDestroy(&sa);
+        return (PIX *)ERROR_PTR("sequence invalid", __func__, NULL);
+    }
+
+        /* Parse and operate */
+    pixa = NULL;
+    if (pdfout) {
+        pixa = pixaCreate(0);
+        pixaAddPix(pixa, pixs, L_CLONE);
+    }
+    pix1 = pixCopy(NULL, pixs);
+    pix2 = NULL;
+    x = 0;
+    for (i = 0; i < nops; i++) {
+        rawop = sarrayGetString(sa, i, L_NOCOPY);
+        op = stringRemoveChars(rawop, " \n\t");
+        switch (op[0])
+        {
+        case 'd':
+        case 'D':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixColorMorph(pix1, L_MORPH_DILATE, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'e':
+        case 'E':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixColorMorph(pix1, L_MORPH_ERODE, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'o':
+        case 'O':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixColorMorph(pix1, L_MORPH_OPEN, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        case 'c':
+        case 'C':
+            sscanf(&op[1], "%d.%d", &w, &h);
+            pix2 = pixColorMorph(pix1, L_MORPH_CLOSE, w, h);
+            pixSwapAndDestroy(&pix1, &pix2);
+            break;
+        default:
+            /* All invalid ops are caught in the first pass */
+            break;
+        }
+        LEPT_FREE(op);
+
+            /* Debug output */
+        if (dispsep > 0) {
+            pixDisplay(pix1, x, dispy);
+            x += dispsep;
+        }
+        if (pdfout)
+            pixaAddPix(pixa, pix1, L_COPY);
+    }
+
+    if (pdfout) {
+        snprintf(fname, sizeof(fname), "/tmp/lept/seq_output_%d.pdf",
+                 L_ABS(dispsep));
+        pixaConvertToPdf(pixa, 0, 1.0, L_FLATE_ENCODE, 0, fname, fname);
+        pixaDestroy(&pixa);
+    }
+
+    sarrayDestroy(&sa);
+    return pix1;
+}