// Returns the maximum strokewidth in the given binary image by doubling // the maximum of the distance function. static int MaxStrokeWidth(Pix* pix) { Pix* dist_pix = pixDistanceFunction(pix, 4, 8, L_BOUNDARY_BG); int width = pixGetWidth(dist_pix); int height = pixGetHeight(dist_pix); int wpl = pixGetWpl(dist_pix); l_uint32* data = pixGetData(dist_pix); // Find the maximum value in the distance image. int max_dist = 0; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { int pixel = GET_DATA_BYTE(data, x); if (pixel > max_dist) max_dist = pixel; } data += wpl; } pixDestroy(&dist_pix); return max_dist * 2; }
/*! * \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 * * <pre> * 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. * </pre> */ l_int32 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.1, &first, &last); na2 = numaClipToInterval(na1, 0, last); numaWriteStream(stderr, 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); fprintf(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; }
static void TestDistance(PIXA *pixa, PIX *pixs, l_int32 conn, l_int32 depth, l_int32 bc, l_int32 *pcount, L_REGPARAMS *rp) { PIX *pixt1, *pixt2, *pixt3, *pixt4, *pixt5; /* Test the distance function and display */ pixInvert(pixs, pixs); pixt1 = pixDistanceFunction(pixs, conn, depth, bc); regTestWritePixAndCheck(pixt1, IFF_PNG, pcount, rp); pixSaveTiled(pixt1, pixa, 1, 1, 20, 0); pixInvert(pixs, pixs); pixt2 = pixMaxDynamicRange(pixt1, L_LOG_SCALE); regTestWritePixAndCheck(pixt2, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt2, pixa, 1, 0, 20, 0); pixDestroy(&pixt1); pixDestroy(&pixt2); /* Test the distance function and display with contour rendering */ pixInvert(pixs, pixs); pixt1 = pixDistanceFunction(pixs, conn, depth, bc); regTestWritePixAndCheck(pixt1, IFF_PNG, pcount, rp); pixSaveTiled(pixt1, pixa, 1, 1, 20, 0); pixInvert(pixs, pixs); pixt2 = pixRenderContours(pixt1, 2, 4, 1); /* binary output */ regTestWritePixAndCheck(pixt2, IFF_PNG, pcount, rp); pixSaveTiled(pixt2, pixa, 1, 0, 20, 0); pixt3 = pixRenderContours(pixt1, 2, 4, depth); pixt4 = pixMaxDynamicRange(pixt3, L_LINEAR_SCALE); regTestWritePixAndCheck(pixt4, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt4, pixa, 1, 0, 20, 0); pixt5 = pixMaxDynamicRange(pixt3, L_LOG_SCALE); regTestWritePixAndCheck(pixt5, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt5, pixa, 1, 0, 20, 0); pixDestroy(&pixt1); pixDestroy(&pixt2); pixDestroy(&pixt3); pixDestroy(&pixt4); pixDestroy(&pixt5); /* Label all pixels in each c.c. with a color equal to the * max distance of any pixel within that c.c. from the bg. * Note that we've normalized so the dynamic range extends * to 255. For the image here, each unit of distance is * represented by about 21 grayscale units. The largest * distance is 12. */ if (depth == 8) { pixt1 = pixDistanceFunction(pixs, conn, depth, bc); pixt4 = pixMaxDynamicRange(pixt1, L_LOG_SCALE); regTestWritePixAndCheck(pixt4, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt4, pixa, 1, 1, 20, 0); pixt2 = pixCreateTemplate(pixt1); pixSetMasked(pixt2, pixs, 255); regTestWritePixAndCheck(pixt2, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt2, pixa, 1, 0, 20, 0); pixSeedfillGray(pixt1, pixt2, 4); pixt3 = pixMaxDynamicRange(pixt1, L_LINEAR_SCALE); regTestWritePixAndCheck(pixt3, IFF_JFIF_JPEG, pcount, rp); pixSaveTiled(pixt3, pixa, 1, 0, 20, 0); pixDestroy(&pixt1); pixDestroy(&pixt2); pixDestroy(&pixt3); pixDestroy(&pixt4); } return; }
/********************************************************************** * SetBlobStrokeWidth * * Set the horizontal and vertical stroke widths in the blob. **********************************************************************/ void SetBlobStrokeWidth(Pix* pix, BLOBNBOX* blob) { // Cut the blob rectangle into a Pix. int pix_height = pixGetHeight(pix); const TBOX& box = blob->bounding_box(); int width = box.width(); int height = box.height(); Box* blob_pix_box = boxCreate(box.left(), pix_height - box.top(), width, height); Pix* pix_blob = pixClipRectangle(pix, blob_pix_box, nullptr); boxDestroy(&blob_pix_box); Pix* dist_pix = pixDistanceFunction(pix_blob, 4, 8, L_BOUNDARY_BG); pixDestroy(&pix_blob); // Compute the stroke widths. uint32_t* data = pixGetData(dist_pix); int wpl = pixGetWpl(dist_pix); // Horizontal width of stroke. STATS h_stats(0, width + 1); for (int y = 0; y < height; ++y) { uint32_t* pixels = data + y*wpl; int prev_pixel = 0; int pixel = GET_DATA_BYTE(pixels, 0); for (int x = 1; x < width; ++x) { int next_pixel = GET_DATA_BYTE(pixels, x); // We are looking for a pixel that is equal to its vertical neighbours, // yet greater than its left neighbour. if (prev_pixel < pixel && (y == 0 || pixel == GET_DATA_BYTE(pixels - wpl, x - 1)) && (y == height - 1 || pixel == GET_DATA_BYTE(pixels + wpl, x - 1))) { if (pixel > next_pixel) { // Single local max, so an odd width. h_stats.add(pixel * 2 - 1, 1); } else if (pixel == next_pixel && x + 1 < width && pixel > GET_DATA_BYTE(pixels, x + 1)) { // Double local max, so an even width. h_stats.add(pixel * 2, 1); } } prev_pixel = pixel; pixel = next_pixel; } } // Vertical width of stroke. STATS v_stats(0, height + 1); for (int x = 0; x < width; ++x) { int prev_pixel = 0; int pixel = GET_DATA_BYTE(data, x); for (int y = 1; y < height; ++y) { uint32_t* pixels = data + y*wpl; int next_pixel = GET_DATA_BYTE(pixels, x); // We are looking for a pixel that is equal to its horizontal neighbours, // yet greater than its upper neighbour. if (prev_pixel < pixel && (x == 0 || pixel == GET_DATA_BYTE(pixels - wpl, x - 1)) && (x == width - 1 || pixel == GET_DATA_BYTE(pixels - wpl, x + 1))) { if (pixel > next_pixel) { // Single local max, so an odd width. v_stats.add(pixel * 2 - 1, 1); } else if (pixel == next_pixel && y + 1 < height && pixel > GET_DATA_BYTE(pixels + wpl, x)) { // Double local max, so an even width. v_stats.add(pixel * 2, 1); } } prev_pixel = pixel; pixel = next_pixel; } } pixDestroy(&dist_pix); // Store the horizontal and vertical width in the blob, keeping both // widths if there is enough information, otherwse only the one with // the most samples. // If there are insufficient samples, store zero, rather than using // 2*area/perimeter, as the numbers that gives do not match the numbers // from the distance method. if (h_stats.get_total() >= (width + height) / 4) { blob->set_horz_stroke_width(h_stats.ile(0.5f)); if (v_stats.get_total() >= (width + height) / 4) blob->set_vert_stroke_width(v_stats.ile(0.5f)); else blob->set_vert_stroke_width(0.0f); } else { if (v_stats.get_total() >= (width + height) / 4 || v_stats.get_total() > h_stats.get_total()) { blob->set_horz_stroke_width(0.0f); blob->set_vert_stroke_width(v_stats.ile(0.5f)); } else { blob->set_horz_stroke_width(h_stats.get_total() > 2 ? h_stats.ile(0.5f) : 0.0f); blob->set_vert_stroke_width(0.0f); } } }
/********************************************************************** * SetBlobStrokeWidth * * Set the horizontal and vertical stroke widths in the blob. **********************************************************************/ void SetBlobStrokeWidth(bool debug, BLOBNBOX* blob) { #ifdef HAVE_LIBLEPT // Cut the blob rectangle into a Pix. // TODO(rays) make the page_image a Pix so this is more direct. const TBOX& box = blob->bounding_box(); IMAGE blob_im; int width = box.width(); int height = box.height(); blob_im.create(width, height, 1); copy_sub_image(&page_image, box.left(), box.bottom(), width, height, &blob_im, 0, 0, false); Pix* pix = blob_im.ToPix(); Pix* dist_pix = pixDistanceFunction(pix, 4, 8, L_BOUNDARY_BG); if (debug) { pixWrite("cutpix.png", pix, IFF_PNG); pixWrite("distpix.png", dist_pix, IFF_PNG); } pixDestroy(&pix); // Compute the stroke widths. uinT32* data = pixGetData(dist_pix); int wpl = pixGetWpl(dist_pix); // Horizontal width of stroke. STATS h_stats(0, width + 1); for (int y = 0; y < height; ++y) { uinT32* pixels = data + y*wpl; int prev_pixel = 0; int pixel = GET_DATA_BYTE(pixels, 0); for (int x = 1; x < width; ++x) { int next_pixel = GET_DATA_BYTE(pixels, x); // We are looking for a pixel that is equal to its vertical neighbours, // yet greater than its left neighbour. if (prev_pixel < pixel && (y == 0 || pixel == GET_DATA_BYTE(pixels - wpl, x - 1)) && (y == height - 1 || pixel == GET_DATA_BYTE(pixels + wpl, x - 1))) { if (pixel > next_pixel) { // Single local max, so an odd width. h_stats.add(pixel * 2 - 1, 1); } else if (pixel == next_pixel && x + 1 < width && pixel > GET_DATA_BYTE(pixels, x + 1)) { // Double local max, so an even width. h_stats.add(pixel * 2, 1); } } prev_pixel = pixel; pixel = next_pixel; } } if (debug) { h_stats.print(stderr, true); } // Vertical width of stroke. STATS v_stats(0, height + 1); for (int x = 0; x < width; ++x) { int prev_pixel = 0; int pixel = GET_DATA_BYTE(data, x); for (int y = 1; y < height; ++y) { uinT32* pixels = data + y*wpl; int next_pixel = GET_DATA_BYTE(pixels, x); // We are looking for a pixel that is equal to its horizontal neighbours, // yet greater than its upper neighbour. if (prev_pixel < pixel && (x == 0 || pixel == GET_DATA_BYTE(pixels - wpl, x - 1)) && (x == width - 1 || pixel == GET_DATA_BYTE(pixels - wpl, x + 1))) { if (pixel > next_pixel) { // Single local max, so an odd width. v_stats.add(pixel * 2 - 1, 1); } else if (pixel == next_pixel && y + 1 < height && pixel > GET_DATA_BYTE(pixels + wpl, x)) { // Double local max, so an even width. v_stats.add(pixel * 2, 1); } } prev_pixel = pixel; pixel = next_pixel; } } if (debug) { v_stats.print(stderr, true); } pixDestroy(&dist_pix); // Store the horizontal and vertical width in the blob, keeping both // widths if there is enough information, otherwse only the one with // the most samples. // If there are insufficent samples, store zero, rather than using // 2*area/perimeter, as the numbers that gives do not match the numbers // from the distance method. if (debug) { tprintf("box=%d,%d->%d,%d, hcount=%d, vcount=%d, target=%d\n", box.left(), box.bottom(), box.right(), box.top(), h_stats.get_total(), v_stats.get_total(), (width+height) /4); tprintf("hstats median=%f, lq=%f, uq=%f, sd=%f\n", h_stats.median(), h_stats.ile(0.25f), h_stats.ile(0.75f), h_stats.sd()); tprintf("vstats median=%f, lq=%f, uq=%f, sd=%f\n", v_stats.median(), v_stats.ile(0.25f), v_stats.ile(0.75f), v_stats.sd()); } if (h_stats.get_total() >= (width + height) / 4) { blob->set_horz_stroke_width(h_stats.ile(0.5f)); if (v_stats.get_total() >= (width + height) / 4) blob->set_vert_stroke_width(v_stats.ile(0.5f)); else blob->set_vert_stroke_width(0.0f); } else { if (v_stats.get_total() >= (width + height) / 4 || v_stats.get_total() > h_stats.get_total()) { blob->set_horz_stroke_width(0.0f); blob->set_vert_stroke_width(v_stats.ile(0.5f)); } else { blob->set_horz_stroke_width(h_stats.get_total() > 2 ? h_stats.ile(0.5f) : 0.0f); blob->set_vert_stroke_width(0.0f); } } #else // Without leptonica present, use the 2*area/perimeter as an approximation. float width = 2.0f * blob->cblob()->area(); width /= blob->cblob()->perimeter(); blob->set_horz_stroke_width(width); blob->set_vert_stroke_width(width); #endif }