diff mupdf-source/thirdparty/leptonica/src/dewarp4.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/dewarp4.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1151 @@
+/*====================================================================*
+ -  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 dewarp4.c
+ * <pre>
+ *
+ *    Single page dewarper
+ *
+ *    Reference model (book-level, dewarpa) operations and debugging output
+ *
+ *      Top-level single page dewarper
+ *          l_int32            dewarpSinglePage()
+ *          l_int32            dewarpSinglePageInit()
+ *          l_int32            dewarpSinglePageRun()
+ *
+ *      Operations on dewarpa
+ *          l_int32            dewarpaListPages()
+ *          l_int32            dewarpaSetValidModels()
+ *          l_int32            dewarpaInsertRefModels()
+ *          l_int32            dewarpaStripRefModels()
+ *          l_int32            dewarpaRestoreModels()
+ *
+ *      Dewarp debugging output
+ *          l_int32            dewarpaInfo()
+ *          l_int32            dewarpaModelStats()
+ *          static l_int32     dewarpaTestForValidModel()
+ *          l_int32            dewarpaShowArrays()
+ *          l_int32            dewarpDebug()
+ *          l_int32            dewarpShowResults()
+ * </pre>
+ */
+
+#ifdef HAVE_CONFIG_H
+#include <config_auto.h>
+#endif  /* HAVE_CONFIG_H */
+
+#include <math.h>
+#include "allheaders.h"
+
+static l_int32 dewarpaTestForValidModel(L_DEWARPA *dewa, L_DEWARP *dew,
+                                        l_int32 notests);
+
+#ifndef  NO_CONSOLE_IO
+#define  DEBUG_INVALID_MODELS      0   /* set this to 1 for debugging */
+#endif  /* !NO_CONSOLE_IO */
+
+    /* Special parameter value */
+static const l_int32  GrayInValue = 200;
+
+/*----------------------------------------------------------------------*
+ *                   Top-level single page dewarper                     *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   dewarpSinglePage()
+ *
+ * \param[in]    pixs            with text, any depth
+ * \param[in]    thresh          for global thresh to 1 bpp; ignore otherwise
+ * \param[in]    adaptive        1 for adaptive thresh; 0 for global threshold
+ * \param[in]    useboth         1 for both horiz and vert; 0 for vertical only
+ * \param[in]    check_columns   1 to skip horizontal if multiple columns;
+ *                               0 otherwise; default is to skip
+ * \param[out]   ppixd           dewarped result
+ * \param[out]   pdewa           [optional] dewa with single page; NULL to skip
+ * \param[in]    debug           1 for debugging output, 0 otherwise
+ * \return  0 if OK, 1 on error list of page numbers, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Dewarps pixs and returns the result in &pixd.
+ *      (2) This uses default values for all model parameters.
+ *      (3) If pixs is 1 bpp, the parameters %adaptive and %thresh are ignored.
+ *      (4) If it can't build a model, returns a copy of pixs in &pixd.
+ * </pre>
+ */
+l_ok
+dewarpSinglePage(PIX         *pixs,
+                 l_int32      thresh,
+                 l_int32      adaptive,
+                 l_int32      useboth,
+                 l_int32      check_columns,
+                 PIX        **ppixd,
+                 L_DEWARPA  **pdewa,
+                 l_int32      debug)
+{
+L_DEWARPA  *dewa;
+PIX        *pixb;
+
+    if (!ppixd)
+        return ERROR_INT("&pixd not defined", __func__, 1);
+    *ppixd = NULL;
+    if (pdewa) *pdewa = NULL;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+
+    dewarpSinglePageInit(pixs, thresh, adaptive, useboth,
+                         check_columns, &pixb, &dewa);
+    if (!pixb) {
+        dewarpaDestroy(&dewa);
+        return ERROR_INT("pixb not made", __func__, 1);
+    }
+
+    dewarpSinglePageRun(pixs, pixb, dewa, ppixd, debug);
+
+    if (pdewa)
+        *pdewa = dewa;
+    else
+        dewarpaDestroy(&dewa);
+    pixDestroy(&pixb);
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpSinglePageInit()
+ *
+ * \param[in]    pixs            with text, any depth
+ * \param[in]    thresh          for global thresh to 1 bpp; ignore otherwise
+ * \param[in]    adaptive        1 for adaptive thresh; 0 for global threshold
+ * \param[in]    useboth         1 for both horiz and vert; 0 for vertical only
+ * \param[in]    check_columns   1 to skip horizontal if multiple columns;
+ *                               0 otherwise; default is to skip
+ * \param[out]   ppixb           1 bpp debug image
+ * \param[out]   pdewa           initialized dewa
+ * \return  0 if OK, 1 on error list of page numbers, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This binarizes the input pixs if necessary, returning the
+ *          binarized image.  It also initializes the dewa to default values
+ *          for the model parameters.
+ *      (2) If pixs is 1 bpp, the parameters %adaptive and %thresh are ignored.
+ *      (3) To change the model parameters, call dewarpaSetCurvatures()
+ *          before running dewarpSinglePageRun().  For example:
+ *             dewarpSinglePageInit(pixs, 0, 1, 1, 1, &pixb, &dewa);
+ *             dewarpaSetCurvatures(dewa, 250, -1, -1, 80, 70, 150);
+ *             dewarpSinglePageRun(pixs, pixb, dewa, &pixd, 0);
+ *             dewarpaDestroy(&dewa);
+ *             pixDestroy(&pixb);
+ * </pre>
+ */
+l_ok
+dewarpSinglePageInit(PIX         *pixs,
+                     l_int32      thresh,
+                     l_int32      adaptive,
+                     l_int32      useboth,
+                     l_int32      check_columns,
+                     PIX        **ppixb,
+                     L_DEWARPA  **pdewa)
+{
+PIX  *pix1, *pix2;
+
+    if (ppixb) *ppixb = NULL;
+    if (pdewa) *pdewa = NULL;
+    if (!ppixb || !pdewa)
+        return ERROR_INT("&pixb and &dewa not both defined", __func__, 1);
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+
+         /* Generate a binary image, if necessary */
+    if (pixGetDepth(pixs) > 1) {
+        if ((pix1 = pixConvertTo8(pixs, 0)) == NULL)
+            return ERROR_INT("pix1 not made", __func__, 1);
+        if (adaptive)
+            pix2 = pixAdaptThresholdToBinary(pix1, NULL, 1.0);
+        else
+            pix2 = pixThresholdToBinary(pix1, thresh);
+        pixDestroy(&pix1);
+        if (!pix2)
+            return ERROR_INT("pix2 not made", __func__, 1);
+        *ppixb = pix2;
+    } else {
+        *ppixb = pixClone(pixs);
+    }
+
+    *pdewa = dewarpaCreate(1, 0, 1, 0, -1);
+    dewarpaUseBothArrays(*pdewa, useboth);
+    dewarpaSetCheckColumns(*pdewa, check_columns);
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpSinglePageRun()
+ *
+ * \param[in]    pixs    any depth
+ * \param[in]    pixb    1 bpp
+ * \param[in]    dewa    initialized
+ * \param[out]   ppixd   dewarped result
+ * \param[in]    debug   1 for debugging output, 0 otherwise
+ * \return  0 if OK, 1 on error list of page numbers, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Dewarps pixs and returns the result in &pixd.
+ *      (2) The 1 bpp version %pixb and %dewa are conveniently generated by
+ *          dewarpSinglePageInit().
+ *      (3) Non-default model parameters must be set before calling this.
+ *      (4) If a model cannot be built, this returns a copy of pixs in &pixd.
+ * </pre>
+ */
+l_ok
+dewarpSinglePageRun(PIX        *pixs,
+                    PIX        *pixb,
+                    L_DEWARPA  *dewa,
+                    PIX       **ppixd,
+                    l_int32     debug)
+{
+const char  *debugfile;
+l_int32      vsuccess, ret;
+L_DEWARP    *dew;
+
+    if (!ppixd)
+        return ERROR_INT("&pixd not defined", __func__, 1);
+    *ppixd = NULL;
+    if (!pixs)
+        return ERROR_INT("pixs not defined", __func__, 1);
+    if (!pixb)
+        return ERROR_INT("pixb not defined", __func__, 1);
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    if (debug)
+        lept_mkdir("lept/dewarp");
+
+        /* Generate the page model */
+    dew = dewarpCreate(pixb, 0);
+    dewarpaInsertDewarp(dewa, dew);
+    debugfile = (debug) ? "/tmp/lept/dewarp/singlepage_model.pdf" : NULL;
+    dewarpBuildPageModel(dew, debugfile);
+    dewarpaModelStatus(dewa, 0, &vsuccess, NULL);
+    if (vsuccess == 0) {
+        L_ERROR("failure to build model for vertical disparity\n", __func__);
+        *ppixd = pixCopy(NULL, pixs);
+        return 0;
+    }
+
+        /* Apply the page model */
+    debugfile = (debug) ? "/tmp/lept/dewarp/singlepage_apply.pdf" : NULL;
+    ret = dewarpaApplyDisparity(dewa, 0, pixs, 255, 0, 0, ppixd, debugfile);
+    if (ret)
+        L_ERROR("invalid model; failure to apply disparity\n", __func__);
+    return 0;
+}
+
+
+/*----------------------------------------------------------------------*
+ *                        Operations on dewarpa                         *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   dewarpaListPages()
+ *
+ * \param[in]    dewa    populated with dewarp structs for pages
+ * \return  0 if OK, 1 on error list of page numbers, or NULL on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates two numas, stored in the dewarpa, that give:
+ *          (a) the page number for each dew that has a page model.
+ *          (b) the page number for each dew that has either a page
+ *              model or a reference model.
+ *          It can be called at any time.
+ *      (2) It is called by the dewarpa serializer before writing.
+ * </pre>
+ */
+l_ok
+dewarpaListPages(L_DEWARPA  *dewa)
+{
+l_int32    i;
+L_DEWARP  *dew;
+NUMA      *namodels, *napages;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    numaDestroy(&dewa->namodels);
+    numaDestroy(&dewa->napages);
+    namodels = numaCreate(dewa->maxpage + 1);
+    napages = numaCreate(dewa->maxpage + 1);
+    dewa->namodels = namodels;
+    dewa->napages = napages;
+    for (i = 0; i <= dewa->maxpage; i++) {
+        if ((dew = dewarpaGetDewarp(dewa, i)) != NULL) {
+            if (dew->hasref == 0)
+                numaAddNumber(namodels, dew->pageno);
+            numaAddNumber(napages, dew->pageno);
+        }
+    }
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaSetValidModels()
+ *
+ * \param[in]    dewa
+ * \param[in]    notests
+ * \param[in]    debug     1 to output information on invalid page models
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) A valid model must meet the rendering requirements, which
+ *          include whether or not a vertical disparity model exists
+ *          and conditions on curvatures for vertical and horizontal
+ *          disparity models.
+ *      (2) If %notests == 1, this ignores the curvature constraints
+ *          and assumes that all successfully built models are valid.
+ *      (3) This function does not need to be called by the application.
+ *          It is called by dewarpaInsertRefModels(), which
+ *          will destroy all invalid dewarps.  Consequently, to inspect
+ *          an invalid dewarp model, it must be done before calling
+ *          dewarpaInsertRefModels().
+ * </pre>
+ */
+l_ok
+dewarpaSetValidModels(L_DEWARPA  *dewa,
+                      l_int32     notests,
+                      l_int32     debug)
+{
+l_int32    i, n, maxcurv, diffcurv, diffedge;
+L_DEWARP  *dew;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    n = dewa->maxpage + 1;
+    for (i = 0; i < n; i++) {
+        if ((dew = dewarpaGetDewarp(dewa, i)) == NULL)
+            continue;
+
+        if (debug) {
+            if (dew->hasref == 1) {
+                L_INFO("page %d: has only a ref model\n", __func__, i);
+            } else if (dew->vsuccess == 0) {
+                L_INFO("page %d: no model successfully built\n",
+                       __func__, i);
+            } else if (!notests) {
+                maxcurv = L_MAX(L_ABS(dew->mincurv), L_ABS(dew->maxcurv));
+                diffcurv = dew->maxcurv - dew->mincurv;
+                if (dewa->useboth && !dew->hsuccess)
+                    L_INFO("page %d: useboth, but no horiz disparity\n",
+                               __func__, i);
+                if (maxcurv > dewa->max_linecurv)
+                    L_INFO("page %d: max curvature %d > max_linecurv\n",
+                                __func__, i, diffcurv);
+                if (diffcurv < dewa->min_diff_linecurv)
+                    L_INFO("page %d: diff curv %d < min_diff_linecurv\n",
+                                __func__, i, diffcurv);
+                if (diffcurv > dewa->max_diff_linecurv)
+                    L_INFO("page %d: abs diff curv %d > max_diff_linecurv\n",
+                                __func__, i, diffcurv);
+                if (dew->hsuccess) {
+                    if (L_ABS(dew->leftslope) > dewa->max_edgeslope)
+                        L_INFO("page %d: abs left slope %d > max_edgeslope\n",
+                                    __func__, i, dew->leftslope);
+                    if (L_ABS(dew->rightslope) > dewa->max_edgeslope)
+                        L_INFO("page %d: abs right slope %d > max_edgeslope\n",
+                                    __func__, i, dew->rightslope);
+                    diffedge = L_ABS(dew->leftcurv - dew->rightcurv);
+                    if (L_ABS(dew->leftcurv) > dewa->max_edgecurv)
+                        L_INFO("page %d: left curvature %d > max_edgecurv\n",
+                                    __func__, i, dew->leftcurv);
+                    if (L_ABS(dew->rightcurv) > dewa->max_edgecurv)
+                        L_INFO("page %d: right curvature %d > max_edgecurv\n",
+                               __func__, i, dew->rightcurv);
+                    if (diffedge > dewa->max_diff_edgecurv)
+                        L_INFO("page %d: abs diff left-right curv %d > "
+                               "max_diff_edgecurv\n", __func__, i, diffedge);
+                }
+            }
+        }
+
+        dewarpaTestForValidModel(dewa, dew, notests);
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaInsertRefModels()
+ *
+ * \param[in]    dewa
+ * \param[in]    notests    if 1, ignore curvature constraints on model
+ * \param[in]    debug      1 to output information on invalid page models
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This destroys all dewarp models that are invalid, and then
+ *          inserts reference models where possible.
+ *      (2) If %notests == 1, this ignores the curvature constraints
+ *          and assumes that all successfully built models are valid.
+ *      (3) If useboth == 0, it uses the closest valid model within the
+ *          distance and parity constraints.  If useboth == 1, it tries
+ *          to use the closest allowed hvalid model; if it doesn't find
+ *          an hvalid model, it uses the closest valid model.
+ *      (4) For all pages without a model, this clears out any existing
+ *          invalid and reference dewarps, finds the nearest valid model
+ *          with the same parity, and inserts an empty dewarp with the
+ *          reference page.
+ *      (5) Then if it is requested to use both vertical and horizontal
+ *          disparity arrays (useboth == 1), it tries to replace any
+ *          hvalid == 0 model or reference with an hvalid == 1 reference.
+ *      (6) The distance constraint is that any reference model must
+ *          be within maxdist.  Note that with the parity constraint,
+ *          no reference models will be used if maxdist < 2.
+ *      (7) This function must be called, even if reference models will
+ *          not be used.  It should be called after building models on all
+ *          available pages, and after setting the rendering parameters.
+ *      (8) If the dewa has been serialized, this function is called by
+ *          dewarpaRead() when it is read back.  It is also called
+ *          any time the rendering parameters are changed.
+ *      (9) Note: if this has been called with useboth == 1, and useboth
+ *          is reset to 0, you should first call dewarpaRestoreModels()
+ *          to bring real models from the cache back to the primary array.
+ * </pre>
+ */
+l_ok
+dewarpaInsertRefModels(L_DEWARPA  *dewa,
+                       l_int32     notests,
+                       l_int32     debug)
+{
+l_int32    i, j, n, val, min, distdown, distup;
+L_DEWARP  *dew;
+NUMA      *na, *nah;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+    if (dewa->maxdist < 2)
+        L_INFO("maxdist < 2; no ref models can be used\n", __func__);
+
+        /* Make an indicator numa for pages with valid models. */
+    dewarpaSetValidModels(dewa, notests, debug);
+    n = dewa->maxpage + 1;
+    na = numaMakeConstant(0, n);
+    for (i = 0; i < n; i++) {
+        dew = dewarpaGetDewarp(dewa, i);
+        if (dew && dew->vvalid)
+            numaReplaceNumber(na, i, 1);
+    }
+
+        /* Remove all existing ref models and restore models from cache */
+    dewarpaRestoreModels(dewa);
+
+        /* Move invalid models to the cache, and insert reference dewarps
+         * for pages that need to borrow a model.
+         * First, try to find a valid model for each page. */
+    for (i = 0; i < n; i++) {
+        numaGetIValue(na, i, &val);
+        if (val == 1) continue;  /* already has a valid model */
+        if ((dew = dewa->dewarp[i]) != NULL) {  /* exists but is not valid; */
+            dewa->dewarpcache[i] = dew;  /* move it to the cache */
+            dewa->dewarp[i] = NULL;
+        }
+        if (dewa->maxdist < 2) continue;  /* can't use a ref model */
+            /* Look back for nearest model */
+        distdown = distup = dewa->maxdist + 1;
+        for (j = i - 2; j >= 0 && distdown > dewa->maxdist; j -= 2) {
+            numaGetIValue(na, j, &val);
+            if (val == 1) distdown = i - j;
+        }
+            /* Look ahead for nearest model */
+        for (j = i + 2; j < n && distup > dewa->maxdist; j += 2) {
+            numaGetIValue(na, j, &val);
+            if (val == 1) distup = j - i;
+        }
+        min = L_MIN(distdown, distup);
+        if (min > dewa->maxdist) continue;  /* no valid model in range */
+        if (distdown <= distup)
+            dewarpaInsertDewarp(dewa, dewarpCreateRef(i, i - distdown));
+        else
+            dewarpaInsertDewarp(dewa, dewarpCreateRef(i, i + distup));
+    }
+    numaDestroy(&na);
+
+        /* If a valid model will do, we're finished. */
+    if (dewa->useboth == 0) {
+        dewa->modelsready = 1;  /* validated */
+        return 0;
+    }
+
+        /* The request is useboth == 1.  Now try to find an hvalid model */
+    nah = numaMakeConstant(0, n);
+    for (i = 0; i < n; i++) {
+        dew = dewarpaGetDewarp(dewa, i);
+        if (dew && dew->hvalid)
+            numaReplaceNumber(nah, i, 1);
+    }
+    for (i = 0; i < n; i++) {
+        numaGetIValue(nah, i, &val);
+        if (val == 1) continue;  /* already has a hvalid model */
+        if (dewa->maxdist < 2) continue;  /* can't use a ref model */
+        distdown = distup = 100000;
+        for (j = i - 2; j >= 0; j -= 2) {  /* look back for nearest model */
+            numaGetIValue(nah, j, &val);
+            if (val == 1) {
+                distdown = i - j;
+                break;
+            }
+        }
+        for (j = i + 2; j < n; j += 2) {  /* look ahead for nearest model */
+            numaGetIValue(nah, j, &val);
+            if (val == 1) {
+                distup = j - i;
+                break;
+            }
+        }
+        min = L_MIN(distdown, distup);
+        if (min > dewa->maxdist) continue;  /* no hvalid model within range */
+
+            /* We can replace the existing valid model with an hvalid model.
+             * If it's not a reference, save it in the cache. */
+        if ((dew = dewarpaGetDewarp(dewa, i)) == NULL) {
+            L_ERROR("dew is null for page %d!\n", __func__, i);
+        } else {
+            if (dew->hasref == 0) {  /* not a ref model */
+                dewa->dewarpcache[i] = dew;  /* move it to the cache */
+                dewa->dewarp[i] = NULL;  /* must null the ptr */
+            }
+        }
+        if (distdown <= distup)  /* insert the hvalid ref model */
+            dewarpaInsertDewarp(dewa, dewarpCreateRef(i, i - distdown));
+        else
+            dewarpaInsertDewarp(dewa, dewarpCreateRef(i, i + distup));
+    }
+    numaDestroy(&nah);
+
+    dewa->modelsready = 1;  /* validated */
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaStripRefModels()
+ *
+ * \param[in]    dewa    populated with dewarp structs for pages
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This examines each dew in a dewarpa, and removes
+ *          all that don't have their own page model (i.e., all
+ *          that have "references" to nearby pages with valid models).
+ *          These references were generated by dewarpaInsertRefModels(dewa).
+ * </pre>
+ */
+l_ok
+dewarpaStripRefModels(L_DEWARPA  *dewa)
+{
+l_int32    i;
+L_DEWARP  *dew;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    for (i = 0; i <= dewa->maxpage; i++) {
+        if ((dew = dewarpaGetDewarp(dewa, i)) != NULL) {
+            if (dew->hasref)
+                dewarpDestroy(&dewa->dewarp[i]);
+        }
+    }
+    dewa->modelsready = 0;
+
+        /* Regenerate the page lists */
+    dewarpaListPages(dewa);
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaRestoreModels()
+ *
+ * \param[in]    dewa     populated with dewarp structs for pages
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This puts all real models (and only real models) in the
+ *          primary dewarpa array.  First remove all dewarps that are
+ *          only references to other page models.  Then move all models
+ *          that had been cached back into the primary dewarp array.
+ *      (2) After this is done, we still need to recompute and insert
+ *          the reference models before dewa->modelsready is true.
+ * </pre>
+ */
+l_ok
+dewarpaRestoreModels(L_DEWARPA  *dewa)
+{
+l_int32    i;
+L_DEWARP  *dew;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+        /* Strip out ref models.  Then only real models will be in the
+         * primary dewarp array. */
+    dewarpaStripRefModels(dewa);
+
+        /* The cache holds only real models, which are not necessarily valid. */
+    for (i = 0; i <= dewa->maxpage; i++) {
+        if ((dew = dewa->dewarpcache[i]) != NULL) {
+            if (dewa->dewarp[i]) {
+                L_ERROR("dew in both cache and main array!: page %d\n",
+                        __func__, i);
+            } else {
+                dewa->dewarp[i] = dew;
+                dewa->dewarpcache[i] = NULL;
+            }
+        }
+    }
+    dewa->modelsready = 0;  /* new ref models not yet inserted */
+
+        /* Regenerate the page lists */
+    dewarpaListPages(dewa);
+    return 0;
+}
+
+
+/*----------------------------------------------------------------------*
+ *                      Dewarp debugging output                         *
+ *----------------------------------------------------------------------*/
+/*!
+ * \brief   dewarpaInfo()
+ *
+ * \param[in]    fp
+ * \param[in]    dewa
+ * \return  0 if OK, 1 on error
+ */
+l_ok
+dewarpaInfo(FILE       *fp,
+            L_DEWARPA  *dewa)
+{
+l_int32    i, n, pageno, nnone, nvsuccess, nvvalid, nhsuccess, nhvalid, nref;
+L_DEWARP  *dew;
+
+    if (!fp)
+        return ERROR_INT("dewa not defined", __func__, 1);
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    fprintf(fp, "\nDewarpaInfo: %p\n", dewa);
+    fprintf(fp, "nalloc = %d, maxpage = %d\n", dewa->nalloc, dewa->maxpage);
+    fprintf(fp, "sampling = %d, redfactor = %d, minlines = %d\n",
+            dewa->sampling, dewa->redfactor, dewa->minlines);
+    fprintf(fp, "maxdist = %d, useboth = %d\n",
+            dewa->maxdist, dewa->useboth);
+
+    dewarpaModelStats(dewa, &nnone, &nvsuccess, &nvvalid,
+                      &nhsuccess, &nhvalid, &nref);
+    n = numaGetCount(dewa->napages);
+    lept_stderr("Total number of pages with a dew = %d\n", n);
+    lept_stderr("Number of pages without any models = %d\n", nnone);
+    lept_stderr("Number of pages with a vert model = %d\n", nvsuccess);
+    lept_stderr("Number of pages with a valid vert model = %d\n", nvvalid);
+    lept_stderr("Number of pages with both models = %d\n", nhsuccess);
+    lept_stderr("Number of pages with both models valid = %d\n", nhvalid);
+    lept_stderr("Number of pages with a ref model = %d\n", nref);
+
+    for (i = 0; i < n; i++) {
+        numaGetIValue(dewa->napages, i, &pageno);
+        if ((dew = dewarpaGetDewarp(dewa, pageno)) == NULL)
+            continue;
+        lept_stderr("Page: %d\n", dew->pageno);
+        lept_stderr("  hasref = %d, refpage = %d\n",
+                    dew->hasref, dew->refpage);
+        lept_stderr("  nlines = %d\n", dew->nlines);
+        lept_stderr("  w = %d, h = %d, nx = %d, ny = %d\n",
+                    dew->w, dew->h, dew->nx, dew->ny);
+        if (dew->sampvdispar)
+            lept_stderr("  Vertical disparity builds:\n"
+                    "    (min,max,abs-diff) line curvature = (%d,%d,%d)\n",
+                    dew->mincurv, dew->maxcurv, dew->maxcurv - dew->mincurv);
+        if (dew->samphdispar)
+            lept_stderr("  Horizontal disparity builds:\n"
+                    "    left edge slope = %d, right edge slope = %d\n"
+                    "    (left,right,abs-diff) edge curvature = (%d,%d,%d)\n",
+                    dew->leftslope, dew->rightslope, dew->leftcurv,
+                    dew->rightcurv, L_ABS(dew->leftcurv - dew->rightcurv));
+    }
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaModelStats()
+ *
+ * \param[in]    dewa
+ * \param[out]   pnnone       [optional] number without any model
+ * \param[out]   pnvsuccess   [optional] number with a vert model
+ * \param[out]   pnvvalid     [optional] number with a valid vert model
+ * \param[out]   pnhsuccess   [optional] number with both models
+ * \param[out]   pnhvalid     [optional] number with both models valid
+ * \param[out]   pnref        [optional] number with a reference model
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) A page without a model has no dew.  It most likely failed to
+ *          generate a vertical model, and has not been assigned a ref
+ *          model from a neighboring page with a valid vertical model.
+ *      (2) A page has vsuccess == 1 if there is at least a model of the
+ *          vertical disparity.  The model may be invalid, in which case
+ *          dewarpaInsertRefModels() will stash it in the cache and
+ *          attempt to replace it by a valid ref model.
+ *      (3) A vvvalid model is a vertical disparity model whose parameters
+ *          satisfy the constraints given in dewarpaSetValidModels().
+ *      (4) A page has hsuccess == 1 if both the vertical and horizontal
+ *          disparity arrays have been constructed.
+ *      (5) An  hvalid model has vertical and horizontal disparity
+ *          models whose parameters satisfy the constraints given
+ *          in dewarpaSetValidModels().
+ *      (6) A page has a ref model if it failed to generate a valid
+ *          model but was assigned a vvalid or hvalid model on another
+ *          page (within maxdist) by dewarpaInsertRefModel().
+ *      (7) This calls dewarpaTestForValidModel(); it ignores the vvalid
+ *          and hvalid fields.
+ * </pre>
+ */
+l_ok
+dewarpaModelStats(L_DEWARPA  *dewa,
+                  l_int32    *pnnone,
+                  l_int32    *pnvsuccess,
+                  l_int32    *pnvvalid,
+                  l_int32    *pnhsuccess,
+                  l_int32    *pnhvalid,
+                  l_int32    *pnref)
+{
+l_int32    i, n, pageno, nnone, nvsuccess, nvvalid, nhsuccess, nhvalid, nref;
+L_DEWARP  *dew;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+
+    dewarpaListPages(dewa);
+    n = numaGetCount(dewa->napages);
+    nnone = nref = nvsuccess = nvvalid = nhsuccess = nhvalid = 0;
+    for (i = 0; i < n; i++) {
+        numaGetIValue(dewa->napages, i, &pageno);
+        dew = dewarpaGetDewarp(dewa, pageno);
+        if (!dew) {
+            nnone++;
+            continue;
+        }
+        if (dew->hasref == 1)
+            nref++;
+        if (dew->vsuccess == 1)
+            nvsuccess++;
+        if (dew->hsuccess == 1)
+            nhsuccess++;
+        dewarpaTestForValidModel(dewa, dew, 0);
+        if (dew->vvalid == 1)
+            nvvalid++;
+        if (dew->hvalid == 1)
+            nhvalid++;
+    }
+
+    if (pnnone) *pnnone = nnone;
+    if (pnref) *pnref = nref;
+    if (pnvsuccess) *pnvsuccess = nvsuccess;
+    if (pnvvalid) *pnvvalid = nvvalid;
+    if (pnhsuccess) *pnhsuccess = nhsuccess;
+    if (pnhvalid) *pnhvalid = nhvalid;
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaTestForValidModel()
+ *
+ * \param[in]    dewa
+ * \param[in]    dew
+ * \param[in]    notests
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Computes validity of vertical (vvalid) model and both
+ *          vertical and horizontal (hvalid) models.
+ *      (2) If %notests == 1, this ignores the curvature constraints
+ *          and assumes that all successfully built models are valid.
+ *      (3) This is just about the models, not the rendering process,
+ *          so the value of useboth is not considered here.
+ * </pre>
+ */
+static l_int32
+dewarpaTestForValidModel(L_DEWARPA  *dewa,
+                         L_DEWARP   *dew,
+                         l_int32     notests)
+{
+l_int32  maxcurv, diffcurv, diffedge;
+
+    if (!dewa || !dew)
+        return ERROR_INT("dewa and dew not both defined", __func__, 1);
+
+    if (notests) {
+       dew->vvalid = dew->vsuccess;
+       dew->hvalid = dew->hsuccess;
+       return 0;
+    }
+
+        /* No actual model was built */
+    if (dew->vsuccess == 0) return 0;
+
+        /* Was previously found not to have a valid model  */
+    if (dew->hasref == 1) return 0;
+
+        /* vsuccess == 1; a vertical (line) model exists.
+         * First test that the vertical curvatures are within allowed
+         * bounds.  Note that all curvatures are signed.*/
+    maxcurv = L_MAX(L_ABS(dew->mincurv), L_ABS(dew->maxcurv));
+    diffcurv = dew->maxcurv - dew->mincurv;
+    if (maxcurv <= dewa->max_linecurv &&
+        diffcurv >= dewa->min_diff_linecurv &&
+        diffcurv <= dewa->max_diff_linecurv) {
+        dew->vvalid = 1;
+    } else {
+        L_INFO("invalid vert model for page %d:\n", __func__, dew->pageno);
+#if DEBUG_INVALID_MODELS
+        lept_stderr("  max line curv = %d, max allowed = %d\n",
+                    maxcurv, dewa->max_linecurv);
+        lept_stderr("  diff line curv = %d, max allowed = %d\n",
+                    diffcurv, dewa->max_diff_linecurv);
+#endif  /* DEBUG_INVALID_MODELS */
+    }
+
+        /* If a horizontal (edge) model exists, test for validity. */
+    if (dew->hsuccess) {
+        diffedge = L_ABS(dew->leftcurv - dew->rightcurv);
+        if (L_ABS(dew->leftslope) <= dewa->max_edgeslope &&
+            L_ABS(dew->rightslope) <= dewa->max_edgeslope &&
+            L_ABS(dew->leftcurv) <= dewa->max_edgecurv &&
+            L_ABS(dew->rightcurv) <= dewa->max_edgecurv &&
+            diffedge <= dewa->max_diff_edgecurv) {
+            dew->hvalid = 1;
+        } else {
+            L_INFO("invalid horiz model for page %d:\n", __func__, dew->pageno);
+#if DEBUG_INVALID_MODELS
+            lept_stderr("  left edge slope = %d, max allowed = %d\n",
+                        dew->leftslope, dewa->max_edgeslope);
+            lept_stderr("  right edge slope = %d, max allowed = %d\n",
+                        dew->rightslope, dewa->max_edgeslope);
+            lept_stderr("  left edge curv = %d, max allowed = %d\n",
+                        dew->leftcurv, dewa->max_edgecurv);
+            lept_stderr("  right edge curv = %d, max allowed = %d\n",
+                        dew->rightcurv, dewa->max_edgecurv);
+            lept_stderr("  diff edge curv = %d, max allowed = %d\n",
+                        diffedge, dewa->max_diff_edgecurv);
+#endif  /* DEBUG_INVALID_MODELS */
+        }
+    }
+
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpaShowArrays()
+ *
+ * \param[in]    dewa
+ * \param[in]    scalefact    on contour images; typ. 0.5
+ * \param[in]    first        first page model to render
+ * \param[in]    last         last page model to render; use 0 to go to end
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Generates a pdf of contour plots of the disparity arrays.
+ *      (2) This only shows actual models; not ref models
+ * </pre>
+ */
+l_ok
+dewarpaShowArrays(L_DEWARPA   *dewa,
+                  l_float32    scalefact,
+                  l_int32      first,
+                  l_int32      last)
+{
+char       buf[256];
+l_int32    i, svd, shd;
+L_BMF     *bmf;
+L_DEWARP  *dew;
+PIX       *pixv, *pixvs, *pixh, *pixhs = NULL, *pixt, *pixd;
+PIXA      *pixa;
+
+    if (!dewa)
+        return ERROR_INT("dew not defined", __func__, 1);
+    if (first < 0 || first > dewa->maxpage)
+        return ERROR_INT("first out of bounds", __func__, 1);
+    if (last <= 0 || last > dewa->maxpage) last = dewa->maxpage;
+    if (last < first)
+        return ERROR_INT("last < first", __func__, 1);
+
+    lept_rmdir("lept/dewarp1");  /* temp directory for contour plots */
+    lept_mkdir("lept/dewarp1");
+    if ((bmf = bmfCreate(NULL, 8)) == NULL)
+        L_ERROR("bmf not made; page info not displayed", __func__);
+
+    lept_stderr("Generating contour plots\n");
+    for (i = first; i <= last; i++) {
+        if (i && ((i % 10) == 0))
+            lept_stderr(" .. %d", i);
+        dew = dewarpaGetDewarp(dewa, i);
+        if (!dew) continue;
+        if (dew->hasref == 1) continue;
+        svd = shd = 0;
+        if (dew->sampvdispar) svd = 1;
+        if (dew->samphdispar) shd = 1;
+        if (!svd) {
+            L_ERROR("sampvdispar not made for page %d!\n", __func__, i);
+            continue;
+        }
+
+            /* Generate contour plots at reduced resolution */
+        dewarpPopulateFullRes(dew, NULL, 0, 0);
+        pixv = fpixRenderContours(dew->fullvdispar, 3.0f, 0.15f);
+        pixvs = pixScaleBySampling(pixv, scalefact, scalefact);
+        pixDestroy(&pixv);
+        if (shd) {
+            pixh = fpixRenderContours(dew->fullhdispar, 3.0f, 0.15f);
+            pixhs = pixScaleBySampling(pixh, scalefact, scalefact);
+            pixDestroy(&pixh);
+        }
+        dewarpMinimize(dew);
+
+            /* Save side-by-side */
+        pixa = pixaCreate(2);
+        pixaAddPix(pixa, pixvs, L_INSERT);
+        if (shd)
+            pixaAddPix(pixa, pixhs, L_INSERT);
+        pixt = pixaDisplayTiledInRows(pixa, 32, 1500, 1.0, 0, 30, 2);
+        snprintf(buf, sizeof(buf), "Page %d", i);
+        pixd = pixAddSingleTextblock(pixt, bmf, buf, 0x0000ff00,
+                                     L_ADD_BELOW, NULL);
+        snprintf(buf, sizeof(buf), "/tmp/lept/dewarp1/arrays_%04d.png", i);
+        pixWriteDebug(buf, pixd, IFF_PNG);
+        pixaDestroy(&pixa);
+        pixDestroy(&pixt);
+        pixDestroy(&pixd);
+    }
+    bmfDestroy(&bmf);
+    lept_stderr("\n");
+
+    lept_stderr("Generating pdf of contour plots\n");
+    convertFilesToPdf("/tmp/lept/dewarp1", "arrays_", 90, 1.0, L_FLATE_ENCODE,
+                      0, "Disparity arrays", "/tmp/lept/disparity_arrays.pdf");
+    lept_stderr("Output written to: /tmp/lept/disparity_arrays.pdf\n");
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpDebug()
+ *
+ * \param[in]    dew
+ * \param[in]    subdirs   one or more subdirectories of /tmp; e.g., "dew1"
+ * \param[in]    index     to help label output images; e.g., the page number
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) Prints dewarp fields and generates disparity array contour images.
+ *          The contour images are written to file:
+ *                /tmp/[subdirs]/pixv_[index].png
+ * </pre>
+ */
+l_ok
+dewarpDebug(L_DEWARP    *dew,
+            const char  *subdirs,
+            l_int32      index)
+{
+char     fname[256];
+char    *outdir;
+l_int32  svd, shd;
+PIX     *pixv, *pixh;
+
+    if (!dew)
+        return ERROR_INT("dew not defined", __func__, 1);
+    if (!subdirs)
+        return ERROR_INT("subdirs not defined", __func__, 1);
+
+    lept_stderr("pageno = %d, hasref = %d, refpage = %d\n",
+                dew->pageno, dew->hasref, dew->refpage);
+    lept_stderr("sampling = %d, redfactor = %d, minlines = %d\n",
+                dew->sampling, dew->redfactor, dew->minlines);
+    svd = shd = 0;
+    if (!dew->hasref) {
+        if (dew->sampvdispar) svd = 1;
+        if (dew->samphdispar) shd = 1;
+        lept_stderr("sampv = %d, samph = %d\n", svd, shd);
+        lept_stderr("w = %d, h = %d\n", dew->w, dew->h);
+        lept_stderr("nx = %d, ny = %d\n", dew->nx, dew->ny);
+        lept_stderr("nlines = %d\n", dew->nlines);
+        if (svd) {
+            lept_stderr("(min,max,abs-diff) line curvature = (%d,%d,%d)\n",
+                       dew->mincurv, dew->maxcurv, dew->maxcurv - dew->mincurv);
+        }
+        if (shd) {
+            lept_stderr("(left edge slope = %d, right edge slope = %d\n",
+                        dew->leftslope, dew->rightslope);
+            lept_stderr("(left,right,abs-diff) edge curvature = "
+                        "(%d,%d,%d)\n", dew->leftcurv, dew->rightcurv,
+                        L_ABS(dew->leftcurv - dew->rightcurv));
+        }
+    }
+    if (!svd && !shd) {
+        lept_stderr("No disparity arrays\n");
+        return 0;
+    }
+
+    dewarpPopulateFullRes(dew, NULL, 0, 0);
+    lept_mkdir(subdirs);
+    outdir = pathJoin("/tmp", subdirs);
+    if (svd) {
+        pixv = fpixRenderContours(dew->fullvdispar, 3.0f, 0.15f);
+        snprintf(fname, sizeof(fname), "%s/pixv_%d.png", outdir, index);
+        pixWriteDebug(fname, pixv, IFF_PNG);
+        pixDestroy(&pixv);
+    }
+    if (shd) {
+        pixh = fpixRenderContours(dew->fullhdispar, 3.0f, 0.15f);
+        snprintf(fname, sizeof(fname), "%s/pixh_%d.png", outdir, index);
+        pixWriteDebug(fname, pixh, IFF_PNG);
+        pixDestroy(&pixh);
+    }
+    LEPT_FREE(outdir);
+    return 0;
+}
+
+
+/*!
+ * \brief   dewarpShowResults()
+ *
+ * \param[in]    dewa
+ * \param[in]    sa          of indexed input images
+ * \param[in]    boxa        crop boxes for input images; can be null
+ * \param[in]    firstpage
+ * \param[in]    lastpage
+ * \param[in]    pdfout      filename
+ * \return  0 if OK, 1 on error
+ *
+ * <pre>
+ * Notes:
+ *      (1) This generates a pdf of image pairs (before, after) for
+ *          the designated set of input pages.
+ *      (2) If the boxa exists, its elements are aligned with numbers
+ *          in the filenames in %sa.  It is used to crop the input images.
+ *          It is assumed that the dewa was generated from the cropped
+ *          images.  No undercropping is applied before rendering.
+ * </pre>
+ */
+l_ok
+dewarpShowResults(L_DEWARPA   *dewa,
+                  SARRAY      *sa,
+                  BOXA        *boxa,
+                  l_int32      firstpage,
+                  l_int32      lastpage,
+                  const char  *pdfout)
+{
+char       bufstr[256];
+l_int32    i, modelpage;
+L_BMF     *bmf;
+BOX       *box;
+L_DEWARP  *dew;
+PIX       *pixs, *pixc, *pixd, *pixt1, *pixt2;
+PIXA      *pixa;
+
+    if (!dewa)
+        return ERROR_INT("dewa not defined", __func__, 1);
+    if (!sa)
+        return ERROR_INT("sa not defined", __func__, 1);
+    if (!pdfout)
+        return ERROR_INT("pdfout not defined", __func__, 1);
+    if (firstpage > lastpage)
+        return ERROR_INT("invalid first/last page numbers", __func__, 1);
+
+    lept_rmdir("lept/dewarp_pdfout");
+    lept_mkdir("lept/dewarp_pdfout");
+    bmf = bmfCreate(NULL, 6);
+
+    lept_stderr("Dewarping and generating s/by/s view\n");
+    for (i = firstpage; i <= lastpage; i++) {
+        if (i && (i % 10 == 0)) lept_stderr(".. %d ", i);
+        pixs = pixReadIndexed(sa, i);
+        if (boxa) {
+            box = boxaGetBox(boxa, i, L_CLONE);
+            pixc = pixClipRectangle(pixs, box, NULL);
+            boxDestroy(&box);
+        }
+        else
+            pixc = pixClone(pixs);
+        dew = dewarpaGetDewarp(dewa, i);
+        pixd = NULL;
+        if (dew) {
+            dewarpaApplyDisparity(dewa, dew->pageno, pixc,
+                                  GrayInValue, 0, 0, &pixd, NULL);
+            dewarpMinimize(dew);
+        }
+        pixa = pixaCreate(2);
+        pixaAddPix(pixa, pixc, L_INSERT);
+        if (pixd)
+            pixaAddPix(pixa, pixd, L_INSERT);
+        pixt1 = pixaDisplayTiledAndScaled(pixa, 32, 500, 2, 0, 35, 2);
+        if (dew) {
+            modelpage = (dew->hasref) ? dew->refpage : dew->pageno;
+            snprintf(bufstr, sizeof(bufstr), "Page %d; using %d\n",
+                     i, modelpage);
+        }
+        else
+            snprintf(bufstr, sizeof(bufstr), "Page %d; no dewarp\n", i);
+        pixt2 = pixAddSingleTextblock(pixt1, bmf, bufstr, 0x0000ff00,
+                                      L_ADD_BELOW, 0);
+        snprintf(bufstr, sizeof(bufstr), "/tmp/lept/dewarp_pdfout/%05d", i);
+        pixWriteDebug(bufstr, pixt2, IFF_JFIF_JPEG);
+        pixaDestroy(&pixa);
+        pixDestroy(&pixs);
+        pixDestroy(&pixt1);
+        pixDestroy(&pixt2);
+    }
+    lept_stderr("\n");
+
+    lept_stderr("Generating pdf of result\n");
+    convertFilesToPdf("/tmp/lept/dewarp_pdfout", NULL, 100, 1.0, L_JPEG_ENCODE,
+                      0, "Dewarp sequence", pdfout);
+    lept_stderr("Output written to: %s\n", pdfout);
+    bmfDestroy(&bmf);
+    return 0;
+}