// 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); }
// Get a set of bounding boxes of possible vertical lines in the image. // The input resolution overrides any resolution set in src_pix. // The output line_pix contains just all the detected lines. Boxa* LineFinder::GetVLineBoxes(int resolution, Pix* src_pix, Pix** line_pix) { #ifdef HAVE_LIBLEPT // Remove any parts of 1 inch/kThinLineFraction wide or more, by opening // away the thin lines and subtracting what's left. // This is very generous and will leave in even quite wide lines. Pix* pixt1 = pixOpenBrick(NULL, src_pix, resolution / kThinLineFraction, 1); pixSubtract(pixt1, src_pix, pixt1); // Spread sideways to allow for some skew. Pix* pixt2 = pixDilateBrick(NULL, pixt1, 3, 1); // Now keep only tall stuff of height at least 1 inch/kMinLineLengthFraction. pixOpenBrick(pixt1, pixt2, 1, resolution / kMinLineLengthFraction); pixDestroy(&pixt2); // Put a single pixel crack in every line at an arbitrary spacing, // so they break up and the bounding boxes can be used to get the // direction accurately enough without needing outlines. int wpl = pixGetWpl(pixt1); int height = pixGetHeight(pixt1); l_uint32* data = pixGetData(pixt1); for (int y = kCrackSpacing; y < height; y += kCrackSpacing) { memset(data + wpl * y, 0, wpl * sizeof(*data)); } if (textord_tabfind_show_vlines) pixWrite("vlines.png", pixt1, IFF_PNG); Boxa* boxa = pixConnComp(pixt1, NULL, 8); *line_pix = pixt1; return boxa; #else return NULL; #endif }
/*! * 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; }
// Finds horizontal line objects in the given pix. // Uses the given resolution to determine size thresholds instead of any // that may be present in the pix. // The output vectors are owned by the list and Frozen (cannot refit) by // having no boxes, as there is no need to refit or merge separator lines. void LineFinder::FindHorizontalLines(int resolution, Pix* pix, TabVector_LIST* vectors) { #ifdef HAVE_LIBLEPT Pix* line_pix; Boxa* boxes = GetHLineBoxes(resolution, pix, &line_pix); C_BLOB_LIST line_cblobs; int width = pixGetWidth(pix); int height = pixGetHeight(pix); ConvertBoxaToBlobs(height, width, &boxes, &line_cblobs); // Make the BLOBNBOXes from the C_BLOBs. BLOBNBOX_LIST line_bblobs; C_BLOB_IT blob_it(&line_cblobs); BLOBNBOX_IT bbox_it(&line_bblobs); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { C_BLOB* cblob = blob_it.data(); BLOBNBOX* bblob = new BLOBNBOX(cblob); bbox_it.add_to_end(bblob); } ICOORD bleft(0, 0); ICOORD tright(height, width); int vertical_x, vertical_y; FindLineVectors(bleft, tright, &line_bblobs, &vertical_x, &vertical_y, vectors); if (!vectors->empty()) { // Some lines were found, so erase the unused blobs from the line image // and then subtract the line image from the source. bbox_it.move_to_first(); for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) { BLOBNBOX* blob = bbox_it.data(); if (blob->left_tab_type() == TT_UNCONFIRMED) { const TBOX& box = blob->bounding_box(); // Coords are in tess format so filp x and y and then covert // to leptonica by height -y. Box* pixbox = boxCreate(box.bottom(), height - box.right(), box.height(), box.width()); pixClearInRect(line_pix, pixbox); boxDestroy(&pixbox); } } pixDilateBrick(line_pix, line_pix, 3, 1); pixSubtract(pix, pix, line_pix); if (textord_tabfind_show_vlines) pixWrite("hlinesclean.png", line_pix, IFF_PNG); ICOORD vertical; vertical.set_with_shrink(vertical_x, vertical_y); TabVector::MergeSimilarTabVectors(vertical, vectors, NULL); // Iterate the vectors to flip them. TabVector_IT h_it(vectors); for (h_it.mark_cycle_pt(); !h_it.cycled_list(); h_it.forward()) { h_it.data()->XYFlip(); } } pixDestroy(&line_pix); #endif }
// Finds vertical line objects in the given pix. // Uses the given resolution to determine size thresholds instead of any // that may be present in the pix. // The output vertical_x and vertical_y contain a sum of the output vectors, // thereby giving the mean vertical direction. // The output vectors are owned by the list and Frozen (cannot refit) by // having no boxes, as there is no need to refit or merge separator lines. void LineFinder::FindVerticalLines(int resolution, Pix* pix, int* vertical_x, int* vertical_y, TabVector_LIST* vectors) { #ifdef HAVE_LIBLEPT Pix* line_pix; Boxa* boxes = GetVLineBoxes(resolution, pix, &line_pix); C_BLOB_LIST line_cblobs; int width = pixGetWidth(pix); int height = pixGetHeight(pix); ConvertBoxaToBlobs(width, height, &boxes, &line_cblobs); // Make the BLOBNBOXes from the C_BLOBs. BLOBNBOX_LIST line_bblobs; C_BLOB_IT blob_it(&line_cblobs); BLOBNBOX_IT bbox_it(&line_bblobs); for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { C_BLOB* cblob = blob_it.data(); BLOBNBOX* bblob = new BLOBNBOX(cblob); bbox_it.add_to_end(bblob); } ICOORD bleft(0, 0); ICOORD tright(width, height); FindLineVectors(bleft, tright, &line_bblobs, vertical_x, vertical_y, vectors); if (!vectors->empty()) { // Some lines were found, so erase the unused blobs from the line image // and then subtract the line image from the source. bbox_it.move_to_first(); for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) { BLOBNBOX* blob = bbox_it.data(); if (blob->left_tab_type() == TT_UNCONFIRMED) { const TBOX& box = blob->bounding_box(); Box* pixbox = boxCreate(box.left(), height - box.top(), box.width(), box.height()); pixClearInRect(line_pix, pixbox); boxDestroy(&pixbox); } } pixDilateBrick(line_pix, line_pix, 1, 3); pixSubtract(pix, pix, line_pix); if (textord_tabfind_show_vlines) pixWrite("vlinesclean.png", line_pix, IFF_PNG); ICOORD vertical; vertical.set_with_shrink(*vertical_x, *vertical_y); TabVector::MergeSimilarTabVectors(vertical, vectors, NULL); } pixDestroy(&line_pix); #endif }
// Get a set of bounding boxes of possible horizontal lines in the image. // The input resolution overrides any resolution set in src_pix. // The output line_pix contains just all the detected lines. // The output boxes undergo the transformation (x,y)->(height-y,x) so the // lines can be found with a vertical line finder afterwards. // This transformation allows a simple x/y flip to reverse it in tesseract // coordinates and it is faster to flip the lines than rotate the image. Boxa* LineFinder::GetHLineBoxes(int resolution, Pix* src_pix, Pix** line_pix) { #ifdef HAVE_LIBLEPT // Remove any parts of 1 inch/kThinLineFraction high or more, by opening // away the thin lines and subtracting what's left. // This is very generous and will leave in even quite wide lines. Pix* pixt1 = pixOpenBrick(NULL, src_pix, 1, resolution / kThinLineFraction); pixSubtract(pixt1, src_pix, pixt1); // Spread vertically to allow for some skew. Pix* pixt2 = pixDilateBrick(NULL, pixt1, 1, 3); // Now keep only wide stuff of width at least 1 inch/kMinLineLengthFraction. pixOpenBrick(pixt1, pixt2, resolution / kMinLineLengthFraction, 1); pixDestroy(&pixt2); // Put a single pixel crack in every line at an arbitrary spacing, // so they break up and the bounding boxes can be used to get the // direction accurately enough without needing outlines. int wpl = pixGetWpl(pixt1); int width = pixGetWidth(pixt1); int height = pixGetHeight(pixt1); l_uint32* data = pixGetData(pixt1); for (int y = 0; y < height; ++y, data += wpl) { for (int x = kCrackSpacing; x < width; x += kCrackSpacing) { CLEAR_DATA_BIT(data, x); } } if (textord_tabfind_show_vlines) pixWrite("hlines.png", pixt1, IFF_PNG); Boxa* boxa = pixConnComp(pixt1, NULL, 8); *line_pix = pixt1; // Iterate the boxes to flip x and y. int nboxes = boxaGetCount(boxa); for (int i = 0; i < nboxes; ++i) { l_int32 x, y, box_width, box_height; boxaGetBoxGeometry(boxa, i, &x, &y, &box_width, &box_height); Box* box = boxCreate(height - (y + box_height), width - (x + box_width), box_height, box_width); boxaReplaceBox(boxa, i, box); } return boxa; #else return NULL; #endif }
/*! * pixGenTextblockMask() * * Input: pixs (1 bpp, textline mask, assumed to be 150 to 200 ppi) * pixvws (vertical white space mask) * debug (flag: 1 for debug output) * Return: pixd (textblock mask), or null on error * * Notes: * (1) Both the input masks (textline and vertical white space) and * the returned textblock mask are at the same resolution. * (2) The result is somewhat noisy, in that small "blocks" of * text may be included. These can be removed by post-processing, * using, e.g., * pixSelectBySize(pix, 60, 60, 4, L_SELECT_IF_EITHER, * L_SELECT_IF_GTE, NULL); */ PIX * pixGenTextblockMask(PIX *pixs, PIX *pixvws, l_int32 debug) { PIX *pixt1, *pixt2, *pixt3, *pixd; PROCNAME("pixGenTextblockMask"); if (!pixs) return (PIX *)ERROR_PTR("pixs not defined", procName, NULL); if (!pixvws) return (PIX *)ERROR_PTR("pixvws not defined", procName, NULL); if (pixGetDepth(pixs) != 1) return (PIX *)ERROR_PTR("pixs not 1 bpp", procName, NULL); /* Join pixels vertically to make a textblock mask */ pixt1 = pixMorphSequence(pixs, "c1.10 + o4.1", 0); pixDisplayWriteFormat(pixt1, debug, IFF_PNG); /* Solidify the textblock mask and remove noise: * (1) For each cc, 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. */ pixt2 = pixMorphSequenceByComponent(pixt1, "c30.30 + d3.3", 8, 0, 0, NULL); pixCloseSafeBrick(pixt2, pixt2, 10, 1); pixDisplayWriteFormat(pixt2, debug, IFF_PNG); pixt3 = pixSubtract(NULL, pixt2, pixvws); pixDisplayWriteFormat(pixt3, debug, IFF_PNG); pixd = pixSelectBySize(pixt3, 25, 5, 8, L_SELECT_IF_BOTH, L_SELECT_IF_GTE, NULL); pixDisplayWriteFormat(pixd, debug, IFF_PNG); pixDestroy(&pixt1); pixDestroy(&pixt2); pixDestroy(&pixt3); return pixd; }
/*! * pixThinGeneral() * * Input: pixs (1 bpp) * type (L_THIN_FG, L_THIN_BG) * sela (of Sels for parallel composite HMTs) * maxiters (max number of iters allowed; use 0 to iterate * until completion) * Return: pixd, or null on error * * Notes: * (1) See notes in pixThin(). That function chooses among * the best of the Sels for thinning. * (2) This is a general function that takes a Sela of HMTs * that are used in parallel for thinning from each * of four directions. One iteration consists of four * such parallel thins. */ PIX * pixThinGeneral(PIX *pixs, l_int32 type, SELA *sela, l_int32 maxiters) { l_int32 i, j, r, nsels, same; PIXA *pixahmt; PIX **pixhmt; /* array owned by pixahmt; do not destroy! */ PIX *pixd, *pixt; SEL *sel, *selr; PROCNAME("pixThinGeneral"); 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 (!sela) return (PIX *)ERROR_PTR("sela not defined", procName, NULL); if (maxiters == 0) maxiters = 10000; /* Set up array of temp pix to hold hmts */ nsels = selaGetCount(sela); pixahmt = pixaCreate(nsels); for (i = 0; i < nsels; i++) { pixt = pixCreateTemplate(pixs); pixaAddPix(pixahmt, pixt, L_INSERT); } pixhmt = pixaGetPixArray(pixahmt); if (!pixhmt) return (PIX *)ERROR_PTR("pixhmt array not made", procName, NULL); #if DEBUG_SELS pixt = selaDisplayInPix(sela, 35, 3, 15, 4); pixDisplayWithTitle(pixt, 100, 100, "allsels", 1); pixDestroy(&pixt); #endif /* DEBUG_SELS */ /* Set up initial image for fg thinning */ if (type == L_THIN_FG) pixd = pixCopy(NULL, pixs); else /* bg thinning */ pixd = pixInvert(NULL, pixs); /* Thin the fg, with up to maxiters iterations */ for (i = 0; i < maxiters; i++) { pixt = pixCopy(NULL, pixd); /* test for completion */ for (r = 0; r < 4; r++) { /* over 90 degree rotations of Sels */ for (j = 0; j < nsels; j++) { /* over individual sels in sela */ sel = selaGetSel(sela, j); /* not a copy */ selr = selRotateOrth(sel, r); pixHMT(pixhmt[j], pixd, selr); selDestroy(&selr); if (j > 0) pixOr(pixhmt[0], pixhmt[0], pixhmt[j]); /* accum result */ } pixSubtract(pixd, pixd, pixhmt[0]); /* remove result */ } pixEqual(pixd, pixt, &same); pixDestroy(&pixt); if (same) { L_INFO("%d iterations to completion\n", procName, i); break; } } if (type == L_THIN_BG) pixInvert(pixd, pixd); pixaDestroy(&pixahmt); return pixd; }
* that might be used * * -------------------------------------------------------------------- */ #if 0 pixd = pixCreateTemplate(pixs); pixd = pixDilate(NULL, pixs, sel); pixd = pixErode(NULL, pixs, sel); pixd = pixOpen(NULL, pixs, sel); pixd = pixClose(NULL, pixs, sel); pixDilate(pixd, pixs, sel); pixErode(pixd, pixs, sel); pixOpen(pixd, pixs, sel); pixClose(pixd, pixs, sel); pixAnd(pixd, pixd, pixs); pixOr(pixd, pixd, pixs); pixXor(pixd, pixd, pixs); pixSubtract(pixd, pixd, pixs); pixInvert(pixd, pixs); pixd = pixAnd(NULL, pixd, pixs); pixd = pixOr(NULL, pixd, pixs); pixd = pixXor(NULL, pixd, pixs); pixd = pixSubtract(NULL, pixd, pixs); pixd = pixInvert(NULL, pixs); pixInvert(pixs, pixs); #endif /* 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); }
/*! * 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; }
/*! * pixGenTextlineMask() * * Input: pixs (1 bpp, assumed to be 150 to 200 ppi) * &pixvws (<return> vertical whitespace mask) * &tlfound (<optional return> 1 if the mask is not empty) * debug (flag: 1 for debug output) * Return: pixd (textline mask), or null on error * * Notes: * (1) The input pixs should be deskewed. * (2) pixs should have no halftone pixels. * (3) Both the input image and the returned textline mask * are at the same resolution. */ PIX * pixGenTextlineMask(PIX *pixs, PIX **ppixvws, l_int32 *ptlfound, l_int32 debug) { l_int32 empty; PIX *pixt1, *pixt2, *pixvws, *pixd; PROCNAME("pixGenTextlineMask"); if (!pixs) return (PIX *)ERROR_PTR("pixs not defined", procName, NULL); if (!ppixvws) return (PIX *)ERROR_PTR("&pixvws not defined", procName, NULL); if (pixGetDepth(pixs) != 1) return (PIX *)ERROR_PTR("pixs not 1 bpp", procName, NULL); /* First we need a vertical whitespace mask. Invert the image. */ pixt1 = pixInvert(NULL, pixs); /* The whitespace mask will break textlines where there * is a large amount of white space below or above. * This can be prevented by identifying regions of the * inverted image that have large horizontal extent (bigger than * the separation between columns) and significant * vertical extent (bigger than the separation between * textlines), and subtracting this from the bg. */ pixt2 = pixMorphCompSequence(pixt1, "o80.60", 0); pixSubtract(pixt1, pixt1, pixt2); pixDisplayWriteFormat(pixt1, debug, IFF_PNG); pixDestroy(&pixt2); /* Identify vertical whitespace by opening the remaining bg. * o5.1 removes thin vertical bg lines and o1.200 extracts * long vertical bg lines. */ pixvws = pixMorphCompSequence(pixt1, "o5.1 + o1.200", 0); *ppixvws = pixvws; pixDisplayWriteFormat(pixvws, debug, IFF_PNG); pixDestroy(&pixt1); /* Three steps to getting text line mask: * (1) close the characters and words in the textlines * (2) open the vertical whitespace corridors back up * (3) small opening to remove noise */ pixt1 = pixCloseSafeBrick(NULL, pixs, 30, 1); pixDisplayWrite(pixt1, debug); pixd = pixSubtract(NULL, pixt1, pixvws); pixOpenBrick(pixd, pixd, 3, 3); pixDisplayWriteFormat(pixd, debug, IFF_PNG); pixDestroy(&pixt1); /* Check if text line mask is empty */ if (ptlfound) { *ptlfound = 0; pixZero(pixd, &empty); if (!empty) *ptlfound = 1; } return pixd; }
/*! * 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; }
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; }
main(int argc, char **argv) { char *filein; l_int32 count; CCBORDA *ccba, *ccba2; PIX *pixs, *pixd, *pixd2, *pixd3; PIX *pixt, *pixc, *pixc2; static char mainName[] = "ccbordtest"; if (argc != 2) exit(ERROR_INT(" Syntax: ccbordtest filein", mainName, 1)); filein = argv[1]; if ((pixs = pixRead(filein)) == NULL) exit(ERROR_INT("pixs not made", mainName, 1)); fprintf(stderr, "Get border representation..."); startTimer(); ccba = pixGetAllCCBorders(pixs); fprintf(stderr, "%6.3f sec\n", stopTimer()); #if 0 /* get global locs directly and display borders */ fprintf(stderr, "Convert from local to global locs..."); startTimer(); ccbaGenerateGlobalLocs(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Display border representation..."); startTimer(); pixd = ccbaDisplayBorder(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkborder1.png", pixd, IFF_PNG); #else /* get step chain code, then global coords, and display borders */ fprintf(stderr, "Get step chain code..."); startTimer(); ccbaGenerateStepChains(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Convert from step chain to global locs..."); startTimer(); ccbaStepChainsToPixCoords(ccba, CCB_GLOBAL_COORDS); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Display border representation..."); startTimer(); pixd = ccbaDisplayBorder(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkborder1.png", pixd, IFF_PNG); #endif /* check if border pixels are in original set */ fprintf(stderr, "Check if border pixels are in original set ...\n"); pixt = pixSubtract(NULL, pixd, pixs); pixCountPixels(pixt, &count, NULL); if (count == 0) fprintf(stderr, " all border pixels are in original set\n"); else fprintf(stderr, " %d border pixels are not in original set\n", count); pixDestroy(&pixt); /* display image */ fprintf(stderr, "Reconstruct image ..."); startTimer(); /* pixc = ccbaDisplayImage1(ccba); */ pixc = ccbaDisplayImage2(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkrecon1.png", pixc, IFF_PNG); /* check with original to see if correct */ fprintf(stderr, "Check with original to see if correct ...\n"); pixXor(pixc, pixc, pixs); pixCountPixels(pixc, &count, NULL); if (count == 0) fprintf(stderr, " perfect direct recon\n"); else { l_int32 w, h, i, j; l_uint32 val; fprintf(stderr, " %d pixels in error in recon\n", count); #if 1 w = pixGetWidth(pixc); h = pixGetHeight(pixc); for (i = 0; i < h; i++) { for (j = 0; j < w; j++) { pixGetPixel(pixc, j, i, &val); if (val == 1) fprintf(stderr, "bad pixel at (%d, %d)\n", j, i); } } pixWrite("/tmp/junkbadpixels.png", pixc, IFF_PNG); #endif } /*----------------------------------------------------------* * write to file (compressed) and read back * *----------------------------------------------------------*/ fprintf(stderr, "Write serialized step data..."); startTimer(); ccbaWrite("/tmp/junkstepout", ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Read serialized step data..."); startTimer(); ccba2 = ccbaRead("/tmp/junkstepout"); fprintf(stderr, "%6.3f sec\n", stopTimer()); /* display the border pixels again */ fprintf(stderr, "Convert from step chain to global locs..."); startTimer(); ccbaStepChainsToPixCoords(ccba2, CCB_GLOBAL_COORDS); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Display border representation..."); startTimer(); pixd2 = ccbaDisplayBorder(ccba2); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkborder2.png", pixd2, IFF_PNG); /* check if border pixels are same as first time */ pixXor(pixd2, pixd2, pixd); pixCountPixels(pixd2, &count, NULL); if (count == 0) fprintf(stderr, " perfect w/r border recon\n"); else { l_int32 w, h, i, j, val; fprintf(stderr, " %d pixels in error in w/r recon\n", count); } pixDestroy(&pixd2); /* display image again */ fprintf(stderr, "Convert from step chain to local coords..."); startTimer(); ccbaStepChainsToPixCoords(ccba2, CCB_LOCAL_COORDS); fprintf(stderr, "%6.3f sec\n", stopTimer()); fprintf(stderr, "Reconstruct image from file ..."); startTimer(); /* pixc2 = ccbaDisplayImage1(ccba2); */ pixc2 = ccbaDisplayImage2(ccba2); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkrecon2.png", pixc2, IFF_PNG); /* check with original to see if correct */ fprintf(stderr, "Check with original to see if correct ...\n"); pixXor(pixc2, pixc2, pixs); pixCountPixels(pixc2, &count, NULL); if (count == 0) fprintf(stderr, " perfect image recon\n"); else { l_int32 w, h, i, j; l_uint32 val; fprintf(stderr, " %d pixels in error in image recon\n", count); #if 1 w = pixGetWidth(pixc2); h = pixGetHeight(pixc2); for (i = 0; i < h; i++) { for (j = 0; j < w; j++) { pixGetPixel(pixc2, j, i, &val); if (val == 1) fprintf(stderr, "bad pixel at (%d, %d)\n", j, i); } } pixWrite("/tmp/junkbadpixels2.png", pixc2, IFF_PNG); #endif } /*----------------------------------------------------------* * make, display and check single path border for svg * *----------------------------------------------------------*/ /* make local single path border for svg */ fprintf(stderr, "Make local single path borders for svg ..."); startTimer(); ccbaGenerateSinglePath(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); /* generate global single path border */ fprintf(stderr, "Generate global single path borders ..."); startTimer(); ccbaGenerateSPGlobalLocs(ccba, CCB_SAVE_TURNING_PTS); fprintf(stderr, "%6.3f sec\n", stopTimer()); /* display border pixels from single path */ fprintf(stderr, "Display border from single path..."); startTimer(); pixd3 = ccbaDisplaySPBorder(ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); pixWrite("/tmp/junkborder3.png", pixd3, IFF_PNG); /* check if border pixels are in original set */ fprintf(stderr, "Check if border pixels are in original set ...\n"); pixt = pixSubtract(NULL, pixd3, pixs); pixCountPixels(pixt, &count, NULL); if (count == 0) fprintf(stderr, " all border pixels are in original set\n"); else fprintf(stderr, " %d border pixels are not in original set\n", count); pixDestroy(&pixt); pixDestroy(&pixd3); /* output in svg file format */ fprintf(stderr, "Write output in svg file format ...\n"); startTimer(); ccbaWriteSVG("/tmp/junksvg", ccba); fprintf(stderr, "%6.3f sec\n", stopTimer()); ccbaDestroy(&ccba2); ccbaDestroy(&ccba); pixDestroy(&pixs); pixDestroy(&pixd); pixDestroy(&pixc); pixDestroy(&pixc2); return 0; }
int main(int argc, char **argv) { PIX *pixs, *pix1, *pix2, *pix3, *pix4; L_REGPARAMS *rp; if (regTestSetup(argc, argv, &rp)) return 1; pixs = pixRead("test1.png"); /* pixInvert */ pix1 = pixInvert(NULL, pixs); pix2 = pixCreateTemplate(pixs); /* into pixd of same size */ pixInvert(pix2, pixs); regTestWritePixAndCheck(rp, pix1, IFF_PNG); /* 0 */ regTestComparePix(rp, pix1, pix2); /* 1 */ pix3 = pixRead("marge.jpg"); /* into pixd of different size */ pixInvert(pix3, pixs); regTestComparePix(rp, pix1, pix3); /* 2 */ pixDestroy(&pix1); pixDestroy(&pix2); pixDestroy(&pix3); pix1 = pixOpenBrick(NULL, pixs, 1, 9); pix2 = pixDilateBrick(NULL, pixs, 1, 9); /* pixOr */ pix3 = pixCreateTemplate(pixs); pixOr(pix3, pixs, pix1); /* existing */ pix4 = pixOr(NULL, pixs, pix1); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 3 */ regTestComparePix(rp, pix3, pix4); /* 4 */ pixCopy(pix4, pix1); pixOr(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 5 */ pixDestroy(&pix3); pixDestroy(&pix4); pix3 = pixCreateTemplate(pixs); pixOr(pix3, pixs, pix2); /* existing */ pix4 = pixOr(NULL, pixs, pix2); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 6 */ regTestComparePix(rp, pix3, pix4); /* 7 */ pixCopy(pix4, pix2); pixOr(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 8 */ pixDestroy(&pix3); pixDestroy(&pix4); /* pixAnd */ pix3 = pixCreateTemplate(pixs); pixAnd(pix3, pixs, pix1); /* existing */ pix4 = pixAnd(NULL, pixs, pix1); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 9 */ regTestComparePix(rp, pix3, pix4); /* 10 */ pixCopy(pix4, pix1); pixAnd(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 11 */ pixDestroy(&pix3); pixDestroy(&pix4); pix3 = pixCreateTemplate(pixs); pixAnd(pix3, pixs, pix2); /* existing */ pix4 = pixAnd(NULL, pixs, pix2); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 12 */ regTestComparePix(rp, pix3, pix4); /* 13 */ pixCopy(pix4, pix2); pixAnd(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 14 */ pixDestroy(&pix3); pixDestroy(&pix4); /* pixXor */ pix3 = pixCreateTemplate(pixs); pixXor(pix3, pixs, pix1); /* existing */ pix4 = pixXor(NULL, pixs, pix1); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 15 */ regTestComparePix(rp, pix3, pix4); /* 16 */ pixCopy(pix4, pix1); pixXor(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 17 */ pixDestroy(&pix3); pixDestroy(&pix4); pix3 = pixCreateTemplate(pixs); pixXor(pix3, pixs, pix2); /* existing */ pix4 = pixXor(NULL, pixs, pix2); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 18 */ regTestComparePix(rp, pix3, pix4); /* 19 */ pixCopy(pix4, pix2); pixXor(pix4, pix4, pixs); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 20 */ pixDestroy(&pix3); pixDestroy(&pix4); /* pixSubtract */ pix3 = pixCreateTemplate(pixs); pixSubtract(pix3, pixs, pix1); /* existing */ pix4 = pixSubtract(NULL, pixs, pix1); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 21 */ regTestComparePix(rp, pix3, pix4); /* 22 */ pixCopy(pix4, pix1); pixSubtract(pix4, pixs, pix4); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 23 */ pixDestroy(&pix3); pixDestroy(&pix4); pix3 = pixCreateTemplate(pixs); pixSubtract(pix3, pixs, pix2); /* existing */ pix4 = pixSubtract(NULL, pixs, pix2); /* new */ regTestWritePixAndCheck(rp, pix3, IFF_PNG); /* 24 */ regTestComparePix(rp, pix3, pix4); /* 25 */ pixCopy(pix4, pix2); pixSubtract(pix4, pixs, pix4); /* in-place */ regTestComparePix(rp, pix3, pix4); /* 26 */ pixDestroy(&pix3); pixDestroy(&pix4); pix4 = pixRead("marge.jpg"); pixSubtract(pix4, pixs, pixs); /* subtract from itself; should be empty */ pix3 = pixCreateTemplate(pixs); regTestComparePix(rp, pix3, pix4); /* 27*/ pixDestroy(&pix3); pixDestroy(&pix4); pixSubtract(pixs, pixs, pixs); /* subtract from itself; should be empty */ pix3 = pixCreateTemplate(pixs); regTestComparePix(rp, pix3, pixs); /* 28*/ pixDestroy(&pix3); pixDestroy(&pixs); pixDestroy(&pix1); pixDestroy(&pix2); return regTestCleanup(rp); }