/*====================================================================* - 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 strokes.c *
 *
 *      Operations on 1 bpp images to:
 *      (1) measure stroke parameters, such as length and average width
 *      (2) change the average stroke width to a given value by eroding
 *          or dilating the image.
 *
 *      These operations are intended to operate on a single text
 *      character, to regularize the stroke width. It is expected
 *      that character matching by correlation, as used in the recog
 *      application, can often be improved by pre-processing both
 *      template and character images to a fixed stroke width.
 *
 *      Stroke parameter measurement
 *            l_int32      pixFindStrokeLength()
 *            l_int32      pixFindStrokeWidth()
 *            NUMA        *pixaFindStrokeWidth()
 *
 *      Stroke width regulation
 *            PIXA        *pixaModifyStrokeWidth()
 *            PIX         *pixModifyStrokeWidth()
 *            PIXA        *pixaSetStrokeWidth()
 *            PIX         *pixSetStrokeWidth()
 * 
*/ #ifdef HAVE_CONFIG_H #include #endif /* HAVE_CONFIG_H */ #include "allheaders.h" /*-----------------------------------------------------------------* * Stroke parameter measurement * *-----------------------------------------------------------------*/ /*! * \brief pixFindStrokeLength() * * \param[in] pixs 1 bpp * \param[in] tab8 [optional] table for counting fg pixels; can be NULL * \param[out] plength estimated length of the strokes * \return 0 if OK, 1 on error * *
 * Notes:
 *      (1) Returns half the number of fg boundary pixels.
 * 
