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