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; }
/*! * pixThinExamples() * * Input: pixs (1 bpp) * type (L_THIN_FG, L_THIN_BG) * index (into specific examples; valid 1-9; see notes) * maxiters (max number of iters allowed; use 0 to iterate * until completion) * selfile (<optional> filename for output sel display) * Return: pixd, or null on error * * Notes: * (1) See notes in pixThin(). The examples are taken from * the paper referenced there. * (2) Here we allow specific sets of HMTs to be used in * parallel for thinning from each of four directions. * One iteration consists of four such parallel thins. * (3) The examples are indexed as follows: * Thinning (e.g., run to completion): * index = 1 sel_4_1, sel_4_5, sel_4_6 * index = 2 sel_4_1, sel_4_7, sel_4_7_rot * index = 3 sel_48_1, sel_48_1_rot, sel_48_2 * index = 4 sel_8_2, sel_8_3, sel_48_2 * index = 5 sel_8_1, sel_8_5, sel_8_6 * index = 6 sel_8_2, sel_8_3, sel_8_8, sel_8_9 * index = 7 sel_8_5, sel_8_6, sel_8_7, sel_8_7_rot * Thickening: * index = 8 sel_4_2, sel_4_3 (e.g,, do just a few iterations) * index = 9 sel_8_4 (e.g., do just a few iterations) */ PIX * pixThinExamples(PIX *pixs, l_int32 type, l_int32 index, l_int32 maxiters, const char *selfile) { PIX *pixd, *pixt; SEL *sel; SELA *sela; PROCNAME("pixThinExamples"); 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); if (type != L_THIN_FG && type != L_THIN_BG) return (PIX *)ERROR_PTR("invalid fg/bg type", procName, NULL); if (index < 1 || index > 9) return (PIX *)ERROR_PTR("invalid index", procName, NULL); if (maxiters == 0) maxiters = 10000; switch(index) { case 1: sela = selaCreate(3); sel = selCreateFromString(sel_4_1, 3, 3, "sel_4_1"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_4_5, 3, 3, "sel_4_5"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_4_6, 3, 3, "sel_4_6"); selaAddSel(sela, sel, NULL, 0); break; case 2: sela = selaCreate(3); sel = selCreateFromString(sel_4_1, 3, 3, "sel_4_1"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_4_7, 3, 3, "sel_4_7"); selaAddSel(sela, sel, NULL, 0); sel = selRotateOrth(sel, 1); selaAddSel(sela, sel, "sel_4_7_rot", 0); break; case 3: sela = selaCreate(3); sel = selCreateFromString(sel_48_1, 3, 3, "sel_48_1"); selaAddSel(sela, sel, NULL, 0); sel = selRotateOrth(sel, 1); selaAddSel(sela, sel, "sel_48_1_rot", 0); sel = selCreateFromString(sel_48_2, 3, 3, "sel_48_2"); selaAddSel(sela, sel, NULL, 0); break; case 4: sela = selaCreate(3); sel = selCreateFromString(sel_8_2, 3, 3, "sel_8_2"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_3, 3, 3, "sel_8_3"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_48_2, 3, 3, "sel_48_2"); selaAddSel(sela, sel, NULL, 0); break; case 5: sela = selaCreate(3); sel = selCreateFromString(sel_8_1, 3, 3, "sel_8_1"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_5, 3, 3, "sel_8_5"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_6, 3, 3, "sel_8_6"); selaAddSel(sela, sel, NULL, 0); break; case 6: sela = selaCreate(4); sel = selCreateFromString(sel_8_2, 3, 3, "sel_8_2"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_3, 3, 3, "sel_8_3"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_8, 3, 3, "sel_8_8"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_9, 3, 3, "sel_8_9"); selaAddSel(sela, sel, NULL, 0); break; case 7: sela = selaCreate(4); sel = selCreateFromString(sel_8_5, 3, 3, "sel_8_5"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_6, 3, 3, "sel_8_6"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_8_7, 3, 3, "sel_8_7"); selaAddSel(sela, sel, NULL, 0); sel = selRotateOrth(sel, 1); selaAddSel(sela, sel, "sel_8_7_rot", 0); break; case 8: /* thicken for this one; just a few iterations */ sela = selaCreate(2); sel = selCreateFromString(sel_4_2, 3, 3, "sel_4_2"); selaAddSel(sela, sel, NULL, 0); sel = selCreateFromString(sel_4_3, 3, 3, "sel_4_3"); selaAddSel(sela, sel, NULL, 0); pixt = pixThinGeneral(pixs, type, sela, maxiters); pixd = pixRemoveBorderConnComps(pixt, 4); pixDestroy(&pixt); break; case 9: /* thicken for this one; just a few iterations */ sela = selaCreate(1); sel = selCreateFromString(sel_8_4, 3, 3, "sel_8_4"); selaAddSel(sela, sel, NULL, 0); pixt = pixThinGeneral(pixs, type, sela, maxiters); pixd = pixRemoveBorderConnComps(pixt, 4); pixDestroy(&pixt); break; default: return (PIX *)ERROR_PTR("invalid index", procName, NULL); } if (index <= 7) pixd = pixThinGeneral(pixs, type, sela, maxiters); /* Optionally display the sels */ if (selfile) { pixt = selaDisplayInPix(sela, 35, 3, 15, 4); pixWrite(selfile, pixt, IFF_PNG); pixDestroy(&pixt); } selaDestroy(&sela); return pixd; }
/*! * 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; }
/*! * Note: this method is generally inferior to pixHasColorRegions(); it * is retained as a reference only * * \brief pixFindColorRegionsLight() * * \param[in] pixs 32 bpp rgb * \param[in] pixm [optional] 1 bpp mask image * \param[in] factor subsample factor; integer >= 1 * \param[in] darkthresh threshold to eliminate dark pixels (e.g., text) * from consideration; typ. 70; -1 for default. * \param[in] lightthresh threshold for minimum gray value at 95% rank * near white; typ. 220; -1 for default * \param[in] mindiff minimum difference from 95% rank value, used * to count darker pixels; typ. 50; -1 for default * \param[in] colordiff minimum difference in (max - min) component to * qualify as a color pixel; typ. 40; -1 for default * \param[out] pcolorfract fraction of 'color' pixels found * \param[out] pcolormask1 [optional] mask over background color, if any * \param[out] pcolormask2 [optional] filtered mask over background color * \param[out] pixadb [optional] debug intermediate results * \return 0 if OK, 1 on error * * <pre> * Notes: * (1) This function tries to determine if there is a significant * color or darker region on a scanned page image where part * of the image is very close to "white". It will also allow * extraction of small regions of lightly colored pixels. * If the background is darker (and reddish), use instead * pixHasColorRegions2(). * (2) If %pixm exists, only pixels under fg are considered. Typically, * the inverse of %pixm would have fg pixels over a photograph. * (3) There are four thresholds. * * %darkthresh: ignore pixels darker than this (typ. fg text). * We make a 1 bpp mask of these pixels, and then dilate it to * remove all vestiges of fg from their vicinity. * * %lightthresh: let val95 be the pixel value for which 95% * of the non-masked pixels have a lower value (darker) of * their min component. Then if val95 is darker than * %lightthresh, the image is not considered to have a * light bg, and this returns 0.0 for %colorfract. * * %mindiff: we are interested in the fraction of pixels that * have two conditions. The first is that their min component * is at least %mindiff darker than val95. * * %colordiff: the second condition is that the max-min diff * of the pixel components exceeds %colordiff. * (4) This returns in %pcolorfract the fraction of pixels that have * both a min component that is at least %mindiff below that at the * 95% rank value (where 100% rank is the lightest value), and * a max-min diff that is at least %colordiff. Without the * %colordiff constraint, gray pixels of intermediate value * could get flagged by this function. * (5) No masks are returned unless light color pixels are found. * If colorfract > 0.0 and %pcolormask1 is defined, this returns * a 1 bpp mask with fg pixels over the color background. * This mask may have some holes in it. * (6) If colorfract > 0.0 and %pcolormask2 is defined, this returns * a filtered version of colormask1. The two changes are * (a) small holes have been filled * (b) components near the border have been removed. * The latter insures that dark pixels near the edge of the * image are not included. * (7) To generate a boxa of rectangular regions from the overlap * of components in the filtered mask: * boxa1 = pixConnCompBB(colormask2, 8); * boxa2 = boxaCombineOverlaps(boxa1); * This is done here in debug mode. * </pre> */ static l_int32 pixFindColorRegionsLight(PIX *pixs, PIX *pixm, l_int32 factor, l_int32 darkthresh, l_int32 lightthresh, l_int32 mindiff, l_int32 colordiff, l_float32 *pcolorfract, PIX **pcolormask1, PIX **pcolormask2, PIXA *pixadb) { l_int32 lightbg, w, h, count; l_float32 ratio, val95, rank; BOXA *boxa1, *boxa2; NUMA *nah; PIX *pix1, *pix2, *pix3, *pix4, *pix5, *pixm1, *pixm2, *pixm3; PROCNAME("pixFindColorRegionsLight"); if (pcolormask1) *pcolormask1 = NULL; if (pcolormask2) *pcolormask2 = NULL; if (!pcolorfract) return ERROR_INT("&colorfract not defined", procName, 1); *pcolorfract = 0.0; if (!pixs || pixGetDepth(pixs) != 32) return ERROR_INT("pixs not defined or not 32 bpp", procName, 1); if (factor < 1) factor = 1; if (darkthresh < 0) darkthresh = 70; /* defaults */ if (lightthresh < 0) lightthresh = 220; if (mindiff < 0) mindiff = 50; if (colordiff < 0) colordiff = 40; /* Check if pixm covers most of the image. If so, just return. */ pixGetDimensions(pixs, &w, &h, NULL); if (pixm) { pixCountPixels(pixm, &count, NULL); ratio = (l_float32)count / ((l_float32)(w) * h); if (ratio > 0.7) { if (pixadb) L_INFO("pixm has big fg: %f5.2\n", procName, ratio); return 0; } } /* Make a mask pixm1 over the dark pixels in the image: * convert to gray using the average of the components; * threshold using %darkthresh; do a small dilation; * combine with pixm. */ pix1 = pixConvertRGBToGray(pixs, 0.33, 0.34, 0.33); if (pixadb) pixaAddPix(pixadb, pixs, L_COPY); if (pixadb) pixaAddPix(pixadb, pix1, L_COPY); pixm1 = pixThresholdToBinary(pix1, darkthresh); pixDilateBrick(pixm1, pixm1, 7, 7); if (pixadb) pixaAddPix(pixadb, pixm1, L_COPY); if (pixm) { pixOr(pixm1, pixm1, pixm); if (pixadb) pixaAddPix(pixadb, pixm1, L_COPY); } pixDestroy(&pix1); /* Convert to gray using the minimum component value and * find the gray value at rank 0.95, that represents the light * pixels in the image. If it is too dark, quit. */ pix1 = pixConvertRGBToGrayMinMax(pixs, L_SELECT_MIN); pix2 = pixInvert(NULL, pixm1); /* pixels that are not dark */ pixGetRankValueMasked(pix1, pix2, 0, 0, factor, 0.95, &val95, &nah); pixDestroy(&pix2); if (pixadb) { L_INFO("val at 0.95 rank = %5.1f\n", procName, val95); gplotSimple1(nah, GPLOT_PNG, "/tmp/lept/histo1", "gray histo"); pix3 = pixRead("/tmp/lept/histo1.png"); pix4 = pixExpandReplicate(pix3, 2); pixaAddPix(pixadb, pix4, L_INSERT); pixDestroy(&pix3); } lightbg = (l_int32)val95 >= lightthresh; numaDestroy(&nah); if (!lightbg) { pixDestroy(&pix1); pixDestroy(&pixm1); return 0; } /* Make mask pixm2 over pixels that are darker than val95 - mindiff. */ pixm2 = pixThresholdToBinary(pix1, val95 - mindiff); if (pixadb) pixaAddPix(pixadb, pixm2, L_COPY); pixDestroy(&pix1); /* Make a mask pixm3 over pixels that have some color saturation, * with a (max - min) component difference >= %colordiff, * and combine using AND with pixm2. */ pix2 = pixConvertRGBToGrayMinMax(pixs, L_CHOOSE_MAXDIFF); pixm3 = pixThresholdToBinary(pix2, colordiff); pixDestroy(&pix2); pixInvert(pixm3, pixm3); /* need pixels above threshold */ if (pixadb) pixaAddPix(pixadb, pixm3, L_COPY); pixAnd(pixm2, pixm2, pixm3); if (pixadb) pixaAddPix(pixadb, pixm2, L_COPY); pixDestroy(&pixm3); /* Subtract the dark pixels represented by pixm1. * pixm2 now holds all the color pixels of interest */ pixSubtract(pixm2, pixm2, pixm1); pixDestroy(&pixm1); if (pixadb) pixaAddPix(pixadb, pixm2, L_COPY); /* But we're not quite finished. Remove pixels from any component * that is touching the image border. False color pixels can * sometimes be found there if the image is much darker near * the border, due to oxidation or reduced illumination. */ pixm3 = pixRemoveBorderConnComps(pixm2, 8); pixDestroy(&pixm2); if (pixadb) pixaAddPix(pixadb, pixm3, L_COPY); /* Get the fraction of light color pixels */ pixCountPixels(pixm3, &count, NULL); *pcolorfract = (l_float32)count / (w * h); if (pixadb) { if (count == 0) L_INFO("no light color pixels found\n", procName); else L_INFO("fraction of light color pixels = %5.3f\n", procName, *pcolorfract); } /* Debug: extract the color pixels from pixs */ if (pixadb && count > 0) { /* Use pixm3 to extract the color pixels */ pix3 = pixCreateTemplate(pixs); pixSetAll(pix3); pixCombineMasked(pix3, pixs, pixm3); pixaAddPix(pixadb, pix3, L_INSERT); /* Use additional filtering to extract the color pixels */ pix3 = pixCloseSafeBrick(NULL, pixm3, 15, 15); pixaAddPix(pixadb, pix3, L_INSERT); pix5 = pixCreateTemplate(pixs); pixSetAll(pix5); pixCombineMasked(pix5, pixs, pix3); pixaAddPix(pixadb, pix5, L_INSERT); /* Get the combined bounding boxes of the mask components * in pix3, and extract those pixels from pixs. */ boxa1 = pixConnCompBB(pix3, 8); boxa2 = boxaCombineOverlaps(boxa1, NULL); pix4 = pixCreateTemplate(pix3); pixMaskBoxa(pix4, pix4, boxa2, L_SET_PIXELS); pixaAddPix(pixadb, pix4, L_INSERT); pix5 = pixCreateTemplate(pixs); pixSetAll(pix5); pixCombineMasked(pix5, pixs, pix4); pixaAddPix(pixadb, pix5, L_INSERT); boxaDestroy(&boxa1); boxaDestroy(&boxa2); pixaAddPix(pixadb, pixs, L_COPY); } /* Optional colormask returns */ if (pcolormask2 && count > 0) *pcolormask2 = pixCloseSafeBrick(NULL, pixm3, 15, 15); if (pcolormask1 && count > 0) *pcolormask1 = pixm3; else pixDestroy(&pixm3); return 0; }