|
tesseract 3.04.01
|
00001 00002 // File: strokewidth.cpp 00003 // Description: Subclass of BBGrid to find uniformity of strokewidth. 00004 // Author: Ray Smith 00005 // Created: Mon Mar 31 16:17:01 PST 2008 00006 // 00007 // (C) Copyright 2008, Google Inc. 00008 // Licensed under the Apache License, Version 2.0 (the "License"); 00009 // you may not use this file except in compliance with the License. 00010 // You may obtain a copy of the License at 00011 // http://www.apache.org/licenses/LICENSE-2.0 00012 // Unless required by applicable law or agreed to in writing, software 00013 // distributed under the License is distributed on an "AS IS" BASIS, 00014 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 00015 // See the License for the specific language governing permissions and 00016 // limitations under the License. 00017 // 00019 00020 #ifdef _MSC_VER 00021 #pragma warning(disable:4244) // Conversion warnings 00022 #endif 00023 00024 #ifdef HAVE_CONFIG_H 00025 #include "config_auto.h" 00026 #endif 00027 00028 #include "strokewidth.h" 00029 00030 #include <math.h> 00031 00032 #include "blobbox.h" 00033 #include "colpartition.h" 00034 #include "colpartitiongrid.h" 00035 #include "imagefind.h" 00036 #include "linlsq.h" 00037 #include "statistc.h" 00038 #include "tabfind.h" 00039 #include "textlineprojection.h" 00040 #include "tordmain.h" // For SetBlobStrokeWidth. 00041 00042 namespace tesseract { 00043 00044 INT_VAR(textord_tabfind_show_strokewidths, 0, "Show stroke widths"); 00045 BOOL_VAR(textord_tabfind_only_strokewidths, false, "Only run stroke widths"); 00046 00048 const double kStrokeWidthFractionTolerance = 0.125; 00053 const double kStrokeWidthTolerance = 1.5; 00054 // Same but for CJK we are a bit more generous. 00055 const double kStrokeWidthFractionCJK = 0.25; 00056 const double kStrokeWidthCJK = 2.0; 00057 // Radius in grid cells of search for broken CJK. Doesn't need to be very 00058 // large as the grid size should be about the size of a character anyway. 00059 const int kCJKRadius = 2; 00060 // Max distance fraction of size to join close but broken CJK characters. 00061 const double kCJKBrokenDistanceFraction = 0.25; 00062 // Max number of components in a broken CJK character. 00063 const int kCJKMaxComponents = 8; 00064 // Max aspect ratio of CJK broken characters when put back together. 00065 const double kCJKAspectRatio = 1.25; 00066 // Max increase in aspect ratio of CJK broken characters when merged. 00067 const double kCJKAspectRatioIncrease = 1.0625; 00068 // Max multiple of the grid size that will be used in computing median CJKsize. 00069 const int kMaxCJKSizeRatio = 5; 00070 // Min fraction of blobs broken CJK to iterate and run it again. 00071 const double kBrokenCJKIterationFraction = 0.125; 00072 // Multiple of gridsize as x-padding for a search box for diacritic base 00073 // characters. 00074 const double kDiacriticXPadRatio = 7.0; 00075 // Multiple of gridsize as y-padding for a search box for diacritic base 00076 // characters. 00077 const double kDiacriticYPadRatio = 1.75; 00078 // Min multiple of diacritic height that a neighbour must be to be a 00079 // convincing base character. 00080 const double kMinDiacriticSizeRatio = 1.0625; 00081 // Max multiple of a textline's median height as a threshold for the sum of 00082 // a diacritic's farthest x and y distances (gap + size). 00083 const double kMaxDiacriticDistanceRatio = 1.25; 00084 // Max x-gap between a diacritic and its base char as a fraction of the height 00085 // of the base char (allowing other blobs to fill the gap.) 00086 const double kMaxDiacriticGapToBaseCharHeight = 1.0; 00087 // Radius of a search for diacritics in grid units. 00088 const int kSearchRadius = 2; 00089 // Ratio between longest side of a line and longest side of a character. 00090 // (neighbor_min > blob_min * kLineTrapShortest && 00091 // neighbor_max < blob_max / kLineTrapLongest) 00092 // => neighbor is a grapheme and blob is a line. 00093 const int kLineTrapLongest = 4; 00094 // Ratio between shortest side of a line and shortest side of a character. 00095 const int kLineTrapShortest = 2; 00096 // Max aspect ratio of the total box before CountNeighbourGaps 00097 // decides immediately based on the aspect ratio. 00098 const int kMostlyOneDirRatio = 3; 00099 // Aspect ratio for a blob to be considered as line residue. 00100 const double kLineResidueAspectRatio = 8.0; 00101 // Padding ratio for line residue search box. 00102 const int kLineResiduePadRatio = 3; 00103 // Min multiple of neighbour size for a line residue to be genuine. 00104 const double kLineResidueSizeRatio = 1.75; 00105 // Aspect ratio filter for OSD. 00106 const float kSizeRatioToReject = 2.0; 00107 // Max number of normal blobs a large blob may overlap before it is rejected 00108 // and determined to be image 00109 const int kMaxLargeOverlaps = 3; 00110 // Expansion factor for search box for good neighbours. 00111 const double kNeighbourSearchFactor = 2.5; 00112 // Factor of increase of overlap when adding diacritics to make an image noisy. 00113 const double kNoiseOverlapGrowthFactor = 4.0; 00114 // Fraction of the image size to add overlap when adding diacritics for an 00115 // image to qualify as noisy. 00116 const double kNoiseOverlapAreaFactor = 1.0 / 512; 00117 // Ratio of perimeter^2/area for a blob to be considered noise vs i dot. 00118 const double kShapePerimeterRatio = 3.0; 00119 00120 StrokeWidth::StrokeWidth(int gridsize, 00121 const ICOORD& bleft, const ICOORD& tright) 00122 : BlobGrid(gridsize, bleft, tright), nontext_map_(NULL), projection_(NULL), 00123 denorm_(NULL), grid_box_(bleft, tright), rerotation_(1.0f, 0.0f) { 00124 leaders_win_ = NULL; 00125 widths_win_ = NULL; 00126 initial_widths_win_ = NULL; 00127 chains_win_ = NULL; 00128 diacritics_win_ = NULL; 00129 textlines_win_ = NULL; 00130 smoothed_win_ = NULL; 00131 } 00132 00133 StrokeWidth::~StrokeWidth() { 00134 if (widths_win_ != NULL) { 00135 #ifndef GRAPHICS_DISABLED 00136 delete widths_win_->AwaitEvent(SVET_DESTROY); 00137 #endif // GRAPHICS_DISABLED 00138 if (textord_tabfind_only_strokewidths) 00139 exit(0); 00140 delete widths_win_; 00141 } 00142 delete leaders_win_; 00143 delete initial_widths_win_; 00144 delete chains_win_; 00145 delete textlines_win_; 00146 delete smoothed_win_; 00147 delete diacritics_win_; 00148 } 00149 00150 // Sets the neighbours member of the medium-sized blobs in the block. 00151 // Searches on 4 sides of each blob for similar-sized, similar-strokewidth 00152 // blobs and sets pointers to the good neighbours. 00153 void StrokeWidth::SetNeighboursOnMediumBlobs(TO_BLOCK* block) { 00154 // Run a preliminary strokewidth neighbour detection on the medium blobs. 00155 InsertBlobList(&block->blobs); 00156 BLOBNBOX_IT blob_it(&block->blobs); 00157 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 00158 SetNeighbours(false, false, blob_it.data()); 00159 } 00160 Clear(); 00161 } 00162 00163 // Sets the neighbour/textline writing direction members of the medium 00164 // and large blobs with optional repair of broken CJK characters first. 00165 // Repair of broken CJK is needed here because broken CJK characters 00166 // can fool the textline direction detection algorithm. 00167 void StrokeWidth::FindTextlineDirectionAndFixBrokenCJK(PageSegMode pageseg_mode, 00168 bool cjk_merge, 00169 TO_BLOCK* input_block) { 00170 // Setup the grid with the remaining (non-noise) blobs. 00171 InsertBlobs(input_block); 00172 // Repair broken CJK characters if needed. 00173 while (cjk_merge && FixBrokenCJK(input_block)); 00174 // Grade blobs by inspection of neighbours. 00175 FindTextlineFlowDirection(pageseg_mode, false); 00176 // Clear the grid ready for rotation or leader finding. 00177 Clear(); 00178 } 00179 00180 // Helper to collect and count horizontal and vertical blobs from a list. 00181 static void CollectHorizVertBlobs(BLOBNBOX_LIST* input_blobs, 00182 int* num_vertical_blobs, 00183 int* num_horizontal_blobs, 00184 BLOBNBOX_CLIST* vertical_blobs, 00185 BLOBNBOX_CLIST* horizontal_blobs, 00186 BLOBNBOX_CLIST* nondescript_blobs) { 00187 BLOBNBOX_C_IT v_it(vertical_blobs); 00188 BLOBNBOX_C_IT h_it(horizontal_blobs); 00189 BLOBNBOX_C_IT n_it(nondescript_blobs); 00190 BLOBNBOX_IT blob_it(input_blobs); 00191 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 00192 BLOBNBOX* blob = blob_it.data(); 00193 const TBOX& box = blob->bounding_box(); 00194 float y_x = static_cast<float>(box.height()) / box.width(); 00195 float x_y = 1.0f / y_x; 00196 // Select a >= 1.0 ratio 00197 float ratio = x_y > y_x ? x_y : y_x; 00198 // If the aspect ratio is small and we want them for osd, save the blob. 00199 bool ok_blob = ratio <= kSizeRatioToReject; 00200 if (blob->UniquelyVertical()) { 00201 ++*num_vertical_blobs; 00202 if (ok_blob) v_it.add_after_then_move(blob); 00203 } else if (blob->UniquelyHorizontal()) { 00204 ++*num_horizontal_blobs; 00205 if (ok_blob) h_it.add_after_then_move(blob); 00206 } else if (ok_blob) { 00207 n_it.add_after_then_move(blob); 00208 } 00209 } 00210 } 00211 00212 00213 // Types all the blobs as vertical or horizontal text or unknown and 00214 // returns true if the majority are vertical. 00215 // If the blobs are rotated, it is necessary to call CorrectForRotation 00216 // after rotating everything, otherwise the work done here will be enough. 00217 // If osd_blobs is not null, a list of blobs from the dominant textline 00218 // direction are returned for use in orientation and script detection. 00219 bool StrokeWidth::TestVerticalTextDirection(double find_vertical_text_ratio, 00220 TO_BLOCK* block, 00221 BLOBNBOX_CLIST* osd_blobs) { 00222 int vertical_boxes = 0; 00223 int horizontal_boxes = 0; 00224 // Count vertical normal and large blobs. 00225 BLOBNBOX_CLIST vertical_blobs; 00226 BLOBNBOX_CLIST horizontal_blobs; 00227 BLOBNBOX_CLIST nondescript_blobs; 00228 CollectHorizVertBlobs(&block->blobs, &vertical_boxes, &horizontal_boxes, 00229 &vertical_blobs, &horizontal_blobs, &nondescript_blobs); 00230 CollectHorizVertBlobs(&block->large_blobs, &vertical_boxes, &horizontal_boxes, 00231 &vertical_blobs, &horizontal_blobs, &nondescript_blobs); 00232 if (textord_debug_tabfind) 00233 tprintf("TextDir hbox=%d vs vbox=%d, %dH, %dV, %dN osd blobs\n", 00234 horizontal_boxes, vertical_boxes, 00235 horizontal_blobs.length(), vertical_blobs.length(), 00236 nondescript_blobs.length()); 00237 if (osd_blobs != NULL && vertical_boxes == 0 && horizontal_boxes == 0) { 00238 // Only nondescript blobs available, so return those. 00239 BLOBNBOX_C_IT osd_it(osd_blobs); 00240 osd_it.add_list_after(&nondescript_blobs); 00241 return false; 00242 } 00243 int min_vert_boxes = static_cast<int>((vertical_boxes + horizontal_boxes) * 00244 find_vertical_text_ratio); 00245 if (vertical_boxes >= min_vert_boxes) { 00246 if (osd_blobs != NULL) { 00247 BLOBNBOX_C_IT osd_it(osd_blobs); 00248 osd_it.add_list_after(&vertical_blobs); 00249 } 00250 return true; 00251 } else { 00252 if (osd_blobs != NULL) { 00253 BLOBNBOX_C_IT osd_it(osd_blobs); 00254 osd_it.add_list_after(&horizontal_blobs); 00255 } 00256 return false; 00257 } 00258 } 00259 00260 // Corrects the data structures for the given rotation. 00261 void StrokeWidth::CorrectForRotation(const FCOORD& rotation, 00262 ColPartitionGrid* part_grid) { 00263 Init(part_grid->gridsize(), part_grid->bleft(), part_grid->tright()); 00264 grid_box_ = TBOX(bleft(), tright()); 00265 rerotation_.set_x(rotation.x()); 00266 rerotation_.set_y(-rotation.y()); 00267 } 00268 00269 // Finds leader partitions and inserts them into the given part_grid. 00270 void StrokeWidth::FindLeaderPartitions(TO_BLOCK* block, 00271 ColPartitionGrid* part_grid) { 00272 Clear(); 00273 // Find and isolate leaders in the noise list. 00274 ColPartition_LIST leader_parts; 00275 FindLeadersAndMarkNoise(block, &leader_parts); 00276 // Setup the strokewidth grid with the block's remaining (non-noise) blobs. 00277 InsertBlobList(&block->blobs); 00278 // Mark blobs that have leader neighbours. 00279 for (ColPartition_IT it(&leader_parts); !it.empty(); it.forward()) { 00280 ColPartition* part = it.extract(); 00281 part->ClaimBoxes(); 00282 MarkLeaderNeighbours(part, LR_LEFT); 00283 MarkLeaderNeighbours(part, LR_RIGHT); 00284 part_grid->InsertBBox(true, true, part); 00285 } 00286 } 00287 00288 // Finds and marks noise those blobs that look like bits of vertical lines 00289 // that would otherwise screw up layout analysis. 00290 void StrokeWidth::RemoveLineResidue(ColPartition_LIST* big_part_list) { 00291 BlobGridSearch gsearch(this); 00292 BLOBNBOX* bbox; 00293 // For every vertical line-like bbox in the grid, search its neighbours 00294 // to find the tallest, and if the original box is taller by sufficient 00295 // margin, then call it line residue and delete it. 00296 gsearch.StartFullSearch(); 00297 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00298 TBOX box = bbox->bounding_box(); 00299 if (box.height() < box.width() * kLineResidueAspectRatio) 00300 continue; 00301 // Set up a rectangle search around the blob to find the size of its 00302 // neighbours. 00303 int padding = box.height() * kLineResiduePadRatio; 00304 TBOX search_box = box; 00305 search_box.pad(padding, padding); 00306 bool debug = AlignedBlob::WithinTestRegion(2, box.left(), 00307 box.bottom()); 00308 // Find the largest object in the search box not equal to bbox. 00309 BlobGridSearch rsearch(this); 00310 int max_size = 0; 00311 BLOBNBOX* n; 00312 rsearch.StartRectSearch(search_box); 00313 while ((n = rsearch.NextRectSearch()) != NULL) { 00314 if (n == bbox) continue; 00315 TBOX nbox = n->bounding_box(); 00316 if (nbox.height() > max_size) { 00317 max_size = nbox.height(); 00318 } 00319 } 00320 if (debug) { 00321 tprintf("Max neighbour size=%d for candidate line box at:", max_size); 00322 box.print(); 00323 } 00324 if (max_size * kLineResidueSizeRatio < box.height()) { 00325 #ifndef GRAPHICS_DISABLED 00326 if (leaders_win_ != NULL) { 00327 // We are debugging, so display deleted in pink blobs in the same 00328 // window that we use to display leader detection. 00329 leaders_win_->Pen(ScrollView::PINK); 00330 leaders_win_->Rectangle(box.left(), box.bottom(), 00331 box.right(), box.top()); 00332 } 00333 #endif // GRAPHICS_DISABLED 00334 ColPartition::MakeBigPartition(bbox, big_part_list); 00335 } 00336 } 00337 } 00338 00339 // Types all the blobs as vertical text or horizontal text or unknown and 00340 // puts them into initial ColPartitions in the supplied part_grid. 00341 // rerotation determines how to get back to the image coordinates from the 00342 // blob coordinates (since they may have been rotated for vertical text). 00343 // block is the single block for the whole page or rectangle to be OCRed. 00344 // nontext_pix (full-size), is a binary mask used to prevent merges across 00345 // photo/text boundaries. It is not kept beyond this function. 00346 // denorm provides a mapping back to the image from the current blob 00347 // coordinate space. 00348 // projection provides a measure of textline density over the image and 00349 // provides functions to assist with diacritic detection. It should be a 00350 // pointer to a new TextlineProjection, and will be setup here. 00351 // part_grid is the output grid of textline partitions. 00352 // Large blobs that cause overlap are put in separate partitions and added 00353 // to the big_parts list. 00354 void StrokeWidth::GradeBlobsIntoPartitions( 00355 PageSegMode pageseg_mode, const FCOORD& rerotation, TO_BLOCK* block, 00356 Pix* nontext_pix, const DENORM* denorm, bool cjk_script, 00357 TextlineProjection* projection, BLOBNBOX_LIST* diacritic_blobs, 00358 ColPartitionGrid* part_grid, ColPartition_LIST* big_parts) { 00359 nontext_map_ = nontext_pix; 00360 projection_ = projection; 00361 denorm_ = denorm; 00362 // Clear and re Insert to take advantage of the tab stops in the blobs. 00363 Clear(); 00364 // Setup the strokewidth grid with the remaining non-noise, non-leader blobs. 00365 InsertBlobs(block); 00366 00367 // Run FixBrokenCJK() again if the page is CJK. 00368 if (cjk_script) { 00369 FixBrokenCJK(block); 00370 } 00371 FindTextlineFlowDirection(pageseg_mode, false); 00372 projection_->ConstructProjection(block, rerotation, nontext_map_); 00373 if (textord_tabfind_show_strokewidths) { 00374 ScrollView* line_blobs_win = MakeWindow(0, 0, "Initial textline Blobs"); 00375 projection_->PlotGradedBlobs(&block->blobs, line_blobs_win); 00376 projection_->PlotGradedBlobs(&block->small_blobs, line_blobs_win); 00377 } 00378 projection_->MoveNonTextlineBlobs(&block->blobs, &block->noise_blobs); 00379 projection_->MoveNonTextlineBlobs(&block->small_blobs, &block->noise_blobs); 00380 // Clear and re Insert to take advantage of the removed diacritics. 00381 Clear(); 00382 InsertBlobs(block); 00383 FCOORD skew; 00384 FindTextlineFlowDirection(pageseg_mode, true); 00385 PartitionFindResult r = 00386 FindInitialPartitions(pageseg_mode, rerotation, true, block, 00387 diacritic_blobs, part_grid, big_parts, &skew); 00388 if (r == PFR_NOISE) { 00389 tprintf("Detected %d diacritics\n", diacritic_blobs->length()); 00390 // Noise was found, and removed. 00391 Clear(); 00392 InsertBlobs(block); 00393 FindTextlineFlowDirection(pageseg_mode, true); 00394 r = FindInitialPartitions(pageseg_mode, rerotation, false, block, 00395 diacritic_blobs, part_grid, big_parts, &skew); 00396 } 00397 nontext_map_ = NULL; 00398 projection_ = NULL; 00399 denorm_ = NULL; 00400 } 00401 00402 static void PrintBoxWidths(BLOBNBOX* neighbour) { 00403 TBOX nbox = neighbour->bounding_box(); 00404 tprintf("Box (%d,%d)->(%d,%d): h-width=%.1f, v-width=%.1f p-width=%1.f\n", 00405 nbox.left(), nbox.bottom(), nbox.right(), nbox.top(), 00406 neighbour->horz_stroke_width(), neighbour->vert_stroke_width(), 00407 2.0 * neighbour->cblob()->area()/neighbour->cblob()->perimeter()); 00408 } 00409 00411 void StrokeWidth::HandleClick(int x, int y) { 00412 BBGrid<BLOBNBOX, BLOBNBOX_CLIST, BLOBNBOX_C_IT>::HandleClick(x, y); 00413 // Run a radial search for blobs that overlap. 00414 BlobGridSearch radsearch(this); 00415 radsearch.StartRadSearch(x, y, 1); 00416 BLOBNBOX* neighbour; 00417 FCOORD click(static_cast<float>(x), static_cast<float>(y)); 00418 while ((neighbour = radsearch.NextRadSearch()) != NULL) { 00419 TBOX nbox = neighbour->bounding_box(); 00420 if (nbox.contains(click) && neighbour->cblob() != NULL) { 00421 PrintBoxWidths(neighbour); 00422 if (neighbour->neighbour(BND_LEFT) != NULL) 00423 PrintBoxWidths(neighbour->neighbour(BND_LEFT)); 00424 if (neighbour->neighbour(BND_RIGHT) != NULL) 00425 PrintBoxWidths(neighbour->neighbour(BND_RIGHT)); 00426 if (neighbour->neighbour(BND_ABOVE) != NULL) 00427 PrintBoxWidths(neighbour->neighbour(BND_ABOVE)); 00428 if (neighbour->neighbour(BND_BELOW) != NULL) 00429 PrintBoxWidths(neighbour->neighbour(BND_BELOW)); 00430 int gaps[BND_COUNT]; 00431 neighbour->NeighbourGaps(gaps); 00432 tprintf("Left gap=%d, right=%d, above=%d, below=%d, horz=%d, vert=%d\n" 00433 "Good= %d %d %d %d\n", 00434 gaps[BND_LEFT], gaps[BND_RIGHT], 00435 gaps[BND_ABOVE], gaps[BND_BELOW], 00436 neighbour->horz_possible(), 00437 neighbour->vert_possible(), 00438 neighbour->good_stroke_neighbour(BND_LEFT), 00439 neighbour->good_stroke_neighbour(BND_RIGHT), 00440 neighbour->good_stroke_neighbour(BND_ABOVE), 00441 neighbour->good_stroke_neighbour(BND_BELOW)); 00442 break; 00443 } 00444 } 00445 } 00446 00447 // Detects and marks leader dots/dashes. 00448 // Leaders are horizontal chains of small or noise blobs that look 00449 // monospace according to ColPartition::MarkAsLeaderIfMonospaced(). 00450 // Detected leaders become the only occupants of the block->small_blobs list. 00451 // Non-leader small blobs get moved to the blobs list. 00452 // Non-leader noise blobs remain singletons in the noise list. 00453 // All small and noise blobs in high density regions are marked BTFT_NONTEXT. 00454 // block is the single block for the whole page or rectangle to be OCRed. 00455 // leader_parts is the output. 00456 void StrokeWidth::FindLeadersAndMarkNoise(TO_BLOCK* block, 00457 ColPartition_LIST* leader_parts) { 00458 InsertBlobList(&block->small_blobs); 00459 InsertBlobList(&block->noise_blobs); 00460 BlobGridSearch gsearch(this); 00461 BLOBNBOX* bbox; 00462 // For every bbox in the grid, set its neighbours. 00463 gsearch.StartFullSearch(); 00464 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00465 SetNeighbours(true, false, bbox); 00466 } 00467 ColPartition_IT part_it(leader_parts); 00468 gsearch.StartFullSearch(); 00469 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00470 if (bbox->flow() == BTFT_NONE) { 00471 if (bbox->neighbour(BND_RIGHT) == NULL && 00472 bbox->neighbour(BND_LEFT) == NULL) 00473 continue; 00474 // Put all the linked blobs into a ColPartition. 00475 ColPartition* part = new ColPartition(BRT_UNKNOWN, ICOORD(0, 1)); 00476 BLOBNBOX* blob; 00477 for (blob = bbox; blob != NULL && blob->flow() == BTFT_NONE; 00478 blob = blob->neighbour(BND_RIGHT)) 00479 part->AddBox(blob); 00480 for (blob = bbox->neighbour(BND_LEFT); blob != NULL && 00481 blob->flow() == BTFT_NONE; 00482 blob = blob->neighbour(BND_LEFT)) 00483 part->AddBox(blob); 00484 if (part->MarkAsLeaderIfMonospaced()) 00485 part_it.add_after_then_move(part); 00486 else 00487 delete part; 00488 } 00489 } 00490 if (textord_tabfind_show_strokewidths) { 00491 leaders_win_ = DisplayGoodBlobs("LeaderNeighbours", 0, 0); 00492 } 00493 // Move any non-leaders from the small to the blobs list, as they are 00494 // most likely dashes or broken characters. 00495 BLOBNBOX_IT blob_it(&block->blobs); 00496 BLOBNBOX_IT small_it(&block->small_blobs); 00497 for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) { 00498 BLOBNBOX* blob = small_it.data(); 00499 if (blob->flow() != BTFT_LEADER) { 00500 if (blob->flow() == BTFT_NEIGHBOURS) 00501 blob->set_flow(BTFT_NONE); 00502 blob->ClearNeighbours(); 00503 blob_it.add_to_end(small_it.extract()); 00504 } 00505 } 00506 // Move leaders from the noise list to the small list, leaving the small 00507 // list exclusively leaders, so they don't get processed further, 00508 // and the remaining small blobs all in the noise list. 00509 BLOBNBOX_IT noise_it(&block->noise_blobs); 00510 for (noise_it.mark_cycle_pt(); !noise_it.cycled_list(); noise_it.forward()) { 00511 BLOBNBOX* blob = noise_it.data(); 00512 if (blob->flow() == BTFT_LEADER || blob->joined_to_prev()) { 00513 small_it.add_to_end(noise_it.extract()); 00514 } else if (blob->flow() == BTFT_NEIGHBOURS) { 00515 blob->set_flow(BTFT_NONE); 00516 blob->ClearNeighbours(); 00517 } 00518 } 00519 // Clear the grid as we don't want the small stuff hanging around in it. 00520 Clear(); 00521 } 00522 00525 void StrokeWidth::InsertBlobs(TO_BLOCK* block) { 00526 InsertBlobList(&block->blobs); 00527 InsertBlobList(&block->large_blobs); 00528 } 00529 00530 // Checks the left or right side of the given leader partition and sets the 00531 // (opposite) leader_on_right or leader_on_left flags for blobs 00532 // that are next to the given side of the given leader partition. 00533 void StrokeWidth::MarkLeaderNeighbours(const ColPartition* part, 00534 LeftOrRight side) { 00535 const TBOX& part_box = part->bounding_box(); 00536 BlobGridSearch blobsearch(this); 00537 // Search to the side of the leader for the nearest neighbour. 00538 BLOBNBOX* best_blob = NULL; 00539 int best_gap = 0; 00540 blobsearch.StartSideSearch(side == LR_LEFT ? part_box.left() 00541 : part_box.right(), 00542 part_box.bottom(), part_box.top()); 00543 BLOBNBOX* blob; 00544 while ((blob = blobsearch.NextSideSearch(side == LR_LEFT)) != NULL) { 00545 const TBOX& blob_box = blob->bounding_box(); 00546 if (!blob_box.y_overlap(part_box)) 00547 continue; 00548 int x_gap = blob_box.x_gap(part_box); 00549 if (x_gap > 2 * gridsize()) { 00550 break; 00551 } else if (best_blob == NULL || x_gap < best_gap) { 00552 best_blob = blob; 00553 best_gap = x_gap; 00554 } 00555 } 00556 if (best_blob != NULL) { 00557 if (side == LR_LEFT) 00558 best_blob->set_leader_on_right(true); 00559 else 00560 best_blob->set_leader_on_left(true); 00561 #ifndef GRAPHICS_DISABLED 00562 if (leaders_win_ != NULL) { 00563 leaders_win_->Pen(side == LR_LEFT ? ScrollView::RED : ScrollView::GREEN); 00564 const TBOX& blob_box = best_blob->bounding_box(); 00565 leaders_win_->Rectangle(blob_box.left(), blob_box.bottom(), 00566 blob_box.right(), blob_box.top()); 00567 } 00568 #endif // GRAPHICS_DISABLED 00569 } 00570 } 00571 00572 // Helper to compute the UQ of the square-ish CJK charcters. 00573 static int UpperQuartileCJKSize(int gridsize, BLOBNBOX_LIST* blobs) { 00574 STATS sizes(0, gridsize * kMaxCJKSizeRatio); 00575 BLOBNBOX_IT it(blobs); 00576 for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { 00577 BLOBNBOX* blob = it.data(); 00578 int width = blob->bounding_box().width(); 00579 int height = blob->bounding_box().height(); 00580 if (width <= height * kCJKAspectRatio && height < width * kCJKAspectRatio) 00581 sizes.add(height, 1); 00582 } 00583 return static_cast<int>(sizes.ile(0.75f) + 0.5); 00584 } 00585 00586 // Fix broken CJK characters, using the fake joined blobs mechanism. 00587 // Blobs are really merged, ie the master takes all the outlines and the 00588 // others are deleted. 00589 // Returns true if sufficient blobs are merged that it may be worth running 00590 // again, due to a better estimate of character size. 00591 bool StrokeWidth::FixBrokenCJK(TO_BLOCK* block) { 00592 BLOBNBOX_LIST* blobs = &block->blobs; 00593 int median_height = UpperQuartileCJKSize(gridsize(), blobs); 00594 int max_dist = static_cast<int>(median_height * kCJKBrokenDistanceFraction); 00595 int max_size = static_cast<int>(median_height * kCJKAspectRatio); 00596 int num_fixed = 0; 00597 BLOBNBOX_IT blob_it(blobs); 00598 00599 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 00600 BLOBNBOX* blob = blob_it.data(); 00601 if (blob->cblob() == NULL || blob->cblob()->out_list()->empty()) 00602 continue; 00603 TBOX bbox = blob->bounding_box(); 00604 bool debug = AlignedBlob::WithinTestRegion(3, bbox.left(), 00605 bbox.bottom()); 00606 if (debug) { 00607 tprintf("Checking for Broken CJK (max size=%d):", max_size); 00608 bbox.print(); 00609 } 00610 // Generate a list of blobs that overlap or are near enough to merge. 00611 BLOBNBOX_CLIST overlapped_blobs; 00612 AccumulateOverlaps(blob, debug, max_size, max_dist, 00613 &bbox, &overlapped_blobs); 00614 if (!overlapped_blobs.empty()) { 00615 // There are overlapping blobs, so qualify them as being satisfactory 00616 // before removing them from the grid and replacing them with the union. 00617 // The final box must be roughly square. 00618 if (bbox.width() > bbox.height() * kCJKAspectRatio || 00619 bbox.height() > bbox.width() * kCJKAspectRatio) { 00620 if (debug) { 00621 tprintf("Bad final aspectratio:"); 00622 bbox.print(); 00623 } 00624 continue; 00625 } 00626 // There can't be too many blobs to merge. 00627 if (overlapped_blobs.length() >= kCJKMaxComponents) { 00628 if (debug) 00629 tprintf("Too many neighbours: %d\n", overlapped_blobs.length()); 00630 continue; 00631 } 00632 // The strokewidths must match amongst the join candidates. 00633 BLOBNBOX_C_IT n_it(&overlapped_blobs); 00634 for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) { 00635 BLOBNBOX* neighbour = NULL; 00636 neighbour = n_it.data(); 00637 if (!blob->MatchingStrokeWidth(*neighbour, kStrokeWidthFractionCJK, 00638 kStrokeWidthCJK)) 00639 break; 00640 } 00641 if (!n_it.cycled_list()) { 00642 if (debug) { 00643 tprintf("Bad stroke widths:"); 00644 PrintBoxWidths(blob); 00645 } 00646 continue; // Not good enough. 00647 } 00648 00649 // Merge all the candidates into blob. 00650 // We must remove blob from the grid and reinsert it after merging 00651 // to maintain the integrity of the grid. 00652 RemoveBBox(blob); 00653 // Everything else will be calculated later. 00654 for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) { 00655 BLOBNBOX* neighbour = n_it.data(); 00656 RemoveBBox(neighbour); 00657 // Mark empty blob for deletion. 00658 neighbour->set_region_type(BRT_NOISE); 00659 blob->really_merge(neighbour); 00660 if (rerotation_.x() != 1.0f || rerotation_.y() != 0.0f) { 00661 blob->rotate_box(rerotation_); 00662 } 00663 } 00664 InsertBBox(true, true, blob); 00665 ++num_fixed; 00666 if (debug) { 00667 tprintf("Done! Final box:"); 00668 bbox.print(); 00669 } 00670 } 00671 } 00672 // Count remaining blobs. 00673 int num_remaining = 0; 00674 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 00675 BLOBNBOX* blob = blob_it.data(); 00676 if (blob->cblob() != NULL && !blob->cblob()->out_list()->empty()) { 00677 ++num_remaining; 00678 } 00679 } 00680 // Permanently delete all the marked blobs after first removing all 00681 // references in the neighbour members. 00682 block->DeleteUnownedNoise(); 00683 return num_fixed > num_remaining * kBrokenCJKIterationFraction; 00684 } 00685 00686 // Helper function to determine whether it is reasonable to merge the 00687 // bbox and the nbox for repairing broken CJK. 00688 // The distance apart must not exceed max_dist, the combined size must 00689 // not exceed max_size, and the aspect ratio must either improve or at 00690 // least not get worse by much. 00691 static bool AcceptableCJKMerge(const TBOX& bbox, const TBOX& nbox, 00692 bool debug, int max_size, int max_dist, 00693 int* x_gap, int* y_gap) { 00694 *x_gap = bbox.x_gap(nbox); 00695 *y_gap = bbox.y_gap(nbox); 00696 TBOX merged(nbox); 00697 merged += bbox; 00698 if (debug) { 00699 tprintf("gaps = %d, %d, merged_box:", *x_gap, *y_gap); 00700 merged.print(); 00701 } 00702 if (*x_gap <= max_dist && *y_gap <= max_dist && 00703 merged.width() <= max_size && merged.height() <= max_size) { 00704 // Close enough to call overlapping. Check aspect ratios. 00705 double old_ratio = static_cast<double>(bbox.width()) / bbox.height(); 00706 if (old_ratio < 1.0) old_ratio = 1.0 / old_ratio; 00707 double new_ratio = static_cast<double>(merged.width()) / merged.height(); 00708 if (new_ratio < 1.0) new_ratio = 1.0 / new_ratio; 00709 if (new_ratio <= old_ratio * kCJKAspectRatioIncrease) 00710 return true; 00711 } 00712 return false; 00713 } 00714 00715 // Collect blobs that overlap or are within max_dist of the input bbox. 00716 // Return them in the list of blobs and expand the bbox to be the union 00717 // of all the boxes. not_this is excluded from the search, as are blobs 00718 // that cause the merged box to exceed max_size in either dimension. 00719 void StrokeWidth::AccumulateOverlaps(const BLOBNBOX* not_this, bool debug, 00720 int max_size, int max_dist, 00721 TBOX* bbox, BLOBNBOX_CLIST* blobs) { 00722 // While searching, nearests holds the nearest failed blob in each 00723 // direction. When we have a nearest in each of the 4 directions, then 00724 // the search is over, and at this point the final bbox must not overlap 00725 // any of the nearests. 00726 BLOBNBOX* nearests[BND_COUNT]; 00727 for (int i = 0; i < BND_COUNT; ++i) { 00728 nearests[i] = NULL; 00729 } 00730 int x = (bbox->left() + bbox->right()) / 2; 00731 int y = (bbox->bottom() + bbox->top()) / 2; 00732 // Run a radial search for blobs that overlap or are sufficiently close. 00733 BlobGridSearch radsearch(this); 00734 radsearch.StartRadSearch(x, y, kCJKRadius); 00735 BLOBNBOX* neighbour; 00736 while ((neighbour = radsearch.NextRadSearch()) != NULL) { 00737 if (neighbour == not_this) continue; 00738 TBOX nbox = neighbour->bounding_box(); 00739 int x_gap, y_gap; 00740 if (AcceptableCJKMerge(*bbox, nbox, debug, max_size, max_dist, 00741 &x_gap, &y_gap)) { 00742 // Close enough to call overlapping. Merge boxes. 00743 *bbox += nbox; 00744 blobs->add_sorted(SortByBoxLeft<BLOBNBOX>, true, neighbour); 00745 if (debug) { 00746 tprintf("Added:"); 00747 nbox.print(); 00748 } 00749 // Since we merged, search the nearests, as some might now me mergeable. 00750 for (int dir = 0; dir < BND_COUNT; ++dir) { 00751 if (nearests[dir] == NULL) continue; 00752 nbox = nearests[dir]->bounding_box(); 00753 if (AcceptableCJKMerge(*bbox, nbox, debug, max_size, 00754 max_dist, &x_gap, &y_gap)) { 00755 // Close enough to call overlapping. Merge boxes. 00756 *bbox += nbox; 00757 blobs->add_sorted(SortByBoxLeft<BLOBNBOX>, true, nearests[dir]); 00758 if (debug) { 00759 tprintf("Added:"); 00760 nbox.print(); 00761 } 00762 nearests[dir] = NULL; 00763 dir = -1; // Restart the search. 00764 } 00765 } 00766 } else if (x_gap < 0 && x_gap <= y_gap) { 00767 // A vertical neighbour. Record the nearest. 00768 BlobNeighbourDir dir = nbox.top() > bbox->top() ? BND_ABOVE : BND_BELOW; 00769 if (nearests[dir] == NULL || 00770 y_gap < bbox->y_gap(nearests[dir]->bounding_box())) { 00771 nearests[dir] = neighbour; 00772 } 00773 } else if (y_gap < 0 && y_gap <= x_gap) { 00774 // A horizontal neighbour. Record the nearest. 00775 BlobNeighbourDir dir = nbox.left() > bbox->left() ? BND_RIGHT : BND_LEFT; 00776 if (nearests[dir] == NULL || 00777 x_gap < bbox->x_gap(nearests[dir]->bounding_box())) { 00778 nearests[dir] = neighbour; 00779 } 00780 } 00781 // If all nearests are non-null, then we have finished. 00782 if (nearests[BND_LEFT] && nearests[BND_RIGHT] && 00783 nearests[BND_ABOVE] && nearests[BND_BELOW]) 00784 break; 00785 } 00786 // Final overlap with a nearest is not allowed. 00787 for (int dir = 0; dir < BND_COUNT; ++dir) { 00788 if (nearests[dir] == NULL) continue; 00789 const TBOX& nbox = nearests[dir]->bounding_box(); 00790 if (debug) { 00791 tprintf("Testing for overlap with:"); 00792 nbox.print(); 00793 } 00794 if (bbox->overlap(nbox)) { 00795 blobs->shallow_clear(); 00796 if (debug) 00797 tprintf("Final box overlaps nearest\n"); 00798 return; 00799 } 00800 } 00801 } 00802 00803 // For each blob in this grid, Finds the textline direction to be horizontal 00804 // or vertical according to distance to neighbours and 1st and 2nd order 00805 // neighbours. Non-text tends to end up without a definite direction. 00806 // Result is setting of the neighbours and vert_possible/horz_possible 00807 // flags in the BLOBNBOXes currently in this grid. 00808 // This function is called more than once if page orientation is uncertain, 00809 // so display_if_debugging is true on the final call to display the results. 00810 void StrokeWidth::FindTextlineFlowDirection(PageSegMode pageseg_mode, 00811 bool display_if_debugging) { 00812 BlobGridSearch gsearch(this); 00813 BLOBNBOX* bbox; 00814 // For every bbox in the grid, set its neighbours. 00815 gsearch.StartFullSearch(); 00816 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00817 SetNeighbours(false, display_if_debugging, bbox); 00818 } 00819 // Where vertical or horizontal wins by a big margin, clarify it. 00820 gsearch.StartFullSearch(); 00821 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00822 SimplifyObviousNeighbours(bbox); 00823 } 00824 // Now try to make the blobs only vertical or horizontal using neighbours. 00825 gsearch.StartFullSearch(); 00826 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00827 if (FindingVerticalOnly(pageseg_mode)) { 00828 bbox->set_vert_possible(true); 00829 bbox->set_horz_possible(false); 00830 } else if (FindingHorizontalOnly(pageseg_mode)) { 00831 bbox->set_vert_possible(false); 00832 bbox->set_horz_possible(true); 00833 } else { 00834 SetNeighbourFlows(bbox); 00835 } 00836 } 00837 if ((textord_tabfind_show_strokewidths && display_if_debugging) || 00838 textord_tabfind_show_strokewidths > 1) { 00839 initial_widths_win_ = DisplayGoodBlobs("InitialStrokewidths", 400, 0); 00840 } 00841 // Improve flow direction with neighbours. 00842 gsearch.StartFullSearch(); 00843 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00844 SmoothNeighbourTypes(pageseg_mode, false, bbox); 00845 } 00846 // Now allow reset of firm values to fix renegades. 00847 gsearch.StartFullSearch(); 00848 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00849 SmoothNeighbourTypes(pageseg_mode, true, bbox); 00850 } 00851 // Repeat. 00852 gsearch.StartFullSearch(); 00853 while ((bbox = gsearch.NextFullSearch()) != NULL) { 00854 SmoothNeighbourTypes(pageseg_mode, true, bbox); 00855 } 00856 if ((textord_tabfind_show_strokewidths && display_if_debugging) || 00857 textord_tabfind_show_strokewidths > 1) { 00858 widths_win_ = DisplayGoodBlobs("ImprovedStrokewidths", 800, 0); 00859 } 00860 } 00861 00862 // Sets the neighbours and good_stroke_neighbours members of the blob by 00863 // searching close on all 4 sides. 00864 // When finding leader dots/dashes, there is a slightly different rule for 00865 // what makes a good neighbour. 00866 void StrokeWidth::SetNeighbours(bool leaders, bool activate_line_trap, 00867 BLOBNBOX* blob) { 00868 int line_trap_count = 0; 00869 for (int dir = 0; dir < BND_COUNT; ++dir) { 00870 BlobNeighbourDir bnd = static_cast<BlobNeighbourDir>(dir); 00871 line_trap_count += FindGoodNeighbour(bnd, leaders, blob); 00872 } 00873 if (line_trap_count > 0 && activate_line_trap) { 00874 // It looks like a line so isolate it by clearing its neighbours. 00875 blob->ClearNeighbours(); 00876 const TBOX& box = blob->bounding_box(); 00877 blob->set_region_type(box.width() > box.height() ? BRT_HLINE : BRT_VLINE); 00878 } 00879 } 00880 00881 00882 // Sets the good_stroke_neighbours member of the blob if it has a 00883 // GoodNeighbour on the given side. 00884 // Also sets the neighbour in the blob, whether or not a good one is found. 00885 // Returns the number of blobs in the nearby search area that would lead us to 00886 // believe that this blob is a line separator. 00887 // Leaders get extra special lenient treatment. 00888 int StrokeWidth::FindGoodNeighbour(BlobNeighbourDir dir, bool leaders, 00889 BLOBNBOX* blob) { 00890 // Search for neighbours that overlap vertically. 00891 TBOX blob_box = blob->bounding_box(); 00892 bool debug = AlignedBlob::WithinTestRegion(2, blob_box.left(), 00893 blob_box.bottom()); 00894 if (debug) { 00895 tprintf("FGN in dir %d for blob:", dir); 00896 blob_box.print(); 00897 } 00898 int top = blob_box.top(); 00899 int bottom = blob_box.bottom(); 00900 int left = blob_box.left(); 00901 int right = blob_box.right(); 00902 int width = right - left; 00903 int height = top - bottom; 00904 00905 // A trap to detect lines tests for the min dimension of neighbours 00906 // being larger than a multiple of the min dimension of the line 00907 // and the larger dimension being smaller than a fraction of the max 00908 // dimension of the line. 00909 int line_trap_max = MAX(width, height) / kLineTrapLongest; 00910 int line_trap_min = MIN(width, height) * kLineTrapShortest; 00911 int line_trap_count = 0; 00912 00913 int min_good_overlap = (dir == BND_LEFT || dir == BND_RIGHT) 00914 ? height / 2 : width / 2; 00915 int min_decent_overlap = (dir == BND_LEFT || dir == BND_RIGHT) 00916 ? height / 3 : width / 3; 00917 if (leaders) 00918 min_good_overlap = min_decent_overlap = 1; 00919 00920 int search_pad = static_cast<int>( 00921 sqrt(static_cast<double>(width * height)) * kNeighbourSearchFactor); 00922 if (gridsize() > search_pad) 00923 search_pad = gridsize(); 00924 TBOX search_box = blob_box; 00925 // Pad the search in the appropriate direction. 00926 switch (dir) { 00927 case BND_LEFT: 00928 search_box.set_left(search_box.left() - search_pad); 00929 break; 00930 case BND_RIGHT: 00931 search_box.set_right(search_box.right() + search_pad); 00932 break; 00933 case BND_BELOW: 00934 search_box.set_bottom(search_box.bottom() - search_pad); 00935 break; 00936 case BND_ABOVE: 00937 search_box.set_top(search_box.top() + search_pad); 00938 break; 00939 case BND_COUNT: 00940 return 0; 00941 } 00942 00943 BlobGridSearch rectsearch(this); 00944 rectsearch.StartRectSearch(search_box); 00945 BLOBNBOX* best_neighbour = NULL; 00946 double best_goodness = 0.0; 00947 bool best_is_good = false; 00948 BLOBNBOX* neighbour; 00949 while ((neighbour = rectsearch.NextRectSearch()) != NULL) { 00950 TBOX nbox = neighbour->bounding_box(); 00951 if (neighbour == blob) 00952 continue; 00953 int mid_x = (nbox.left() + nbox.right()) / 2; 00954 if (mid_x < blob->left_rule() || mid_x > blob->right_rule()) 00955 continue; // In a different column. 00956 if (debug) { 00957 tprintf("Neighbour at:"); 00958 nbox.print(); 00959 } 00960 00961 // Last-minute line detector. There is a small upper limit to the line 00962 // width accepted by the morphological line detector. 00963 int n_width = nbox.width(); 00964 int n_height = nbox.height(); 00965 if (MIN(n_width, n_height) > line_trap_min && 00966 MAX(n_width, n_height) < line_trap_max) 00967 ++line_trap_count; 00968 // Heavily joined text, such as Arabic may have very different sizes when 00969 // looking at the maxes, but the heights may be almost identical, so check 00970 // for a difference in height if looking sideways or width vertically. 00971 if (TabFind::VeryDifferentSizes(MAX(n_width, n_height), 00972 MAX(width, height)) && 00973 (((dir == BND_LEFT || dir ==BND_RIGHT) && 00974 TabFind::DifferentSizes(n_height, height)) || 00975 ((dir == BND_BELOW || dir ==BND_ABOVE) && 00976 TabFind::DifferentSizes(n_width, width)))) { 00977 if (debug) tprintf("Bad size\n"); 00978 continue; // Could be a different font size or non-text. 00979 } 00980 // Amount of vertical overlap between the blobs. 00981 int overlap; 00982 // If the overlap is along the short side of the neighbour, and it 00983 // is fully overlapped, then perp_overlap holds the length of the long 00984 // side of the neighbour. A measure to include hyphens and dashes as 00985 // legitimate neighbours. 00986 int perp_overlap; 00987 int gap; 00988 if (dir == BND_LEFT || dir == BND_RIGHT) { 00989 overlap = MIN(nbox.top(), top) - MAX(nbox.bottom(), bottom); 00990 if (overlap == nbox.height() && nbox.width() > nbox.height()) 00991 perp_overlap = nbox.width(); 00992 else 00993 perp_overlap = overlap; 00994 gap = dir == BND_LEFT ? left - nbox.left() : nbox.right() - right; 00995 if (gap <= 0) { 00996 if (debug) tprintf("On wrong side\n"); 00997 continue; // On the wrong side. 00998 } 00999 gap -= n_width; 01000 } else { 01001 overlap = MIN(nbox.right(), right) - MAX(nbox.left(), left); 01002 if (overlap == nbox.width() && nbox.height() > nbox.width()) 01003 perp_overlap = nbox.height(); 01004 else 01005 perp_overlap = overlap; 01006 gap = dir == BND_BELOW ? bottom - nbox.bottom() : nbox.top() - top; 01007 if (gap <= 0) { 01008 if (debug) tprintf("On wrong side\n"); 01009 continue; // On the wrong side. 01010 } 01011 gap -= n_height; 01012 } 01013 if (-gap > overlap) { 01014 if (debug) tprintf("Overlaps wrong way\n"); 01015 continue; // Overlaps the wrong way. 01016 } 01017 if (perp_overlap < min_decent_overlap) { 01018 if (debug) tprintf("Doesn't overlap enough\n"); 01019 continue; // Doesn't overlap enough. 01020 } 01021 bool bad_sizes = TabFind::DifferentSizes(height, n_height) && 01022 TabFind::DifferentSizes(width, n_width); 01023 bool is_good = overlap >= min_good_overlap && !bad_sizes && 01024 blob->MatchingStrokeWidth(*neighbour, 01025 kStrokeWidthFractionTolerance, 01026 kStrokeWidthTolerance); 01027 // Best is a fuzzy combination of gap, overlap and is good. 01028 // Basically if you make one thing twice as good without making 01029 // anything else twice as bad, then it is better. 01030 if (gap < 1) gap = 1; 01031 double goodness = (1.0 + is_good) * overlap / gap; 01032 if (debug) { 01033 tprintf("goodness = %g vs best of %g, good=%d, overlap=%d, gap=%d\n", 01034 goodness, best_goodness, is_good, overlap, gap); 01035 } 01036 if (goodness > best_goodness) { 01037 best_neighbour = neighbour; 01038 best_goodness = goodness; 01039 best_is_good = is_good; 01040 } 01041 } 01042 blob->set_neighbour(dir, best_neighbour, best_is_good); 01043 return line_trap_count; 01044 } 01045 01046 // Helper to get a list of 1st-order neighbours. 01047 static void ListNeighbours(const BLOBNBOX* blob, 01048 BLOBNBOX_CLIST* neighbours) { 01049 for (int dir = 0; dir < BND_COUNT; ++dir) { 01050 BlobNeighbourDir bnd = static_cast<BlobNeighbourDir>(dir); 01051 BLOBNBOX* neighbour = blob->neighbour(bnd); 01052 if (neighbour != NULL) { 01053 neighbours->add_sorted(SortByBoxLeft<BLOBNBOX>, true, neighbour); 01054 } 01055 } 01056 } 01057 01058 // Helper to get a list of 1st and 2nd order neighbours. 01059 static void List2ndNeighbours(const BLOBNBOX* blob, 01060 BLOBNBOX_CLIST* neighbours) { 01061 ListNeighbours(blob, neighbours); 01062 for (int dir = 0; dir < BND_COUNT; ++dir) { 01063 BlobNeighbourDir bnd = static_cast<BlobNeighbourDir>(dir); 01064 BLOBNBOX* neighbour = blob->neighbour(bnd); 01065 if (neighbour != NULL) { 01066 ListNeighbours(neighbour, neighbours); 01067 } 01068 } 01069 } 01070 01071 // Helper to get a list of 1st, 2nd and 3rd order neighbours. 01072 static void List3rdNeighbours(const BLOBNBOX* blob, 01073 BLOBNBOX_CLIST* neighbours) { 01074 List2ndNeighbours(blob, neighbours); 01075 for (int dir = 0; dir < BND_COUNT; ++dir) { 01076 BlobNeighbourDir bnd = static_cast<BlobNeighbourDir>(dir); 01077 BLOBNBOX* neighbour = blob->neighbour(bnd); 01078 if (neighbour != NULL) { 01079 List2ndNeighbours(neighbour, neighbours); 01080 } 01081 } 01082 } 01083 01084 // Helper to count the evidence for verticalness or horizontalness 01085 // in a list of neighbours. 01086 static void CountNeighbourGaps(bool debug, BLOBNBOX_CLIST* neighbours, 01087 int* pure_h_count, int* pure_v_count) { 01088 if (neighbours->length() <= kMostlyOneDirRatio) 01089 return; 01090 BLOBNBOX_C_IT it(neighbours); 01091 for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { 01092 BLOBNBOX* blob = it.data(); 01093 int h_min, h_max, v_min, v_max; 01094 blob->MinMaxGapsClipped(&h_min, &h_max, &v_min, &v_max); 01095 if (debug) 01096 tprintf("Hgaps [%d,%d], vgaps [%d,%d]:", h_min, h_max, v_min, v_max); 01097 if (h_max < v_min || 01098 blob->leader_on_left() || blob->leader_on_right()) { 01099 // Horizontal gaps are clear winners. Count a pure horizontal. 01100 ++*pure_h_count; 01101 if (debug) tprintf("Horz at:"); 01102 } else if (v_max < h_min) { 01103 // Vertical gaps are clear winners. Clear a pure vertical. 01104 ++*pure_v_count; 01105 if (debug) tprintf("Vert at:"); 01106 } else { 01107 if (debug) tprintf("Neither at:"); 01108 } 01109 if (debug) 01110 blob->bounding_box().print(); 01111 } 01112 } 01113 01114 // Makes the blob to be only horizontal or vertical where evidence 01115 // is clear based on gaps of 2nd order neighbours, or definite individual 01116 // blobs. 01117 void StrokeWidth::SetNeighbourFlows(BLOBNBOX* blob) { 01118 if (blob->DefiniteIndividualFlow()) 01119 return; 01120 bool debug = AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(), 01121 blob->bounding_box().bottom()); 01122 if (debug) { 01123 tprintf("SetNeighbourFlows (current flow=%d, type=%d) on:", 01124 blob->flow(), blob->region_type()); 01125 blob->bounding_box().print(); 01126 } 01127 BLOBNBOX_CLIST neighbours; 01128 List3rdNeighbours(blob, &neighbours); 01129 // The number of pure horizontal and vertical neighbours. 01130 int pure_h_count = 0; 01131 int pure_v_count = 0; 01132 CountNeighbourGaps(debug, &neighbours, &pure_h_count, &pure_v_count); 01133 if (debug) { 01134 HandleClick(blob->bounding_box().left() + 1, 01135 blob->bounding_box().bottom() + 1); 01136 tprintf("SetFlows: h_count=%d, v_count=%d\n", 01137 pure_h_count, pure_v_count); 01138 } 01139 if (!neighbours.empty()) { 01140 blob->set_vert_possible(true); 01141 blob->set_horz_possible(true); 01142 if (pure_h_count > 2 * pure_v_count) { 01143 // Horizontal gaps are clear winners. Clear vertical neighbours. 01144 blob->set_vert_possible(false); 01145 } else if (pure_v_count > 2 * pure_h_count) { 01146 // Vertical gaps are clear winners. Clear horizontal neighbours. 01147 blob->set_horz_possible(false); 01148 } 01149 } else { 01150 // Lonely blob. Can't tell its flow direction. 01151 blob->set_vert_possible(false); 01152 blob->set_horz_possible(false); 01153 } 01154 } 01155 01156 01157 // Helper to count the number of horizontal and vertical blobs in a list. 01158 static void CountNeighbourTypes(BLOBNBOX_CLIST* neighbours, 01159 int* pure_h_count, int* pure_v_count) { 01160 BLOBNBOX_C_IT it(neighbours); 01161 for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { 01162 BLOBNBOX* blob = it.data(); 01163 if (blob->UniquelyHorizontal()) 01164 ++*pure_h_count; 01165 if (blob->UniquelyVertical()) 01166 ++*pure_v_count; 01167 } 01168 } 01169 01170 // Nullify the neighbours in the wrong directions where the direction 01171 // is clear-cut based on a distance margin. Good for isolating vertical 01172 // text from neighbouring horizontal text. 01173 void StrokeWidth::SimplifyObviousNeighbours(BLOBNBOX* blob) { 01174 // Case 1: We have text that is likely several characters, blurry and joined 01175 // together. 01176 if ((blob->bounding_box().width() > 3 * blob->area_stroke_width() && 01177 blob->bounding_box().height() > 3 * blob->area_stroke_width())) { 01178 // The blob is complex (not stick-like). 01179 if (blob->bounding_box().width() > 4 * blob->bounding_box().height()) { 01180 // Horizontal conjoined text. 01181 blob->set_neighbour(BND_ABOVE, NULL, false); 01182 blob->set_neighbour(BND_BELOW, NULL, false); 01183 return; 01184 } 01185 if (blob->bounding_box().height() > 4 * blob->bounding_box().width()) { 01186 // Vertical conjoined text. 01187 blob->set_neighbour(BND_LEFT, NULL, false); 01188 blob->set_neighbour(BND_RIGHT, NULL, false); 01189 return; 01190 } 01191 } 01192 01193 // Case 2: This blob is likely a single character. 01194 int margin = gridsize() / 2; 01195 int h_min, h_max, v_min, v_max; 01196 blob->MinMaxGapsClipped(&h_min, &h_max, &v_min, &v_max); 01197 if ((h_max + margin < v_min && h_max < margin / 2) || 01198 blob->leader_on_left() || blob->leader_on_right()) { 01199 // Horizontal gaps are clear winners. Clear vertical neighbours. 01200 blob->set_neighbour(BND_ABOVE, NULL, false); 01201 blob->set_neighbour(BND_BELOW, NULL, false); 01202 } else if (v_max + margin < h_min && v_max < margin / 2) { 01203 // Vertical gaps are clear winners. Clear horizontal neighbours. 01204 blob->set_neighbour(BND_LEFT, NULL, false); 01205 blob->set_neighbour(BND_RIGHT, NULL, false); 01206 } 01207 } 01208 01209 // Smoothes the vertical/horizontal type of the blob based on the 01210 // 2nd-order neighbours. If reset_all is true, then all blobs are 01211 // changed. Otherwise, only ambiguous blobs are processed. 01212 void StrokeWidth::SmoothNeighbourTypes(PageSegMode pageseg_mode, bool reset_all, 01213 BLOBNBOX* blob) { 01214 if ((blob->vert_possible() && blob->horz_possible()) || reset_all) { 01215 // There are both horizontal and vertical so try to fix it. 01216 BLOBNBOX_CLIST neighbours; 01217 List2ndNeighbours(blob, &neighbours); 01218 // The number of pure horizontal and vertical neighbours. 01219 int pure_h_count = 0; 01220 int pure_v_count = 0; 01221 CountNeighbourTypes(&neighbours, &pure_h_count, &pure_v_count); 01222 if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(), 01223 blob->bounding_box().bottom())) { 01224 HandleClick(blob->bounding_box().left() + 1, 01225 blob->bounding_box().bottom() + 1); 01226 tprintf("pure_h=%d, pure_v=%d\n", 01227 pure_h_count, pure_v_count); 01228 } 01229 if (pure_h_count > pure_v_count && !FindingVerticalOnly(pageseg_mode)) { 01230 // Horizontal gaps are clear winners. Clear vertical neighbours. 01231 blob->set_vert_possible(false); 01232 blob->set_horz_possible(true); 01233 } else if (pure_v_count > pure_h_count && 01234 !FindingHorizontalOnly(pageseg_mode)) { 01235 // Vertical gaps are clear winners. Clear horizontal neighbours. 01236 blob->set_horz_possible(false); 01237 blob->set_vert_possible(true); 01238 } 01239 } else if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(), 01240 blob->bounding_box().bottom())) { 01241 HandleClick(blob->bounding_box().left() + 1, 01242 blob->bounding_box().bottom() + 1); 01243 tprintf("Clean on pass 3!\n"); 01244 } 01245 } 01246 01247 // Partition creation. Accumulates vertical and horizontal text chains, 01248 // puts the remaining blobs in as unknowns, and then merges/splits to 01249 // minimize overlap and smoothes the types with neighbours and the color 01250 // image if provided. rerotation is used to rotate the coordinate space 01251 // back to the nontext_map_ image. 01252 // If find_problems is true, detects possible noise pollution by the amount 01253 // of partition overlap that is created by the diacritics. If excessive, the 01254 // noise is separated out into diacritic blobs, and PFR_NOISE is returned. 01255 // [TODO(rays): if the partition overlap is caused by heavy skew, deskews 01256 // the components, saves the skew_angle and returns PFR_SKEW.] If the return 01257 // is not PFR_OK, the job is incomplete, and FindInitialPartitions must be 01258 // called again after cleaning up the partly done work. 01259 PartitionFindResult StrokeWidth::FindInitialPartitions( 01260 PageSegMode pageseg_mode, const FCOORD& rerotation, bool find_problems, 01261 TO_BLOCK* block, BLOBNBOX_LIST* diacritic_blobs, 01262 ColPartitionGrid* part_grid, ColPartition_LIST* big_parts, 01263 FCOORD* skew_angle) { 01264 if (!FindingHorizontalOnly(pageseg_mode)) FindVerticalTextChains(part_grid); 01265 if (!FindingVerticalOnly(pageseg_mode)) FindHorizontalTextChains(part_grid); 01266 if (textord_tabfind_show_strokewidths) { 01267 chains_win_ = MakeWindow(0, 400, "Initial text chains"); 01268 part_grid->DisplayBoxes(chains_win_); 01269 projection_->DisplayProjection(); 01270 } 01271 if (find_problems) { 01272 // TODO(rays) Do something to find skew, set skew_angle and return if there 01273 // is some. 01274 } 01275 part_grid->SplitOverlappingPartitions(big_parts); 01276 EasyMerges(part_grid); 01277 RemoveLargeUnusedBlobs(block, part_grid, big_parts); 01278 TBOX grid_box(bleft(), tright()); 01279 while (part_grid->GridSmoothNeighbours(BTFT_CHAIN, nontext_map_, grid_box, 01280 rerotation)); 01281 while (part_grid->GridSmoothNeighbours(BTFT_NEIGHBOURS, nontext_map_, 01282 grid_box, rerotation)); 01283 int pre_overlap = part_grid->ComputeTotalOverlap(NULL); 01284 TestDiacritics(part_grid, block); 01285 MergeDiacritics(block, part_grid); 01286 if (find_problems && diacritic_blobs != NULL && 01287 DetectAndRemoveNoise(pre_overlap, grid_box, block, part_grid, 01288 diacritic_blobs)) { 01289 return PFR_NOISE; 01290 } 01291 if (textord_tabfind_show_strokewidths) { 01292 textlines_win_ = MakeWindow(400, 400, "GoodTextline blobs"); 01293 part_grid->DisplayBoxes(textlines_win_); 01294 diacritics_win_ = DisplayDiacritics("Diacritics", 0, 0, block); 01295 } 01296 PartitionRemainingBlobs(pageseg_mode, part_grid); 01297 part_grid->SplitOverlappingPartitions(big_parts); 01298 EasyMerges(part_grid); 01299 while (part_grid->GridSmoothNeighbours(BTFT_CHAIN, nontext_map_, grid_box, 01300 rerotation)); 01301 while (part_grid->GridSmoothNeighbours(BTFT_NEIGHBOURS, nontext_map_, 01302 grid_box, rerotation)); 01303 // Now eliminate strong stuff in a sea of the opposite. 01304 while (part_grid->GridSmoothNeighbours(BTFT_STRONG_CHAIN, nontext_map_, 01305 grid_box, rerotation)); 01306 if (textord_tabfind_show_strokewidths) { 01307 smoothed_win_ = MakeWindow(800, 400, "Smoothed blobs"); 01308 part_grid->DisplayBoxes(smoothed_win_); 01309 } 01310 return PFR_OK; 01311 } 01312 01313 // Detects noise by a significant increase in partition overlap from 01314 // pre_overlap to now, and removes noise from the union of all the overlapping 01315 // partitions, placing the blobs in diacritic_blobs. Returns true if any noise 01316 // was found and removed. 01317 bool StrokeWidth::DetectAndRemoveNoise(int pre_overlap, const TBOX& grid_box, 01318 TO_BLOCK* block, 01319 ColPartitionGrid* part_grid, 01320 BLOBNBOX_LIST* diacritic_blobs) { 01321 ColPartitionGrid* noise_grid = NULL; 01322 int post_overlap = part_grid->ComputeTotalOverlap(&noise_grid); 01323 if (pre_overlap == 0) pre_overlap = 1; 01324 BLOBNBOX_IT diacritic_it(diacritic_blobs); 01325 if (noise_grid != NULL) { 01326 if (post_overlap > pre_overlap * kNoiseOverlapGrowthFactor && 01327 post_overlap > grid_box.area() * kNoiseOverlapAreaFactor) { 01328 // This is noisy enough to fix. 01329 if (textord_tabfind_show_strokewidths) { 01330 ScrollView* noise_win = MakeWindow(1000, 500, "Noise Areas"); 01331 noise_grid->DisplayBoxes(noise_win); 01332 } 01333 part_grid->DeleteNonLeaderParts(); 01334 BLOBNBOX_IT blob_it(&block->noise_blobs); 01335 ColPartitionGridSearch rsearch(noise_grid); 01336 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 01337 BLOBNBOX* blob = blob_it.data(); 01338 blob->ClearNeighbours(); 01339 if (!blob->IsDiacritic() || blob->owner() != NULL) 01340 continue; // Not a noise candidate. 01341 TBOX blob_box(blob->bounding_box()); 01342 TBOX search_box(blob->bounding_box()); 01343 search_box.pad(gridsize(), gridsize()); 01344 rsearch.StartRectSearch(search_box); 01345 ColPartition* part = rsearch.NextRectSearch(); 01346 if (part != NULL) { 01347 // Consider blob as possible noise. 01348 blob->set_owns_cblob(true); 01349 blob->compute_bounding_box(); 01350 diacritic_it.add_after_then_move(blob_it.extract()); 01351 } 01352 } 01353 noise_grid->DeleteParts(); 01354 delete noise_grid; 01355 return true; 01356 } 01357 noise_grid->DeleteParts(); 01358 delete noise_grid; 01359 } 01360 return false; 01361 } 01362 01363 // Helper verifies that blob's neighbour in direction dir is good to add to a 01364 // vertical text chain by returning the neighbour if it is not null, not owned, 01365 // and not uniquely horizontal, as well as its neighbour in the opposite 01366 // direction is blob. 01367 static BLOBNBOX* MutualUnusedVNeighbour(const BLOBNBOX* blob, 01368 BlobNeighbourDir dir) { 01369 BLOBNBOX* next_blob = blob->neighbour(dir); 01370 if (next_blob == NULL || next_blob->owner() != NULL || 01371 next_blob->UniquelyHorizontal()) 01372 return NULL; 01373 if (next_blob->neighbour(DirOtherWay(dir)) == blob) 01374 return next_blob; 01375 return NULL; 01376 } 01377 01378 // Finds vertical chains of text-like blobs and puts them in ColPartitions. 01379 void StrokeWidth::FindVerticalTextChains(ColPartitionGrid* part_grid) { 01380 // A PageSegMode that forces vertical textlines with the current rotation. 01381 PageSegMode pageseg_mode = 01382 rerotation_.y() == 0.0f ? PSM_SINGLE_BLOCK_VERT_TEXT : PSM_SINGLE_COLUMN; 01383 BlobGridSearch gsearch(this); 01384 BLOBNBOX* bbox; 01385 gsearch.StartFullSearch(); 01386 while ((bbox = gsearch.NextFullSearch()) != NULL) { 01387 // Only process boxes that have no horizontal hope and have not yet 01388 // been included in a chain. 01389 BLOBNBOX* blob; 01390 if (bbox->owner() == NULL && bbox->UniquelyVertical() && 01391 (blob = MutualUnusedVNeighbour(bbox, BND_ABOVE)) != NULL) { 01392 // Put all the linked blobs into a ColPartition. 01393 ColPartition* part = new ColPartition(BRT_VERT_TEXT, ICOORD(0, 1)); 01394 part->AddBox(bbox); 01395 while (blob != NULL) { 01396 part->AddBox(blob); 01397 blob = MutualUnusedVNeighbour(blob, BND_ABOVE); 01398 } 01399 blob = MutualUnusedVNeighbour(bbox, BND_BELOW); 01400 while (blob != NULL) { 01401 part->AddBox(blob); 01402 blob = MutualUnusedVNeighbour(blob, BND_BELOW); 01403 } 01404 CompletePartition(pageseg_mode, part, part_grid); 01405 } 01406 } 01407 } 01408 01409 // Helper verifies that blob's neighbour in direction dir is good to add to a 01410 // horizontal text chain by returning the neighbour if it is not null, not 01411 // owned, and not uniquely vertical, as well as its neighbour in the opposite 01412 // direction is blob. 01413 static BLOBNBOX* MutualUnusedHNeighbour(const BLOBNBOX* blob, 01414 BlobNeighbourDir dir) { 01415 BLOBNBOX* next_blob = blob->neighbour(dir); 01416 if (next_blob == NULL || next_blob->owner() != NULL || 01417 next_blob->UniquelyVertical()) 01418 return NULL; 01419 if (next_blob->neighbour(DirOtherWay(dir)) == blob) 01420 return next_blob; 01421 return NULL; 01422 } 01423 01424 // Finds horizontal chains of text-like blobs and puts them in ColPartitions. 01425 void StrokeWidth::FindHorizontalTextChains(ColPartitionGrid* part_grid) { 01426 // A PageSegMode that forces horizontal textlines with the current rotation. 01427 PageSegMode pageseg_mode = 01428 rerotation_.y() == 0.0f ? PSM_SINGLE_COLUMN : PSM_SINGLE_BLOCK_VERT_TEXT; 01429 BlobGridSearch gsearch(this); 01430 BLOBNBOX* bbox; 01431 gsearch.StartFullSearch(); 01432 while ((bbox = gsearch.NextFullSearch()) != NULL) { 01433 BLOBNBOX* blob; 01434 if (bbox->owner() == NULL && bbox->UniquelyHorizontal() && 01435 (blob = MutualUnusedHNeighbour(bbox, BND_RIGHT)) != NULL) { 01436 // Put all the linked blobs into a ColPartition. 01437 ColPartition* part = new ColPartition(BRT_TEXT, ICOORD(0, 1)); 01438 part->AddBox(bbox); 01439 while (blob != NULL) { 01440 part->AddBox(blob); 01441 blob = MutualUnusedHNeighbour(blob, BND_RIGHT); 01442 } 01443 blob = MutualUnusedHNeighbour(bbox, BND_LEFT); 01444 while (blob != NULL) { 01445 part->AddBox(blob); 01446 blob = MutualUnusedVNeighbour(blob, BND_LEFT); 01447 } 01448 CompletePartition(pageseg_mode, part, part_grid); 01449 } 01450 } 01451 } 01452 01453 // Finds diacritics and saves their base character in the blob. 01454 // The objective is to move all diacritics to the noise_blobs list, so 01455 // they don't mess up early textline finding/merging, or force splits 01456 // on textlines that overlap a bit. Blobs that become diacritics must be 01457 // either part of no ColPartition (NULL owner) or in a small partition in 01458 // which ALL the blobs are diacritics, in which case the partition is 01459 // exploded (deleted) back to its blobs. 01460 void StrokeWidth::TestDiacritics(ColPartitionGrid* part_grid, TO_BLOCK* block) { 01461 BlobGrid small_grid(gridsize(), bleft(), tright()); 01462 small_grid.InsertBlobList(&block->noise_blobs); 01463 small_grid.InsertBlobList(&block->blobs); 01464 int medium_diacritics = 0; 01465 int small_diacritics = 0; 01466 BLOBNBOX_IT small_it(&block->noise_blobs); 01467 for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) { 01468 BLOBNBOX* blob = small_it.data(); 01469 if (blob->owner() == NULL && !blob->IsDiacritic() && 01470 DiacriticBlob(&small_grid, blob)) { 01471 ++small_diacritics; 01472 } 01473 } 01474 BLOBNBOX_IT blob_it(&block->blobs); 01475 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { 01476 BLOBNBOX* blob = blob_it.data(); 01477 if (blob->IsDiacritic()) { 01478 small_it.add_to_end(blob_it.extract()); 01479 continue; // Already a diacritic. 01480 } 01481 ColPartition* part = blob->owner(); 01482 if (part == NULL && DiacriticBlob(&small_grid, blob)) { 01483 ++medium_diacritics; 01484 RemoveBBox(blob); 01485 small_it.add_to_end(blob_it.extract()); 01486 } else if (part != NULL && !part->block_owned() && 01487 part->boxes_count() < 3) { 01488 // We allow blobs in small partitions to become diacritics if ALL the 01489 // blobs in the partition qualify as we can then cleanly delete the 01490 // partition, turn all the blobs in it to diacritics and they can be 01491 // merged into the base character partition more easily than merging 01492 // the partitions. 01493 BLOBNBOX_C_IT box_it(part->boxes()); 01494 for (box_it.mark_cycle_pt(); !box_it.cycled_list() && 01495 DiacriticBlob(&small_grid, box_it.data()); 01496 box_it.forward()); 01497 if (box_it.cycled_list()) { 01498 // They are all good. 01499 while (!box_it.empty()) { 01500 // Liberate the blob from its partition so it can be treated 01501 // as a diacritic and merged explicitly with the base part. 01502 // The blob is really owned by the block. The partition "owner" 01503 // is NULLed to allow the blob to get merged with its base character 01504 // partition. 01505 BLOBNBOX* box = box_it.extract(); 01506 box->set_owner(NULL); 01507 box_it.forward(); 01508 ++medium_diacritics; 01509 // We remove the blob from the grid so it isn't found by subsequent 01510 // searches where we might not want to include diacritics. 01511 RemoveBBox(box); 01512 } 01513 // We only move the one blob to the small list here, but the others 01514 // all get moved by the test at the top of the loop. 01515 small_it.add_to_end(blob_it.extract()); 01516 part_grid->RemoveBBox(part); 01517 delete part; 01518 } 01519 } else if (AlignedBlob::WithinTestRegion(2, blob->bounding_box().left(), 01520 blob->bounding_box().bottom())) { 01521 tprintf("Blob not available to be a diacritic at:"); 01522 blob->bounding_box().print(); 01523 } 01524 } 01525 if (textord_tabfind_show_strokewidths) { 01526 tprintf("Found %d small diacritics, %d medium\n", 01527 small_diacritics, medium_diacritics); 01528 } 01529 } 01530 01531 // Searches this grid for an appropriately close and sized neighbour of the 01532 // given [small] blob. If such a blob is found, the diacritic base is saved 01533 // in the blob and true is returned. 01534 // The small_grid is a secondary grid that contains the small/noise objects 01535 // that are not in this grid, but may be useful for determining a connection 01536 // between blob and its potential base character. (See DiacriticXGapFilled.) 01537 bool StrokeWidth::DiacriticBlob(BlobGrid* small_grid, BLOBNBOX* blob) { 01538 if (BLOBNBOX::UnMergeableType(blob->region_type()) || 01539 blob->region_type() == BRT_VERT_TEXT) 01540 return false; 01541 TBOX small_box(blob->bounding_box()); 01542 bool debug = AlignedBlob::WithinTestRegion(2, small_box.left(), 01543 small_box.bottom()); 01544 if (debug) { 01545 tprintf("Testing blob for diacriticness at:"); 01546 small_box.print(); 01547 } 01548 int x = (small_box.left() + small_box.right()) / 2; 01549 int y = (small_box.bottom() + small_box.top()) / 2; 01550 int grid_x, grid_y; 01551 GridCoords(x, y, &grid_x, &grid_y); 01552 int height = small_box.height(); 01553 // Setup a rectangle search to find its nearest base-character neighbour. 01554 // We keep 2 different best candidates: 01555 // best_x_overlap is a category of base characters that have an overlap in x 01556 // (like a acute) in which we look for the least y-gap, computed using the 01557 // projection to favor base characters in the same textline. 01558 // best_y_overlap is a category of base characters that have no x overlap, 01559 // (nominally a y-overlap is preferrecd but not essential) in which we 01560 // look for the least weighted sum of x-gap and y-gap, with x-gap getting 01561 // a lower weight to catch quotes at the end of a textline. 01562 // NOTE that x-gap and y-gap are measured from the nearest side of the base 01563 // character to the FARTHEST side of the diacritic to allow small diacritics 01564 // to be a reasonable distance away, but not big diacritics. 01565 BLOBNBOX* best_x_overlap = NULL; 01566 BLOBNBOX* best_y_overlap = NULL; 01567 int best_total_dist = 0; 01568 int best_y_gap = 0; 01569 TBOX best_xbox; 01570 // TODO(rays) the search box could be setup using the projection as a guide. 01571 TBOX search_box(small_box); 01572 int x_pad = IntCastRounded(gridsize() * kDiacriticXPadRatio); 01573 int y_pad = IntCastRounded(gridsize() * kDiacriticYPadRatio); 01574 search_box.pad(x_pad, y_pad); 01575 BlobGridSearch rsearch(this); 01576 rsearch.SetUniqueMode(true); 01577 int min_height = height * kMinDiacriticSizeRatio; 01578 rsearch.StartRectSearch(search_box); 01579 BLOBNBOX* neighbour; 01580 while ((neighbour = rsearch.NextRectSearch()) != NULL) { 01581 if (BLOBNBOX::UnMergeableType(neighbour->region_type()) || 01582 neighbour == blob || neighbour->owner() == blob->owner()) 01583 continue; 01584 TBOX nbox = neighbour->bounding_box(); 01585 if (neighbour->owner() == NULL || neighbour->owner()->IsVerticalType() || 01586 (neighbour->flow() != BTFT_CHAIN && 01587 neighbour->flow() != BTFT_STRONG_CHAIN)) { 01588 if (debug) { 01589 tprintf("Neighbour not strong enough:"); 01590 nbox.print(); 01591 } 01592 continue; // Diacritics must be attached to strong text. 01593 } 01594 if (nbox.height() < min_height) { 01595 if (debug) { 01596 tprintf("Neighbour not big enough:"); 01597 nbox.print(); 01598 } 01599 continue; // Too small to be the base character. 01600 } 01601 int x_gap = small_box.x_gap(nbox); 01602 int y_gap = small_box.y_gap(nbox); 01603 int total_distance = projection_->DistanceOfBoxFromBox(small_box, nbox, 01604 true, denorm_, 01605 debug); 01606 if (debug) tprintf("xgap=%d, y=%d, total dist=%d\n", 01607 x_gap, y_gap, total_distance); 01608 if (total_distance > 01609 neighbour->owner()->median_size() * kMaxDiacriticDistanceRatio) { 01610 if (debug) { 01611 tprintf("Neighbour with median size %d too far away:", 01612 neighbour->owner()->median_size()); 01613 neighbour->bounding_box().print(); 01614 } 01615 continue; // Diacritics must not be too distant. 01616 } 01617 if (x_gap <= 0) { 01618 if (debug) { 01619 tprintf("Computing reduced box for :"); 01620 nbox.print(); 01621 } 01622 int left = small_box.left() - small_box.width(); 01623 int right = small_box.right() + small_box.width(); 01624 nbox = neighbour->BoundsWithinLimits(left, right); 01625 y_gap = small_box.y_gap(nbox); 01626 if (best_x_overlap == NULL || y_gap < best_y_gap) { 01627 best_x_overlap = neighbour; 01628 best_xbox = nbox; 01629 best_y_gap = y_gap; 01630 if (debug) { 01631 tprintf("New best:"); 01632 nbox.print(); 01633 } 01634 } else if (debug) { 01635 tprintf("Shrunken box doesn't win:"); 01636 nbox.print(); 01637 } 01638 } else if (blob->ConfirmNoTabViolation(*neighbour)) { 01639 if (best_y_overlap == NULL || total_distance < best_total_dist) { 01640 if (debug) { 01641 tprintf("New best y overlap:"); 01642 nbox.print(); 01643 } 01644 best_y_overlap = neighbour; 01645 best_total_dist = total_distance; 01646 } else if (debug) { 01647 tprintf("New y overlap box doesn't win:"); 01648 nbox.print(); 01649 } 01650 } else if (debug) { 01651 tprintf("Neighbour wrong side of a tab:"); 01652 nbox.print(); 01653 } 01654 } 01655 if (best_x_overlap != NULL && 01656 (best_y_overlap == NULL || 01657 best_xbox.major_y_overlap(best_y_overlap->bounding_box()))) { 01658 blob->set_diacritic_box(best_xbox); 01659 blob->set_base_char_blob(best_x_overlap); 01660 if (debug) { 01661 tprintf("DiacriticBlob OK! (x-overlap:"); 01662 small_box.print(); 01663 best_xbox.print(); 01664 } 01665 return true; 01666 } 01667 if (best_y_overlap != NULL && 01668 DiacriticXGapFilled(small_grid, small_box, 01669 best_y_overlap->bounding_box()) && 01670 NoNoiseInBetween(small_box, best_y_overlap->bounding_box())) { 01671 blob->set_diacritic_box(best_y_overlap->bounding_box()); 01672 blob->set_base_char_blob(best_y_overlap); 01673 if (debug) { 01674 tprintf("DiacriticBlob OK! (y-overlap:"); 01675 small_box.print(); 01676 best_y_overlap->bounding_box().print(); 01677 } 01678 return true; 01679 } 01680 if (debug) { 01681 tprintf("DiacriticBlob fails:"); 01682 small_box.print(); 01683 tprintf("Best x+y gap = %d, y = %d\n", best_total_dist, best_y_gap); 01684 if (best_y_overlap != NULL) { 01685 tprintf("XGapFilled=%d, NoiseBetween=%d\n", 01686 DiacriticXGapFilled(small_grid, small_box, 01687 best_y_overlap->bounding_box()), 01688 NoNoiseInBetween(small_box, best_y_overlap->bounding_box())); 01689 } 01690 } 01691 return false; 01692 } 01693 01694 // Returns true if there is no gap between the base char and the diacritic 01695 // bigger than a fraction of the height of the base char: 01696 // Eg: line end.....' 01697 // The quote is a long way from the end of the line, yet it needs to be a 01698 // diacritic. To determine that the quote is not part of an image, or 01699 // a different text block, we check for other marks in the gap between 01700 // the base char and the diacritic. 01701 // '<--Diacritic 01702 // |---------| 01703 // | |<-toobig-gap-> 01704 // | Base |<ok gap> 01705 // |---------| x<-----Dot occupying gap 01706 // The grid is const really. 01707 bool StrokeWidth::DiacriticXGapFilled(BlobGrid* grid, 01708 const TBOX& diacritic_box, 01709 const TBOX& base_box) { 01710 // Since most gaps are small, use an iterative algorithm to search the gap. 01711 int max_gap = IntCastRounded(base_box.height() * 01712 kMaxDiacriticGapToBaseCharHeight); 01713 TBOX occupied_box(base_box); 01714 int diacritic_gap; 01715 while ((diacritic_gap = diacritic_box.x_gap(occupied_box)) > max_gap) { 01716 TBOX search_box(occupied_box); 01717 if (diacritic_box.left() > search_box.right()) { 01718 // We are looking right. 01719 search_box.set_left(search_box.right()); 01720 search_box.set_right(search_box.left() + max_gap); 01721 } else { 01722 // We are looking left. 01723 search_box.set_right(search_box.left()); 01724 search_box.set_left(search_box.left() - max_gap); 01725 } 01726 BlobGridSearch rsearch(grid); 01727 rsearch.StartRectSearch(search_box); 01728 BLOBNBOX* neighbour; 01729 while ((neighbour = rsearch.NextRectSearch()) != NULL) { 01730 const TBOX& nbox = neighbour->bounding_box(); 01731 if (nbox.x_gap(diacritic_box) < diacritic_gap) { 01732 if (nbox.left() < occupied_box.left()) 01733 occupied_box.set_left(nbox.left()); 01734 if (nbox.right() > occupied_box.right()) 01735 occupied_box.set_right(nbox.right()); 01736 break; 01737 } 01738 } 01739 if (neighbour == NULL) 01740 return false; // Found a big gap. 01741 } 01742 return true; // The gap was filled. 01743 } 01744 01745 // Merges diacritics with the ColPartition of the base character blob. 01746 void StrokeWidth::MergeDiacritics(TO_BLOCK* block, 01747 ColPartitionGrid* part_grid) { 01748 BLOBNBOX_IT small_it(&block->noise_blobs); 01749 for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) { 01750 BLOBNBOX* blob = small_it.data(); 01751 if (blob->base_char_blob() != NULL) { 01752 ColPartition* part = blob->base_char_blob()->owner(); 01753 // The base character must be owned by a partition and that partition 01754 // must not be on the big_parts list (not block owned). 01755 if (part != NULL && !part->block_owned() && blob->owner() == NULL && 01756 blob->IsDiacritic()) { 01757 // The partition has to be removed from the grid and reinserted 01758 // because its bounding box may change. 01759 part_grid->RemoveBBox(part); 01760 part->AddBox(blob); 01761 blob->set_region_type(part->blob_type()); 01762 blob->set_flow(part->flow()); 01763 blob->set_owner(part); 01764 part_grid->InsertBBox(true, true, part); 01765 } 01766 // Set all base chars to NULL before any blobs get deleted. 01767 blob->set_base_char_blob(NULL); 01768 } 01769 } 01770 } 01771 01772 // Any blobs on the large_blobs list of block that are still unowned by a 01773 // ColPartition, are probably drop-cap or vertically touching so the blobs 01774 // are removed to the big_parts list and treated separately. 01775 void StrokeWidth::RemoveLargeUnusedBlobs(TO_BLOCK* block, 01776 ColPartitionGrid* part_grid, 01777 ColPartition_LIST* big_parts) { 01778 BLOBNBOX_IT large_it(&block->large_blobs); 01779 for (large_it.mark_cycle_pt(); !large_it.cycled_list(); large_it.forward()) { 01780 BLOBNBOX* blob = large_it.data(); 01781 ColPartition* big_part = blob->owner(); 01782 if (big_part == NULL) { 01783 // Large blobs should have gone into partitions by now if they are 01784 // genuine characters, so move any unowned ones out to the big parts 01785 // list. This will include drop caps and vertically touching characters. 01786 ColPartition::MakeBigPartition(blob, big_parts); 01787 } 01788 } 01789 } 01790 01791 // All remaining unused blobs are put in individual ColPartitions. 01792 void StrokeWidth::PartitionRemainingBlobs(PageSegMode pageseg_mode, 01793 ColPartitionGrid* part_grid) { 01794 BlobGridSearch gsearch(this); 01795 BLOBNBOX* bbox; 01796 int prev_grid_x = -1; 01797 int prev_grid_y = -1; 01798 BLOBNBOX_CLIST cell_list; 01799 BLOBNBOX_C_IT cell_it(&cell_list); 01800 bool cell_all_noise = true; 01801 gsearch.StartFullSearch(); 01802 while ((bbox = gsearch.NextFullSearch()) != NULL) { 01803 int grid_x = gsearch.GridX(); 01804 int grid_y = gsearch.GridY(); 01805 if (grid_x != prev_grid_x || grid_y != prev_grid_y) { 01806 // New cell. Process old cell. 01807 MakePartitionsFromCellList(pageseg_mode, cell_all_noise, part_grid, 01808 &cell_list); 01809 cell_it.set_to_list(&cell_list); 01810 prev_grid_x = grid_x; 01811 prev_grid_y = grid_y; 01812 cell_all_noise = true; 01813 } 01814 if (bbox->owner() == NULL) { 01815 cell_it.add_to_end(bbox); 01816 if (bbox->flow() != BTFT_NONTEXT) 01817 cell_all_noise = false; 01818 } else { 01819 cell_all_noise = false; 01820 } 01821 } 01822 MakePartitionsFromCellList(pageseg_mode, cell_all_noise, part_grid, 01823 &cell_list); 01824 } 01825 01826 // If combine, put all blobs in the cell_list into a single partition, otherwise 01827 // put each one into its own partition. 01828 void StrokeWidth::MakePartitionsFromCellList(PageSegMode pageseg_mode, 01829 bool combine, 01830 ColPartitionGrid* part_grid, 01831 BLOBNBOX_CLIST* cell_list) { 01832 if (cell_list->empty()) 01833 return; 01834 BLOBNBOX_C_IT cell_it(cell_list); 01835 if (combine) { 01836 BLOBNBOX* bbox = cell_it.extract(); 01837 ColPartition* part = new ColPartition(bbox->region_type(), ICOORD(0, 1)); 01838 part->AddBox(bbox); 01839 part->set_flow(bbox->flow()); 01840 for (cell_it.forward(); !cell_it.empty(); cell_it.forward()) { 01841 part->AddBox(cell_it.extract()); 01842 } 01843 CompletePartition(pageseg_mode, part, part_grid); 01844 } else { 01845 for (; !cell_it.empty(); cell_it.forward()) { 01846 BLOBNBOX* bbox = cell_it.extract(); 01847 ColPartition* part = new ColPartition(bbox->region_type(), ICOORD(0, 1)); 01848 part->set_flow(bbox->flow()); 01849 part->AddBox(bbox); 01850 CompletePartition(pageseg_mode, part, part_grid); 01851 } 01852 } 01853 } 01854 01855 // Helper function to finish setting up a ColPartition and insert into 01856 // part_grid. 01857 void StrokeWidth::CompletePartition(PageSegMode pageseg_mode, 01858 ColPartition* part, 01859 ColPartitionGrid* part_grid) { 01860 part->ComputeLimits(); 01861 TBOX box = part->bounding_box(); 01862 bool debug = AlignedBlob::WithinTestRegion(2, box.left(), 01863 box.bottom()); 01864 int value = projection_->EvaluateColPartition(*part, denorm_, debug); 01865 // Override value if pageseg_mode disagrees. 01866 if (value > 0 && FindingVerticalOnly(pageseg_mode)) { 01867 value = part->boxes_count() == 1 ? 0 : -2; 01868 } else if (value < 0 && FindingHorizontalOnly(pageseg_mode)) { 01869 value = part->boxes_count() == 1 ? 0 : 2; 01870 } 01871 part->SetRegionAndFlowTypesFromProjectionValue(value); 01872 part->ClaimBoxes(); 01873 part_grid->InsertBBox(true, true, part); 01874 } 01875 01876 // Merge partitions where the merge appears harmless. 01877 // As this 01878 void StrokeWidth::EasyMerges(ColPartitionGrid* part_grid) { 01879 part_grid->Merges( 01880 NewPermanentTessCallback(this, &StrokeWidth::OrientationSearchBox), 01881 NewPermanentTessCallback(this, &StrokeWidth::ConfirmEasyMerge)); 01882 } 01883 01884 // Compute a search box based on the orientation of the partition. 01885 // Returns true if a suitable box can be calculated. 01886 // Callback for EasyMerges. 01887 bool StrokeWidth::OrientationSearchBox(ColPartition* part, TBOX* box) { 01888 if (part->IsVerticalType()) { 01889 box->set_top(box->top() + box->width()); 01890 box->set_bottom(box->bottom() - box->width()); 01891 } else { 01892 box->set_left(box->left() - box->height()); 01893 box->set_right(box->right() + box->height()); 01894 } 01895 return true; 01896 } 01897 01898 // Merge confirmation callback for EasyMerges. 01899 bool StrokeWidth::ConfirmEasyMerge(const ColPartition* p1, 01900 const ColPartition* p2) { 01901 ASSERT_HOST(p1 != NULL && p2 != NULL); 01902 ASSERT_HOST(!p1->IsEmpty() && !p2->IsEmpty()); 01903 if ((p1->flow() == BTFT_NONTEXT && p2->flow() >= BTFT_CHAIN) || 01904 (p1->flow() >= BTFT_CHAIN && p2->flow() == BTFT_NONTEXT)) 01905 return false; // Don't merge confirmed image with text. 01906 if ((p1->IsVerticalType() || p2->IsVerticalType()) && 01907 p1->HCoreOverlap(*p2) <= 0 && 01908 ((!p1->IsSingleton() && 01909 !p2->IsSingleton()) || 01910 !p1->bounding_box().major_overlap(p2->bounding_box()))) 01911 return false; // Overlap must be in the text line. 01912 if ((p1->IsHorizontalType() || p2->IsHorizontalType()) && 01913 p1->VCoreOverlap(*p2) <= 0 && 01914 ((!p1->IsSingleton() && 01915 !p2->IsSingleton()) || 01916 (!p1->bounding_box().major_overlap(p2->bounding_box()) && 01917 !p1->OKDiacriticMerge(*p2, false) && 01918 !p2->OKDiacriticMerge(*p1, false)))) 01919 return false; // Overlap must be in the text line. 01920 if (!p1->ConfirmNoTabViolation(*p2)) 01921 return false; 01922 if (p1->flow() <= BTFT_NONTEXT && p2->flow() <= BTFT_NONTEXT) 01923 return true; 01924 return NoNoiseInBetween(p1->bounding_box(), p2->bounding_box()); 01925 } 01926 01927 // Returns true if there is no significant noise in between the boxes. 01928 bool StrokeWidth::NoNoiseInBetween(const TBOX& box1, const TBOX& box2) const { 01929 return ImageFind::BlankImageInBetween(box1, box2, grid_box_, rerotation_, 01930 nontext_map_); 01931 } 01932 01936 ScrollView* StrokeWidth::DisplayGoodBlobs(const char* window_name, 01937 int x, int y) { 01938 ScrollView* window = NULL; 01939 #ifndef GRAPHICS_DISABLED 01940 window = MakeWindow(x, y, window_name); 01941 // For every blob in the grid, display it. 01942 window->Brush(ScrollView::NONE); 01943 01944 // For every bbox in the grid, display it. 01945 BlobGridSearch gsearch(this); 01946 gsearch.StartFullSearch(); 01947 BLOBNBOX* bbox; 01948 while ((bbox = gsearch.NextFullSearch()) != NULL) { 01949 TBOX box = bbox->bounding_box(); 01950 int left_x = box.left(); 01951 int right_x = box.right(); 01952 int top_y = box.top(); 01953 int bottom_y = box.bottom(); 01954 int goodness = bbox->GoodTextBlob(); 01955 BlobRegionType blob_type = bbox->region_type(); 01956 if (bbox->UniquelyVertical()) 01957 blob_type = BRT_VERT_TEXT; 01958 if (bbox->UniquelyHorizontal()) 01959 blob_type = BRT_TEXT; 01960 BlobTextFlowType flow = bbox->flow(); 01961 if (flow == BTFT_NONE) { 01962 if (goodness == 0) 01963 flow = BTFT_NEIGHBOURS; 01964 else if (goodness == 1) 01965 flow = BTFT_CHAIN; 01966 else 01967 flow = BTFT_STRONG_CHAIN; 01968 } 01969 window->Pen(BLOBNBOX::TextlineColor(blob_type, flow)); 01970 window->Rectangle(left_x, bottom_y, right_x, top_y); 01971 } 01972 window->Update(); 01973 #endif 01974 return window; 01975 } 01976 01977 static void DrawDiacriticJoiner(const BLOBNBOX* blob, ScrollView* window) { 01978 #ifndef GRAPHICS_DISABLED 01979 const TBOX& blob_box(blob->bounding_box()); 01980 int top = MAX(blob_box.top(), blob->base_char_top()); 01981 int bottom = MIN(blob_box.bottom(), blob->base_char_bottom()); 01982 int x = (blob_box.left() + blob_box.right()) / 2; 01983 window->Line(x, top, x, bottom); 01984 #endif // GRAPHICS_DISABLED 01985 } 01986 01987 // Displays blobs colored according to whether or not they are diacritics. 01988 ScrollView* StrokeWidth::DisplayDiacritics(const char* window_name, 01989 int x, int y, TO_BLOCK* block) { 01990 ScrollView* window = NULL; 01991 #ifndef GRAPHICS_DISABLED 01992 window = MakeWindow(x, y, window_name); 01993 // For every blob in the grid, display it. 01994 window->Brush(ScrollView::NONE); 01995 01996 BLOBNBOX_IT it(&block->blobs); 01997 for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { 01998 BLOBNBOX* blob = it.data(); 01999 if (blob->IsDiacritic()) { 02000 window->Pen(ScrollView::GREEN); 02001 DrawDiacriticJoiner(blob, window); 02002 } else { 02003 window->Pen(blob->BoxColor()); 02004 } 02005 const TBOX& box = blob->bounding_box(); 02006 window->Rectangle(box.left(), box. bottom(), box.right(), box.top()); 02007 } 02008 it.set_to_list(&block->noise_blobs); 02009 for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) { 02010 BLOBNBOX* blob = it.data(); 02011 if (blob->IsDiacritic()) { 02012 window->Pen(ScrollView::GREEN); 02013 DrawDiacriticJoiner(blob, window); 02014 } else { 02015 window->Pen(ScrollView::WHITE); 02016 } 02017 const TBOX& box = blob->bounding_box(); 02018 window->Rectangle(box.left(), box. bottom(), box.right(), box.top()); 02019 } 02020 window->Update(); 02021 #endif 02022 return window; 02023 } 02024 02025 } // namespace tesseract.