tesseract 3.04.01

textord/strokewidth.cpp

Go to the documentation of this file.
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.
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Defines