*/ l_ok pixFindStrokeLength(PIX *pixs, l_int32 *tab8, l_int32 *plength) { l_int32 n; l_int32 *tab; PIX *pix1; PROCNAME("pixFindStrokeLength"); if (!plength) return ERROR_INT("&length not defined", procName, 1); *plength = 0; if (!pixs) return ERROR_INT("pixs not defined", procName, 1); pix1 = pixExtractBoundary(pixs, 1); tab = (tab8) ? tab8 : makePixelSumTab8(); pixCountPixels(pix1, &n, tab); *plength = n / 2; if (!tab8) LEPT_FREE(tab); pixDestroy(&pix1); return 0; } /*! * \brief pixFindStrokeWidth() * * \param[in] pixs 1 bpp * \param[in] thresh fractional count threshold relative to distance 1 * \param[in] tab8 [optional] table for counting fg pixels; can be NULL * \param[out] pwidth estimated width of the strokes * \param[out] pnahisto [optional] histo of pixel distances from bg * \return 0 if OK, 1 on error * *
 * Notes:
 *      (1) This uses two methods to estimate the stroke width:
 *          (a) half the fg boundary length
 *          (b) a value derived from the histogram of the fg distance transform
 *      (2) Distance is measured in 8-connected
 *      (3) %thresh is the minimum fraction N(dist=d)/N(dist=1) of pixels
 *          required to determine if the pixels at distance d are above
 *          the noise. It is typically about 0.15.
 * 
*/ l_ok pixFindStrokeWidth(PIX *pixs, l_float32 thresh, l_int32 *tab8, l_float32 *pwidth, NUMA **pnahisto) { l_int32 i, n, count, length, first, last; l_int32 *tab; l_float32 width1, width2, ratio, extra; l_float32 *fa; NUMA *na1, *na2; PIX *pix1; PROCNAME("pixFindStrokeWidth"); if (!pwidth) return ERROR_INT("&width not defined", procName, 1); *pwidth = 0; if (!pixs) return ERROR_INT("pixs not defined", procName, 1); tab = (tab8) ? tab8 : makePixelSumTab8(); /* ------- Method 1: via boundary length ------- */ /* The computed stroke length is a bit larger than that actual * length, because of the addition of the 'caps' at the * stroke ends. Therefore the computed width is a bit * smaller than the average width. */ pixFindStrokeLength(pixs, tab8, &length); pixCountPixels(pixs, &count, tab8); width1 = (l_float32)count / (l_float32)length; /* ------- Method 2: via distance transform ------- */ /* First get the histogram of distances */ pix1 = pixDistanceFunction(pixs, 8, 8, L_BOUNDARY_BG); na1 = pixGetGrayHistogram(pix1, 1); pixDestroy(&pix1); numaGetNonzeroRange(na1, 0.1f, &first, &last); na2 = numaClipToInterval(na1, 0, last); numaWriteStderr(na2); /* Find the bucket with the largest distance whose contents * exceed the threshold. */ fa = numaGetFArray(na2, L_NOCOPY); n = numaGetCount(na2); for (i = n - 1; i > 0; i--) { ratio = fa[i] / fa[1]; if (ratio > thresh) break; } /* Let the last skipped bucket contribute to the stop bucket. * This is the 'extra' term below. The result may be a slight * over-correction, so the computed width may be a bit larger * than the average width. */ extra = (i < n - 1) ? fa[i + 1] / fa[1] : 0; width2 = 2.0 * (i - 1.0 + ratio + extra); lept_stderr("width1 = %5.2f, width2 = %5.2f\n", width1, width2); /* Average the two results */ *pwidth = (width1 + width2) / 2.0; if (!tab8) LEPT_FREE(tab); numaDestroy(&na1); if (pnahisto) *pnahisto = na2; else numaDestroy(&na2); return 0; } /*! * \brief pixaFindStrokeWidth() * * \param[in] pixa of 1 bpp images * \param[in] thresh fractional count threshold relative to distance 1 * \param[in] tab8 [optional] table for counting fg pixels; can be NULL * \param[in] debug 1 for debug output; 0 to skip * \return na array of stroke widths for each pix in %pixa; NULL on error * *
 * Notes:
 *      (1) See pixFindStrokeWidth() for details.
 * 
*/ NUMA * pixaFindStrokeWidth(PIXA *pixa, l_float32 thresh, l_int32 *tab8, l_int32 debug) { l_int32 i, n, same, maxd; l_int32 *tab; l_float32 width; NUMA *na; PIX *pix; PROCNAME("pixaFindStrokeWidth"); if (!pixa) return (NUMA *)ERROR_PTR("pixa not defined", procName, NULL); pixaVerifyDepth(pixa, &same, &maxd); if (maxd > 1) return (NUMA *)ERROR_PTR("pix not all 1 bpp", procName, NULL); tab = (tab8) ? tab8 : makePixelSumTab8(); n = pixaGetCount(pixa); na = numaCreate(n); for (i = 0; i < n; i++) { pix = pixaGetPix(pixa, i, L_CLONE); pixFindStrokeWidth(pix, thresh, tab8, &width, NULL); numaAddNumber(na, width); pixDestroy(&pix); } if (!tab8) LEPT_FREE(tab); return na; } /*-----------------------------------------------------------------* * Change stroke width * *-----------------------------------------------------------------*/ /*! * \brief pixaModifyStrokeWidth() * * \param[in] pixas of 1 bpp pix * \param[out] targetw desired width for strokes in each pix * \return pixa with modified stroke widths, or NULL on error */ PIXA * pixaModifyStrokeWidth(PIXA *pixas, l_float32 targetw) { l_int32 i, n, same, maxd; l_float32 width; NUMA *na; PIX *pix1, *pix2; PIXA *pixad; PROCNAME("pixaModifyStrokeWidth"); if (!pixas) return (PIXA *)ERROR_PTR("pixas not defined", procName, NULL); if (targetw < 1) return (PIXA *)ERROR_PTR("target width < 1", procName, NULL); pixaVerifyDepth(pixas, &same, &maxd); if (maxd > 1) return (PIXA *)ERROR_PTR("pix not all 1 bpp", procName, NULL); na = pixaFindStrokeWidth(pixas, 0.1f, NULL, 0); n = pixaGetCount(pixas); pixad = pixaCreate(n); for (i = 0; i < n; i++) { pix1 = pixaGetPix(pixas, i, L_CLONE); numaGetFValue(na, i, &width); pix2 = pixModifyStrokeWidth(pix1, width, targetw); pixaAddPix(pixad, pix2, L_INSERT); pixDestroy(&pix1); } numaDestroy(&na); return pixad; } /*! * \brief pixModifyStrokeWidth() * * \param[in] pixs of 1 bpp pix * \param[in] width measured average stroke width * \param[in] targetw desired stroke width * \return pix with modified stroke width, or NULL on error */ PIX * pixModifyStrokeWidth(PIX *pixs, l_float32 width, l_float32 targetw) { char buf[32]; l_int32 diff, size; PROCNAME("pixModifyStrokeWidth"); if (!pixs || (pixGetDepth(pixs) != 1)) return (PIX *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL); if (targetw < 1) return (PIX *)ERROR_PTR("target width < 1", procName, NULL); diff = lept_roundftoi(targetw - width); if (diff == 0) return pixCopy(NULL, pixs); size = L_ABS(diff) + 1; if (diff < 0) /* erode */ snprintf(buf, sizeof(buf), "e%d.%d", size, size); else /* diff > 0; dilate */ snprintf(buf, sizeof(buf), "d%d.%d", size, size); return pixMorphSequence(pixs, buf, 0); } /*! * \brief pixaSetStrokeWidth() * * \param[in] pixas of 1 bpp pix * \param[in] width set stroke width to this value, in [1 ... 100]. * \param[in] thinfirst 1 to thin all pix to a skeleton first; 0 to skip * \param[in] connectivity 4 or 8, to be used if %thinfirst == 1 * \return pixa with all stroke widths being %width, or NULL on error * *
 * Notes:
 *      (1) If %thinfirst == 1, thin to a skeleton using the specified
 *          %connectivity.  Use %thinfirst == 0 if all pix in pixas
 *          have already been thinned as far as possible.
 *      (2) The image is dilated to the required %width.  This dilation
 *          is not connectivity preserving, so this is typically
 *          used in a situation where merging of c.c. in the individual
 *          pix is not a problem; e.g., where each pix is a single c.c.
 * 
*/ PIXA * pixaSetStrokeWidth(PIXA *pixas, l_int32 width, l_int32 thinfirst, l_int32 connectivity) { l_int32 i, n, maxd, same; PIX *pix1, *pix2; PIXA *pixad; PROCNAME("pixaSetStrokeWidth"); if (!pixas) return (PIXA *)ERROR_PTR("pixas not defined", procName, NULL); if (width < 1 || width > 100) return (PIXA *)ERROR_PTR("width not in [1 ... 100]", procName, NULL); if (connectivity != 4 && connectivity != 8) return (PIXA *)ERROR_PTR("connectivity not 4 or 8", procName, NULL); pixaVerifyDepth(pixas, &same, &maxd); if (maxd > 1) return (PIXA *)ERROR_PTR("pix are not all 1 bpp", procName, NULL); n = pixaGetCount(pixas); pixad = pixaCreate(n); for (i = 0; i < n; i++) { pix1 = pixaGetPix(pixas, i, L_CLONE); pix2 = pixSetStrokeWidth(pix1, width, thinfirst, connectivity); pixaAddPix(pixad, pix2, L_INSERT); pixDestroy(&pix1); } return pixad; } /*! * \brief pixSetStrokeWidth() * * \param[in] pixs 1 bpp * \param[in] width set stroke width to this value, in [1 ... 100]. * \param[in] thinfirst 1 to thin all pix to a skeleton first; 0 to skip * \param[in] connectivity 4 or 8, to be used if %thinfirst == 1 * \return pixd with stroke width set to %width, or NULL on error * *
 * Notes:
 *      (1) See notes in pixaSetStrokeWidth().
 *      (2) A white border of sufficient width to avoid boundary
 *          artifacts in the thickening step is added before thinning.
 *      (3) %connectivity == 8 usually gives a slightly smoother result.
 * 
*/ PIX * pixSetStrokeWidth(PIX *pixs, l_int32 width, l_int32 thinfirst, l_int32 connectivity) { char buf[16]; l_int32 border; PIX *pix1, *pix2, *pixd; PROCNAME("pixSetStrokeWidth"); if (!pixs || (pixGetDepth(pixs) != 1)) return (PIX *)ERROR_PTR("pixs undefined or not 1 bpp", procName, NULL); if (width < 1 || width > 100) return (PIX *)ERROR_PTR("width not in [1 ... 100]", procName, NULL); if (connectivity != 4 && connectivity != 8) return (PIX *)ERROR_PTR("connectivity not 4 or 8", procName, NULL); if (!thinfirst && width == 1) /* nothing to do */ return pixCopy(NULL, pixs); /* Add a white border */ border = width / 2; pix1 = pixAddBorder(pixs, border, 0); /* Thin to a skeleton */ if (thinfirst) pix2 = pixThinConnected(pix1, L_THIN_FG, connectivity, 0); else pix2 = pixClone(pix1); pixDestroy(&pix1); /* Dilate */ snprintf(buf, sizeof(buf), "D%d.%d", width, width); pixd = pixMorphSequence(pix2, buf, 0); pixCopyText(pixd, pixs); pixDestroy(&pix2); return pixd; }