/*====================================================================* - 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 dewarp2.c *
 *
 *    Build the page disparity model
 *
 *      Build basic page disparity model
 *          l_int32            dewarpBuildPageModel()
 *          l_int32            dewarpFindVertDisparity()
 *          l_int32            dewarpFindHorizDisparity()
 *          PTAA              *dewarpGetTextlineCenters()
 *          static PTA        *dewarpGetMeanVerticals()
 *          PTAA              *dewarpRemoveShortLines()
 *          static l_int32     dewarpGetLineEndPoints()
 *          static l_int32     dewarpFilterLineEndPoints()
 *          static PTA        *dewarpRemoveBadEndPoints()
 *          static l_int32     dewarpIsLineCoverageValid()
 *          static l_int32     dewarpLinearLSF()
 *          static l_int32     dewarpQuadraticLSF()
 *
 *      Build disparity model for slope near binding
 *          l_int32            dewarpFindHorizSlopeDisparity()
 *
 *      Build the line disparity model
 *          l_int32            dewarpBuildLineModel()
 *
 *      Query model status
 *          l_int32            dewarpaModelStatus()
 *
 *      Rendering helpers
 *          static l_int32     pixRenderMidYs()
 *          static l_int32     pixRenderHorizEndPoints
 * 
*/ #ifdef HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ #include #include "allheaders.h" static PTA *dewarpGetMeanVerticals(PIX *pixs, l_int32 x, l_int32 y); static l_int32 dewarpGetLineEndPoints(l_int32 h, PTAA *ptaa, PTA **pptal, PTA **pptar); static l_int32 dewarpFilterLineEndPoints(L_DEWARP *dew, PTA *ptal1, PTA *ptar1, PTA **pptal2, PTA **pptar2); static PTA *dewarpRemoveBadEndPoints(l_int32 w, PTA *ptas); static l_int32 dewarpIsLineCoverageValid(PTAA *ptaa2, l_int32 h, l_int32 *pntop, l_int32 *pnbot, l_int32 *pytop, l_int32 *pybot); static l_int32 dewarpLinearLSF(PTA *ptad, l_float32 *pa, l_float32 *pb, l_float32 *pmederr); static l_int32 dewarpQuadraticLSF(PTA *ptad, l_float32 *pa, l_float32 *pb, l_float32 *pc, l_float32 *pmederr); static l_int32 pixRenderMidYs(PIX *pixs, NUMA *namidys, l_int32 linew); static l_int32 pixRenderHorizEndPoints(PIX *pixs, PTA *ptal, PTA *ptar, l_uint32 color); #ifndef NO_CONSOLE_IO #define DEBUG_TEXTLINE_CENTERS 0 /* set this to 1 for debugging */ #define DEBUG_SHORT_LINES 0 /* ditto */ #else #define DEBUG_TEXTLINE_CENTERS 0 /* always must be 0 */ #define DEBUG_SHORT_LINES 0 /* ditto */ #endif /* !NO_CONSOLE_IO */ /* Special parameter values for reducing horizontal disparity */ static const l_float32 MinRatioLinesToHeight = 0.45f; static const l_int32 MinLinesForHoriz1 = 10; /* initially */ static const l_int32 MinLinesForHoriz2 = 3; /* after, in each half */ static const l_float32 AllowedWidthFract = 0.05f; /* no bigger */ /*----------------------------------------------------------------------* * Build basic page disparity model * *----------------------------------------------------------------------*/ /*! * \brief dewarpBuildPageModel() * * \param[in] dew * \param[in] debugfile use NULL to skip writing this * \return 0 if OK, 1 if unable to build the model or on error * *
 * Notes:
 *      (1) This is the basic function that builds the horizontal and
 *          vertical disparity arrays, which allow determination of the
 *          src pixel in the input image corresponding to each
 *          dest pixel in the dewarped image.
 *      (2) Sets vsuccess = 1 if the vertical disparity array builds.
 *          Always attempts to build the horizontal disparity array,
 *          even if it will not be requested (useboth == 0).
 *          Sets hsuccess = 1 if horizontal disparity builds.
 *      (3) The method is as follows:
 *          (a) Estimate the points along the centers of all the
 *              long textlines.  If there are too few lines, no
 *              disparity models are built.
 *          (b) From the vertical deviation of the lines, estimate
 *              the vertical disparity.
 *          (c) From the ends of the lines, estimate the horizontal
 *              disparity, assuming that the text is made of lines
 *              that are close to left and right justified.
 *          (d) One can also compute an additional contribution to the
 *              horizontal disparity, inferred from slopes of the top
 *              and bottom lines.  We do not do this.
 *      (4) In more detail for the vertical disparity:
 *          (a) Fit a LS quadratic to center locations along each line.
 *              This smooths the curves.
 *          (b) Sample each curve at a regular interval, find the y-value
 *              of the mid-point on each curve, and subtract the sampled
 *              curve value from this value.  This is the vertical
 *              disparity at sampled points along each curve.
 *          (c) Fit a LS quadratic to each set of vertically aligned
 *              disparity samples.  This smooths the disparity values
 *              in the vertical direction.  Then resample at the same
 *              regular interval.  We now have a regular grid of smoothed
 *              vertical disparity valuels.
 *      (5) Once the sampled vertical disparity array is found, it can be
 *          interpolated to get a full resolution vertical disparity map.
 *          This can be applied directly to the src image pixels
 *          to dewarp the image in the vertical direction, making
 *          all textlines horizontal.  Likewise, the horizontal
 *          disparity array is used to left- and right-align the
 *          longest textlines.
 * 
