Mercurial > hgrepos > Python2 > PyMuPDF
diff mupdf-source/thirdparty/leptonica/src/boxfunc5.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/boxfunc5.c Mon Sep 15 11:43:07 2025 +0200 @@ -0,0 +1,1761 @@ +/*====================================================================* + - 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 boxfunc5.c + * <pre> + * + * Boxa sequence fitting + * BOXA *boxaSmoothSequenceMedian() + * BOXA *boxaWindowedMedian() + * BOXA *boxaModifyWithBoxa() + * BOXA *boxaReconcilePairWidth() + * l_int32 boxaSizeConsistency() + * BOXA *boxaReconcileAllByMedian() + * BOXA *boxaReconcileSidesByMedian() + * static void adjustSidePlotName() -- debug + * BOXA *boxaReconcileSizeByMedian() + * l_int32 boxaPlotSides() [for debugging] + * l_int32 boxaPlotSizes() [for debugging] + * BOXA *boxaFillSequence() + * static l_int32 boxaFillAll() + * l_int32 boxaSizeVariation() + * l_int32 boxaMedianDimensions() + * </pre> + */ + +#ifdef HAVE_CONFIG_H +#include <config_auto.h> +#endif /* HAVE_CONFIG_H */ + +#include <math.h> +#include "allheaders.h" + +static l_int32 boxaFillAll(BOXA *boxa); +static void adjustSidePlotName(char *buf, size_t size, const char *preface, + l_int32 select); + +/*---------------------------------------------------------------------* + * Boxa sequence fitting * + *---------------------------------------------------------------------*/ +/*! + * \brief boxaSmoothSequenceMedian() + * + * \param[in] boxas source boxa + * \param[in] halfwin half-width of sliding window; used to find median + * \param[in] subflag L_USE_MINSIZE, L_USE_MAXSIZE, + * L_SUB_ON_LOC_DIFF, L_SUB_ON_SIZE_DIFF, + * L_USE_CAPPED_MIN, L_USE_CAPPED_MAX + * \param[in] maxdiff parameter used with L_SUB_ON_LOC_DIFF, + * L_SUB_ON_SIZE_DIFF, L_USE_CAPPED_MIN, + * L_USE_CAPPED_MAX + * \param[in] extrapixels pixels added on all sides (or subtracted + * if %extrapixels < 0) when using + * L_SUB_ON_LOC_DIFF and L_SUB_ON_SIZE_DIFF + * \param[in] debug 1 for debug output + * \return boxad fitted boxa, or NULL on error + * + * <pre> + * Notes: + * (1) The target width of the sliding window is 2 * %halfwin + 1. + * If necessary, this will be reduced by boxaWindowedMedian(). + * (2) This returns a modified version of %boxas by constructing + * for each input box a box that has been smoothed with windowed + * median filtering. The filtering is done to each of the + * box sides independently, and it is computed separately for + * sequences of even and odd boxes. The output %boxad is + * constructed from the input boxa and the filtered boxa, + * depending on %subflag. See boxaModifyWithBoxa() for + * details on the use of %subflag, %maxdiff and %extrapixels. + * (3) This is useful for removing noise separately in the even + * and odd sets, where the box edge locations can have + * discontinuities but otherwise vary roughly linearly within + * intervals of size %halfwin or larger. + * (4) If you don't need to handle even and odd sets separately, + * just do this: + * boxam = boxaWindowedMedian(boxas, halfwin, debug); + * boxad = boxaModifyWithBoxa(boxas, boxam, subflag, maxdiff, + * extrapixels); + * boxaDestroy(&boxam); + * </pre> + */ +BOXA * +boxaSmoothSequenceMedian(BOXA *boxas, + l_int32 halfwin, + l_int32 subflag, + l_int32 maxdiff, + l_int32 extrapixels, + l_int32 debug) +{ +l_int32 n; +BOXA *boxae, *boxao, *boxamede, *boxamedo, *boxame, *boxamo, *boxad; +PIX *pix1; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (halfwin <= 0) { + L_WARNING("halfwin must be > 0; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (maxdiff < 0) { + L_WARNING("maxdiff must be >= 0; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (subflag != L_USE_MINSIZE && subflag != L_USE_MAXSIZE && + subflag != L_SUB_ON_LOC_DIFF && subflag != L_SUB_ON_SIZE_DIFF && + subflag != L_USE_CAPPED_MIN && subflag != L_USE_CAPPED_MAX) { + L_WARNING("invalid subflag; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if ((n = boxaGetCount(boxas)) < 6) { + L_WARNING("need at least 6 boxes; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + boxaSplitEvenOdd(boxas, 0, &boxae, &boxao); + if (debug) { + lept_mkdir("lept/smooth"); + boxaWriteDebug("/tmp/lept/smooth/boxae.ba", boxae); + boxaWriteDebug("/tmp/lept/smooth/boxao.ba", boxao); + } + + boxamede = boxaWindowedMedian(boxae, halfwin, debug); + boxamedo = boxaWindowedMedian(boxao, halfwin, debug); + if (debug) { + boxaWriteDebug("/tmp/lept/smooth/boxamede.ba", boxamede); + boxaWriteDebug("/tmp/lept/smooth/boxamedo.ba", boxamedo); + } + + boxame = boxaModifyWithBoxa(boxae, boxamede, subflag, maxdiff, extrapixels); + boxamo = boxaModifyWithBoxa(boxao, boxamedo, subflag, maxdiff, extrapixels); + if (debug) { + boxaWriteDebug("/tmp/lept/smooth/boxame.ba", boxame); + boxaWriteDebug("/tmp/lept/smooth/boxamo.ba", boxamo); + } + + boxad = boxaMergeEvenOdd(boxame, boxamo, 0); + if (debug) { + boxaPlotSides(boxas, NULL, NULL, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/smooth/plotsides1.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSides(boxad, NULL, NULL, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/smooth/plotsides2.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSizes(boxas, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/smooth/plotsizes1.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSizes(boxad, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/smooth/plotsizes2.png", pix1, IFF_PNG); + pixDestroy(&pix1); + } + + boxaDestroy(&boxae); + boxaDestroy(&boxao); + boxaDestroy(&boxamede); + boxaDestroy(&boxamedo); + boxaDestroy(&boxame); + boxaDestroy(&boxamo); + return boxad; +} + + +/*! + * \brief boxaWindowedMedian() + * + * \param[in] boxas source boxa + * \param[in] halfwin half width of window over which the median is found + * \param[in] debug 1 for debug output + * \return boxad smoothed boxa, or NULL on error + * + * <pre> + * Notes: + * (1) This finds a set of boxes (boxad) where each edge of each box is + * a windowed median smoothed value to the edges of the + * input set of boxes (boxas). + * (2) Invalid input boxes are filled from nearby ones. + * (3) The returned boxad can then be used in boxaModifyWithBoxa() + * to selectively change the boxes in the source boxa. + * </pre> + */ +BOXA * +boxaWindowedMedian(BOXA *boxas, + l_int32 halfwin, + l_int32 debug) +{ +l_int32 n, i, left, top, right, bot; +BOX *box; +BOXA *boxaf, *boxad; +NUMA *nal, *nat, *nar, *nab, *naml, *namt, *namr, *namb; +PIX *pix1; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if ((n = boxaGetCount(boxas)) < 3) { + L_WARNING("less than 3 boxes; returning a copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (halfwin <= 0) { + L_WARNING("halfwin must be > 0; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + /* Fill invalid boxes in the input sequence */ + if ((boxaf = boxaFillSequence(boxas, L_USE_ALL_BOXES, debug)) == NULL) + return (BOXA *)ERROR_PTR("filled boxa not made", __func__, NULL); + + /* Get the windowed median output from each of the sides */ + boxaExtractAsNuma(boxaf, &nal, &nat, &nar, &nab, NULL, NULL, 0); + naml = numaWindowedMedian(nal, halfwin); + namt = numaWindowedMedian(nat, halfwin); + namr = numaWindowedMedian(nar, halfwin); + namb = numaWindowedMedian(nab, halfwin); + + n = boxaGetCount(boxaf); + boxad = boxaCreate(n); + for (i = 0; i < n; i++) { + numaGetIValue(naml, i, &left); + numaGetIValue(namt, i, &top); + numaGetIValue(namr, i, &right); + numaGetIValue(namb, i, &bot); + box = boxCreate(left, top, right - left + 1, bot - top + 1); + boxaAddBox(boxad, box, L_INSERT); + } + + if (debug) { + lept_mkdir("lept/windowed"); + boxaPlotSides(boxaf, NULL, NULL, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/windowed/plotsides1.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSides(boxad, NULL, NULL, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/windowed/plotsides2.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSizes(boxaf, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/windowed/plotsizes1.png", pix1, IFF_PNG); + pixDestroy(&pix1); + boxaPlotSizes(boxad, NULL, NULL, NULL, &pix1); + pixWrite("/tmp/lept/windowed/plotsizes2.png", pix1, IFF_PNG); + pixDestroy(&pix1); + } + + boxaDestroy(&boxaf); + numaDestroy(&nal); + numaDestroy(&nat); + numaDestroy(&nar); + numaDestroy(&nab); + numaDestroy(&naml); + numaDestroy(&namt); + numaDestroy(&namr); + numaDestroy(&namb); + return boxad; +} + + +/*! + * \brief boxaModifyWithBoxa() + * + * \param[in] boxas + * \param[in] boxam boxa with boxes used to modify those in boxas + * \param[in] subflag L_USE_MINSIZE, L_USE_MAXSIZE, + * L_SUB_ON_LOC_DIFF, L_SUB_ON_SIZE_DIFF, + * L_USE_CAPPED_MIN, L_USE_CAPPED_MAX + * \param[in] maxdiff parameter used with L_SUB_ON_LOC_DIFF, + * L_SUB_ON_SIZE_DIFF, L_USE_CAPPED_MIN, + * L_USE_CAPPED_MAX + * \param[in] extrapixels pixels added on all sides (or subtracted + * if %extrapixels < 0) when using + * L_SUB_ON_LOC_DIFF and L_SUB_ON_SIZE_DIFF + * \return boxad result after adjusting boxes in boxas, or NULL on error. + * + * <pre> + * Notes: + * (1) This takes two input boxa (boxas, boxam) and constructs boxad, + * where each box in boxad is generated from the corresponding + * boxes in boxas and boxam. The rule for constructing each + * output box depends on %subflag and %maxdiff. Let boxs be + * a box from %boxas and boxm be a box from %boxam. + * * If %subflag == L_USE_MINSIZE: the output box is the intersection + * of the two input boxes. + * * If %subflag == L_USE_MAXSIZE: the output box is the union of the + * two input boxes; i.e., the minimum bounding rectangle for the + * two input boxes. + * * If %subflag == L_SUB_ON_LOC_DIFF: each side of the output box + * is found separately from the corresponding side of boxs and boxm. + * Use the boxm side, expanded by %extrapixels, if greater than + * %maxdiff pixels from the boxs side. + * * If %subflag == L_SUB_ON_SIZE_DIFF: the sides of the output box + * are determined in pairs from the width and height of boxs + * and boxm. If the boxm width differs by more than %maxdiff + * pixels from boxs, use the boxm left and right sides, + * expanded by %extrapixels. Ditto for the height difference. + * For the last two flags, each side of the output box is found + * separately from the corresponding side of boxs and boxm, + * according to these rules, where "smaller"("bigger") mean in a + * direction that decreases(increases) the size of the output box: + * * If %subflag == L_USE_CAPPED_MIN: use the Min of boxm + * with the Max of (boxs, boxm +- %maxdiff), where the sign + * is adjusted to make the box smaller (e.g., use "+" on left side). + * * If %subflag == L_USE_CAPPED_MAX: use the Max of boxm + * with the Min of (boxs, boxm +- %maxdiff), where the sign + * is adjusted to make the box bigger (e.g., use "-" on left side). + * Use of the last 2 flags is further explained in (3) and (4). + * (2) boxas and boxam must be the same size. If boxam == NULL, + * this returns a copy of boxas with a warning. + * (3) If %subflag == L_SUB_ON_LOC_DIFF, use boxm for each side + * where the corresponding sides differ by more than %maxdiff. + * Two extreme cases: + * (a) set %maxdiff == 0 to use only values from boxam in boxad. + * (b) set %maxdiff == 10000 to ignore all values from boxam; + * then boxad will be the same as boxas. + * (4) If %subflag == L_USE_CAPPED_MAX: use boxm if boxs is smaller; + * use boxs if boxs is bigger than boxm by an amount up to %maxdiff; + * and use boxm +- %maxdiff (the 'capped' value) if boxs is + * bigger than boxm by an amount larger than %maxdiff. + * Similarly, with interchange of Min/Max and sign of %maxdiff, + * for %subflag == L_USE_CAPPED_MIN. + * (5) If either of corresponding boxes in boxas and boxam is invalid, + * an invalid box is copied to the result. + * (6) Typical input for boxam may be the output of boxaLinearFit(). + * where outliers have been removed and each side is LS fit to a line. + * (7) Unlike boxaAdjustWidthToTarget() and boxaAdjustHeightToTarget(), + * this uses two boxes and does not specify target dimensions. + * </pre> + */ +BOXA * +boxaModifyWithBoxa(BOXA *boxas, + BOXA *boxam, + l_int32 subflag, + l_int32 maxdiff, + l_int32 extrapixels) +{ +l_int32 n, i, ls, ts, rs, bs, ws, hs, lm, tm, rm, bm, wm, hm, ld, td, rd, bd; +BOX *boxs, *boxm, *boxd, *boxempty; +BOXA *boxad; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (!boxam) { + L_WARNING("boxam not defined; returning copy", __func__); + return boxaCopy(boxas, L_COPY); + } + if (subflag != L_USE_MINSIZE && subflag != L_USE_MAXSIZE && + subflag != L_SUB_ON_LOC_DIFF && subflag != L_SUB_ON_SIZE_DIFF && + subflag != L_USE_CAPPED_MIN && subflag != L_USE_CAPPED_MAX) { + L_WARNING("invalid subflag; returning copy", __func__); + return boxaCopy(boxas, L_COPY); + } + n = boxaGetCount(boxas); + if (n != boxaGetCount(boxam)) { + L_WARNING("boxas and boxam sizes differ; returning copy", __func__); + return boxaCopy(boxas, L_COPY); + } + + boxad = boxaCreate(n); + boxempty = boxCreate(0, 0, 0, 0); /* placeholders */ + for (i = 0; i < n; i++) { + boxs = boxaGetValidBox(boxas, i, L_CLONE); + boxm = boxaGetValidBox(boxam, i, L_CLONE); + if (!boxs || !boxm) { + boxaAddBox(boxad, boxempty, L_COPY); + } else { + boxGetGeometry(boxs, &ls, &ts, &ws, &hs); + boxGetGeometry(boxm, &lm, &tm, &wm, &hm); + rs = ls + ws - 1; + bs = ts + hs - 1; + rm = lm + wm - 1; + bm = tm + hm - 1; + if (subflag == L_USE_MINSIZE) { + ld = L_MAX(ls, lm); + rd = L_MIN(rs, rm); + td = L_MAX(ts, tm); + bd = L_MIN(bs, bm); + } else if (subflag == L_USE_MAXSIZE) { + ld = L_MIN(ls, lm); + rd = L_MAX(rs, rm); + td = L_MIN(ts, tm); + bd = L_MAX(bs, bm); + } else if (subflag == L_SUB_ON_LOC_DIFF) { + ld = (L_ABS(lm - ls) <= maxdiff) ? ls : lm - extrapixels; + td = (L_ABS(tm - ts) <= maxdiff) ? ts : tm - extrapixels; + rd = (L_ABS(rm - rs) <= maxdiff) ? rs : rm + extrapixels; + bd = (L_ABS(bm - bs) <= maxdiff) ? bs : bm + extrapixels; + } else if (subflag == L_SUB_ON_SIZE_DIFF) { + ld = (L_ABS(wm - ws) <= maxdiff) ? ls : lm - extrapixels; + td = (L_ABS(hm - hs) <= maxdiff) ? ts : tm - extrapixels; + rd = (L_ABS(wm - ws) <= maxdiff) ? rs : rm + extrapixels; + bd = (L_ABS(hm - hs) <= maxdiff) ? bs : bm + extrapixels; + } else if (subflag == L_USE_CAPPED_MIN) { + ld = L_MAX(lm, L_MIN(ls, lm + maxdiff)); + td = L_MAX(tm, L_MIN(ts, tm + maxdiff)); + rd = L_MIN(rm, L_MAX(rs, rm - maxdiff)); + bd = L_MIN(bm, L_MAX(bs, bm - maxdiff)); + } else { /* subflag == L_USE_CAPPED_MAX */ + ld = L_MIN(lm, L_MAX(ls, lm - maxdiff)); + td = L_MIN(tm, L_MAX(ts, tm - maxdiff)); + rd = L_MAX(rm, L_MIN(rs, rm + maxdiff)); + bd = L_MAX(bm, L_MIN(bs, bm + maxdiff)); + } + boxd = boxCreate(ld, td, rd - ld + 1, bd - td + 1); + boxaAddBox(boxad, boxd, L_INSERT); + } + boxDestroy(&boxs); + boxDestroy(&boxm); + } + boxDestroy(&boxempty); + + return boxad; +} + + +/*! + * \brief boxaReconcilePairWidth() + * + * \param[in] boxas + * \param[in] delw threshold on adjacent width difference + * \param[in] op L_ADJUST_CHOOSE_MIN, L_ADJUST_CHOOSE_MAX + * \param[in] factor > 0.0, typically near 1.0 + * \param[in] na [optional] indicator array allowing change + * \return boxad adjusted, or a copy of boxas on error + * + * <pre> + * Notes: + * (1) This reconciles differences in the width of adjacent boxes, + * by moving one side of one of the boxes in each pair. + * If the widths in the pair differ by more than some + * threshold, move either the left side for even boxes or + * the right side for odd boxes, depending on if we're choosing + * the min or max. If choosing min, the width of the max is + * set to factor * (width of min). If choosing max, the width + * of the min is set to factor * (width of max). + * (2) If %na exists, it is an indicator array corresponding to the + * boxes in %boxas. If %na != NULL, only boxes with an + * indicator value of 1 are allowed to adjust; otherwise, + * all boxes can adjust. + * (3) Typical input might be the output of boxaSmoothSequenceMedian(), + * where even and odd boxa have been independently regulated. + * </pre> + */ +BOXA * +boxaReconcilePairWidth(BOXA *boxas, + l_int32 delw, + l_int32 op, + l_float32 factor, + NUMA *na) +{ +l_int32 i, ne, no, nmin, xe, we, xo, wo, inde, indo, x, w; +BOX *boxe, *boxo; +BOXA *boxae, *boxao, *boxad; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (factor <= 0.0) { + L_WARNING("invalid factor; setting to 1.0\n", __func__); + factor = 1.0; + } + + /* Taking the boxes in pairs, if the difference in width reaches + * the threshold %delw, adjust the left or right side of one + * of the pair. */ + boxaSplitEvenOdd(boxas, 0, &boxae, &boxao); + ne = boxaGetCount(boxae); + no = boxaGetCount(boxao); + nmin = L_MIN(ne, no); + for (i = 0; i < nmin; i++) { + /* Set indicator values */ + if (na) { + numaGetIValue(na, 2 * i, &inde); + numaGetIValue(na, 2 * i + 1, &indo); + } else { + inde = indo = 1; + } + if (inde == 0 && indo == 0) continue; + + boxe = boxaGetBox(boxae, i, L_CLONE); + boxo = boxaGetBox(boxao, i, L_CLONE); + boxGetGeometry(boxe, &xe, NULL, &we, NULL); + boxGetGeometry(boxo, &xo, NULL, &wo, NULL); + if (we == 0 || wo == 0) { /* if either is invalid; skip */ + boxDestroy(&boxe); + boxDestroy(&boxo); + continue; + } else if (L_ABS(we - wo) > delw) { + if (op == L_ADJUST_CHOOSE_MIN) { + if (we > wo && inde == 1) { + /* move left side of even to the right */ + w = factor * wo; + x = xe + (we - w); + boxSetGeometry(boxe, x, -1, w, -1); + } else if (we < wo && indo == 1) { + /* move right side of odd to the left */ + w = factor * we; + boxSetGeometry(boxo, -1, -1, w, -1); + } + } else { /* maximize width */ + if (we < wo && inde == 1) { + /* move left side of even to the left */ + w = factor * wo; + x = L_MAX(0, xe + (we - w)); + w = we + (xe - x); /* covers both cases for the max */ + boxSetGeometry(boxe, x, -1, w, -1); + } else if (we > wo && indo == 1) { + /* move right side of odd to the right */ + w = factor * we; + boxSetGeometry(boxo, -1, -1, w, -1); + } + } + } + boxDestroy(&boxe); + boxDestroy(&boxo); + } + + boxad = boxaMergeEvenOdd(boxae, boxao, 0); + boxaDestroy(&boxae); + boxaDestroy(&boxao); + return boxad; +} + + +/*! + * \brief boxaSizeConsistency() + * + * \param[in] boxas of size >= 10 + * \param[in] type L_CHECK_WIDTH, L_CHECK_HEIGHT + * \param[in] threshp threshold for pairwise fractional variation + * \param[in] threshm threshold for fractional variation from median + * \param[out] pfvarp [optional] average fractional pairwise variation + * \param[out] pfvarm [optional] average fractional median variation + * \param[out] psame decision for uniformity of page size (1, 0, -1) + * + * <pre> + * Notes: + * (1) This evaluates a boxa for particular types of dimensional + * variation. Select either width or height variation. Then + * it returns two numbers: one is based on pairwise (even/odd) + * variation; the other is based on the average variation + * from the boxa median. + * (2) For the pairwise variation, get the fraction of the absolute + * difference in dimension of each pair of boxes, and take + * the average value. The median variation is simply the + * the average of the fractional deviation from the median + * of all the boxes. + * (3) Use 0 for default values of %threshp and %threshm. They are + * threshp: 0.02 + * threshm: 0.015 + * (4) The intended application is that the boxes are a sequence of + * page regions in a book scan, and we calculate two numbers + * that can give an indication if the pages are approximately + * the same size. The pairwise variation should be small if + * the boxes are correctly calculated. If there are a + * significant number of random or systematic outliers, the + * pairwise variation will be large, and no decision will be made + * (i.e., return same == -1). Here are the possible outcomes: + * Pairwise Var Median Var Decision + * ------------ ---------- -------- + * small small same size (1) + * small large different size (0) + * large small/large unknown (-1) + * </pre> + */ +l_ok +boxaSizeConsistency(BOXA *boxas, + l_int32 type, + l_float32 threshp, + l_float32 threshm, + l_float32 *pfvarp, + l_float32 *pfvarm, + l_int32 *psame) +{ +l_int32 i, n, bw1, bh1, bw2, bh2, npairs; +l_float32 ave, fdiff, sumdiff, med, fvarp, fvarm; +NUMA *na1; + + if (pfvarp) *pfvarp = 0.0; + if (pfvarm) *pfvarm = 0.0; + if (!psame) + return ERROR_INT("&same not defined", __func__, 1); + *psame = -1; + if (!boxas) + return ERROR_INT("boxas not defined", __func__, 1); + if (boxaGetValidCount(boxas) < 6) + return ERROR_INT("need a least 6 valid boxes", __func__, 1); + if (type != L_CHECK_WIDTH && type != L_CHECK_HEIGHT) + return ERROR_INT("invalid type", __func__, 1); + if (threshp < 0.0 || threshp >= 0.5) + return ERROR_INT("invalid threshp", __func__, 1); + if (threshm < 0.0 || threshm >= 0.5) + return ERROR_INT("invalid threshm", __func__, 1); + if (threshp == 0.0) threshp = 0.02f; + if (threshm == 0.0) threshm = 0.015f; + + /* Evaluate pairwise variation */ + n = boxaGetCount(boxas); + na1 = numaCreate(0); + for (i = 0, npairs = 0, sumdiff = 0; i < n - 1; i += 2) { + boxaGetBoxGeometry(boxas, i, NULL, NULL, &bw1, &bh1); + boxaGetBoxGeometry(boxas, i + 1, NULL, NULL, &bw2, &bh2); + if (bw1 == 0 || bh1 == 0 || bw2 == 0 || bh2 == 0) + continue; + npairs++; + if (type == L_CHECK_WIDTH) { + ave = (bw1 + bw2) / 2.0; + fdiff = L_ABS(bw1 - bw2) / ave; + numaAddNumber(na1, bw1); + numaAddNumber(na1, bw2); + } else { /* type == L_CHECK_HEIGHT) */ + ave = (bh1 + bh2) / 2.0; + fdiff = L_ABS(bh1 - bh2) / ave; + numaAddNumber(na1, bh1); + numaAddNumber(na1, bh2); + } + sumdiff += fdiff; + } + fvarp = sumdiff / npairs; + if (pfvarp) *pfvarp = fvarp; + + /* Evaluate the average abs fractional deviation from the median */ + numaGetMedian(na1, &med); + if (med == 0.0) { + L_WARNING("median value is 0\n", __func__); + } else { + numaGetMeanDevFromMedian(na1, med, &fvarm); + fvarm /= med; + if (pfvarm) *pfvarm = fvarm; + } + numaDestroy(&na1); + + /* Make decision */ + if (fvarp < threshp && fvarm < threshm) + *psame = 1; + else if (fvarp < threshp && fvarm > threshm) + *psame = 0; + else + *psame = -1; /* unknown */ + return 0; +} + + +/*! + * \brief boxaReconcileAllByMedian() + * + * \param[in] boxas containing at least 6 valid boxes + * \param[in] select1 L_ADJUST_LEFT_AND_RIGHT or L_ADJUST_SKIP + * \param[in] select2 L_ADJUST_TOP_AND_BOT or L_ADJUST_SKIP + * \param[in] thresh threshold number of pixels to make adjustment + * \param[in] extra extra pixels to add beyond median value + * \param[in] pixadb use NULL to skip debug output + * \return boxad possibly adjusted from boxas; a copy of boxas on error + * + * <pre> + * Notes: + * (1) This uses boxaReconcileSidesByMedian() to reconcile + * the left-and-right and/or top-and-bottom sides of the + * even and odd boxes, separately. + * (2) See boxaReconcileSidesByMedian() for use of %thresh and %extra. + * (3) If all box sides are within %thresh of the median value, + * the returned box will be identical to %boxas. + * </pre> + */ +BOXA * +boxaReconcileAllByMedian(BOXA *boxas, + l_int32 select1, + l_int32 select2, + l_int32 thresh, + l_int32 extra, + PIXA *pixadb) + { +l_int32 ncols; +BOXA *boxa1e, *boxa1o, *boxa2e, *boxa2o, *boxa3e, *boxa3o, *boxad; +PIX *pix1; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (select1 != L_ADJUST_LEFT_AND_RIGHT && select1 != L_ADJUST_SKIP) { + L_WARNING("invalid select1; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (select2 != L_ADJUST_TOP_AND_BOT && select2 != L_ADJUST_SKIP) { + L_WARNING("invalid select2; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (thresh < 0) { + L_WARNING("thresh must be >= 0; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (boxaGetValidCount(boxas) < 3) { + L_WARNING("need at least 3 valid boxes; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + /* Adjust even and odd box sides separately */ + boxaSplitEvenOdd(boxas, 0, &boxa1e, &boxa1o); + ncols = 1; + if (select1 == L_ADJUST_LEFT_AND_RIGHT) { + ncols += 2; + boxa2e = boxaReconcileSidesByMedian(boxa1e, select1, thresh, + extra, pixadb); + } else { + boxa2e = boxaCopy(boxa1e, L_COPY); + } + if (select2 == L_ADJUST_TOP_AND_BOT) { + ncols += 2; + boxa3e = boxaReconcileSidesByMedian(boxa2e, select2, thresh, + extra, pixadb); + } else { + boxa3e = boxaCopy(boxa2e, L_COPY); + } + if (select1 == L_ADJUST_LEFT_AND_RIGHT) + boxa2o = boxaReconcileSidesByMedian(boxa1o, select1, thresh, + extra, pixadb); + else + boxa2o = boxaCopy(boxa1o, L_COPY); + if (select2 == L_ADJUST_TOP_AND_BOT) + boxa3o = boxaReconcileSidesByMedian(boxa2o, select2, thresh, + extra, pixadb); + else + boxa3o = boxaCopy(boxa2o, L_COPY); + boxad = boxaMergeEvenOdd(boxa3e, boxa3o, 0); + + /* This generates 2 sets of 3 or 5 plots in a row, depending + * on whether select1 and select2 are true (not skipping). + * The top row is for even boxes; the bottom row is for odd boxes. */ + if (pixadb) { + lept_mkdir("lept/boxa"); + pix1 = pixaDisplayTiledInColumns(pixadb, ncols, 1.0, 30, 2); + pixWrite("/tmp/lept/boxa/recon_sides.png", pix1, IFF_PNG); + pixDestroy(&pix1); + } + + boxaDestroy(&boxa1e); + boxaDestroy(&boxa1o); + boxaDestroy(&boxa2e); + boxaDestroy(&boxa2o); + boxaDestroy(&boxa3e); + boxaDestroy(&boxa3o); + return boxad; +} + + +/*! + * \brief boxaReconcileSidesByMedian() + * + * \param[in] boxas containing at least 3 valid boxes + * \param[in] select L_ADJUST_LEFT, L_ADJUST_RIGHT, etc. + * \param[in] thresh threshold number of pixels to make adjustment + * \param[in] extra extra pixels to add beyond median value + * \param[in] pixadb use NULL to skip debug output + * \return boxad possibly adjusted from boxas; a copy of boxas on error + * + * <pre> + * Notes: + * (1) This modifies individual box sides if their location differs + * significantly (>= %thresh) from the median value. + * (2) %select specifies which sides are to be checked. + * (3) %thresh specifies the tolerance for different side locations. + * Any box side that differs from the median by this much will + * be set to the median value, plus the %extra amount. + * (4) If %extra is positive, the box dimensions are expanded. + * For example, for the left side, a positive %extra results in + * moving the left side farther to the left (i.e., in a negative + * direction). + * (5) If all box sides are within %thresh - 1 of the median value, + * the returned box will be identical to %boxas. + * (6) N.B. If you expect that even and odd box sides should be + * significantly different, this function must be called separately + * on the even and odd boxes in %boxas. Note also that the + * higher level function boxaReconcileAllByMedian() handles the + * even and odd box sides separately. + * </pre> + */ +BOXA * +boxaReconcileSidesByMedian(BOXA *boxas, + l_int32 select, + l_int32 thresh, + l_int32 extra, + PIXA *pixadb) + { +char buf[128]; +l_int32 i, n, diff; +l_int32 left, right, top, bot, medleft, medright, medtop, medbot; +BOX *box; +BOXA *boxa1, *boxad; +PIX *pix; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (select != L_ADJUST_LEFT && select != L_ADJUST_RIGHT && + select != L_ADJUST_TOP && select != L_ADJUST_BOT && + select != L_ADJUST_LEFT_AND_RIGHT && select != L_ADJUST_TOP_AND_BOT) { + L_WARNING("invalid select; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (thresh < 0) { + L_WARNING("thresh must be >= 0; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (boxaGetValidCount(boxas) < 3) { + L_WARNING("need at least 3 valid boxes; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + if (select == L_ADJUST_LEFT_AND_RIGHT) { + boxa1 = boxaReconcileSidesByMedian(boxas, L_ADJUST_LEFT, thresh, extra, + pixadb); + boxad = boxaReconcileSidesByMedian(boxa1, L_ADJUST_RIGHT, thresh, extra, + pixadb); + boxaDestroy(&boxa1); + return boxad; + } + if (select == L_ADJUST_TOP_AND_BOT) { + boxa1 = boxaReconcileSidesByMedian(boxas, L_ADJUST_TOP, thresh, extra, + pixadb); + boxad = boxaReconcileSidesByMedian(boxa1, L_ADJUST_BOT, thresh, extra, + pixadb); + boxaDestroy(&boxa1); + return boxad; + } + + if (pixadb) { + l_int32 ndb = pixaGetCount(pixadb); + if (ndb == 0 || ndb == 5) { /* first of even and odd box sets */ + adjustSidePlotName(buf, sizeof(buf), "init", select); + boxaPlotSides(boxas, buf, NULL, NULL, NULL, NULL, &pix); + pixaAddPix(pixadb, pix, L_INSERT); + } + } + + n = boxaGetCount(boxas); + boxad = boxaCreate(n); + if (select == L_ADJUST_LEFT) { + boxaGetMedianVals(boxas, &medleft, NULL, NULL, NULL, NULL, NULL); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxGetSideLocations(box, &left, NULL, NULL, NULL); + diff = medleft - left; + if (L_ABS(diff) >= thresh) + boxAdjustSides(box, box, diff - extra, 0, 0, 0); + boxaAddBox(boxad, box, L_INSERT); + } + } else if (select == L_ADJUST_RIGHT) { + boxaGetMedianVals(boxas, NULL, NULL, &medright, NULL, NULL, NULL); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxGetSideLocations(box, NULL, &right, NULL, NULL); + diff = medright - right; + if (L_ABS(diff) >= thresh) + boxAdjustSides(box, box, 0, diff + extra, 0, 0); + boxaAddBox(boxad, box, L_INSERT); + } + } else if (select == L_ADJUST_TOP) { + boxaGetMedianVals(boxas, NULL, &medtop, NULL, NULL, NULL, NULL); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxGetSideLocations(box, NULL, NULL, &top, NULL); + diff = medtop - top; + if (L_ABS(diff) >= thresh) + boxAdjustSides(box, box, 0, 0, diff - extra, 0); + boxaAddBox(boxad, box, L_INSERT); + } + } else { /* select == L_ADJUST_BOT */ + boxaGetMedianVals(boxas, NULL, NULL, NULL, &medbot, NULL, NULL); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxGetSideLocations(box, NULL, NULL, NULL, &bot); + diff = medbot - bot; + if (L_ABS(diff) >= thresh) + boxAdjustSides(box, box, 0, 0, 0, diff + extra); + boxaAddBox(boxad, box, L_INSERT); + } + } + + if (pixadb) { + adjustSidePlotName(buf, sizeof(buf), "final", select); + boxaPlotSides(boxad, buf, NULL, NULL, NULL, NULL, &pix); + pixaAddPix(pixadb, pix, L_INSERT); + } + return boxad; +} + + +static void +adjustSidePlotName(char *buf, + size_t size, + const char *preface, + l_int32 select) +{ + stringCopy(buf, preface, size - 8); + if (select == L_ADJUST_LEFT) + stringCat(buf, size, "-left"); + else if (select == L_ADJUST_RIGHT) + stringCat(buf, size, "-right"); + else if (select == L_ADJUST_TOP) + stringCat(buf, size, "-top"); + else if (select == L_ADJUST_BOT) + stringCat(buf, size, "-bot"); +} + + +/*! + * \brief boxaReconcileSizeByMedian() + * + * \param[in] boxas containing at least 6 valid boxes + * \param[in] type L_CHECK_WIDTH, L_CHECK_HEIGHT, L_CHECK_BOTH + * \param[in] dfract threshold fraction of dimensional variation from + * median; in range (0 ... 1); typ. about 0.05. + * \param[in] sfract threshold fraction of side variation from median; + * in range (0 ... 1); typ. about 0.04. + * \param[in] factor expansion for fixed box beyond median width; + * should be near 1.0. + * \param[out] pnadelw [optional] diff from median width for boxes + * above threshold + * \param[out] pnadelh [optional] diff from median height for boxes + * above threshold + * \param[out] pratiowh [optional] ratio of median width/height of boxas + * \return boxad possibly adjusted from boxas; a copy of boxas on error + * + * <pre> + * Notes: + * (1) The basic idea is to identify significant differences in box + * dimension (either width or height) and modify the outlier boxes. + * (2) %type specifies if we are reconciling the width, height or both. + * (3) %dfract specifies the tolerance for different dimensions. Any + * box with a fractional difference from the median size that + * exceeds %dfract will be altered. + * (4) %sfract specifies the tolerance for different side locations. + * If a box has been marked by (3) for alteration, any side + * location that differs from the median side location by + * more than %sfract of the median dimension (medw or medh) + * will be moved. + * (5) Median width and height are found for all valid boxes (i.e., + * for all boxes with width and height > 0. + * Median side locations are found separately for even and odd boxes, + * using only boxes that are "inliers"; i.e., that have been + * found by (3) to be within tolerance for width or height. + * (6) If all box dimensions are within threshold of the median size, + * just return a copy. Otherwise, box sides of the outliers + * will be adjusted. + * (7) Using %sfract, sides that are sufficiently far from the median + * are first moved to the median value. Then they are moved + * together (in or out) so that the final box dimension + * is %factor times the median dimension. + * (8) The arrays that are the initial deviation from median size + * (width and height) are optionally returned. Also optionally + * returned is the median w/h asperity ratio of the input %boxas. + * </pre> + */ +BOXA * +boxaReconcileSizeByMedian(BOXA *boxas, + l_int32 type, + l_float32 dfract, + l_float32 sfract, + l_float32 factor, + NUMA **pnadelw, + NUMA **pnadelh, + l_float32 *pratiowh) +{ +l_int32 i, n, ne, no, outfound, isvalid, ind, del, maxdel; +l_int32 medw, medh, bw, bh, left, right, top, bot; +l_int32 medleft, medlefte, medlefto, medright, medrighte, medrighto; +l_int32 medtop, medtope, medtopo, medbot, medbote, medboto; +l_float32 brat; +BOX *box; +BOXA *boxa1, *boxae, *boxao, *boxad; +NUMA *naind, *nadelw, *nadelh; + + if (pnadelw) *pnadelw = NULL; + if (pnadelh) *pnadelh = NULL; + if (pratiowh) *pratiowh = 0.0; + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (type != L_CHECK_WIDTH && type != L_CHECK_HEIGHT && + type != L_CHECK_BOTH) { + L_WARNING("invalid type; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (dfract <= 0.0 || dfract >= 0.5) { + L_WARNING("invalid dimensional fract; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (sfract <= 0.0 || sfract >= 0.5) { + L_WARNING("invalid side fract; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + if (factor < 0.8 || factor > 1.25) + L_WARNING("factor %5.3f is typ. closer to 1.0\n", __func__, factor); + if (boxaGetValidCount(boxas) < 6) { + L_WARNING("need at least 6 valid boxes; returning copy\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + /* If reconciling both width and height, optionally return array of + * median deviations and even/odd ratio for width measurements */ + if (type == L_CHECK_BOTH) { + boxa1 = boxaReconcileSizeByMedian(boxas, L_CHECK_WIDTH, dfract, sfract, + factor, pnadelw, NULL, pratiowh); + boxad = boxaReconcileSizeByMedian(boxa1, L_CHECK_HEIGHT, dfract, sfract, + factor, NULL, pnadelh, NULL); + boxaDestroy(&boxa1); + return boxad; + } + + n = boxaGetCount(boxas); + naind = numaCreate(n); /* outlier indicator array */ + boxae = boxaCreate(0); /* even inliers */ + boxao = boxaCreate(0); /* odd inliers */ + outfound = FALSE; + if (type == L_CHECK_WIDTH) { + boxaMedianDimensions(boxas, &medw, &medh, NULL, NULL, NULL, NULL, + &nadelw, NULL); + if (pratiowh) { + *pratiowh = (l_float32)medw / (l_float32)medh; + L_INFO("median ratio w/h = %5.3f\n", __func__, *pratiowh); + } + if (pnadelw) + *pnadelw = nadelw; + else + numaDestroy(&nadelw); + + /* Check for outliers; assemble inliers */ + for (i = 0; i < n; i++) { + if ((box = boxaGetValidBox(boxas, i, L_COPY)) == NULL) { + numaAddNumber(naind, 0); + continue; + } + boxGetGeometry(box, NULL, NULL, &bw, NULL); + brat = (l_float32)bw / (l_float32)medw; + if (brat < 1.0 - dfract || brat > 1.0 + dfract) { + outfound = TRUE; + numaAddNumber(naind, 1); + boxDestroy(&box); + } else { /* add to inliers */ + numaAddNumber(naind, 0); + if (i % 2 == 0) + boxaAddBox(boxae, box, L_INSERT); + else + boxaAddBox(boxao, box, L_INSERT); + } + } + if (!outfound) { /* nothing to do */ + numaDestroy(&naind); + boxaDestroy(&boxae); + boxaDestroy(&boxao); + L_INFO("no width outlier boxes found\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + /* Get left/right parameters from inliers. Handle the case + * where there are no inliers for one of the sets. For example, + * when all the even boxes have a different dimension from + * the odd boxes, and the median arbitrarily gets assigned + * to the even boxes, there are no odd inliers; in that case, + * use the even inliers sides to decide whether to adjust + * the left or the right sides of individual outliers. */ + L_INFO("fixing width of outlier boxes\n", __func__); + medlefte = medrighte = medlefto = medrighto = 0; + if ((ne = boxaGetValidCount(boxae)) > 0) + boxaGetMedianVals(boxae, &medlefte, NULL, &medrighte, NULL, + NULL, NULL); + if ((no = boxaGetValidCount(boxao)) > 0) + boxaGetMedianVals(boxao, &medlefto, NULL, &medrighto, NULL, + NULL, NULL); + if (ne == 0) { /* use odd inliers values for both */ + medlefte = medlefto; + medrighte = medrighto; + } else if (no == 0) { /* use even inliers values for both */ + medlefto = medlefte; + medrighto = medrighte; + } + + /* Adjust the left and/or right sides of outliers. + * For each box that is a dimensional outlier, consider each side. + * Any side that differs fractionally from the median value + * by more than %sfract times the median width (medw) is set to + * the median value for that side. Then both sides are moved + * an equal distance in or out to make w = %factor * medw. */ + boxad = boxaCreate(n); + maxdel = (l_int32)(sfract * medw + 0.5); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxIsValid(box, &isvalid); + numaGetIValue(naind, i, &ind); + medleft = (i % 2 == 0) ? medlefte : medlefto; + medright = (i % 2 == 0) ? medrighte : medrighto; + if (ind == 1 && isvalid) { /* adjust sides */ + boxGetSideLocations(box, &left, &right, NULL, NULL); + if (L_ABS(left - medleft) > maxdel) left = medleft; + if (L_ABS(right - medright) > maxdel) right = medright; + del = (l_int32)(factor * medw - (right - left)) / 2; + boxSetSide(box, L_SET_LEFT, left - del, 0); + boxSetSide(box, L_SET_RIGHT, right + del, 0); + } + boxaAddBox(boxad, box, L_INSERT); + } + } else { /* L_CHECK_HEIGHT */ + boxaMedianDimensions(boxas, &medw, &medh, NULL, NULL, NULL, NULL, + NULL, &nadelh); + if (pratiowh) { + *pratiowh = (l_float32)medw / (l_float32)medh; + L_INFO("median ratio w/h = %5.3f\n", __func__, *pratiowh); + } + if (pnadelh) + *pnadelh = nadelh; + else + numaDestroy(&nadelh); + + /* Check for outliers; assemble inliers */ + for (i = 0; i < n; i++) { + if ((box = boxaGetValidBox(boxas, i, L_COPY)) == NULL) { + numaAddNumber(naind, 0); + continue; + } + boxGetGeometry(box, NULL, NULL, NULL, &bh); + brat = (l_float32)bh / (l_float32)medh; + if (brat < 1.0 - dfract || brat > 1.0 + dfract) { + outfound = TRUE; + numaAddNumber(naind, 1); + boxDestroy(&box); + } else { /* add to inliers */ + numaAddNumber(naind, 0); + if (i % 2 == 0) + boxaAddBox(boxae, box, L_INSERT); + else + boxaAddBox(boxao, box, L_INSERT); + } + } + if (!outfound) { /* nothing to do */ + numaDestroy(&naind); + boxaDestroy(&boxae); + boxaDestroy(&boxao); + L_INFO("no height outlier boxes found\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + /* Get top/bot parameters from inliers. Handle the case + * where there are no inliers for one of the sets. For example, + * when all the even boxes have a different dimension from + * the odd boxes, and the median arbitrarily gets assigned + * to the even boxes, there are no odd inliers; in that case, + * use the even inlier sides to decide whether to adjust + * the top or the bottom sides of individual outliers. */ + L_INFO("fixing height of outlier boxes\n", __func__); + medlefte = medtope = medbote = medtopo = medboto = 0; + if ((ne = boxaGetValidCount(boxae)) > 0) + boxaGetMedianVals(boxae, NULL, &medtope, NULL, &medbote, + NULL, NULL); + if ((no = boxaGetValidCount(boxao)) > 0) + boxaGetMedianVals(boxao, NULL, &medtopo, NULL, &medboto, + NULL, NULL); + if (ne == 0) { /* use odd inliers values for both */ + medtope = medtopo; + medbote = medboto; + } else if (no == 0) { /* use even inliers values for both */ + medtopo = medtope; + medboto = medbote; + } + + /* Adjust the top and/or bottom sides of outliers. + * For each box that is a dimensional outlier, consider each side. + * Any side that differs fractionally from the median value + * by more than %sfract times the median height (medh) is + * set to the median value for that that side. Then both + * sides are moved an equal distance in or out to make + * h = %factor * medh). */ + boxad = boxaCreate(n); + maxdel = (l_int32)(sfract * medh + 0.5); + for (i = 0; i < n; i++) { + box = boxaGetBox(boxas, i, L_COPY); + boxIsValid(box, &isvalid); + numaGetIValue(naind, i, &ind); + medtop = (i % 2 == 0) ? medtope : medtopo; + medbot = (i % 2 == 0) ? medbote : medboto; + if (ind == 1 && isvalid) { /* adjust sides */ + boxGetSideLocations(box, NULL, NULL, &top, &bot); + if (L_ABS(top - medtop) > maxdel) top = medtop; + if (L_ABS(bot - medbot) > maxdel) bot = medbot; + del = (l_int32)(factor * medh - (bot - top)) / 2; /* typ > 0 */ + boxSetSide(box, L_SET_TOP, L_MAX(0, top - del), 0); + boxSetSide(box, L_SET_BOT, bot + del, 0); + } + boxaAddBox(boxad, box, L_INSERT); + } + } + numaDestroy(&naind); + boxaDestroy(&boxae); + boxaDestroy(&boxao); + return boxad; +} + + +/*! + * \brief boxaPlotSides() + * + * \param[in] boxa source boxa + * \param[in] plotname [optional], can be NULL + * \param[out] pnal [optional] na of left sides + * \param[out] pnat [optional] na of top sides + * \param[out] pnar [optional] na of right sides + * \param[out] pnab [optional] na of bottom sides + * \param[out] ppixd pix of the output plot + * \return 0 if OK, 1 on error + * + * <pre> + * Notes: + * (1) This debugging function shows the progression of the four + * sides in the boxa. There must be at least 2 boxes. + * (2) If there are invalid boxes (e.g., if only even or odd + * indices have valid boxes), this will fill them with the + * nearest valid box before plotting. + * (3) The plotfiles are put in /tmp/lept/plots/, and are named + * either with %plotname or, if NULL, a default name. If + * %plotname is used, make sure it has no whitespace characters. + * </pre> + */ +l_ok +boxaPlotSides(BOXA *boxa, + const char *plotname, + NUMA **pnal, + NUMA **pnat, + NUMA **pnar, + NUMA **pnab, + PIX **ppixd) +{ +char buf[128], titlebuf[128]; +char *dataname; +static l_int32 plotid = 0; +l_int32 n, i, w, h, left, top, right, bot; +l_int32 debugprint = FALSE; /* change to TRUE to spam stderr */ +l_float32 med, dev; +BOXA *boxat; +GPLOT *gplot; +NUMA *nal, *nat, *nar, *nab; + + if (pnal) *pnal = NULL; + if (pnat) *pnat = NULL; + if (pnar) *pnar = NULL; + if (pnab) *pnab = NULL; + if (ppixd) *ppixd = NULL; + if (!boxa) + return ERROR_INT("boxa not defined", __func__, 1); + if ((n = boxaGetCount(boxa)) < 2) + return ERROR_INT("less than 2 boxes", __func__, 1); + if (!ppixd) + return ERROR_INT("&pixd not defined", __func__, 1); + + boxat = boxaFillSequence(boxa, L_USE_ALL_BOXES, 0); + + /* Build the numas for each side */ + nal = numaCreate(n); + nat = numaCreate(n); + nar = numaCreate(n); + nab = numaCreate(n); + + for (i = 0; i < n; i++) { + boxaGetBoxGeometry(boxat, i, &left, &top, &w, &h); + right = left + w - 1; + bot = top + h - 1; + numaAddNumber(nal, left); + numaAddNumber(nat, top); + numaAddNumber(nar, right); + numaAddNumber(nab, bot); + } + boxaDestroy(&boxat); + + lept_mkdir("lept/plots"); + if (plotname) { + snprintf(buf, sizeof(buf), "/tmp/lept/plots/sides.%s", plotname); + snprintf(titlebuf, sizeof(titlebuf), "%s: Box sides vs. box index", + plotname); + } else { + snprintf(buf, sizeof(buf), "/tmp/lept/plots/sides.%d", plotid++); + snprintf(titlebuf, sizeof(titlebuf), "Box sides vs. box index"); + } + gplot = gplotCreate(buf, GPLOT_PNG, titlebuf, + "box index", "side location"); + gplotAddPlot(gplot, NULL, nal, GPLOT_LINES, "left side"); + gplotAddPlot(gplot, NULL, nat, GPLOT_LINES, "top side"); + gplotAddPlot(gplot, NULL, nar, GPLOT_LINES, "right side"); + gplotAddPlot(gplot, NULL, nab, GPLOT_LINES, "bottom side"); + *ppixd = gplotMakeOutputPix(gplot); + gplotDestroy(&gplot); + + if (debugprint) { + dataname = (plotname) ? stringNew(plotname) : stringNew("no_name"); + numaGetMedian(nal, &med); + numaGetMeanDevFromMedian(nal, med, &dev); + lept_stderr("%s left: med = %7.3f, meandev = %7.3f\n", + dataname, med, dev); + numaGetMedian(nat, &med); + numaGetMeanDevFromMedian(nat, med, &dev); + lept_stderr("%s top: med = %7.3f, meandev = %7.3f\n", + dataname, med, dev); + numaGetMedian(nar, &med); + numaGetMeanDevFromMedian(nar, med, &dev); + lept_stderr("%s right: med = %7.3f, meandev = %7.3f\n", + dataname, med, dev); + numaGetMedian(nab, &med); + numaGetMeanDevFromMedian(nab, med, &dev); + lept_stderr("%s bot: med = %7.3f, meandev = %7.3f\n", + dataname, med, dev); + LEPT_FREE(dataname); + } + + if (pnal) + *pnal = nal; + else + numaDestroy(&nal); + if (pnat) + *pnat = nat; + else + numaDestroy(&nat); + if (pnar) + *pnar = nar; + else + numaDestroy(&nar); + if (pnab) + *pnab = nab; + else + numaDestroy(&nab); + return 0; +} + + +/*! + * \brief boxaPlotSizes() + * + * \param[in] boxa source boxa + * \param[in] plotname [optional], can be NULL + * \param[out] pnaw [optional] na of widths + * \param[out] pnah [optional] na of heights + * \param[out] ppixd pix of the output plot + * \return 0 if OK, 1 on error + * + * <pre> + * Notes: + * (1) This debugging function shows the progression of box width + * and height in the boxa. There must be at least 2 boxes. + * (2) If there are invalid boxes (e.g., if only even or odd + * indices have valid boxes), this will fill them with the + * nearest valid box before plotting. + * (3) The plotfiles are put in /tmp/lept/plots/, and are named + * either with %plotname or, if NULL, a default name. If + * %plotname is used, make sure it has no whitespace characters. + * </pre> + */ +l_ok +boxaPlotSizes(BOXA *boxa, + const char *plotname, + NUMA **pnaw, + NUMA **pnah, + PIX **ppixd) +{ +char buf[128], titlebuf[128]; +static l_int32 plotid = 0; +l_int32 n, i, w, h; +BOXA *boxat; +GPLOT *gplot; +NUMA *naw, *nah; + + if (pnaw) *pnaw = NULL; + if (pnah) *pnah = NULL; + if (ppixd) *ppixd = NULL; + if (!boxa) + return ERROR_INT("boxa not defined", __func__, 1); + if ((n = boxaGetCount(boxa)) < 2) + return ERROR_INT("less than 2 boxes", __func__, 1); + if (!ppixd) + return ERROR_INT("&pixd not defined", __func__, 1); + + boxat = boxaFillSequence(boxa, L_USE_ALL_BOXES, 0); + + /* Build the numas for the width and height */ + naw = numaCreate(n); + nah = numaCreate(n); + for (i = 0; i < n; i++) { + boxaGetBoxGeometry(boxat, i, NULL, NULL, &w, &h); + numaAddNumber(naw, w); + numaAddNumber(nah, h); + } + boxaDestroy(&boxat); + + lept_mkdir("lept/plots"); + if (plotname) { + snprintf(buf, sizeof(buf), "/tmp/lept/plots/size.%s", plotname); + snprintf(titlebuf, sizeof(titlebuf), "%s: Box size vs. box index", + plotname); + } else { + snprintf(buf, sizeof(buf), "/tmp/lept/plots/size.%d", plotid++); + snprintf(titlebuf, sizeof(titlebuf), "Box size vs. box index"); + } + gplot = gplotCreate(buf, GPLOT_PNG, titlebuf, + "box index", "box dimension"); + gplotAddPlot(gplot, NULL, naw, GPLOT_LINES, "width"); + gplotAddPlot(gplot, NULL, nah, GPLOT_LINES, "height"); + *ppixd = gplotMakeOutputPix(gplot); + gplotDestroy(&gplot); + + if (pnaw) + *pnaw = naw; + else + numaDestroy(&naw); + if (pnah) + *pnah = nah; + else + numaDestroy(&nah); + return 0; +} + + +/*! + * \brief boxaFillSequence() + * + * \param[in] boxas with at least 3 boxes + * \param[in] useflag L_USE_ALL_BOXES, L_USE_SAME_PARITY_BOXES + * \param[in] debug 1 for debug output + * \return boxad filled boxa, or NULL on error + * + * <pre> + * Notes: + * (1) This simple function replaces invalid boxes with a copy of + * the nearest valid box, selected from either the entire + * sequence (L_USE_ALL_BOXES) or from the boxes with the + * same parity (L_USE_SAME_PARITY_BOXES). It returns a new boxa. + * (2) This is useful if you expect boxes in the sequence to + * vary slowly with index. + * </pre> + */ +BOXA * +boxaFillSequence(BOXA *boxas, + l_int32 useflag, + l_int32 debug) +{ +l_int32 n, nv; +BOXA *boxae, *boxao, *boxad; + + if (!boxas) + return (BOXA *)ERROR_PTR("boxas not defined", __func__, NULL); + if (useflag != L_USE_ALL_BOXES && useflag != L_USE_SAME_PARITY_BOXES) + return (BOXA *)ERROR_PTR("invalid useflag", __func__, NULL); + + n = boxaGetCount(boxas); + nv = boxaGetValidCount(boxas); + if (n == nv) + return boxaCopy(boxas, L_COPY); /* all valid */ + if (debug) + L_INFO("%d valid boxes, %d invalid boxes\n", __func__, nv, n - nv); + if (useflag == L_USE_SAME_PARITY_BOXES && n < 3) { + L_WARNING("n < 3; some invalid\n", __func__); + return boxaCopy(boxas, L_COPY); + } + + if (useflag == L_USE_ALL_BOXES) { + boxad = boxaCopy(boxas, L_COPY); + boxaFillAll(boxad); + } else { + boxaSplitEvenOdd(boxas, 0, &boxae, &boxao); + boxaFillAll(boxae); + boxaFillAll(boxao); + boxad = boxaMergeEvenOdd(boxae, boxao, 0); + boxaDestroy(&boxae); + boxaDestroy(&boxao); + } + + nv = boxaGetValidCount(boxad); + if (n != nv) + L_WARNING("there are still %d invalid boxes\n", __func__, n - nv); + + return boxad; +} + + +/*! + * \brief boxaFillAll() + * + * \param[in] boxa + * \return 0 if OK, 1 on error + * + * <pre> + * Notes: + * (1) This static function replaces every invalid box with the + * nearest valid box. If there are no valid boxes, it + * issues a warning. + * </pre> + */ +static l_int32 +boxaFillAll(BOXA *boxa) +{ +l_int32 n, nv, i, j, spandown, spanup; +l_int32 *indic; +BOX *box, *boxt; + + if (!boxa) + return ERROR_INT("boxa not defined", __func__, 1); + n = boxaGetCount(boxa); + nv = boxaGetValidCount(boxa); + if (n == nv) return 0; + if (nv == 0) { + L_WARNING("no valid boxes out of %d boxes\n", __func__, n); + return 0; + } + + /* Make indicator array for valid boxes */ + if ((indic = (l_int32 *)LEPT_CALLOC(n, sizeof(l_int32))) == NULL) + return ERROR_INT("indic not made", __func__, 1); + for (i = 0; i < n; i++) { + box = boxaGetValidBox(boxa, i, L_CLONE); + if (box) + indic[i] = 1; + boxDestroy(&box); + } + + /* Replace invalid boxes with the nearest valid one */ + for (i = 0; i < n; i++) { + box = boxaGetValidBox(boxa, i, L_CLONE); + if (!box) { + spandown = spanup = 10000000; + for (j = i - 1; j >= 0; j--) { + if (indic[j] == 1) { + spandown = i - j; + break; + } + } + for (j = i + 1; j < n; j++) { + if (indic[j] == 1) { + spanup = j - i; + break; + } + } + if (spandown < spanup) + boxt = boxaGetBox(boxa, i - spandown, L_COPY); + else + boxt = boxaGetBox(boxa, i + spanup, L_COPY); + boxaReplaceBox(boxa, i, boxt); + } + boxDestroy(&box); + } + + LEPT_FREE(indic); + return 0; +} + + +/*! + * \brief boxaSizeVariation() + * + * \param[in] boxa at least 4 boxes + * \param[in] type L_SELECT_WIDTH, L_SELECT_HEIGHT + * \param[out] pdel_evenodd [optional] average absolute value of + * (even - odd) size pairs + * \param[out] prms_even [optional] rms deviation of even boxes + * \param[out] prms_odd [optional] rms deviation of odd boxes + * \param[out] prms_all [optional] rms deviation of all boxes + * \return 0 if OK, 1 on error + * + * <pre> + * Notes: + * (1) This gives several measures of the smoothness of either the + * width or height of a sequence of boxes. + * See boxaMedianDimensions() for some other measures. + * (2) Statistics can be found separately for even and odd boxes. + * Additionally, the average pair-wise difference between + * adjacent even and odd boxes can be returned. + * (3) The use case is bounding boxes for scanned page images, + * where ideally the sizes should have little variance. + * </pre> + */ +l_ok +boxaSizeVariation(BOXA *boxa, + l_int32 type, + l_float32 *pdel_evenodd, + l_float32 *prms_even, + l_float32 *prms_odd, + l_float32 *prms_all) +{ +l_int32 n, ne, no, nmin, vale, valo, i; +l_float32 sum; +BOXA *boxae, *boxao; +NUMA *nae, *nao, *na_all; + + if (pdel_evenodd) *pdel_evenodd = 0.0; + if (prms_even) *prms_even = 0.0; + if (prms_odd) *prms_odd = 0.0; + if (prms_all) *prms_all = 0.0; + if (!boxa) + return ERROR_INT("boxa not defined", __func__, 1); + if (type != L_SELECT_WIDTH && type != L_SELECT_HEIGHT) + return ERROR_INT("invalid type", __func__, 1); + if (!pdel_evenodd && !prms_even && !prms_odd && !prms_all) + return ERROR_INT("nothing to do", __func__, 1); + n = boxaGetCount(boxa); + if (n < 4) + return ERROR_INT("too few boxes", __func__, 1); + + boxaSplitEvenOdd(boxa, 0, &boxae, &boxao); + ne = boxaGetCount(boxae); + no = boxaGetCount(boxao); + nmin = L_MIN(ne, no); + if (nmin == 0) { + boxaDestroy(&boxae); + boxaDestroy(&boxao); + return ERROR_INT("either no even or no odd boxes", __func__, 1); + } + + if (type == L_SELECT_WIDTH) { + boxaGetSizes(boxae, &nae, NULL); + boxaGetSizes(boxao, &nao, NULL); + boxaGetSizes(boxa, &na_all, NULL); + } else { /* L_SELECT_HEIGHT) */ + boxaGetSizes(boxae, NULL, &nae); + boxaGetSizes(boxao, NULL, &nao); + boxaGetSizes(boxa, NULL, &na_all); + } + + if (pdel_evenodd) { + sum = 0.0; + for (i = 0; i < nmin; i++) { + numaGetIValue(nae, i, &vale); + numaGetIValue(nao, i, &valo); + sum += L_ABS(vale - valo); + } + *pdel_evenodd = sum / nmin; + } + if (prms_even) + numaSimpleStats(nae, 0, -1, NULL, NULL, prms_even); + if (prms_odd) + numaSimpleStats(nao, 0, -1, NULL, NULL, prms_odd); + if (prms_all) + numaSimpleStats(na_all, 0, -1, NULL, NULL, prms_all); + + boxaDestroy(&boxae); + boxaDestroy(&boxao); + numaDestroy(&nae); + numaDestroy(&nao); + numaDestroy(&na_all); + return 0; +} + + +/*! + * \brief boxaMedianDimensions() + * + * \param[in] boxas containing at least 3 valid boxes in even and odd + * \param[out] pmedw [optional] median width of all boxes + * \param[out] pmedh [optional] median height of all boxes + * \param[out] pmedwe [optional] median width of even boxes + * \param[out] pmedwo [optional] median width of odd boxes + * \param[out] pmedhe [optional] median height of even boxes + * \param[out] pmedho [optional] median height of odd boxes + * \param[out] pnadelw [optional] width diff of each box from median + * \param[out] pnadelh [optional] height diff of each box from median + * \return 0 if OK, 1 on error + * + * <pre> + * Notes: + * (1) This provides information that (1) allows identification of + * boxes that have unusual (outlier) width or height, and (2) can + * be used to regularize the sizes of the outlier boxes, assuming + * that the boxes satisfy a fairly regular sequence and should + * mostly have the same width and height. + * (2) This finds the median width and height, as well as separate + * median widths and heights of even and odd boxes. It also + * generates arrays that give the difference in width and height + * of each box from the median, which can be used to correct + * individual boxes. + * (3) All return values are optional. + * </pre> + */ +l_ok +boxaMedianDimensions(BOXA *boxas, + l_int32 *pmedw, + l_int32 *pmedh, + l_int32 *pmedwe, + l_int32 *pmedwo, + l_int32 *pmedhe, + l_int32 *pmedho, + NUMA **pnadelw, + NUMA **pnadelh) +{ +l_int32 i, n, bw, bh, medw, medh, medwe, medwo, medhe, medho; +BOXA *boxae, *boxao; +NUMA *nadelw, *nadelh; + + if (pmedw) *pmedw = 0; + if (pmedh) *pmedh = 0; + if (pmedwe) *pmedwe= 0; + if (pmedwo) *pmedwo= 0; + if (pmedhe) *pmedhe= 0; + if (pmedho) *pmedho= 0; + if (pnadelw) *pnadelw = NULL; + if (pnadelh) *pnadelh = NULL; + if (!boxas) + return ERROR_INT("boxas not defined", __func__, 1); + if (boxaGetValidCount(boxas) < 6) + return ERROR_INT("need at least 6 valid boxes", __func__, 1); + + /* Require at least 3 valid boxes of both types */ + boxaSplitEvenOdd(boxas, 0, &boxae, &boxao); + if (boxaGetValidCount(boxae) < 3 || boxaGetValidCount(boxao) < 3) { + boxaDestroy(&boxae); + boxaDestroy(&boxao); + return ERROR_INT("don't have 3+ valid boxes of each type", __func__, 1); + } + + /* Get the relevant median widths and heights */ + boxaGetMedianVals(boxas, NULL, NULL, NULL, NULL, &medw, &medh); + boxaGetMedianVals(boxae, NULL, NULL, NULL, NULL, &medwe, &medhe); + boxaGetMedianVals(boxao, NULL, NULL, NULL, NULL, &medwo, &medho); + if (pmedw) *pmedw = medw; + if (pmedh) *pmedh = medh; + if (pmedwe) *pmedwe = medwe; + if (pmedwo) *pmedwo = medwo; + if (pmedhe) *pmedhe = medhe; + if (pmedho) *pmedho = medho; + + /* Find the variation from median dimension for each box */ + n = boxaGetCount(boxas); + nadelw = numaCreate(n); + nadelh = numaCreate(n); + for (i = 0; i < n; i++) { + boxaGetBoxGeometry(boxas, i, NULL, NULL, &bw, &bh); + if (bw == 0 || bh == 0) { /* invalid box */ + numaAddNumber(nadelw, 0); + numaAddNumber(nadelh, 0); + } else { + numaAddNumber(nadelw, bw - medw); + numaAddNumber(nadelh, bh - medh); + } + } + if (pnadelw) + *pnadelw = nadelw; + else + numaDestroy(&nadelw); + if (pnadelh) + *pnadelh = nadelh; + else + numaDestroy(&nadelh); + + boxaDestroy(&boxae); + boxaDestroy(&boxao); + return 0; +} +
