// Helper to remove an enclosing circle from an image. // If there isn't one, then the image will most likely get badly mangled. // The returned pix must be pixDestroyed after use. NULL may be returned // if the image doesn't meet the trivial conditions that it uses to determine // success. static Pix* RemoveEnclosingCircle(Pix* pixs) { Pix* pixsi = pixInvert(NULL, pixs); Pix* pixc = pixCreateTemplate(pixs); pixSetOrClearBorder(pixc, 1, 1, 1, 1, PIX_SET); pixSeedfillBinary(pixc, pixc, pixsi, 4); pixInvert(pixc, pixc); pixDestroy(&pixsi); Pix* pixt = pixAnd(NULL, pixs, pixc); l_int32 max_count; pixCountConnComp(pixt, 8, &max_count); // The count has to go up before we start looking for the minimum. l_int32 min_count = MAX_INT32; Pix* pixout = NULL; for (int i = 1; i < kMaxCircleErosions; i++) { pixDestroy(&pixt); pixErodeBrick(pixc, pixc, 3, 3); pixt = pixAnd(NULL, pixs, pixc); l_int32 count; pixCountConnComp(pixt, 8, &count); if (i == 1 || count > max_count) { max_count = count; min_count = count; } else if (i > 1 && count < min_count) { min_count = count; pixDestroy(&pixout); pixout = pixCopy(NULL, pixt); // Save the best. } else if (count >= min_count) { break; // We have passed by the best. } } pixDestroy(&pixt); pixDestroy(&pixc); return pixout; }
/*! * pixGenHalftoneMask() * * Input: pixs (1 bpp, assumed to be 150 to 200 ppi) * &pixtext (<optional return> text part of pixs) * &htfound (<optional return> 1 if the mask is not empty) * debug (flag: 1 for debug output) * Return: pixd (halftone mask), or null on error */ PIX * pixGenHalftoneMask(PIX *pixs, PIX **ppixtext, l_int32 *phtfound, l_int32 debug) { l_int32 empty; PIX *pixt1, *pixt2, *pixhs, *pixhm, *pixd; PROCNAME("pixGenHalftoneMask"); if (ppixtext) *ppixtext = NULL; if (!pixs) return (PIX *)ERROR_PTR("pixs not defined", procName, NULL); if (pixGetDepth(pixs) != 1) return (PIX *)ERROR_PTR("pixs not 1 bpp", procName, NULL); /* Compute seed for halftone parts at 8x reduction */ pixt1 = pixReduceRankBinaryCascade(pixs, 4, 4, 3, 0); pixt2 = pixOpenBrick(NULL, pixt1, 5, 5); pixhs = pixExpandReplicate(pixt2, 8); /* back to 2x reduction */ pixDestroy(&pixt1); pixDestroy(&pixt2); pixDisplayWriteFormat(pixhs, debug, IFF_PNG); /* Compute mask for connected regions */ pixhm = pixCloseSafeBrick(NULL, pixs, 4, 4); pixDisplayWriteFormat(pixhm, debug, IFF_PNG); /* Fill seed into mask to get halftone mask */ pixd = pixSeedfillBinary(NULL, pixhs, pixhm, 4); #if 0 /* Moderate opening to remove thin lines, etc. */ pixOpenBrick(pixd, pixd, 10, 10); pixDisplayWrite(pixd, debug); #endif /* Check if mask is empty */ pixZero(pixd, &empty); if (phtfound) { *phtfound = 0; if (!empty) *phtfound = 1; } /* Optionally, get all pixels that are not under the halftone mask */ if (ppixtext) { if (empty) *ppixtext = pixCopy(NULL, pixs); else *ppixtext = pixSubtract(NULL, pixs, pixd); pixDisplayWriteFormat(*ppixtext, debug, IFF_PNG); } pixDestroy(&pixhs); pixDestroy(&pixhm); return pixd; }
// Helper subtracts the line_pix image from the src_pix, and removes residue // as well by removing components that touch the line, but are not in the // non_line_pix mask. It is assumed that the non_line_pix mask has already // been prepared to required accuracy. static void SubtractLinesAndResidue(Pix* line_pix, Pix* non_line_pix, int resolution, Pix* src_pix) { // First remove the lines themselves. pixSubtract(src_pix, src_pix, line_pix); // Subtract the non-lines from the image to get the residue. Pix* residue_pix = pixSubtract(NULL, src_pix, non_line_pix); // Dilate the lines so they touch the residue. Pix* fat_line_pix = pixDilateBrick(NULL, line_pix, 3, 3); // Seed fill the fat lines to get all the residue. pixSeedfillBinary(fat_line_pix, fat_line_pix, residue_pix, 8); // Subtract the residue from the original image. pixSubtract(src_pix, src_pix, fat_line_pix); pixDestroy(&fat_line_pix); pixDestroy(&residue_pix); }
PIX * MakeReplacementMask(PIX *pixs) { PIX *pix1, *pix2, *pix3, *pix4; pix1 = pixMaskOverColorPixels(pixs, 95, 3); pix2 = pixMorphSequence(pix1, "o15.15", 0); pixSeedfillBinary(pix2, pix2, pix1, 8); pix3 = pixMorphSequence(pix2, "c15.15 + d61.31", 0); pix4 = pixRemoveBorderConnComps(pix3, 8); pixXor(pix4, pix4, pix3); pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); return pix4; }
l_int32 main(int argc, char **argv) { l_int32 irval, igval, ibval; l_float32 rval, gval, bval, fract, fgfract; L_BMF *bmf; BOX *box; BOXA *boxa; FPIX *fpix; PIX *pixs, *pix1, *pix2, *pix3, *pix4, *pix5, *pix6, *pix7; PIX *pix8, *pix9, *pix10, *pix11, *pix12, *pix13, *pix14, *pix15; PIXA *pixa; L_REGPARAMS *rp; if (regTestSetup(argc, argv, &rp)) return 1; pixa = pixaCreate(0); pixs = pixRead("breviar38.150.jpg"); /* pixs = pixRead("breviar32.150.jpg"); */ pixaAddPix(pixa, pixs, L_CLONE); regTestWritePixAndCheck(rp, pixs, IFF_JFIF_JPEG); /* 0 */ pixDisplayWithTitle(pixs, 0, 0, "Input image", rp->display); /* Extract the blue component, which is small in all the text * regions, including in the highlight color region */ pix1 = pixGetRGBComponent(pixs, COLOR_BLUE); pixaAddPix(pixa, pix1, L_CLONE); regTestWritePixAndCheck(rp, pix1, IFF_JFIF_JPEG); /* 1 */ pixDisplayWithTitle(pix1, 200, 0, "Blue component", rp->display); /* Do a background normalization, with the background set to * approximately 200 */ pix2 = pixBackgroundNormSimple(pix1, NULL, NULL); pixaAddPix(pixa, pix2, L_COPY); regTestWritePixAndCheck(rp, pix2, IFF_JFIF_JPEG); /* 2 */ pixDisplayWithTitle(pix2, 400, 0, "BG normalized to 200", rp->display); /* Do a linear transform on the gray pixels, with 50 going to * black and 160 going to white. 50 is sufficiently low to * make both the red and black print quite dark. Quantize * to a few equally spaced gray levels. This is the image * to which highlight color will be applied. */ pixGammaTRC(pix2, pix2, 1.0, 50, 160); pix3 = pixThresholdOn8bpp(pix2, 7, 1); pixaAddPix(pixa, pix3, L_CLONE); regTestWritePixAndCheck(rp, pix3, IFF_JFIF_JPEG); /* 3 */ pixDisplayWithTitle(pix3, 600, 0, "Basic quantized with white bg", rp->display); /* Identify the regions of red text. First, make a mask * consisting of all pixels such that (R-B)/B is larger * than 2.0. This will have all the red, plus a lot of * the dark pixels. */ fpix = pixComponentFunction(pixs, 1.0, 0.0, -1.0, 0.0, 0.0, 1.0); pix4 = fpixThresholdToPix(fpix, 2.0); pixInvert(pix4, pix4); /* red plus some dark text */ pixaAddPix(pixa, pix4, L_CLONE); regTestWritePixAndCheck(rp, pix4, IFF_PNG); /* 4 */ pixDisplayWithTitle(pix4, 800, 0, "Red plus dark pixels", rp->display); /* Make a mask consisting of all the red and background pixels */ pix5 = pixGetRGBComponent(pixs, COLOR_RED); pix6 = pixThresholdToBinary(pix5, 128); pixInvert(pix6, pix6); /* red plus background (white) */ /* Intersect the two masks to get a mask consisting of pixels * that are almost certainly red. This is the seed. */ pix7 = pixAnd(NULL, pix4, pix6); /* red only (seed) */ pixaAddPix(pixa, pix7, L_COPY); regTestWritePixAndCheck(rp, pix7, IFF_PNG); /* 5 */ pixDisplayWithTitle(pix7, 0, 600, "Seed for red color", rp->display); /* Make the clipping mask by thresholding the image with * the background cleaned to white. */ pix8 = pixThresholdToBinary(pix2, 230); /* mask */ pixaAddPix(pixa, pix8, L_CLONE); regTestWritePixAndCheck(rp, pix8, IFF_PNG); /* 6 */ pixDisplayWithTitle(pix8, 200, 600, "Clipping mask for red components", rp->display); /* Fill into the mask from the seed */ pixSeedfillBinary(pix7, pix7, pix8, 8); /* filled: red plus touching */ regTestWritePixAndCheck(rp, pix7, IFF_PNG); /* 7 */ pixDisplayWithTitle(pix7, 400, 600, "Red component mask filled", rp->display); /* Remove long horizontal and vertical lines from the filled result */ pix9 = pixMorphSequence(pix7, "o40.1", 0); pixSubtract(pix7, pix7, pix9); /* remove long horizontal lines */ pixDestroy(&pix9); pix9 = pixMorphSequence(pix7, "o1.40", 0); pixSubtract(pix7, pix7, pix9); /* remove long vertical lines */ /* Close the regions to be colored */ pix10 = pixMorphSequence(pix7, "c5.1", 0); pixaAddPix(pixa, pix10, L_CLONE); regTestWritePixAndCheck(rp, pix10, IFF_PNG); /* 8 */ pixDisplayWithTitle(pix10, 600, 600, "Components defining regions allowing coloring", rp->display); /* Sanity check on amount to be colored. Only accept images * with less than 10% of all the pixels with highlight color */ pixForegroundFraction(pix10, &fgfract); if (fgfract >= 0.10) { L_INFO("too much highlighting: fract = %6.3f; removing it\n", rp->testname, fgfract); pixClearAll(pix10); pixSetPixel(pix10, 0, 0, 1); } /* Get the bounding boxes of the regions to be colored */ boxa = pixConnCompBB(pix10, 8); /* Get a color to paint that is representative of the * actual highlight color in the image. Scale each * color component up from the average by an amount necessary * to saturate the red. Then divide the green and * blue components by 2.0. */ pixGetAverageMaskedRGB(pixs, pix7, 0, 0, 1, L_MEAN_ABSVAL, &rval, &gval, &bval); fract = 255.0 / rval; irval = lept_roundftoi(fract * rval); igval = lept_roundftoi(fract * gval / 2.0); ibval = lept_roundftoi(fract * bval / 2.0); fprintf(stderr, "(r,g,b) = (%d,%d,%d)\n", irval, igval, ibval); /* Color the quantized gray version in the selected regions */ pix11 = pixColorGrayRegions(pix3, boxa, L_PAINT_DARK, 220, irval, igval, ibval); pixaAddPix(pixa, pix11, L_CLONE); regTestWritePixAndCheck(rp, pix11, IFF_PNG); /* 9 */ pixDisplayWithTitle(pix11, 800, 600, "Final colored result", rp->display); pixaAddPix(pixa, pixs, L_CLONE); /* Test colorization on gray and cmapped gray */ pix12 = pixColorGrayRegions(pix2, boxa, L_PAINT_DARK, 220, 0, 255, 0); pixaAddPix(pixa, pix12, L_CLONE); regTestWritePixAndCheck(rp, pix12, IFF_PNG); /* 10 */ pixDisplayWithTitle(pix12, 900, 600, "Colorizing boxa gray", rp->display); box = boxCreate(200, 200, 250, 350); pix13 = pixCopy(NULL, pix2); pixColorGray(pix13, box, L_PAINT_DARK, 220, 0, 0, 255); pixaAddPix(pixa, pix13, L_CLONE); regTestWritePixAndCheck(rp, pix13, IFF_PNG); /* 11 */ pixDisplayWithTitle(pix13, 1000, 600, "Colorizing box gray", rp->display); pix14 = pixThresholdTo4bpp(pix2, 6, 1); pix15 = pixColorGrayRegions(pix14, boxa, L_PAINT_DARK, 220, 0, 0, 255); pixaAddPix(pixa, pix15, L_CLONE); regTestWritePixAndCheck(rp, pix15, IFF_PNG); /* 12 */ pixDisplayWithTitle(pix15, 1100, 600, "Colorizing boxa cmap", rp->display); pixColorGrayCmap(pix14, box, L_PAINT_DARK, 0, 255, 255); pixaAddPix(pixa, pix14, L_CLONE); regTestWritePixAndCheck(rp, pix14, IFF_PNG); /* 13 */ pixDisplayWithTitle(pix14, 1200, 600, "Colorizing box cmap", rp->display); boxDestroy(&box); /* Generate a pdf of the intermediate results */ lept_mkdir("lept"); L_INFO("Writing to /tmp/lept/colorize.pdf\n", rp->testname); pixaConvertToPdf(pixa, 90, 1.0, 0, 0, "Colorizing highlighted text", "/tmp/lept/colorize.pdf"); pixaDestroy(&pixa); fpixDestroy(&fpix); boxDestroy(&box); boxaDestroy(&boxa); pixDestroy(&pixs); pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); pixDestroy(&pix4); pixDestroy(&pix5); pixDestroy(&pix6); pixDestroy(&pix7); pixDestroy(&pix8); pixDestroy(&pix9); pixDestroy(&pix10); pixDestroy(&pix11); pixDestroy(&pix12); pixDestroy(&pix13); pixDestroy(&pix14); pixDestroy(&pix15); /* Test the color detector */ pixa = pixaCreate(7); bmf = bmfCreate("./fonts", 4); pix1 = TestForRedColor(rp, "brev06.75.jpg", 1, bmf); /* 14 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev10.75.jpg", 0, bmf); /* 15 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev14.75.jpg", 1, bmf); /* 16 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev20.75.jpg", 1, bmf); /* 17 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev36.75.jpg", 0, bmf); /* 18 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev53.75.jpg", 1, bmf); /* 19 */ pixaAddPix(pixa, pix1, L_INSERT); pix1 = TestForRedColor(rp, "brev56.75.jpg", 1, bmf); /* 20 */ pixaAddPix(pixa, pix1, L_INSERT); /* Generate a pdf of the color detector results */ L_INFO("Writing to /tmp/lept/colordetect.pdf\n", rp->testname); pixaConvertToPdf(pixa, 45, 1.0, 0, 0, "Color detection", "/tmp/lept/colordetect.pdf"); pixaDestroy(&pixa); bmfDestroy(&bmf); return regTestCleanup(rp); }
l_int32 DoPageSegmentation(PIX *pixs, /* should be at least 300 ppi */ l_int32 which) /* 1, 2, 3, 4 */ { char buf[256]; l_int32 zero; BOXA *boxatm, *boxahm; PIX *pixr; /* image reduced to 150 ppi */ PIX *pixhs; /* image of halftone seed, 150 ppi */ PIX *pixm; /* image of mask of components, 150 ppi */ PIX *pixhm1; /* image of halftone mask, 150 ppi */ PIX *pixhm2; /* image of halftone mask, 300 ppi */ PIX *pixht; /* image of halftone components, 150 ppi */ PIX *pixnht; /* image without halftone components, 150 ppi */ PIX *pixi; /* inverted image, 150 ppi */ PIX *pixvws; /* image of vertical whitespace, 150 ppi */ PIX *pixm1; /* image of closed textlines, 150 ppi */ PIX *pixm2; /* image of refined text line mask, 150 ppi */ PIX *pixm3; /* image of refined text line mask, 300 ppi */ PIX *pixb1; /* image of text block mask, 150 ppi */ PIX *pixb2; /* image of text block mask, 300 ppi */ PIX *pixnon; /* image of non-text or halftone, 150 ppi */ PIX *pix1, *pix2, *pix3, *pix4; PIXA *pixa; PIXCMAP *cmap; PTAA *ptaa; l_int32 ht_flag = 0; l_int32 ws_flag = 0; l_int32 text_flag = 0; l_int32 block_flag = 0; PROCNAME("DoPageSegmentation"); if (which == 1) ht_flag = 1; else if (which == 2) ws_flag = 1; else if (which == 3) text_flag = 1; else if (which == 4) block_flag = 1; else return ERROR_INT("invalid parameter: not in [1...4]", procName, 1); pixa = pixaCreate(0); lept_mkdir("lept/livre"); /* Reduce to 150 ppi */ pix1 = pixScaleToGray2(pixs); if (ws_flag || ht_flag || block_flag) pixaAddPix(pixa, pix1, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/orig.gray.150.png", pix1, IFF_PNG); pixDestroy(&pix1); pixr = pixReduceRankBinaryCascade(pixs, 1, 0, 0, 0); /* Get seed for halftone parts */ pix1 = pixReduceRankBinaryCascade(pixr, 4, 4, 3, 0); pix2 = pixOpenBrick(NULL, pix1, 5, 5); pixhs = pixExpandBinaryPower2(pix2, 8); if (ht_flag) pixaAddPix(pixa, pixhs, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/htseed.150.png", pixhs, IFF_PNG); pixDestroy(&pix1); pixDestroy(&pix2); /* Get mask for connected regions */ pixm = pixCloseSafeBrick(NULL, pixr, 4, 4); if (ht_flag) pixaAddPix(pixa, pixm, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/ccmask.150.png", pixm, IFF_PNG); /* Fill seed into mask to get halftone mask */ pixhm1 = pixSeedfillBinary(NULL, pixhs, pixm, 4); if (ht_flag) pixaAddPix(pixa, pixhm1, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/htmask.150.png", pixhm1, IFF_PNG); pixhm2 = pixExpandBinaryPower2(pixhm1, 2); /* Extract halftone stuff */ pixht = pixAnd(NULL, pixhm1, pixr); if (which == 1) pixWrite("/tmp/lept/livre/ht.150.png", pixht, IFF_PNG); /* Extract non-halftone stuff */ pixnht = pixXor(NULL, pixht, pixr); if (text_flag) pixaAddPix(pixa, pixnht, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/text.150.png", pixnht, IFF_PNG); pixZero(pixht, &zero); if (zero) fprintf(stderr, "No halftone parts found\n"); else fprintf(stderr, "Halftone parts found\n"); /* Get bit-inverted image */ pixi = pixInvert(NULL, pixnht); if (ws_flag) pixaAddPix(pixa, pixi, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/invert.150.png", pixi, IFF_PNG); /* The whitespace mask will break textlines where there * is a large amount of white space below or above. * We can prevent this by identifying regions of the * inverted image that have large horizontal (bigger than * the separation between columns) and significant * vertical extent (bigger than the separation between * textlines), and subtracting this from the whitespace mask. */ pix1 = pixMorphCompSequence(pixi, "o80.60", 0); pix2 = pixSubtract(NULL, pixi, pix1); if (ws_flag) pixaAddPix(pixa, pix2, L_COPY); pixDestroy(&pix1); /* Identify vertical whitespace by opening inverted image */ pix3 = pixOpenBrick(NULL, pix2, 5, 1); /* removes thin vertical lines */ pixvws = pixOpenBrick(NULL, pix3, 1, 200); /* gets long vertical lines */ if (text_flag || ws_flag) pixaAddPix(pixa, pixvws, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/vertws.150.png", pixvws, IFF_PNG); pixDestroy(&pix2); pixDestroy(&pix3); /* Get proto (early processed) text line mask. */ /* First close the characters and words in the textlines */ pixm1 = pixCloseSafeBrick(NULL, pixnht, 30, 1); if (text_flag) pixaAddPix(pixa, pixm1, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/textmask1.150.png", pixm1, IFF_PNG); /* Next open back up the vertical whitespace corridors */ pixm2 = pixSubtract(NULL, pixm1, pixvws); if (which == 1) pixWrite("/tmp/lept/livre/textmask2.150.png", pixm2, IFF_PNG); /* Do a small opening to remove noise */ pixOpenBrick(pixm2, pixm2, 3, 3); if (text_flag) pixaAddPix(pixa, pixm2, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/textmask3.150.png", pixm2, IFF_PNG); pixm3 = pixExpandBinaryPower2(pixm2, 2); /* Join pixels vertically to make text block mask */ pixb1 = pixMorphSequence(pixm2, "c1.10 + o4.1", 0); if (block_flag) pixaAddPix(pixa, pixb1, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/textblock1.150.png", pixb1, IFF_PNG); /* Solidify the textblock mask and remove noise: * (1) For each c.c., close the blocks and dilate slightly * to form a solid mask. * (2) Small horizontal closing between components * (3) Open the white space between columns, again * (4) Remove small components */ pix1 = pixMorphSequenceByComponent(pixb1, "c30.30 + d3.3", 8, 0, 0, NULL); pixCloseSafeBrick(pix1, pix1, 10, 1); if (block_flag) pixaAddPix(pixa, pix1, L_COPY); pix2 = pixSubtract(NULL, pix1, pixvws); pix3 = pixSelectBySize(pix2, 25, 5, 8, L_SELECT_IF_BOTH, L_SELECT_IF_GTE, NULL); if (block_flag) pixaAddPix(pixa, pix3, L_COPY); if (which == 1) pixWrite("/tmp/lept/livre/textblock2.150.png", pix3, IFF_PNG); pixb2 = pixExpandBinaryPower2(pix3, 2); pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); /* Identify the outlines of each textblock */ ptaa = pixGetOuterBordersPtaa(pixb2); pix1 = pixRenderRandomCmapPtaa(pixb2, ptaa, 1, 8, 1); cmap = pixGetColormap(pix1); pixcmapResetColor(cmap, 0, 130, 130, 130); /* set interior to gray */ if (which == 1) pixWrite("/tmp/lept/livre/textblock3.300.png", pix1, IFF_PNG); pixDisplayWithTitle(pix1, 480, 360, "textblock mask with outlines", DFLAG); ptaaDestroy(&ptaa); pixDestroy(&pix1); /* Fill line mask (as seed) into the original */ pix1 = pixSeedfillBinary(NULL, pixm3, pixs, 8); pixOr(pixm3, pixm3, pix1); pixDestroy(&pix1); if (which == 1) pixWrite("/tmp/lept/livre/textmask.300.png", pixm3, IFF_PNG); pixDisplayWithTitle(pixm3, 480, 360, "textline mask 4", DFLAG); /* Fill halftone mask (as seed) into the original */ pix1 = pixSeedfillBinary(NULL, pixhm2, pixs, 8); pixOr(pixhm2, pixhm2, pix1); pixDestroy(&pix1); if (which == 1) pixWrite("/tmp/lept/livre/htmask.300.png", pixhm2, IFF_PNG); pixDisplayWithTitle(pixhm2, 520, 390, "halftonemask 2", DFLAG); /* Find objects that are neither text nor halftones */ pix1 = pixSubtract(NULL, pixs, pixm3); /* remove text pixels */ pixnon = pixSubtract(NULL, pix1, pixhm2); /* remove halftone pixels */ pixDestroy(&pix1); if (which == 1) pixWrite("/tmp/lept/livre/other.300.png", pixnon, IFF_PNG); pixDisplayWithTitle(pixnon, 540, 420, "other stuff", DFLAG); /* Write out b.b. for text line mask and halftone mask components */ boxatm = pixConnComp(pixm3, NULL, 4); boxahm = pixConnComp(pixhm2, NULL, 8); if (which == 1) { boxaWrite("/tmp/lept/livre/textmask.boxa", boxatm); boxaWrite("/tmp/lept/livre/htmask.boxa", boxahm); } pix1 = pixaDisplayTiledAndScaled(pixa, 8, 250, 4, 0, 25, 2); pixDisplay(pix1, 0, 375 * (which - 1)); snprintf(buf, sizeof(buf), "/tmp/lept/livre/segout.%d.png", which); pixWrite(buf, pix1, IFF_PNG); pixDestroy(&pix1); pixaDestroy(&pixa); /* clean up to test with valgrind */ pixDestroy(&pixr); pixDestroy(&pixhs); pixDestroy(&pixm); pixDestroy(&pixhm1); pixDestroy(&pixhm2); pixDestroy(&pixht); pixDestroy(&pixi); pixDestroy(&pixnht); pixDestroy(&pixvws); pixDestroy(&pixm1); pixDestroy(&pixm2); pixDestroy(&pixm3); pixDestroy(&pixb1); pixDestroy(&pixb2); pixDestroy(&pixnon); boxaDestroy(&boxatm); boxaDestroy(&boxahm); return 0; }
/*! * pixGetRegionsBinary() * * Input: pixs (1 bpp, assumed to be 300 to 400 ppi) * &pixhm (<optional return> halftone mask) * &pixtm (<optional return> textline mask) * &pixtb (<optional return> textblock mask) * debug (flag: set to 1 for debug output) * Return: 0 if OK, 1 on error * * Notes: * (1) It is best to deskew the image before segmenting. * (2) The debug flag enables a number of outputs. These * are included to show how to generate and save/display * these results. */ l_int32 pixGetRegionsBinary(PIX *pixs, PIX **ppixhm, PIX **ppixtm, PIX **ppixtb, l_int32 debug) { char *tempname; l_int32 htfound, tlfound; PIX *pixr, *pixt1, *pixt2; PIX *pixtext; /* text pixels only */ PIX *pixhm2; /* halftone mask; 2x reduction */ PIX *pixhm; /* halftone mask; */ PIX *pixtm2; /* textline mask; 2x reduction */ PIX *pixtm; /* textline mask */ PIX *pixvws; /* vertical white space mask */ PIX *pixtb2; /* textblock mask; 2x reduction */ PIX *pixtbf2; /* textblock mask; 2x reduction; small comps filtered */ PIX *pixtb; /* textblock mask */ PROCNAME("pixGetRegionsBinary"); if (ppixhm) *ppixhm = NULL; if (ppixtm) *ppixtm = NULL; if (ppixtb) *ppixtb = NULL; if (!pixs) return ERROR_INT("pixs not defined", procName, 1); if (pixGetDepth(pixs) != 1) return ERROR_INT("pixs not 1 bpp", procName, 1); /* 2x reduce, to 150 -200 ppi */ pixr = pixReduceRankBinaryCascade(pixs, 1, 0, 0, 0); pixDisplayWrite(pixr, debug); /* Get the halftone mask */ pixhm2 = pixGenHalftoneMask(pixr, &pixtext, &htfound, debug); /* Get the textline mask from the text pixels */ pixtm2 = pixGenTextlineMask(pixtext, &pixvws, &tlfound, debug); /* Get the textblock mask from the textline mask */ pixtb2 = pixGenTextblockMask(pixtm2, pixvws, debug); pixDestroy(&pixr); pixDestroy(&pixtext); pixDestroy(&pixvws); /* Remove small components from the mask, where a small * component is defined as one with both width and height < 60 */ pixtbf2 = pixSelectBySize(pixtb2, 60, 60, 4, L_SELECT_IF_EITHER, L_SELECT_IF_GTE, NULL); pixDestroy(&pixtb2); pixDisplayWriteFormat(pixtbf2, debug, IFF_PNG); /* Expand all masks to full resolution, and do filling or * small dilations for better coverage. */ pixhm = pixExpandReplicate(pixhm2, 2); pixt1 = pixSeedfillBinary(NULL, pixhm, pixs, 8); pixOr(pixhm, pixhm, pixt1); pixDestroy(&pixt1); pixDisplayWriteFormat(pixhm, debug, IFF_PNG); pixt1 = pixExpandReplicate(pixtm2, 2); pixtm = pixDilateBrick(NULL, pixt1, 3, 3); pixDestroy(&pixt1); pixDisplayWriteFormat(pixtm, debug, IFF_PNG); pixt1 = pixExpandReplicate(pixtbf2, 2); pixtb = pixDilateBrick(NULL, pixt1, 3, 3); pixDestroy(&pixt1); pixDisplayWriteFormat(pixtb, debug, IFF_PNG); pixDestroy(&pixhm2); pixDestroy(&pixtm2); pixDestroy(&pixtbf2); /* Debug: identify objects that are neither text nor halftone image */ if (debug) { pixt1 = pixSubtract(NULL, pixs, pixtm); /* remove text pixels */ pixt2 = pixSubtract(NULL, pixt1, pixhm); /* remove halftone pixels */ pixDisplayWriteFormat(pixt2, 1, IFF_PNG); pixDestroy(&pixt1); pixDestroy(&pixt2); } /* Debug: display textline components with random colors */ if (debug) { l_int32 w, h; BOXA *boxa; PIXA *pixa; boxa = pixConnComp(pixtm, &pixa, 8); pixGetDimensions(pixtm, &w, &h, NULL); pixt1 = pixaDisplayRandomCmap(pixa, w, h); pixcmapResetColor(pixGetColormap(pixt1), 0, 255, 255, 255); pixDisplay(pixt1, 100, 100); pixDisplayWriteFormat(pixt1, 1, IFF_PNG); pixaDestroy(&pixa); boxaDestroy(&boxa); pixDestroy(&pixt1); } /* Debug: identify the outlines of each textblock */ if (debug) { PIXCMAP *cmap; PTAA *ptaa; ptaa = pixGetOuterBordersPtaa(pixtb); tempname = genTempFilename("/tmp", "tb_outlines.ptaa", 0, 0); ptaaWrite(tempname, ptaa, 1); FREE(tempname); pixt1 = pixRenderRandomCmapPtaa(pixtb, ptaa, 1, 16, 1); cmap = pixGetColormap(pixt1); pixcmapResetColor(cmap, 0, 130, 130, 130); pixDisplay(pixt1, 500, 100); pixDisplayWriteFormat(pixt1, 1, IFF_PNG); pixDestroy(&pixt1); ptaaDestroy(&ptaa); } /* Debug: get b.b. for all mask components */ if (debug) { BOXA *bahm, *batm, *batb; bahm = pixConnComp(pixhm, NULL, 4); batm = pixConnComp(pixtm, NULL, 4); batb = pixConnComp(pixtb, NULL, 4); tempname = genTempFilename("/tmp", "htmask.boxa", 0, 0); boxaWrite(tempname, bahm); FREE(tempname); tempname = genTempFilename("/tmp", "textmask.boxa", 0, 0); boxaWrite(tempname, batm); FREE(tempname); tempname = genTempFilename("/tmp", "textblock.boxa", 0, 0); boxaWrite(tempname, batb); FREE(tempname); boxaDestroy(&bahm); boxaDestroy(&batm); boxaDestroy(&batb); } if (ppixhm) *ppixhm = pixhm; else pixDestroy(&pixhm); if (ppixtm) *ppixtm = pixtm; else pixDestroy(&pixtm); if (ppixtb) *ppixtb = pixtb; else pixDestroy(&pixtb); return 0; }
/*! * pixFindPageForeground() * * Input: pixs (full resolution (any type or depth) * threshold (for binarization; typically about 128) * mindist (min distance of text from border to allow * cleaning near border; at 2x reduction, this * should be larger than 50; typically about 70) * erasedist (when conditions are satisfied, erase anything * within this distance of the edge; * typically 30 at 2x reduction) * pagenum (use for debugging when called repeatedly; labels * debug images that are assembled into pdfdir) * showmorph (set to a negative integer to show steps in * generating masks; this is typically used * for debugging region extraction) * display (set to 1 to display mask and selected region * for debugging a single page) * pdfdir (subdirectory of /tmp where images showing the * result are placed when called repeatedly; use * null if no output requested) * Return: box (region including foreground, with some pixel noise * removed), or null if not found * * Notes: * (1) This doesn't simply crop to the fg. It attempts to remove * pixel noise and junk at the edge of the image before cropping. * The input @threshold is used if pixs is not 1 bpp. * (2) There are several debugging options, determined by the * last 4 arguments. * (3) If you want pdf output of results when called repeatedly, * the pagenum arg labels the images written, which go into * /tmp/<pdfdir>/<pagenum>.png. In that case, * you would clean out the /tmp directory before calling this * function on each page: * lept_rmdir(pdfdir); * lept_mkdir(pdfdir); */ BOX * pixFindPageForeground(PIX *pixs, l_int32 threshold, l_int32 mindist, l_int32 erasedist, l_int32 pagenum, l_int32 showmorph, l_int32 display, const char *pdfdir) { char buf[64]; l_int32 flag, nbox, intersects; l_int32 w, h, bx, by, bw, bh, left, right, top, bottom; PIX *pixb, *pixb2, *pixseed, *pixsf, *pixm, *pix1, *pixg2; BOX *box, *boxfg, *boxin, *boxd; BOXA *ba1, *ba2; PROCNAME("pixFindPageForeground"); if (!pixs) return (BOX *)ERROR_PTR("pixs not defined", procName, NULL); /* Binarize, downscale by 0.5, remove the noise to generate a seed, * and do a seedfill back from the seed into those 8-connected * components of the binarized image for which there was at least * one seed pixel. Also clear out any components that are within * 10 pixels of the edge at 2x reduction. */ flag = (showmorph) ? -1 : 0; /* if showmorph == -1, write intermediate * images to /tmp/seq_output_1.pdf */ pixb = pixConvertTo1(pixs, threshold); pixb2 = pixScale(pixb, 0.5, 0.5); pixseed = pixMorphSequence(pixb2, "o1.2 + c9.9 + o3.5", flag); pixsf = pixSeedfillBinary(NULL, pixseed, pixb2, 8); pixSetOrClearBorder(pixsf, 10, 10, 10, 10, PIX_SET); pixm = pixRemoveBorderConnComps(pixsf, 8); if (display) pixDisplay(pixm, 100, 100); /* Now, where is the main block of text? We want to remove noise near * the edge of the image, but to do that, we have to be convinced that * (1) there is noise and (2) it is far enough from the text block * and close enough to the edge. For each edge, if the block * is more than mindist from that edge, then clean 'erasedist' * pixels from the edge. */ pix1 = pixMorphSequence(pixm, "c50.50", flag - 1); ba1 = pixConnComp(pix1, NULL, 8); ba2 = boxaSort(ba1, L_SORT_BY_AREA, L_SORT_DECREASING, NULL); pixGetDimensions(pix1, &w, &h, NULL); nbox = boxaGetCount(ba2); if (nbox > 1) { box = boxaGetBox(ba2, 0, L_CLONE); boxGetGeometry(box, &bx, &by, &bw, &bh); left = (bx > mindist) ? erasedist : 0; right = (w - bx - bw > mindist) ? erasedist : 0; top = (by > mindist) ? erasedist : 0; bottom = (h - by - bh > mindist) ? erasedist : 0; pixSetOrClearBorder(pixm, left, right, top, bottom, PIX_CLR); boxDestroy(&box); } pixDestroy(&pix1); boxaDestroy(&ba1); boxaDestroy(&ba2); /* Locate the foreground region; don't bother cropping */ pixClipToForeground(pixm, NULL, &boxfg); /* Sanity check the fg region. Make sure it's not confined * to a thin boundary on the left and right sides of the image, * in which case it is likely to be noise. */ if (boxfg) { boxin = boxCreate(0.1 * w, 0, 0.8 * w, h); boxIntersects(boxfg, boxin, &intersects); if (!intersects) { L_INFO("found only noise on page %d\n", procName, pagenum); boxDestroy(&boxfg); } boxDestroy(&boxin); } boxd = NULL; if (!boxfg) { L_INFO("no fg region found for page %d\n", procName, pagenum); } else { boxAdjustSides(boxfg, boxfg, -2, 2, -2, 2); /* tiny expansion */ boxd = boxTransform(boxfg, 0, 0, 2.0, 2.0); /* Write image showing box for this page. This is to be * bundled up into a pdf of all the pages, which can be * generated by convertFilesToPdf() */ if (pdfdir) { pixg2 = pixConvert1To4Cmap(pixb); pixRenderBoxArb(pixg2, boxd, 3, 255, 0, 0); snprintf(buf, sizeof(buf), "/tmp/%s/%05d.png", pdfdir, pagenum); if (display) pixDisplay(pixg2, 700, 100); pixWrite(buf, pixg2, IFF_PNG); pixDestroy(&pixg2); } } pixDestroy(&pixb); pixDestroy(&pixb2); pixDestroy(&pixseed); pixDestroy(&pixsf); pixDestroy(&pixm); boxDestroy(&boxfg); return boxd; }
int main(int argc, char **argv) { char *filein, *fileout; l_int32 i; l_uint32 val; l_float32 size; PIX *pixs, *pixd, *pixm, *pixmi, *pixt1, *pixt2, *pixt3; static char mainName[] = "seedfilltest"; if (argc != 3) return ERROR_INT(" Syntax: seedfilltest filein fileout", mainName, 1); filein = argv[1]; fileout = argv[2]; pixd = NULL; if ((pixm = pixRead(filein)) == NULL) return ERROR_INT("pixm not made", mainName, 1); pixmi = pixInvert(NULL, pixm); size = pixGetWidth(pixm) * pixGetHeight(pixm); pixs = pixCreateTemplate(pixm); for (i = 0; i < 100; i++) { pixGetPixel(pixm, XS + 5 * i, YS + 5 * i, &val); if (val == 0) break; } if (i == 100) return ERROR_INT("no seed pixel found", mainName, 1); pixSetPixel(pixs, XS + 5 * i, YS + 5 * i, 1); #if 0 /* hole filling; use "hole-filler.png" */ pixt1 = pixHDome(pixmi, 100, 4); pixt2 = pixThresholdToBinary(pixt1, 10); /* pixInvert(pixt1, pixt1); */ pixDisplay(pixt1, 100, 500); pixDisplay(pixt2, 600, 500); pixt3 = pixHolesByFilling(pixt2, 4); pixDilateBrick(pixt3, pixt3, 7, 7); pixd = pixConvertTo8(pixt3, FALSE); pixDisplay(pixd, 0, 100); pixSeedfillGray(pixd, pixmi, CONNECTIVITY); pixInvert(pixd, pixd); pixDisplay(pixmi, 500, 100); pixDisplay(pixd, 1000, 100); pixWrite("/tmp/junkpixm.png", pixmi, IFF_PNG); pixWrite("/tmp/junkpixd.png", pixd, IFF_PNG); #endif #if 0 /* hole filling; use "hole-filler.png" */ pixt1 = pixThresholdToBinary(pixm, 110); pixInvert(pixt1, pixt1); pixDisplay(pixt1, 100, 500); pixt2 = pixHolesByFilling(pixt1, 4); pixd = pixConvertTo8(pixt2, FALSE); pixDisplay(pixd, 0, 100); pixSeedfillGray(pixd, pixmi, CONNECTIVITY); pixInvert(pixd, pixd); pixDisplay(pixmi, 500, 100); pixDisplay(pixd, 1000, 100); pixWrite("/tmp/junkpixm.png", pixmi, IFF_PNG); pixWrite("/tmp/junkpixd.png", pixd, IFF_PNG); #endif #if 0 /* hole filling; use "hole-filler.png" */ pixd = pixInvert(NULL, pixm); pixAddConstantGray(pixd, -50); pixDisplay(pixd, 0, 100); /* pixt1 = pixThresholdToBinary(pixd, 20); pixDisplayWithTitle(pixt1, 600, 600, "pixt1", DFLAG); */ pixSeedfillGray(pixd, pixmi, CONNECTIVITY); /* pixInvert(pixd, pixd); */ pixDisplay(pixmi, 500, 100); pixDisplay(pixd, 1000, 100); pixWrite("/tmp/junkpixm.png", pixmi, IFF_PNG); pixWrite("/tmp/junkpixd.png", pixd, IFF_PNG); #endif #if 0 /* test in-place seedfill for speed */ pixd = pixClone(pixs); startTimer(); pixSeedfillBinary(pixs, pixs, pixmi, CONNECTIVITY); fprintf(stderr, "Filling rate: %7.4f Mpix/sec\n", (size/1000000.) / stopTimer()); pixWrite(fileout, pixd, IFF_PNG); pixOr(pixd, pixd, pixm); pixWrite("/tmp/junkout1.png", pixd, IFF_PNG); #endif #if 0 /* test seedfill to dest for speed */ pixd = pixCreateTemplate(pixm); startTimer(); for (i = 0; i < NTIMES; i++) { pixSeedfillBinary(pixd, pixs, pixmi, CONNECTIVITY); } fprintf(stderr, "Filling rate: %7.4f Mpix/sec\n", (size/1000000.) * NTIMES / stopTimer()); pixWrite(fileout, pixd, IFF_PNG); pixOr(pixd, pixd, pixm); pixWrite("/tmp/junkout1.png", pixd, IFF_PNG); #endif /* use same connectivity to compare with the result of the * slow parallel operation */ #if 1 pixDestroy(&pixd); pixd = pixSeedfillMorph(pixs, pixmi, 100, CONNECTIVITY); pixOr(pixd, pixd, pixm); pixWrite("/tmp/junkout2.png", pixd, IFF_PNG); #endif pixDestroy(&pixs); pixDestroy(&pixm); pixDestroy(&pixmi); pixDestroy(&pixd); return 0; }
/*! * \brief pixItalicWords() * * \param[in] pixs 1 bpp * \param[in] boxaw [optional] word bounding boxes; can be NULL * \param[in] pixw [optional] word box mask; can be NULL * \param[out] pboxa boxa of italic words * \param[in] debugflag 1 for debug output; 0 otherwise * \return 0 if OK, 1 on error * * <pre> * Notes: * (1) You can input the bounding boxes for the words in one of * two forms: as bounding boxes (%boxaw) or as a word mask with * the word bounding boxes filled (%pixw). For example, * to compute %pixw, you can use pixWordMaskByDilation(). * (2) Alternatively, you can set both of these inputs to NULL, * in which case the word mask is generated here. This is * done by dilating and closing the input image to connect * letters within a word, while leaving the words separated. * The parameters are chosen under the assumption that the * input is 10 to 12 pt text, scanned at about 300 ppi. * (3) sel_ital1 and sel_ital2 detect the right edges that are * nearly vertical, at approximately the angle of italic * strokes. We use the right edge to avoid getting seeds * from lower-case 'y'. The typical italic slant has a smaller * angle with the vertical than the 'W', so in most cases we * will not trigger on the slanted lines in the 'W'. * (4) Note that sel_ital2 is shorter than sel_ital1. It is * more appropriate for a typical font scanned at 200 ppi. * </pre> */ l_int32 pixItalicWords(PIX *pixs, BOXA *boxaw, PIX *pixw, BOXA **pboxa, l_int32 debugflag) { char opstring[32]; l_int32 size; BOXA *boxa; PIX *pixsd, *pixm, *pixd; SEL *sel_ital1, *sel_ital2, *sel_ital3; PROCNAME("pixItalicWords"); if (!pixs) return ERROR_INT("pixs not defined", procName, 1); if (!pboxa) return ERROR_INT("&boxa not defined", procName, 1); if (boxaw && pixw) return ERROR_INT("both boxaw and pixw are defined", procName, 1); sel_ital1 = selCreateFromString(str_ital1, 13, 6, NULL); sel_ital2 = selCreateFromString(str_ital2, 10, 6, NULL); sel_ital3 = selCreateFromString(str_ital3, 4, 2, NULL); /* Make the italic seed: extract with HMT; remove noise. * The noise removal close/open is important to exclude * situations where a small slanted line accidentally * matches sel_ital1. */ pixsd = pixHMT(NULL, pixs, sel_ital1); pixClose(pixsd, pixsd, sel_ital3); pixOpen(pixsd, pixsd, sel_ital3); /* Make the word mask. Use input boxes or mask if given. */ size = 0; /* init */ if (boxaw) { pixm = pixCreateTemplate(pixs); pixMaskBoxa(pixm, pixm, boxaw, L_SET_PIXELS); } else if (pixw) { pixm = pixClone(pixw); } else { pixWordMaskByDilation(pixs, NULL, &size, NULL); L_INFO("dilation size = %d\n", procName, size); snprintf(opstring, sizeof(opstring), "d1.5 + c%d.1", size); pixm = pixMorphSequence(pixs, opstring, 0); } /* Binary reconstruction to fill in those word mask * components for which there is at least one seed pixel. */ pixd = pixSeedfillBinary(NULL, pixsd, pixm, 8); boxa = pixConnComp(pixd, NULL, 8); *pboxa = boxa; if (debugflag) { /* Save results at at 2x reduction */ lept_mkdir("lept/ital"); l_int32 res, upper; BOXA *boxat; GPLOT *gplot; NUMA *na; PIXA *pad; PIX *pix1, *pix2, *pix3; pad = pixaCreate(0); boxat = pixConnComp(pixm, NULL, 8); boxaWrite("/tmp/lept/ital/ital.ba", boxat); pixSaveTiledOutline(pixs, pad, 0.5, 1, 20, 2, 32); /* orig */ pixSaveTiledOutline(pixsd, pad, 0.5, 1, 20, 2, 0); /* seed */ pix1 = pixConvertTo32(pixm); pixRenderBoxaArb(pix1, boxat, 3, 255, 0, 0); pixSaveTiledOutline(pix1, pad, 0.5, 1, 20, 2, 0); /* mask + outline */ pixDestroy(&pix1); pixSaveTiledOutline(pixd, pad, 0.5, 1, 20, 2, 0); /* ital mask */ pix1 = pixConvertTo32(pixs); pixRenderBoxaArb(pix1, boxa, 3, 255, 0, 0); pixSaveTiledOutline(pix1, pad, 0.5, 1, 20, 2, 0); /* orig + outline */ pixDestroy(&pix1); pix1 = pixCreateTemplate(pixs); pix2 = pixSetBlackOrWhiteBoxa(pix1, boxa, L_SET_BLACK); pixCopy(pix1, pixs); pix3 = pixDilateBrick(NULL, pixs, 3, 3); pixCombineMasked(pix1, pix3, pix2); pixSaveTiledOutline(pix1, pad, 0.5, 1, 20, 2, 0); /* ital bolded */ pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); pix2 = pixaDisplay(pad, 0, 0); pixWrite("/tmp/lept/ital/ital.png", pix2, IFF_PNG); pixDestroy(&pix2); /* Assuming the image represents 6 inches of actual page width, * the pixs resolution is approximately * (width of pixs in pixels) / 6 * and the images have been saved at half this resolution. */ res = pixGetWidth(pixs) / 12; L_INFO("resolution = %d\n", procName, res); l_pdfSetDateAndVersion(0); pixaConvertToPdf(pad, res, 1.0, L_FLATE_ENCODE, 75, "Italic Finder", "/tmp/lept/ital/ital.pdf"); l_pdfSetDateAndVersion(1); pixaDestroy(&pad); boxaDestroy(&boxat); /* Plot histogram of horizontal white run sizes. A small * initial vertical dilation removes most runs that are neither * inter-character nor inter-word. The larger first peak is * from inter-character runs, and the smaller second peak is * from inter-word runs. */ pix1 = pixDilateBrick(NULL, pixs, 1, 15); upper = L_MAX(30, 3 * size); na = pixRunHistogramMorph(pix1, L_RUN_OFF, L_HORIZ, upper); pixDestroy(&pix1); gplot = gplotCreate("/tmp/lept/ital/runhisto", GPLOT_PNG, "Histogram of horizontal runs of white pixels, vs length", "run length", "number of runs"); gplotAddPlot(gplot, NULL, na, GPLOT_LINES, "plot1"); gplotMakeOutput(gplot); gplotDestroy(&gplot); numaDestroy(&na); } selDestroy(&sel_ital1); selDestroy(&sel_ital2); selDestroy(&sel_ital3); pixDestroy(&pixsd); pixDestroy(&pixm); pixDestroy(&pixd); return 0; }
main(int argc, char **argv) { char *filein, *fileout; l_int32 thresh; PIX *pixs, *pixg, *pixb; PIX *pixmask4, *pixseed4, *pixsf4, *pixd4, *pixd; static char mainName[] = "pagesegtest2"; if (argc != 4) exit(ERROR_INT(" Syntax: pagesegtest2 filein thresh fileout", mainName, 1)); filein = argv[1]; thresh = atoi(argv[2]); fileout = argv[3]; /* Get a 1 bpp version of the page */ if ((pixs = pixRead(filein)) == NULL) exit(ERROR_INT("pixs not made", mainName, 1)); if (pixGetDepth(pixs) == 32) pixg = pixConvertRGBToGrayFast(pixs); else pixg = pixClone(pixs); if (pixGetDepth(pixg) == 8) pixb = pixThresholdToBinary(pixg, thresh); else pixb = pixClone(pixg); /* Make seed and mask, and fill seed into mask */ pixseed4 = pixMorphSequence(pixb, seed_sequence, 0); pixmask4 = pixMorphSequence(pixb, mask_sequence, 0); pixsf4 = pixSeedfillBinary(NULL, pixseed4, pixmask4, 8); pixd4 = pixMorphSequence(pixsf4, dilation_sequence, 0); /* Mask at full resolution */ pixd = pixExpandBinaryPower2(pixd4, 4); pixWrite(fileout, pixd, IFF_TIFF_G4); /* Extract non-image parts (e.g., text) at full resolution */ pixSubtract(pixb, pixb, pixd); pixDisplayWithTitle(pixseed4, 400, 100, "halftone seed", DFLAG); pixDisplayWithTitle(pixmask4, 100, 100, "halftone seed mask", DFLAG); pixDisplayWithTitle(pixd4, 700, 100, "halftone mask", DFLAG); pixDisplayWithTitle(pixb, 1000, 100, "non-halftone", DFLAG); #if 1 pixWrite("junkseed", pixseed4, IFF_TIFF_G4); pixWrite("junkmask", pixmask4, IFF_TIFF_G4); pixWrite("junkfill", pixd4, IFF_TIFF_G4); pixWrite("junktext", pixb, IFF_TIFF_G4); #endif pixDestroy(&pixs); pixDestroy(&pixg); pixDestroy(&pixb); pixDestroy(&pixseed4); pixDestroy(&pixmask4); pixDestroy(&pixsf4); pixDestroy(&pixd4); pixDestroy(&pixd); exit(0); }
// Finds image regions within the source pix (page image) and returns // the image regions as a Boxa, Pixa pair, analgous to pixConnComp. // The returned boxa, pixa may be NULL, meaning no images found. // If not NULL, they must be destroyed by the caller. void ImageFinder::FindImages(Pix* pix, Boxa** boxa, Pixa** pixa) { *boxa = NULL; *pixa = NULL; #ifdef HAVE_LIBLEPT if (pixGetWidth(pix) < kMinImageFindSize || pixGetHeight(pix) < kMinImageFindSize) return; // Not worth looking at small images. // Reduce by factor 2. Pix *pixr = pixReduceRankBinaryCascade(pix, 1, 0, 0, 0); pixDisplayWrite(pixr, textord_tabfind_show_images); // Get the halftone mask directly from Leptonica. Pix *pixht2 = pixGenHalftoneMask(pixr, NULL, NULL, textord_tabfind_show_images); pixDestroy(&pixr); if (pixht2 == NULL) return; // Expand back up again. Pix *pixht = pixExpandReplicate(pixht2, 2); pixDisplayWrite(pixht, textord_tabfind_show_images); pixDestroy(&pixht2); // Fill to capture pixels near the mask edges that were missed Pix *pixt = pixSeedfillBinary(NULL, pixht, pix, 8); pixOr(pixht, pixht, pixt); pixDestroy(&pixt); // Eliminate lines and bars that may be joined to images. Pix* pixfinemask = pixReduceRankBinaryCascade(pixht, 1, 1, 3, 3); pixDilateBrick(pixfinemask, pixfinemask, 5, 5); pixDisplayWrite(pixfinemask, textord_tabfind_show_images); Pix* pixreduced = pixReduceRankBinaryCascade(pixht, 1, 1, 1, 1); Pix* pixreduced2 = pixReduceRankBinaryCascade(pixreduced, 3, 3, 3, 0); pixDestroy(&pixreduced); pixDilateBrick(pixreduced2, pixreduced2, 5, 5); Pix* pixcoarsemask = pixExpandReplicate(pixreduced2, 8); pixDestroy(&pixreduced2); pixDisplayWrite(pixcoarsemask, textord_tabfind_show_images); // Combine the coarse and fine image masks. pixAnd(pixcoarsemask, pixcoarsemask, pixfinemask); pixDestroy(&pixfinemask); // Dilate a bit to make sure we get everything. pixDilateBrick(pixcoarsemask, pixcoarsemask, 3, 3); Pix* pixmask = pixExpandReplicate(pixcoarsemask, 16); pixDestroy(&pixcoarsemask); pixDisplayWrite(pixmask, textord_tabfind_show_images); // And the image mask with the line and bar remover. pixAnd(pixht, pixht, pixmask); pixDestroy(&pixmask); pixDisplayWrite(pixht, textord_tabfind_show_images); // Find the individual image regions in the mask image. *boxa = pixConnComp(pixht, pixa, 8); pixDestroy(&pixht); // Rectangularize the individual images. If a sharp edge in vertical and/or // horizontal occupancy can be found, it indicates a probably rectangular // image with unwanted bits merged on, so clip to the approximate rectangle. int npixes = pixaGetCount(*pixa); for (int i = 0; i < npixes; ++i) { int x_start, x_end, y_start, y_end; Pix* img_pix = pixaGetPix(*pixa, i, L_CLONE); pixDisplayWrite(img_pix, textord_tabfind_show_images); if (pixNearlyRectangular(img_pix, kMinRectangularFraction, kMaxRectangularFraction, kMaxRectangularGradient, &x_start, &y_start, &x_end, &y_end)) { // Add 1 to the size as a kludgy flag to indicate to the later stages // of processing that it is a clipped rectangular image . Pix* simple_pix = pixCreate(pixGetWidth(img_pix) + 1, pixGetHeight(img_pix), 1); pixDestroy(&img_pix); pixRasterop(simple_pix, x_start, y_start, x_end - x_start, y_end - y_start, PIX_SET, NULL, 0, 0); // pixaReplacePix takes ownership of the simple_pix. pixaReplacePix(*pixa, i, simple_pix, NULL); img_pix = pixaGetPix(*pixa, i, L_CLONE); } // Subtract the pix from the correct location in the master image. l_int32 x, y, width, height; pixDisplayWrite(img_pix, textord_tabfind_show_images); boxaGetBoxGeometry(*boxa, i, &x, &y, &width, &height); pixRasterop(pix, x, y, width, height, PIX_NOT(PIX_SRC) & PIX_DST, img_pix, 0, 0); pixDestroy(&img_pix); } #endif }