*/ l_ok dewarpBuildPageModel(L_DEWARP *dew, const char *debugfile) { l_int32 linecount, ntop, nbot, ytop, ybot, ret; PIX *pixs, *pix1, *pix2, *pix3; PTA *pta; PTAA *ptaa1, *ptaa2; PROCNAME("dewarpBuildPageModel"); if (!dew) return ERROR_INT("dew not defined", procName, 1); dew->debug = (debugfile) ? 1 : 0; dew->vsuccess = dew->hsuccess = 0; pixs = dew->pixs; if (debugfile) { lept_rmdir("lept/dewmod"); /* erase previous images */ lept_mkdir("lept/dewmod"); pixDisplayWithTitle(pixs, 0, 0, "pixs", 1); pixWriteDebug("/tmp/lept/dewmod/0010.png", pixs, IFF_PNG); } /* Make initial estimate of centers of textlines */ ptaa1 = dewarpGetTextlineCenters(pixs, debugfile || DEBUG_TEXTLINE_CENTERS); if (!ptaa1) { L_WARNING("textline centers not found; model not built\n", procName); return 1; } if (debugfile) { pix1 = pixConvertTo32(pixs); pta = generatePtaFilledCircle(1); pix2 = pixGenerateFromPta(pta, 5, 5); pix3 = pixDisplayPtaaPattern(NULL, pix1, ptaa1, pix2, 2, 2); pixWriteDebug("/tmp/lept/dewmod/0020.png", pix3, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); ptaDestroy(&pta); } /* Remove all lines that are not at least 0.8 times the length * of the longest line. */ ptaa2 = dewarpRemoveShortLines(pixs, ptaa1, 0.8f, debugfile || DEBUG_SHORT_LINES); if (debugfile) { pix1 = pixConvertTo32(pixs); pta = generatePtaFilledCircle(1); pix2 = pixGenerateFromPta(pta, 5, 5); pix3 = pixDisplayPtaaPattern(NULL, pix1, ptaa2, pix2, 2, 2); pixWriteDebug("/tmp/lept/dewmod/0030.png", pix3, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); ptaDestroy(&pta); } ptaaDestroy(&ptaa1); /* Verify that there are sufficient "long" lines */ linecount = ptaaGetCount(ptaa2); if (linecount < dew->minlines) { ptaaDestroy(&ptaa2); L_WARNING("linecount %d < min req'd number of lines (%d) for model\n", procName, linecount, dew->minlines); return 1; } /* Verify that the lines have a reasonable coverage of the * vertical extent of the page. */ if (dewarpIsLineCoverageValid(ptaa2, pixGetHeight(pixs), &ntop, &nbot, &ytop, &ybot) == FALSE) { ptaaDestroy(&ptaa2); L_WARNING("invalid line coverage: ntop = %d, nbot = %d;" " spanning [%d ... %d] in height %d\n", procName, ntop, nbot, ytop, ybot, pixGetHeight(pixs)); return 1; } /* Get the sampled vertical disparity from the textline centers. * The disparity array will push pixels vertically so that each * textline is flat and centered at the y-position of the mid-point. */ if (dewarpFindVertDisparity(dew, ptaa2, 0) != 0) { L_WARNING("vertical disparity not built\n", procName); ptaaDestroy(&ptaa2); return 1; } /* Get the sampled horizontal disparity from the left and right * edges of the text. The disparity array will expand the image * linearly outward to align the text edges vertically. * Do this even if useboth == 0; we still calculate it even * if we don't plan to use it. */ if ((ret = dewarpFindHorizDisparity(dew, ptaa2)) == 0) L_INFO("hsuccess = 1\n", procName); /* Debug output */ if (debugfile) { dewarpPopulateFullRes(dew, NULL, 0, 0); pix1 = fpixRenderContours(dew->fullvdispar, 3.0, 0.15f); pixWriteDebug("/tmp/lept/dewmod/0060.png", pix1, IFF_PNG); pixDisplay(pix1, 1000, 0); pixDestroy(&pix1); if (ret == 0) { pix1 = fpixRenderContours(dew->fullhdispar, 3.0, 0.15f); pixWriteDebug("/tmp/lept/dewmod/0070.png", pix1, IFF_PNG); pixDisplay(pix1, 1000, 0); pixDestroy(&pix1); } convertFilesToPdf("/tmp/lept/dewmod", NULL, 135, 1.0, 0, 0, "Dewarp Build Model", debugfile); lept_stderr("pdf file: %s\n", debugfile); } ptaaDestroy(&ptaa2); return 0; } /*! * \brief dewarpFindVertDisparity() * * \param[in] dew * \param[in] ptaa unsmoothed lines, not vertically ordered * \param[in] rotflag 0 if using dew->pixs; 1 if rotated by 90 degrees cw * \return 0 if OK, 1 on error * *
 * Notes:
 *      (1) This starts with points along the centers of textlines.
 *          It does quadratic fitting (and smoothing), first along the
 *          lines and then in the vertical direction, to generate
 *          the sampled vertical disparity map.  This can then be
 *          interpolated to full resolution and used to remove
 *          the vertical line warping.
 *      (2) Use %rotflag == 1 if you are dewarping vertical lines, as
 *          is done in dewarpBuildLineModel().  The usual case is for
 *          %rotflag == 0.
 *      (3) Note that this builds a vertical disparity model (VDM), but
 *          does not check it against constraints for validity.
 *          Constraint checking is done after building the models,
 *          and before inserting reference models.
 *      (4) This sets the vsuccess flag to 1 on success.
 *      (5) Pix debug output goes to /tmp/dewvert/ for collection into
 *          a pdf.  Non-pix debug output goes to /tmp.
 * 
*/ l_ok dewarpFindVertDisparity(L_DEWARP *dew, PTAA *ptaa, l_int32 rotflag) { l_int32 i, j, nlines, npts, nx, ny, sampling; l_float32 c0, c1, c2, x, y, midy, val, medval, meddev, minval, maxval; l_float32 *famidys; NUMA *nax, *nafit, *nacurve0, *nacurve1, *nacurves; NUMA *namidy, *namidys, *namidysi; PIX *pix1, *pix2, *pixcirc, *pixdb; PTA *pta, *ptad, *ptacirc; PTAA *ptaa0, *ptaa1, *ptaa2, *ptaa3, *ptaa4, *ptaa5, *ptaat; FPIX *fpix; PROCNAME("dewarpFindVertDisparity"); if (!dew) return ERROR_INT("dew not defined", procName, 1); dew->vsuccess = 0; if (!ptaa) return ERROR_INT("ptaa not defined", procName, 1); if (dew->debug) L_INFO("finding vertical disparity\n", procName); /* Do quadratic fit to smooth each line. A single quadratic * over the entire width of the line appears to be sufficient. * Quartics tend to overfit to noise. Each line is thus * represented by three coefficients: y(x) = c2 * x^2 + c1 * x + c0. * Using the coefficients, sample each fitted curve uniformly * across the full width of the image. The result is in ptaa0. */ sampling = dew->sampling; nx = (rotflag) ? dew->ny : dew->nx; ny = (rotflag) ? dew->nx : dew->ny; nlines = ptaaGetCount(ptaa); dew->nlines = nlines; ptaa0 = ptaaCreate(nlines); nacurve0 = numaCreate(nlines); /* stores curvature coeff c2 */ pixdb = (rotflag) ? pixRotateOrth(dew->pixs, 1) : pixClone(dew->pixs); for (i = 0; i < nlines; i++) { /* for each line */ pta = ptaaGetPta(ptaa, i, L_CLONE); ptaGetQuadraticLSF(pta, &c2, &c1, &c0, NULL); numaAddNumber(nacurve0, c2); ptad = ptaCreate(nx); for (j = 0; j < nx; j++) { /* uniformly sampled in x */ x = j * sampling; applyQuadraticFit(c2, c1, c0, x, &y); ptaAddPt(ptad, x, y); } ptaaAddPta(ptaa0, ptad, L_INSERT); ptaDestroy(&pta); } if (dew->debug) { lept_mkdir("lept/dewarp"); lept_mkdir("lept/dewdebug"); lept_mkdir("lept/dewmod"); ptaat = ptaaCreate(nlines); for (i = 0; i < nlines; i++) { pta = ptaaGetPta(ptaa, i, L_CLONE); ptaGetArrays(pta, &nax, NULL); ptaGetQuadraticLSF(pta, NULL, NULL, NULL, &nafit); ptad = ptaCreateFromNuma(nax, nafit); ptaaAddPta(ptaat, ptad, L_INSERT); ptaDestroy(&pta); numaDestroy(&nax); numaDestroy(&nafit); } pix1 = pixConvertTo32(pixdb); pta = generatePtaFilledCircle(1); pixcirc = pixGenerateFromPta(pta, 5, 5); pix2 = pixDisplayPtaaPattern(NULL, pix1, ptaat, pixcirc, 2, 2); pixWriteDebug("/tmp/lept/dewmod/0041.png", pix2, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); ptaDestroy(&pta); pixDestroy(&pixcirc); ptaaDestroy(&ptaat); } /* Remove lines with outlier curvatures. * Note that this is just looking for internal consistency in * the line curvatures. It is not rejecting lines based on * the magnitude of the curvature. That is done when constraints * are applied for valid models. */ numaGetMedianDevFromMedian(nacurve0, &medval, &meddev); L_INFO("\nPage %d\n", procName, dew->pageno); L_INFO("Pass 1: Curvature: medval = %f, meddev = %f\n", procName, medval, meddev); ptaa1 = ptaaCreate(nlines); nacurve1 = numaCreate(nlines); for (i = 0; i < nlines; i++) { /* for each line */ numaGetFValue(nacurve0, i, &val); if (L_ABS(val - medval) > 7.0 * meddev) /* TODO: reduce to ~ 3.0 */ continue; pta = ptaaGetPta(ptaa0, i, L_CLONE); ptaaAddPta(ptaa1, pta, L_INSERT); numaAddNumber(nacurve1, val); } nlines = ptaaGetCount(ptaa1); numaDestroy(&nacurve0); /* Save the min and max curvature (in micro-units) */ numaGetMin(nacurve1, &minval, NULL); numaGetMax(nacurve1, &maxval, NULL); dew->mincurv = lept_roundftoi(1000000. * minval); dew->maxcurv = lept_roundftoi(1000000. * maxval); L_INFO("Pass 2: Min/max curvature = (%d, %d)\n", procName, dew->mincurv, dew->maxcurv); /* Find and save the y values at the mid-points in each curve. * If the slope is zero anywhere, it will typically be here. */ namidy = numaCreate(nlines); for (i = 0; i < nlines; i++) { pta = ptaaGetPta(ptaa1, i, L_CLONE); npts = ptaGetCount(pta); ptaGetPt(pta, npts / 2, NULL, &midy); numaAddNumber(namidy, midy); ptaDestroy(&pta); } /* Sort the lines in ptaa1c by their vertical position, going down */ namidysi = numaGetSortIndex(namidy, L_SORT_INCREASING); namidys = numaSortByIndex(namidy, namidysi); nacurves = numaSortByIndex(nacurve1, namidysi); numaDestroy(&dew->namidys); /* in case previously made */ numaDestroy(&dew->nacurves); dew->namidys = namidys; dew->nacurves = nacurves; ptaa2 = ptaaSortByIndex(ptaa1, namidysi); numaDestroy(&namidy); numaDestroy(&nacurve1); numaDestroy(&namidysi); if (dew->debug) { numaWriteDebug("/tmp/lept/dewdebug/midys.na", namidys); numaWriteDebug("/tmp/lept/dewdebug/curves.na", nacurves); pix1 = pixConvertTo32(pixdb); ptacirc = generatePtaFilledCircle(5); pixcirc = pixGenerateFromPta(ptacirc, 11, 11); srand(3); pixDisplayPtaaPattern(pix1, pix1, ptaa2, pixcirc, 5, 5); srand(3); /* use the same colors for text and reference lines */ pixRenderMidYs(pix1, namidys, 2); pix2 = (rotflag) ? pixRotateOrth(pix1, 3) : pixClone(pix1); pixWriteDebug("/tmp/lept/dewmod/0042.png", pix2, IFF_PNG); pixDisplay(pix2, 0, 0); ptaDestroy(&ptacirc); pixDestroy(&pixcirc); pixDestroy(&pix1); pixDestroy(&pix2); } pixDestroy(&pixdb); /* Convert the sampled points in ptaa2 to a sampled disparity with * with respect to the y value at the mid point in the curve. * The disparity is the distance the point needs to move; * plus is downward. */ ptaa3 = ptaaCreate(nlines); for (i = 0; i < nlines; i++) { pta = ptaaGetPta(ptaa2, i, L_CLONE); numaGetFValue(namidys, i, &midy); ptad = ptaCreate(nx); for (j = 0; j < nx; j++) { ptaGetPt(pta, j, &x, &y); ptaAddPt(ptad, x, midy - y); } ptaaAddPta(ptaa3, ptad, L_INSERT); ptaDestroy(&pta); } if (dew->debug) { ptaaWriteDebug("/tmp/lept/dewdebug/ptaa3.ptaa", ptaa3, 0); } /* Generate ptaa4 by taking vertical 'columns' from ptaa3. * We want to fit the vertical disparity on the column to the * vertical position of the line, which we call 'y' here and * obtain from namidys. So each pta in ptaa4 is the set of * vertical disparities down a column of points. The columns * in ptaa4 are equally spaced in x. */ ptaa4 = ptaaCreate(nx); famidys = numaGetFArray(namidys, L_NOCOPY); for (j = 0; j < nx; j++) { pta = ptaCreate(nlines); for (i = 0; i < nlines; i++) { y = famidys[i]; ptaaGetPt(ptaa3, i, j, NULL, &val); /* disparity value */ ptaAddPt(pta, y, val); } ptaaAddPta(ptaa4, pta, L_INSERT); } if (dew->debug) { ptaaWriteDebug("/tmp/lept/dewdebug/ptaa4.ptaa", ptaa4, 0); } /* Do quadratic fit vertically on each of the pixel columns * in ptaa4, for the vertical displacement (which identifies the * src pixel(s) for each dest pixel) as a function of y (the * y value of the mid-points for each line). Then generate * ptaa5 by sampling the fitted vertical displacement on a * regular grid in the vertical direction. Each pta in ptaa5 * gives the vertical displacement for regularly sampled y values * at a fixed x. */ ptaa5 = ptaaCreate(nx); /* uniformly sampled across full height of image */ for (j = 0; j < nx; j++) { /* for each column */ pta = ptaaGetPta(ptaa4, j, L_CLONE); ptaGetQuadraticLSF(pta, &c2, &c1, &c0, NULL); ptad = ptaCreate(ny); for (i = 0; i < ny; i++) { /* uniformly sampled in y */ y = i * sampling; applyQuadraticFit(c2, c1, c0, y, &val); ptaAddPt(ptad, y, val); } ptaaAddPta(ptaa5, ptad, L_INSERT); ptaDestroy(&pta); } if (dew->debug) { ptaaWriteDebug("/tmp/lept/dewdebug/ptaa5.ptaa", ptaa5, 0); convertFilesToPdf("/tmp/lept/dewmod", "004", 135, 1.0, 0, 0, "Dewarp Vert Disparity", "/tmp/lept/dewarp/vert_disparity.pdf"); lept_stderr("pdf file: /tmp/lept/dewarp/vert_disparity.pdf\n"); } /* Save the result in a fpix at the specified subsampling */ fpix = fpixCreate(nx, ny); for (i = 0; i < ny; i++) { for (j = 0; j < nx; j++) { ptaaGetPt(ptaa5, j, i, NULL, &val); fpixSetPixel(fpix, j, i, val); } } dew->sampvdispar = fpix; dew->vsuccess = 1; ptaaDestroy(&ptaa0); ptaaDestroy(&ptaa1); ptaaDestroy(&ptaa2); ptaaDestroy(&ptaa3); ptaaDestroy(&ptaa4); ptaaDestroy(&ptaa5); return 0; } /*! * \brief dewarpFindHorizDisparity() * * \param[in] dew * \param[in] ptaa unsmoothed lines, not vertically ordered * \return 0 if OK, 1 if horizontal disparity array is not built, or on error * *
 * Notes:
 *      (1) This builds a horizontal disparity model (HDM), but
 *          does not check it against constraints for validity.
 *          Constraint checking is done at rendering time.
 *      (2) Horizontal disparity is not required for a successful model;
 *          only the vertical disparity is required.  This will not be
 *          called if the function to build the vertical disparity fails.
 *      (3) This sets the hsuccess flag to 1 on success.
 *      (4) Internally in ptal1, ptar1, ptal2, ptar2: x and y are reversed,
 *          so the 'y' value is horizontal distance across the image width.
 *      (5) Debug output goes to /tmp/lept/dewmod/ for collection into a pdf.
 * 
*/ l_ok dewarpFindHorizDisparity(L_DEWARP *dew, PTAA *ptaa) { l_int32 i, j, h, nx, ny, sampling, ret, linear_edgefit; l_float32 c0, c1, cl0, cl1, cl2, cr0, cr1, cr2; l_float32 x, y, refl, refr; l_float32 val, mederr; NUMA *nald, *nard; PIX *pix1; PTA *ptal1, *ptar1; /* left/right end points of lines; initial */ PTA *ptal2, *ptar2; /* left/right end points; after filtering */ PTA *ptal3, *ptar3; /* left and right block, fitted, uniform spacing */ PTA *pta, *ptat, *pta1, *pta2; PTAA *ptaah; FPIX *fpix; PROCNAME("dewarpFindHorizDisparity"); if (!dew) return ERROR_INT("dew not defined", procName, 1); dew->hsuccess = 0; if (!ptaa) return ERROR_INT("ptaa not defined", procName, 1); if (dew->debug) L_INFO("finding horizontal disparity\n", procName); /* Get the endpoints of the lines, and sort from top to bottom */ h = pixGetHeight(dew->pixs); ret = dewarpGetLineEndPoints(h, ptaa, &ptal1, &ptar1); if (ret) { L_INFO("Horiz disparity not built\n", procName); return 1; } if (dew->debug) { lept_mkdir("lept/dewdebug"); lept_mkdir("lept/dewarp"); ptaWriteDebug("/tmp/lept/dewdebug/endpts_left1.pta", ptal1, 1); ptaWriteDebug("/tmp/lept/dewdebug/endpts_right1.pta", ptar1, 1); } /* Filter the points by x-location to prevent 2-column images * from getting confused about left and right endpoints. We * require valid left points to not be farther than * 0.20 * (remaining distance to the right edge of the image) * to the right of the leftmost endpoint, and similarly for * the right endpoints. (Note: x and y are reversed in the pta.) * Also require end points to be near the medians in the * upper and lower halves. */ ret = dewarpFilterLineEndPoints(dew, ptal1, ptar1, &ptal2, &ptar2); ptaDestroy(&ptal1); ptaDestroy(&ptar1); if (ret) { L_INFO("Not enough filtered end points\n", procName); return 1; } /* Do either a linear or a quadratic fit to the left and right * endpoints of the longest lines. It is not necessary to use * the noisy LSF fit function, because we've removed outlier * end points by selecting the long lines. * For the linear fit, each line is represented by 2 coefficients: * x(y) = c1 * y + c0. * For the quadratic fit, each line is represented by 3 coefficients: * x(y) = c2 * y^2 + c1 * y + c0. * Then using the coefficients, sample each fitted curve uniformly * along the full height of the image. */ sampling = dew->sampling; nx = dew->nx; ny = dew->ny; linear_edgefit = (dew->dewa->max_edgecurv == 0) ? TRUE : FALSE; if (linear_edgefit) { /* Fit the left side, using linear LSF on the set of long lines. */ dewarpLinearLSF(ptal2, &cl1, &cl0, &mederr); dew->leftslope = lept_roundftoi(1000. * cl1); /* milli-units */ dew->leftcurv = 0; /* micro-units */ L_INFO("Left linear LSF median error = %5.2f\n", procName, mederr); L_INFO("Left edge slope = %d\n", procName, dew->leftslope); ptal3 = ptaCreate(ny); for (i = 0; i < ny; i++) { /* uniformly sample in y */ y = i * sampling; applyLinearFit(cl1, cl0, y, &x); ptaAddPt(ptal3, x, y); } /* Do a linear LSF on the right side. */ dewarpLinearLSF(ptar2, &cr1, &cr0, &mederr); dew->rightslope = lept_roundftoi(1000.0 * cr1); /* milli-units */ dew->rightcurv = 0; /* micro-units */ L_INFO("Right linear LSF median error = %5.2f\n", procName, mederr); L_INFO("Right edge slope = %d\n", procName, dew->rightslope); ptar3 = ptaCreate(ny); for (i = 0; i < ny; i++) { /* uniformly sample in y */ y = i * sampling; applyLinearFit(cr1, cr0, y, &x); ptaAddPt(ptar3, x, y); } } else { /* quadratic edge fit */ /* Fit the left side, using quadratic LSF on the long lines. */ dewarpQuadraticLSF(ptal2, &cl2, &cl1, &cl0, &mederr); dew->leftslope = lept_roundftoi(1000. * cl1); /* milli-units */ dew->leftcurv = lept_roundftoi(1000000. * cl2); /* micro-units */ L_INFO("Left quad LSF median error = %5.2f\n", procName, mederr); L_INFO("Left edge slope = %d\n", procName, dew->leftslope); L_INFO("Left edge curvature = %d\n", procName, dew->leftcurv); ptal3 = ptaCreate(ny); for (i = 0; i < ny; i++) { /* uniformly sample in y */ y = i * sampling; applyQuadraticFit(cl2, cl1, cl0, y, &x); ptaAddPt(ptal3, x, y); } /* Do a quadratic LSF on the right side. */ dewarpQuadraticLSF(ptar2, &cr2, &cr1, &cr0, &mederr); dew->rightslope = lept_roundftoi(1000.0 * cr1); /* milli-units */ dew->rightcurv = lept_roundftoi(1000000. * cr2); /* micro-units */ L_INFO("Right quad LSF median error = %5.2f\n", procName, mederr); L_INFO("Right edge slope = %d\n", procName, dew->rightslope); L_INFO("Right edge curvature = %d\n", procName, dew->rightcurv); ptar3 = ptaCreate(ny); for (i = 0; i < ny; i++) { /* uniformly sample in y */ y = i * sampling; applyQuadraticFit(cr2, cr1, cr0, y, &x); ptaAddPt(ptar3, x, y); } } if (dew->debug) { PTA *ptalft, *ptarft; h = pixGetHeight(dew->pixs); pta1 = ptaCreate(h); pta2 = ptaCreate(h); if (linear_edgefit) { for (i = 0; i < h; i++) { applyLinearFit(cl1, cl0, i, &x); ptaAddPt(pta1, x, i); applyLinearFit(cr1, cr0, i, &x); ptaAddPt(pta2, x, i); } } else { /* quadratic edge fit */ for (i = 0; i < h; i++) { applyQuadraticFit(cl2, cl1, cl0, i, &x); ptaAddPt(pta1, x, i); applyQuadraticFit(cr2, cr1, cr0, i, &x); ptaAddPt(pta2, x, i); } } pix1 = pixDisplayPta(NULL, dew->pixs, pta1); pixDisplayPta(pix1, pix1, pta2); pixRenderHorizEndPoints(pix1, ptal2, ptar2, 0xff000000); pixDisplay(pix1, 600, 800); pixWriteDebug("/tmp/lept/dewmod/0051.png", pix1, IFF_PNG); pixDestroy(&pix1); pix1 = pixDisplayPta(NULL, dew->pixs, pta1); pixDisplayPta(pix1, pix1, pta2); ptalft = ptaTranspose(ptal3); ptarft = ptaTranspose(ptar3); pixRenderHorizEndPoints(pix1, ptalft, ptarft, 0x0000ff00); pixDisplay(pix1, 800, 800); pixWriteDebug("/tmp/lept/dewmod/0052.png", pix1, IFF_PNG); convertFilesToPdf("/tmp/lept/dewmod", "005", 135, 1.0, 0, 0, "Dewarp Horiz Disparity", "/tmp/lept/dewarp/horiz_disparity.pdf"); lept_stderr("pdf file: /tmp/lept/dewarp/horiz_disparity.pdf\n"); pixDestroy(&pix1); ptaDestroy(&pta1); ptaDestroy(&pta2); ptaDestroy(&ptalft); ptaDestroy(&ptarft); } /* Find the x value at the midpoints (in y) of the two vertical lines, * ptal3 and ptar3. These are the reference values for each of the * lines. Then use the difference between the these midpoint * values and the actual x coordinates of the lines to represent * the horizontal disparity (nald, nard) on the vertical lines * for the sampled y values. */ ptaGetPt(ptal3, ny / 2, &refl, NULL); ptaGetPt(ptar3, ny / 2, &refr, NULL); nald = numaCreate(ny); nard = numaCreate(ny); for (i = 0; i < ny; i++) { ptaGetPt(ptal3, i, &x, NULL); numaAddNumber(nald, refl - x); ptaGetPt(ptar3, i, &x, NULL); numaAddNumber(nard, refr - x); } /* Now for each pair of sampled values of the two lines (at the * same value of y), do a linear interpolation to generate * the horizontal disparity on all sampled points between them. */ ptaah = ptaaCreate(ny); for (i = 0; i < ny; i++) { pta = ptaCreate(2); numaGetFValue(nald, i, &val); ptaAddPt(pta, refl, val); numaGetFValue(nard, i, &val); ptaAddPt(pta, refr, val); ptaGetLinearLSF(pta, &c1, &c0, NULL); /* horiz disparity along line */ ptat = ptaCreate(nx); for (j = 0; j < nx; j++) { x = j * sampling; applyLinearFit(c1, c0, x, &val); ptaAddPt(ptat, x, val); } ptaaAddPta(ptaah, ptat, L_INSERT); ptaDestroy(&pta); } numaDestroy(&nald); numaDestroy(&nard); /* Save the result in a fpix at the specified subsampling */ fpix = fpixCreate(nx, ny); for (i = 0; i < ny; i++) { for (j = 0; j < nx; j++) { ptaaGetPt(ptaah, i, j, NULL, &val); fpixSetPixel(fpix, j, i, val); } } dew->samphdispar = fpix; dew->hsuccess = 1; ptaDestroy(&ptal2); ptaDestroy(&ptar2); ptaDestroy(&ptal3); ptaDestroy(&ptar3); ptaaDestroy(&ptaah); return 0; } /*! * \brief dewarpGetTextlineCenters() * * \param[in] pixs 1 bpp * \param[in] debugflag 1 for debug output * \return ptaa of center values of textlines * *
 * Notes:
 *      (1) This in general does not have a point for each value
 *          of x, because there will be gaps between words.
 *          It doesn't matter because we will fit a quadratic to the
 *          points that we do have.
 * 
*/ PTAA * dewarpGetTextlineCenters(PIX *pixs, l_int32 debugflag) { char buf[64]; l_int32 i, w, h, bx, by, nsegs, csize1, csize2; BOXA *boxa; PIX *pix1, *pix2; PIXA *pixa1, *pixa2; PTA *pta; PTAA *ptaa; PROCNAME("dewarpGetTextlineCenters"); if (!pixs || pixGetDepth(pixs) != 1) return (PTAA *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL); pixGetDimensions(pixs, &w, &h, NULL); if (debugflag) L_INFO("finding text line centers\n", procName); /* Filter to solidify the text lines within the x-height region, * and to remove most of the ascenders and descenders. * We start with a small vertical opening to remove noise beyond * the line that can cause an error in the line end points. * The small closing (csize1) is used to bridge the gaps between * letters. The large closing (csize2) bridges the gaps between * words; using 1/30 of the page width usually suffices. */ csize1 = L_MAX(15, w / 80); csize2 = L_MAX(40, w / 30); snprintf(buf, sizeof(buf), "o1.3 + c%d.1 + o%d.1 + c%d.1", csize1, csize1, csize2); pix1 = pixMorphSequence(pixs, buf, 0); /* Remove the components (e.g., embedded images) that have * long vertical runs (>= 50 pixels). You can't use bounding * boxes because connected component b.b. of lines can be quite * tall due to slope and curvature. */ pix2 = pixMorphSequence(pix1, "e1.50", 0); /* seed */ pixSeedfillBinary(pix2, pix2, pix1, 8); /* tall components */ pixXor(pix2, pix2, pix1); /* remove tall */ if (debugflag) { lept_mkdir("lept/dewmod"); pixWriteDebug("/tmp/lept/dewmod/0011.tif", pix1, IFF_TIFF_G4); pixDisplayWithTitle(pix1, 0, 600, "pix1", 1); pixWriteDebug("/tmp/lept/dewmod/0012.tif", pix2, IFF_TIFF_G4); pixDisplayWithTitle(pix2, 0, 800, "pix2", 1); } pixDestroy(&pix1); /* Get the 8-connected components ... */ boxa = pixConnComp(pix2, &pixa1, 8); pixDestroy(&pix2); boxaDestroy(&boxa); if (pixaGetCount(pixa1) == 0) { pixaDestroy(&pixa1); return NULL; } /* ... and remove the short width and very short height c.c */ pixa2 = pixaSelectBySize(pixa1, 100, 4, L_SELECT_IF_BOTH, L_SELECT_IF_GT, NULL); if ((nsegs = pixaGetCount(pixa2)) == 0) { pixaDestroy(&pixa1); pixaDestroy(&pixa2); return NULL; } if (debugflag) { pix2 = pixaDisplay(pixa2, w, h); pixWriteDebug("/tmp/lept/dewmod/0013.tif", pix2, IFF_TIFF_G4); pixDisplayWithTitle(pix2, 0, 1000, "pix2", 1); pixDestroy(&pix2); } /* For each c.c., get the weighted center of each vertical column. * The result is a set of points going approximately through * the center of the x-height part of the text line. */ ptaa = ptaaCreate(nsegs); for (i = 0; i < nsegs; i++) { pixaGetBoxGeometry(pixa2, i, &bx, &by, NULL, NULL); pix2 = pixaGetPix(pixa2, i, L_CLONE); pta = dewarpGetMeanVerticals(pix2, bx, by); ptaaAddPta(ptaa, pta, L_INSERT); pixDestroy(&pix2); } if (debugflag) { pix1 = pixCreateTemplate(pixs); pix2 = pixDisplayPtaa(pix1, ptaa); pixWriteDebug("/tmp/lept/dewmod/0014.tif", pix2, IFF_PNG); pixDisplayWithTitle(pix2, 0, 1200, "pix3", 1); pixDestroy(&pix1); pixDestroy(&pix2); } pixaDestroy(&pixa1); pixaDestroy(&pixa2); return ptaa; } /*! * \brief dewarpGetMeanVerticals() * * \param[in] pixs 1 bpp, single c.c. * \param[in] x,y location of UL corner of pixs, relative to page image * \return pta (mean y-values in component for each x-value, * both translated by (x,y */ static PTA * dewarpGetMeanVerticals(PIX *pixs, l_int32 x, l_int32 y) { l_int32 w, h, i, j, wpl, sum, count; l_uint32 *line, *data; PTA *pta; PROCNAME("pixGetMeanVerticals"); if (!pixs || pixGetDepth(pixs) != 1) return (PTA *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL); pixGetDimensions(pixs, &w, &h, NULL); pta = ptaCreate(w); data = pixGetData(pixs); wpl = pixGetWpl(pixs); for (j = 0; j < w; j++) { line = data; sum = count = 0; for (i = 0; i < h; i++) { if (GET_DATA_BIT(line, j) == 1) { sum += i; count += 1; } line += wpl; } if (count == 0) continue; ptaAddPt(pta, x + j, y + (sum / count)); } return pta; } /*! * \brief dewarpRemoveShortLines() * * \param[in] pixs 1 bpp * \param[in] ptaas input lines * \param[in] fract minimum fraction of longest line to keep * \param[in] debugflag * \return ptaad containing only lines of sufficient length, * or NULL on error */ PTAA * dewarpRemoveShortLines(PIX *pixs, PTAA *ptaas, l_float32 fract, l_int32 debugflag) { l_int32 w, n, i, index, maxlen, len; l_float32 minx, maxx; NUMA *na, *naindex; PIX *pix1, *pix2; PTA *pta; PTAA *ptaad; PROCNAME("dewarpRemoveShortLines"); if (!pixs || pixGetDepth(pixs) != 1) return (PTAA *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL); if (!ptaas) return (PTAA *)ERROR_PTR("ptaas undefined", procName, NULL); pixGetDimensions(pixs, &w, NULL, NULL); n = ptaaGetCount(ptaas); ptaad = ptaaCreate(n); na = numaCreate(n); for (i = 0; i < n; i++) { pta = ptaaGetPta(ptaas, i, L_CLONE); ptaGetRange(pta, &minx, &maxx, NULL, NULL); numaAddNumber(na, maxx - minx + 1); ptaDestroy(&pta); } /* Sort by length and find all that are long enough */ naindex = numaGetSortIndex(na, L_SORT_DECREASING); numaGetIValue(naindex, 0, &index); numaGetIValue(na, index, &maxlen); if (maxlen < 0.5 * w) L_WARNING("lines are relatively short\n", procName); pta = ptaaGetPta(ptaas, index, L_CLONE); ptaaAddPta(ptaad, pta, L_INSERT); for (i = 1; i < n; i++) { numaGetIValue(naindex, i, &index); numaGetIValue(na, index, &len); if (len < fract * maxlen) break; pta = ptaaGetPta(ptaas, index, L_CLONE); ptaaAddPta(ptaad, pta, L_INSERT); } if (debugflag) { pix1 = pixCopy(NULL, pixs); pix2 = pixDisplayPtaa(pix1, ptaad); pixDisplayWithTitle(pix2, 0, 200, "pix4", 1); pixDestroy(&pix1); pixDestroy(&pix2); } numaDestroy(&na); numaDestroy(&naindex); return ptaad; } /*! * \brief dewarpGetLineEndPoints() * * \param[in] h height of pixs * \param[in] ptaa lines * \param[out] pptal left end points of each line * \param[out] pptar right end points of each line * \return 0 if OK, 1 on error. * *
 * Notes:
 *      (1) We require that the set of end points extends over 45% of the
 *          height of the input image, to insure good coverage and
 *          avoid extrapolating the curvature too far beyond the
 *          actual textlines.  Large extrapolations are particularly
 *          dangerous if used as a reference model.  We also require
 *          at least 10 lines of text.
 *      (2) We sort the lines from top to bottom (sort by x in the ptas).
 *      (3) For fitting the endpoints, x = f(y), we transpose x and y.
 *          Thus all these ptas have x and y swapped!
 * 
*/ static l_int32 dewarpGetLineEndPoints(l_int32 h, PTAA *ptaa, PTA **pptal, PTA **pptar) { l_int32 i, n, npt, x, y; l_float32 miny, maxy, ratio; PTA *pta, *ptal1, *ptar1; PROCNAME("dewarpGetLineEndPoints"); if (!pptal || !pptar) return ERROR_INT("&ptal and &ptar not both defined", procName, 1); *pptal = *pptar = NULL; if (!ptaa) return ERROR_INT("ptaa undefined", procName, 1); /* Are there at least 10 lines? */ n = ptaaGetCount(ptaa); if (n < MinLinesForHoriz1) { L_INFO("only %d lines; too few\n", procName, n); return 1; } /* Extract the line end points, and transpose x and y values */ ptal1 = ptaCreate(n); ptar1 = ptaCreate(n); for (i = 0; i < n; i++) { pta = ptaaGetPta(ptaa, i, L_CLONE); ptaGetIPt(pta, 0, &x, &y); ptaAddPt(ptal1, y, x); /* transpose */ npt = ptaGetCount(pta); ptaGetIPt(pta, npt - 1, &x, &y); ptaAddPt(ptar1, y, x); /* transpose */ ptaDestroy(&pta); } /* Use the min and max of the y value on the left side. */ ptaGetRange(ptal1, &miny, &maxy, NULL, NULL); ratio = (maxy - miny) / (l_float32)h; if (ratio < MinRatioLinesToHeight) { L_INFO("ratio lines to height, %f, too small\n", procName, ratio); ptaDestroy(&ptal1); ptaDestroy(&ptar1); return 1; } /* Sort from top to bottom */ *pptal = ptaSort(ptal1, L_SORT_BY_X, L_SORT_INCREASING, NULL); *pptar = ptaSort(ptar1, L_SORT_BY_X, L_SORT_INCREASING, NULL); ptaDestroy(&ptal1); ptaDestroy(&ptar1); return 0; } /*! * \brief dewarpFilterLineEndPoints() * * \param[in] dew * \param[in] ptal input left end points of each line * \param[in] ptar input right end points of each line * \param[out] pptalf filtered left end points * \param[out] pptarf filtered right end points * \return 0 if OK, 1 on error. * *
 * Notes:
 *      (1) Avoid confusion with multiple columns by requiring that line
 *          end points be close enough to leftmost and rightmost end points.
 *          Must have at least 8 points on left and right after this step.
 *      (2) Apply second filtering step, find the median positions in
 *          top and bottom halves, and removing end points that are
 *          displaced too much from these in the x direction.
 *          Must have at least 6 points on left and right after this step.
 *      (3) Reminder: x and y in the pta are transposed; think x = f(y).
 * 
*/ static l_int32 dewarpFilterLineEndPoints(L_DEWARP *dew, PTA *ptal, PTA *ptar, PTA **pptalf, PTA **pptarf) { l_int32 w, i, n; l_float32 ymin, ymax, xvall, xvalr, yvall, yvalr; PTA *ptal1, *ptar1, *ptal2, *ptar2; PROCNAME("dewarpFilterLineEndPoints"); if (!ptal || !ptar) return ERROR_INT("ptal or ptar not defined", procName, 1); *pptalf = *pptarf = NULL; /* First filter for lines near left and right margins */ w = pixGetWidth(dew->pixs); ptaGetMinMax(ptal, NULL, &ymin, NULL, NULL); ptaGetMinMax(ptar, NULL, NULL, NULL, &ymax); n = ptaGetCount(ptal); /* ptar is the same size; at least 10 */ ptal1 = ptaCreate(n); ptar1 = ptaCreate(n); for (i = 0; i < n; i++) { ptaGetPt(ptal, i, &xvall, &yvall); ptaGetPt(ptar, i, &xvalr, &yvalr); if (yvall < ymin + 0.20 * (w - ymin) && yvalr > 0.80 * ymax) { ptaAddPt(ptal1, xvall, yvall); ptaAddPt(ptar1, xvalr, yvalr); } } if (dew->debug) { ptaWriteDebug("/tmp/lept/dewdebug/endpts_left2.pta", ptal1, 1); ptaWriteDebug("/tmp/lept/dewdebug/endpts_right2.pta", ptar1, 1); } n = L_MIN(ptaGetCount(ptal1), ptaGetCount(ptar1)); if (n < MinLinesForHoriz1 - 2) { ptaDestroy(&ptal1); ptaDestroy(&ptar1); L_INFO("First filter: only %d endpoints; needed 8\n", procName, n); return 1; } /* Remove outlier points */ ptal2 = dewarpRemoveBadEndPoints(w, ptal1); ptar2 = dewarpRemoveBadEndPoints(w, ptar1); ptaDestroy(&ptal1); ptaDestroy(&ptar1); if (!ptal2 || !ptar2) { ptaDestroy(&ptal2); ptaDestroy(&ptar2); L_INFO("Second filter: too few endpoints left after outliers removed\n", procName); return 1; } if (dew->debug) { ptaWriteDebug("/tmp/lept/dewdebug/endpts_left3.pta", ptal2, 1); ptaWriteDebug("/tmp/lept/dewdebug/endpts_right3.pta", ptar2, 1); } *pptalf = ptal2; *pptarf = ptar2; return 0; } /*! * \brief dewarpRemoveBadEndPoints() * * \param[in] w width of input image * \param[in] ptas left or right line end points * \return ptad filtered left or right end points, or NULL on error. * *
 * Notes:
 *      (1) The input set is sorted by line position (x value).
 *          Break into two (upper and lower); for each find the median
 *          horizontal (y value), and remove all points farther than
 *          a fraction of the image width from this.  Make sure each
 *          part still has at least 3 points, and join the two sections
 *          before returning.
 *      (2) Reminder: x and y in the pta are transposed; think x = f(y).
 * 
*/ static PTA * dewarpRemoveBadEndPoints(l_int32 w, PTA *ptas) { l_int32 i, n, nu, nd; l_float32 rval, xval, yval, delta; PTA *ptau1, *ptau2, *ptad1, *ptad2; PROCNAME("dewarpRemoveBadEndPoints"); if (!ptas) return (PTA *)ERROR_PTR("ptas not defined", procName, NULL); delta = AllowedWidthFract * w; n = ptaGetCount(ptas); /* will be at least 8 */ /* Check the upper half */ ptau1 = ptaSelectRange(ptas, 0, n / 2); ptaGetRankValue(ptau1, 0.5, NULL, L_SORT_BY_Y, &rval); nu = ptaGetCount(ptau1); ptau2 = ptaCreate(nu); for (i = 0; i < nu; i++) { ptaGetPt(ptau1, i, &xval, &yval); /* transposed */ if (L_ABS(rval - yval) <= delta) ptaAddPt(ptau2, xval, yval); } ptaDestroy(&ptau1); if (ptaGetCount(ptau2) < MinLinesForHoriz2) { ptaDestroy(&ptau2); L_INFO("Second filter: upper set is too small after outliers removed\n", procName); return NULL; } /* Check the lower half */ ptad1 = ptaSelectRange(ptas, n / 2 + 1, -1); ptaGetRankValue(ptad1, 0.5, NULL, L_SORT_BY_Y, &rval); nd = ptaGetCount(ptad1); ptad2 = ptaCreate(nd); for (i = 0; i < nd; i++) { ptaGetPt(ptad1, i, &xval, &yval); /* transposed */ if (L_ABS(rval - yval) <= delta) ptaAddPt(ptad2, xval, yval); } ptaDestroy(&ptad1); if (ptaGetCount(ptad2) < MinLinesForHoriz2) { ptaDestroy(&ptau2); ptaDestroy(&ptad2); L_INFO("Second filter: lower set is too small after outliers removed\n", procName); return NULL; } ptaJoin(ptau2, ptad2, 0, -1); ptaDestroy(&ptad2); return ptau2; } /*! * \brief dewarpIsLineCoverageValid() * * \param[in] ptaa of validated lines * \param[in] h height of pix * \param[out] pntop number of lines in top half * \param[out] pnbot number of lines in bottom half * \param[out] pytop location of top line * \param[out] pybot location of bottom line * \return 1 if coverage is valid, 0 if not or on error. * *
 * Notes:
 *      (1) The criterion for valid coverage is:
 *          (a) there must be at least 4 lines in each half (top and bottom)
 *              of the image.
 *          (b) the coverage must be at least 50% of the image height
 * 
*/ static l_int32 dewarpIsLineCoverageValid(PTAA *ptaa, l_int32 h, l_int32 *pntop, l_int32 *pnbot, l_int32 *pytop, l_int32 *pybot) { l_int32 i, n, iy, both_halves, ntop, nbot, ytop, ybot, nmin; l_float32 y, fraction; NUMA *na; PROCNAME("dewarpIsLineCoverageValid"); if (!ptaa) return ERROR_INT("ptaa not defined", procName, 0); if ((n = ptaaGetCount(ptaa)) == 0) return ERROR_INT("ptaa empty", procName, 0); if (h <= 0) return ERROR_INT("invalid h", procName, 0); if (!pntop || !pnbot) return ERROR_INT("&ntop and &nbot not defined", procName, 0); if (!pytop || !pybot) return ERROR_INT("&ytop and &ybot not defined", procName, 0); na = numaCreate(n); for (i = 0; i < n; i++) { ptaaGetPt(ptaa, i, 0, NULL, &y); numaAddNumber(na, y); } numaSort(na, na, L_SORT_INCREASING); for (i = 0, ntop = 0; i < n; i++) { numaGetIValue(na, i, &iy); if (i == 0) ytop = iy; if (i == n - 1) ybot = iy; if (iy < 0.5 * h) ntop++; } numaDestroy(&na); nbot = n - ntop; *pntop = ntop; *pnbot = nbot; *pytop = ytop; *pybot = ybot; nmin = 4; /* minimum number of lines required in each half */ both_halves = (ntop >= nmin) && (nbot >= nmin); fraction = (l_float32)(ybot - ytop) / (l_float32)h; if (both_halves && fraction > 0.50) return 1; return 0; } /*! * \brief dewarpLinearLSF() * * \param[in] ptad left or right end points of longest lines * \param[out] pa coeff a of LSF: y = ax + b * \param[out] pb coeff b of LSF: y = ax + b * \param[out] pmederr [optional] median error * \return 0 if OK, 1 on error. * *
 * Notes:
 *      (1) This is used for finding the left or right sides of the text
 *          block, computed as a best-fit line.  Only the longest lines
 *          are input, so there are no outlier line ends.
 *      (2) The ptas for the end points all have x and y swapped.
 * 
*/ static l_int32 dewarpLinearLSF(PTA *ptad, l_float32 *pa, l_float32 *pb, l_float32 *pmederr) { l_int32 i, n; l_float32 x, y, xp, c0, c1; NUMA *naerr; PROCNAME("dewarpLinearLSF"); if (pmederr) *pmederr = 0.0; if (!pa || !pb) return ERROR_INT("not all ptrs are defined", procName, 1); *pa = *pb = 0.0; if (!ptad) return ERROR_INT("ptad not defined", procName, 1); /* Fit to the longest lines */ ptaGetLinearLSF(ptad, &c1, &c0, NULL); *pa = c1; *pb = c0; /* Optionally, find the median error */ if (pmederr) { n = ptaGetCount(ptad); naerr = numaCreate(n); for (i = 0; i < n; i++) { ptaGetPt(ptad, i, &y, &xp); applyLinearFit(c1, c0, y, &x); numaAddNumber(naerr, L_ABS(x - xp)); } numaGetMedian(naerr, pmederr); numaDestroy(&naerr); } return 0; } /*! * \brief dewarpQuadraticLSF() * * \param[in] ptad left or right end points of longest lines * \param[out] pa coeff a of LSF: y = ax^2 + bx + c * \param[out] pb coeff b of LSF: y = ax^2 + bx + c * \param[out] pc coeff c of LSF: y = ax^2 + bx + c * \param[out] pmederr [optional] median error * \return 0 if OK, 1 on error. * *
 * Notes:
 *      (1) This is used for finding the left or right sides of the text
 *          block, computed as a best-fit quadratic curve.  Only the
 *          longest lines are input, so there are no outlier line ends.
 *      (2) The ptas for the end points all have x and y swapped.
 * 
*/ static l_int32 dewarpQuadraticLSF(PTA *ptad, l_float32 *pa, l_float32 *pb, l_float32 *pc, l_float32 *pmederr) { l_int32 i, n; l_float32 x, y, xp, c0, c1, c2; NUMA *naerr; PROCNAME("dewarpQuadraticLSF"); if (pmederr) *pmederr = 0.0; if (!pa || !pb || !pc) return ERROR_INT("not all ptrs are defined", procName, 1); *pa = *pb = *pc = 0.0; if (!ptad) return ERROR_INT("ptad not defined", procName, 1); /* Fit to the longest lines */ ptaGetQuadraticLSF(ptad, &c2, &c1, &c0, NULL); *pa = c2; *pb = c1; *pc = c0; /* Optionally, find the median error */ if (pmederr) { n = ptaGetCount(ptad); naerr = numaCreate(n); for (i = 0; i < n; i++) { ptaGetPt(ptad, i, &y, &xp); applyQuadraticFit(c2, c1, c0, y, &x); numaAddNumber(naerr, L_ABS(x - xp)); } numaGetMedian(naerr, pmederr); numaDestroy(&naerr); } return 0; } /*----------------------------------------------------------------------* * Build disparity model for slope near binding * *----------------------------------------------------------------------*/ /*! * \brief dewarpFindHorizSlopeDisparity() * * \param[in] dew * \param[in] pixb 1 bpp, with vert and horiz disparity removed * \param[in] fractthresh threshold fractional difference in density * \param[in] parity 0 if even page, 1 if odd page * \return 0 if OK, 1 on error * *
 * Notes:
 *      (1) %fractthresh is a threshold on the fractional difference in stroke
 *          density between between left and right sides.  Process this
 *          disparity only if the absolute value of the fractional
 *          difference equals or exceeds this threshold.
 *      (2) %parity indicates where the binding is: on the left for
 *          %parity == 0 and on the right for %parity == 1.
 *      (3) This takes a 1 bpp %pixb where both vertical and horizontal
 *          disparity have been applied, so the text lines are straight and,
 *          more importantly, the line end points are vertically aligned.
 *          It estimates the foreshortening of the characters on the
 *          binding side, and if significant, computes a one-dimensional
 *          horizontal disparity function to compensate.
 *      (4) The first attempt was to use the average width of the
 *          connected components (c.c.) in vertical slices.  This does not work
 *          reliably, because the horizontal compression of the text is
 *          often accompanied by horizontal joining of c.c.
 *      (5) We use the density of vertical strokes, measured by first using
 *          a vertical opening, which improves the signal.  The result
 *          is relatively insensitive to the size of the opening; we use
 *          a 10-pixel opening.  The relative density is measured by
 *          finding the number of c.c. in a full height sliding window
 *          of width 50 pixels, and compute every 25 pixels.  Similar results
 *          are obtained counting c.c. that either intersect the window
 *          or are fully contained within it.
 *      (6) Debug output goes to /tmp/lept/dewmod/ for collection into a pdf.
 * 
*/ l_ok dewarpFindHorizSlopeDisparity(L_DEWARP *dew, PIX *pixb, l_float32 fractthresh, l_int32 parity) { l_int32 i, j, x, n1, n2, nb, ne, count, w, h, ival, prev; l_int32 istart, iend, first, last, x0, x1, nx, ny; l_float32 fract, delta, sum, aveval, fval, del, denom; l_float32 ca, cb, cc, cd, ce, y; BOX *box; BOXA *boxa1, *boxa2; GPLOT *gplot; NUMA *na1, *na2, *na3, *na4, *nasum; PIX *pix1; PTA *pta1; FPIX *fpix; PROCNAME("dewarpFindHorizSlopeDisparity"); if (!dew) return ERROR_INT("dew not defined", procName, 1); if (!dew->vvalid || !dew->hvalid) return ERROR_INT("invalid vert or horiz disparity model", procName, 1); if (!pixb || pixGetDepth(pixb) != 1) return ERROR_INT("pixb not defined or not 1 bpp", procName, 1); if (dew->debug) L_INFO("finding slope horizontal disparity\n", procName); /* Find the bounding boxes of the vertical strokes; remove noise */ pix1 = pixMorphSequence(pixb, "o1.10", 0); pixDisplay(pix1, 100, 100); boxa1 = pixConnCompBB(pix1, 4); boxa2 = boxaSelectBySize(boxa1, 0, 5, L_SELECT_HEIGHT, L_SELECT_IF_GT, NULL); nb = boxaGetCount(boxa2); lept_stderr("number of components: %d\n", nb); boxaDestroy(&boxa1); /* Estimate the horizontal density of vertical strokes */ na1 = numaCreate(0); numaSetParameters(na1, 0, 25); pixGetDimensions(pixb, &w, &h, NULL); for (x = 0; x + 50 < w; x += 25) { box = boxCreate(x, 0, 50, h); boxaContainedInBoxCount(boxa2, box, &count); numaAddNumber(na1, count); boxDestroy(&box); } if (dew->debug) { lept_mkdir("lept/dew"); gplotSimple1(na1, GPLOT_PNG, "/tmp/lept/dew/0091", NULL); lept_mv("/tmp/lept/dew/0091.png", "lept/dewmod", NULL, NULL); pixWriteDebug("/tmp/lept/dewmod/0090.png", pix1, IFF_PNG); } pixDestroy(&pix1); boxaDestroy(&boxa2); /* Find the left and right end local maxima; if the difference * is small, quit. */ n1 = numaGetCount(na1); prev = 0; istart = 0; first = 0; for (i = 0; i < n1; i++) { numaGetIValue(na1, i, &ival); if (ival >= prev) { prev = ival; continue; } else { first = prev; istart = i - 1; break; } } prev = 0; last = 0; iend = n1 - 1; for (i = n1 - 1; i >= 0; i--) { numaGetIValue(na1, i, &ival); if (ival >= prev) { prev = ival; continue; } else { last = prev; iend = i + 1; break; } } na2 = numaClipToInterval(na1, istart, iend); numaDestroy(&na1); n2 = numaGetCount(na2); delta = (parity == 0) ? last - first : first - last; denom = L_MAX(1.0, (l_float32)(L_MIN(first, last))); fract = (l_float32)delta / denom; if (dew->debug) { L_INFO("Slope-disparity: first = %d, last = %d, fract = %7.3f\n", procName, first, last, fract); gplotSimple1(na2, GPLOT_PNG, "/tmp/lept/dew/0092", NULL); lept_mv("/tmp/lept/dew/0092.png", "lept/dewmod", NULL, NULL); } if (fract < fractthresh) { L_INFO("Small slope-disparity: first = %d, last = %d, fract = %7.3f\n", procName, first, last, fract); numaDestroy(&na2); return 0; } /* Find the density far from the binding, and normalize to 1. */ ne = n2 - n2 % 2; if (parity == 0) numaGetSumOnInterval(na2, 0, ne / 2 - 1, &sum); else /* parity == 1 */ numaGetSumOnInterval(na2, ne / 2, ne - 1, &sum); denom = L_MAX(1.0, (l_float32)(ne / 2)); aveval = sum / denom; na3 = numaMakeConstant(aveval, n2); numaArithOp(na2, na2, na3, L_ARITH_DIVIDE); numaDestroy(&na3); if (dew->debug) { L_INFO("Average background density: %5.1f\n", procName, aveval); gplotSimple1(na2, GPLOT_PNG, "/tmp/lept/dew/0093", NULL); lept_mv("/tmp/lept/dew/0093.png", "lept/dewmod", NULL, NULL); } /* Fit the normalized density curve to a quartic */ pta1 = numaConvertToPta1(na2); ptaWriteStream(stderr, pta1, 0); /* ptaGetQuadraticLSF(pta1, NULL, NULL, NULL, &na3); */ ptaGetQuarticLSF(pta1, &ca, &cb, &cc, &cd, &ce, &na3); ptaGetArrays(pta1, &na4, NULL); if (dew->debug) { gplot = gplotSimpleXY1(na4, na3, GPLOT_LINES, GPLOT_PNG, "/tmp/lept/dew/0094", NULL); gplotDestroy(&gplot); lept_mv("/tmp/lept/dew/0094.png", "lept/dewmod", NULL, NULL); } ptaDestroy(&pta1); /* Integrate from the high point down to 1 (or v.v) to get the * disparity needed to make the density constant. */ nasum = numaMakeConstant(0, w); /* area under the curve above 1.0 */ if (parity == 0) { for (i = n2 - 1; i >= 0; i--) { numaGetFValue(na3, i, &fval); if (fval < 1.0) break; } numaGetIValue(na4, i + 1, &x0); numaGetIValue(na4, n2 - 1, &x1); numaSetParameters(nasum, x0, 1); sum = 0.0; for (x = x0; x < x1; x++) { applyQuarticFit(ca, cb, cc, cd, ce, (l_float32)x, &y); sum += (y - 1.0); numaReplaceNumber(nasum, x, sum); } for (x = x1; x < w; x++) numaReplaceNumber(nasum, x, sum); } else { /* parity == 1 */ for (i = 0; i < n2; i++) { numaGetFValue(na3, i, &fval); if (fval < 1.0) break; } numaGetIValue(na4, 0, &x0); numaGetIValue(na4, i - 1, &x1); numaSetParameters(nasum, x0, 1); sum = 0.0; for (x = x1; x >= x0; x--) { applyQuarticFit(ca, cb, cc, cd, ce, (l_float32)x, &y); sum += (y - 1.0); numaReplaceNumber(nasum, x, sum); } for (x = x0; x >= 0; x--) numaReplaceNumber(nasum, x, sum); } /* Save the result in a fpix at the specified subsampling */ nx = dew->nx; ny = dew->ny; fpix = fpixCreate(nx, ny); del = (l_float32)w / (l_float32)nx; for (i = 0; i < ny; i++) { for (j = 0; j < nx; j++) { x = del * j; numaGetFValue(nasum, x, &fval); fpixSetPixel(fpix, j, i, fval); } } dew->sampydispar = fpix; dew->ysuccess = 1; numaDestroy(&na2); numaDestroy(&na3); numaDestroy(&na4); numaDestroy(&nasum); return 0; } /*----------------------------------------------------------------------* * Build line disparity model * *----------------------------------------------------------------------*/ /*! * \brief dewarpBuildLineModel() * * \param[in] dew * \param[in] opensize size of opening to remove perpendicular lines * \param[in] debugfile use NULL to skip writing this * \return 0 if OK, 1 if unable to build the model or on error * *
 * Notes:
 *      (1) This builds the horizontal and vertical disparity arrays
 *          for an input of ruled lines, typically for calibration.
 *          In book scanning, you could lay the ruled paper over a page.
 *          Then for that page and several below it, you can use the
 *          disparity correction of the line model to dewarp the pages.
 *      (2) The dew has been initialized with the image of ruled lines.
 *          These lines must be continuous, but we do a small amount
 *          of pre-processing here to insure that.
 *      (3) %opensize is typically about 8.  It must be larger than
 *          the thickness of the lines to be extracted.  This is the
 *          default value, which is applied if %opensize < 3.
 *      (4) Sets vsuccess = 1 and hsuccess = 1 if the vertical and/or
 *          horizontal disparity arrays build.
 *      (5) Similar to dewarpBuildPageModel(), except here the vertical
 *          and horizontal disparity arrays are both built from ruled lines.
 *          See notes there.
 * 
*/ l_ok dewarpBuildLineModel(L_DEWARP *dew, l_int32 opensize, const char *debugfile) { char buf[64]; l_int32 i, j, bx, by, ret, nlines; BOXA *boxa; PIX *pixs, *pixh, *pixv, *pix, *pix1, *pix2; PIXA *pixa1, *pixa2; PTA *pta; PTAA *ptaa1, *ptaa2; PROCNAME("dewarpBuildLineModel"); if (!dew) return ERROR_INT("dew not defined", procName, 1); if (opensize < 3) { L_WARNING("opensize should be >= 3; setting to 8\n", procName); opensize = 8; /* default */ } dew->debug = (debugfile) ? 1 : 0; dew->vsuccess = dew->hsuccess = 0; pixs = dew->pixs; if (debugfile) { lept_rmdir("lept/dewline"); /* erase previous images */ lept_mkdir("lept/dewline"); lept_rmdir("lept/dewmod"); /* erase previous images */ lept_mkdir("lept/dewmod"); lept_mkdir("lept/dewarp"); pixDisplayWithTitle(pixs, 0, 0, "pixs", 1); pixWriteDebug("/tmp/lept/dewline/001.png", pixs, IFF_PNG); } /* Extract and solidify the horizontal and vertical lines. We use * the horizontal lines to derive the vertical disparity, and v.v. * Both disparities are computed using the vertical disparity * algorithm; the horizontal disparity is found from the * vertical lines by rotating them clockwise by 90 degrees. * On the first pass, we compute the horizontal disparity, from * the vertical lines, by rotating them by 90 degrees (so they * are horizontal) and computing the vertical disparity on them; * we rotate the resulting fpix array for the horizontal disparity * back by -90 degrees. On the second pass, we compute the vertical * disparity from the horizontal lines in the usual fashion. */ snprintf(buf, sizeof(buf), "d1.3 + c%d.1 + o%d.1", opensize - 2, opensize); pixh = pixMorphSequence(pixs, buf, 0); /* horiz */ snprintf(buf, sizeof(buf), "d3.1 + c1.%d + o1.%d", opensize - 2, opensize); pix1 = pixMorphSequence(pixs, buf, 0); /* vert */ pixv = pixRotateOrth(pix1, 1); /* vert rotated to horizontal */ pixa1 = pixaCreate(2); pixaAddPix(pixa1, pixv, L_INSERT); /* get horizontal disparity first */ pixaAddPix(pixa1, pixh, L_INSERT); pixDestroy(&pix1); /*--------------------------------------------------------------*/ /* Process twice: first for horiz disparity, then for vert */ /*--------------------------------------------------------------*/ for (i = 0; i < 2; i++) { pix = pixaGetPix(pixa1, i, L_CLONE); pixDisplay(pix, 0, 900); boxa = pixConnComp(pix, &pixa2, 8); nlines = boxaGetCount(boxa); boxaDestroy(&boxa); if (nlines < dew->minlines) { L_WARNING("only found %d lines\n", procName, nlines); pixDestroy(&pix); pixaDestroy(&pixa1); continue; } /* Identify the pixels along the skeleton of each line */ ptaa1 = ptaaCreate(nlines); for (j = 0; j < nlines; j++) { pixaGetBoxGeometry(pixa2, j, &bx, &by, NULL, NULL); pix1 = pixaGetPix(pixa2, j, L_CLONE); pta = dewarpGetMeanVerticals(pix1, bx, by); ptaaAddPta(ptaa1, pta, L_INSERT); pixDestroy(&pix1); } pixaDestroy(&pixa2); if (debugfile) { pix1 = pixConvertTo32(pix); pix2 = pixDisplayPtaa(pix1, ptaa1); snprintf(buf, sizeof(buf), "/tmp/lept/dewline/%03d.png", 2 + 2 * i); pixWriteDebug(buf, pix2, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); } /* Remove all lines that are not at least 0.75 times the length * of the longest line. */ ptaa2 = dewarpRemoveShortLines(pix, ptaa1, 0.75, DEBUG_SHORT_LINES); if (debugfile) { pix1 = pixConvertTo32(pix); pix2 = pixDisplayPtaa(pix1, ptaa2); snprintf(buf, sizeof(buf), "/tmp/lept/dewline/%03d.png", 3 + 2 * i); pixWriteDebug(buf, pix2, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); } ptaaDestroy(&ptaa1); nlines = ptaaGetCount(ptaa2); if (nlines < dew->minlines) { pixDestroy(&pix); ptaaDestroy(&ptaa2); L_WARNING("%d lines: too few to build model\n", procName, nlines); continue; } /* Get the sampled 'vertical' disparity from the textline * centers. The disparity array will push pixels vertically * so that each line is flat and centered at the y-position * of the mid-point. */ ret = dewarpFindVertDisparity(dew, ptaa2, 1 - i); /* If i == 0, move the result to the horizontal disparity, * rotating it back by -90 degrees. */ if (i == 0) { /* horizontal disparity, really */ if (ret) { L_WARNING("horizontal disparity not built\n", procName); } else { L_INFO("hsuccess = 1\n", procName); dew->samphdispar = fpixRotateOrth(dew->sampvdispar, 3); fpixDestroy(&dew->sampvdispar); if (debugfile) lept_mv("/tmp/lept/dewarp/vert_disparity.pdf", "lept/dewarp", "horiz_disparity.pdf", NULL); } dew->hsuccess = dew->vsuccess; dew->vsuccess = 0; } else { /* i == 1 */ if (ret) L_WARNING("vertical disparity not built\n", procName); else L_INFO("vsuccess = 1\n", procName); } ptaaDestroy(&ptaa2); pixDestroy(&pix); } pixaDestroy(&pixa1); /* Debug output */ if (debugfile) { if (dew->vsuccess == 1) { dewarpPopulateFullRes(dew, NULL, 0, 0); pix1 = fpixRenderContours(dew->fullvdispar, 3.0, 0.15f); pixWriteDebug("/tmp/lept/dewline/006.png", pix1, IFF_PNG); pixDisplay(pix1, 1000, 0); pixDestroy(&pix1); } if (dew->hsuccess == 1) { pix1 = fpixRenderContours(dew->fullhdispar, 3.0, 0.15f); pixWriteDebug("/tmp/lept/dewline/007.png", pix1, IFF_PNG); pixDisplay(pix1, 1000, 0); pixDestroy(&pix1); } convertFilesToPdf("/tmp/lept/dewline", NULL, 135, 1.0, 0, 0, "Dewarp Build Line Model", debugfile); lept_stderr("pdf file: %s\n", debugfile); } return 0; } /*----------------------------------------------------------------------* * Query model status * *----------------------------------------------------------------------*/ /*! * \brief dewarpaModelStatus() * * \param[in] dewa * \param[in] pageno * \param[out] pvsuccess [optional] 1 on success * \param[out] phsuccess [optional] 1 on success * \return 0 if OK, 1 on error * *
 * Notes:
 *      (1) This tests if a model has been built, not if it is valid.
 * 
*/ l_ok dewarpaModelStatus(L_DEWARPA *dewa, l_int32 pageno, l_int32 *pvsuccess, l_int32 *phsuccess) { L_DEWARP *dew; PROCNAME("dewarpaModelStatus"); if (pvsuccess) *pvsuccess = 0; if (phsuccess) *phsuccess = 0; if (!dewa) return ERROR_INT("dewa not defined", procName, 1); if ((dew = dewarpaGetDewarp(dewa, pageno)) == NULL) return ERROR_INT("dew not retrieved", procName, 1); if (pvsuccess) *pvsuccess = dew->vsuccess; if (phsuccess) *phsuccess = dew->hsuccess; return 0; } /*----------------------------------------------------------------------* * Rendering helpers * *----------------------------------------------------------------------*/ /*! * \brief pixRenderMidYs() * * \param[in] pixs 32 bpp * \param[in] namidys y location of reference lines for vertical disparity * \param[in] linew width of rendered line; typ 2 * \return 0 if OK, 1 on error */ static l_int32 pixRenderMidYs(PIX *pixs, NUMA *namidys, l_int32 linew) { l_int32 i, n, w, yval, rval, gval, bval; PIXCMAP *cmap; PROCNAME("pixRenderMidYs"); if (!pixs) return ERROR_INT("pixs not defined", procName, 1); if (!namidys) return ERROR_INT("namidys not defined", procName, 1); w = pixGetWidth(pixs); n = numaGetCount(namidys); cmap = pixcmapCreateRandom(8, 0, 0); for (i = 0; i < n; i++) { pixcmapGetColor(cmap, i % 256, &rval, &gval, &bval); numaGetIValue(namidys, i, &yval); pixRenderLineArb(pixs, 0, yval, w, yval, linew, rval, gval, bval); } pixcmapDestroy(&cmap); return 0; } /*! * \brief pixRenderHorizEndPoints() * * \param[in] pixs 32 bpp * \param[in] ptal left side line end points * \param[in] ptar right side line end points * \param[in] color 0xrrggbb00 * \return 0 if OK, 1 on error */ static l_int32 pixRenderHorizEndPoints(PIX *pixs, PTA *ptal, PTA *ptar, l_uint32 color) { PIX *pixcirc; PTA *ptalt, *ptart, *ptacirc; PROCNAME("pixRenderHorizEndPoints"); if (!pixs) return ERROR_INT("pixs not defined", procName, 1); if (!ptal || !ptar) return ERROR_INT("ptal and ptar not both defined", procName, 1); ptacirc = generatePtaFilledCircle(5); pixcirc = pixGenerateFromPta(ptacirc, 11, 11); ptalt = ptaTranspose(ptal); ptart = ptaTranspose(ptar); pixDisplayPtaPattern(pixs, pixs, ptalt, pixcirc, 5, 5, color); pixDisplayPtaPattern(pixs, pixs, ptart, pixcirc, 5, 5, color); ptaDestroy(&ptacirc); ptaDestroy(&ptalt); ptaDestroy(&ptart); pixDestroy(&pixcirc); return 0; }