All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules Pages
tablefind.cpp
Go to the documentation of this file.
1 // File: tablefind.cpp
3 // Description: Helper classes to find tables from ColPartitions.
4 // Author: Faisal Shafait (faisal.shafait@dfki.de)
5 // Created: Tue Jan 06 11:13:01 PST 2009
6 //
7 // (C) Copyright 2009, Google Inc.
8 // Licensed under the Apache License, Version 2.0 (the "License");
9 // you may not use this file except in compliance with the License.
10 // You may obtain a copy of the License at
11 // http://www.apache.org/licenses/LICENSE-2.0
12 // Unless required by applicable law or agreed to in writing, software
13 // distributed under the License is distributed on an "AS IS" BASIS,
14 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 // See the License for the specific language governing permissions and
16 // limitations under the License.
17 //
19 
20 #ifdef _MSC_VER
21 #pragma warning(disable:4244) // Conversion warnings
22 #endif
23 
24 #ifdef HAVE_CONFIG_H
25 #include "config_auto.h"
26 #endif
27 
28 #include "tablefind.h"
29 #include <math.h>
30 
31 #include "allheaders.h"
32 
33 #include "colpartitionset.h"
34 #include "tablerecog.h"
35 
36 namespace tesseract {
37 
38 // These numbers are used to calculate the global median stats.
39 // They just set an upper bound on the stats objects.
40 // Maximum vertical spacing between neighbor partitions.
41 const int kMaxVerticalSpacing = 500;
42 // Maximum width of a blob in a partition.
43 const int kMaxBlobWidth = 500;
44 
45 // Minimum whitespace size to split a partition (measured as a multiple
46 // of a partition's median width).
47 const double kSplitPartitionSize = 2.0;
48 // To insert text, the partition must satisfy these size constraints
49 // in AllowTextPartition(). The idea is to filter noise partitions
50 // determined by the size compared to the global medians.
51 // TODO(nbeato): Need to find good numbers again.
52 const double kAllowTextHeight = 0.5;
53 const double kAllowTextWidth = 0.6;
54 const double kAllowTextArea = 0.8;
55 // The same thing applies to blobs (to filter noise).
56 // TODO(nbeato): These numbers are a shot in the dark...
57 // height and width are 0.5 * gridsize() in colfind.cpp
58 // area is a rough guess for the size of a period.
59 const double kAllowBlobHeight = 0.3;
60 const double kAllowBlobWidth = 0.4;
61 const double kAllowBlobArea = 0.05;
62 
63 // Minimum number of components in a text partition. A partition having fewer
64 // components than that is more likely a data partition and is a candidate
65 // table cell.
66 const int kMinBoxesInTextPartition = 10;
67 
68 // Maximum number of components that a data partition can have
69 const int kMaxBoxesInDataPartition = 20;
70 
71 // Maximum allowed gap in a text partitions as a multiple of its median size.
72 const double kMaxGapInTextPartition = 4.0;
73 
74 // Minimum value that the maximum gap in a text partition should have as a
75 // factor of its median size.
76 const double kMinMaxGapInTextPartition = 0.5;
77 
78 // The amount of overlap that is "normal" for adjacent blobs in a text
79 // partition. This is used to calculate gap between overlapping blobs.
80 const double kMaxBlobOverlapFactor = 4.0;
81 
82 // Maximum x-height a table partition can have as a multiple of global
83 // median x-height
84 const double kMaxTableCellXheight = 2.0;
85 
86 // Maximum line spacing between a table column header and column contents
87 // for merging the two (as a multiple of the partition's median_size).
89 
90 // Minimum ratio of num_table_partitions to num_text_partitions in a column
91 // block to be called it a table column
92 const double kTableColumnThreshold = 3.0;
93 
94 // Search for horizontal ruling lines within the vertical margin as a
95 // multiple of grid size
96 const int kRulingVerticalMargin = 3;
97 
98 // Minimum overlap that a colpartition must have with a table region
99 // to become part of that table
100 const double kMinOverlapWithTable = 0.6;
101 
102 // Maximum side space (distance from column boundary) that a typical
103 // text-line in flowing text should have as a multiple of its x-height
104 // (Median size).
105 const int kSideSpaceMargin = 10;
106 
107 // Fraction of the peak of x-projection of a table region to set the
108 // threshold for the x-projection histogram
109 const double kSmallTableProjectionThreshold = 0.35;
110 const double kLargeTableProjectionThreshold = 0.45;
111 // Minimum number of rows required to look for more rows in the projection.
112 const int kLargeTableRowCount = 6;
113 
114 // Minimum number of rows in a table
115 const int kMinRowsInTable = 3;
116 
117 // The number of "whitespace blobs" that should appear between the
118 // ColPartition's bounding box and the column tab stops to the left/right
119 // when looking for center justified tab stops.
120 const double kRequiredFullJustifiedSpacing = 4.0;
121 
122 // The amount of padding (multiplied by global_median_xheight_ during use)
123 // that is vertically added to the search adjacent leader search during
124 // ColPartition marking.
126 
127 // Used when filtering false positives. When finding the last line
128 // of a paragraph (typically left-aligned), the previous line should have
129 // its center to the right of the last line by this scaled amount.
131 
132 // The maximum amount of whitespace allowed left of a paragraph ending.
133 // Do not filter a ColPartition with more than this space left of it.
135 
136 // Used when filtering false positives. The last line of a paragraph
137 // should be preceded by a line that is predominantly text. This is the
138 // ratio of text to whitespace (to the right of the text) that is required
139 // for the previous line to be a text.
141 
142 // When counting table columns, this is the required gap between two columns
143 // (it is multiplied by global_median_xheight_).
144 const double kMaxXProjectionGapFactor = 2.0;
145 
146 // Used for similarity in partitions using stroke width. Values copied
147 // from ColFind.cpp in Ray's CL.
149 const double kStrokeWidthConstantTolerance = 2.0;
150 
151 BOOL_VAR(textord_dump_table_images, false, "Paint table detection output");
152 BOOL_VAR(textord_show_tables, false, "Show table regions");
154  "Debug table marking steps in detail");
156  "Show page stats used in table finding");
158  "Enables the table recognizer for table layout and filtering.");
159 
162 
163 // Templated helper function used to create destructor callbacks for the
164 // BBGrid::ClearGridData() method.
165 template <typename T> void DeleteObject(T *object) {
166  delete object;
167 }
168 
170  : resolution_(0),
171  global_median_xheight_(0),
172  global_median_blob_width_(0),
173  global_median_ledding_(0),
174  left_to_right_language_(true) {
175 }
176 
178  // ColPartitions and ColSegments created by this class for storage in grids
179  // need to be deleted explicitly.
180  clean_part_grid_.ClearGridData(&DeleteObject<ColPartition>);
181  leader_and_ruling_grid_.ClearGridData(&DeleteObject<ColPartition>);
182  fragmented_text_grid_.ClearGridData(&DeleteObject<ColPartition>);
183  col_seg_grid_.ClearGridData(&DeleteObject<ColSegment>);
184  table_grid_.ClearGridData(&DeleteObject<ColSegment>);
185 }
186 
188  left_to_right_language_ = order;
189 }
190 
191 void TableFinder::Init(int grid_size, const ICOORD& bottom_left,
192  const ICOORD& top_right) {
193  // Initialize clean partitions list and grid
194  clean_part_grid_.Init(grid_size, bottom_left, top_right);
195  leader_and_ruling_grid_.Init(grid_size, bottom_left, top_right);
196  fragmented_text_grid_.Init(grid_size, bottom_left, top_right);
197  col_seg_grid_.Init(grid_size, bottom_left, top_right);
198  table_grid_.Init(grid_size, bottom_left, top_right);
199 }
200 
201 // Copy cleaned partitions from part_grid_ to clean_part_grid_ and
202 // insert leaders and rulers into the leader_and_ruling_grid_
204  TO_BLOCK* block) {
205  // Calculate stats. This lets us filter partitions in AllowTextPartition()
206  // and filter blobs in AllowBlob().
207  SetGlobalSpacings(grid);
208 
209  // Iterate the ColPartitions in the grid.
210  ColPartitionGridSearch gsearch(grid);
211  gsearch.SetUniqueMode(true);
212  gsearch.StartFullSearch();
213  ColPartition* part = NULL;
214  while ((part = gsearch.NextFullSearch()) != NULL) {
215  // Reject partitions with nothing useful inside of them.
216  if (part->blob_type() == BRT_NOISE || part->bounding_box().area() <= 0)
217  continue;
218  ColPartition* clean_part = part->ShallowCopy();
219  ColPartition* leader_part = NULL;
220  if (part->IsLineType()) {
221  InsertRulingPartition(clean_part);
222  continue;
223  }
224  // Insert all non-text partitions to clean_parts
225  if (!part->IsTextType()) {
226  InsertImagePartition(clean_part);
227  continue;
228  }
229  // Insert text colpartitions after removing noisy components from them
230  // The leaders are split into a separate grid.
231  BLOBNBOX_CLIST* part_boxes = part->boxes();
232  BLOBNBOX_C_IT pit(part_boxes);
233  for (pit.mark_cycle_pt(); !pit.cycled_list(); pit.forward()) {
234  BLOBNBOX *pblob = pit.data();
235  // Bad blobs... happens in UNLV set.
236  // news.3G1, page 17 (around x=6)
237  if (!AllowBlob(*pblob))
238  continue;
239  if (pblob->flow() == BTFT_LEADER) {
240  if (leader_part == NULL) {
241  leader_part = part->ShallowCopy();
242  leader_part->set_flow(BTFT_LEADER);
243  }
244  leader_part->AddBox(pblob);
245  } else if (pblob->region_type() != BRT_NOISE) {
246  clean_part->AddBox(pblob);
247  }
248  }
249  clean_part->ComputeLimits();
250  ColPartition* fragmented = clean_part->CopyButDontOwnBlobs();
251  InsertTextPartition(clean_part);
253  if (leader_part != NULL) {
254  // TODO(nbeato): Note that ComputeLimits does not update the column
255  // information. So the leader may appear to span more columns than it
256  // really does later on when IsInSameColumnAs gets called to test
257  // for adjacent leaders.
258  leader_part->ComputeLimits();
259  InsertLeaderPartition(leader_part);
260  }
261  }
262 
263  // Make the partition partners better for upper and lower neighbors.
266 }
267 
268 // High level function to perform table detection
270  ColPartitionSet** all_columns,
271  WidthCallback* width_cb,
272  const FCOORD& reskew) {
273  // initialize spacing, neighbors, and columns
274  InitializePartitions(all_columns);
275 
276 #ifndef GRAPHICS_DISABLED
277  if (textord_show_tables) {
278  ScrollView* table_win = MakeWindow(0, 300, "Column Partitions & Neighbors");
284 
285  table_win = MakeWindow(100, 300, "Fragmented Text");
287  }
288 #endif // GRAPHICS_DISABLED
289 
290  // mark, filter, and smooth candidate table partitions
292 
293  // Make single-column blocks from good_columns_ partitions. col_segments are
294  // moved to a grid later which takes the ownership
295  ColSegment_LIST column_blocks;
296  GetColumnBlocks(all_columns, &column_blocks);
297  // Set the ratio of candidate table partitions in each column
298  SetColumnsType(&column_blocks);
299 
300  // Move column segments to col_seg_grid_
301  MoveColSegmentsToGrid(&column_blocks, &col_seg_grid_);
302 
303  // Detect split in column layout that might have occurred due to the
304  // presence of a table. In such a case, merge the corresponding columns.
306 
307  // Group horizontally overlapping table partitions into table columns.
308  // table_columns created here get deleted at the end of this method.
309  ColSegment_LIST table_columns;
310  GetTableColumns(&table_columns);
311 
312  // Within each column, mark the range table regions occupy based on the
313  // table columns detected. table_regions are moved to a grid later which
314  // takes the ownership
315  ColSegment_LIST table_regions;
316  GetTableRegions(&table_columns, &table_regions);
317 
318 #ifndef GRAPHICS_DISABLED
320  ScrollView* table_win = MakeWindow(1200, 300, "Table Columns and Regions");
321  DisplayColSegments(table_win, &table_columns, ScrollView::DARK_TURQUOISE);
322  DisplayColSegments(table_win, &table_regions, ScrollView::YELLOW);
323  }
324 #endif // GRAPHICS_DISABLED
325 
326  // Merge table regions across columns for tables spanning multiple
327  // columns
328  MoveColSegmentsToGrid(&table_regions, &table_grid_);
330 
331  // Adjust table boundaries by including nearby horizontal lines and left
332  // out column headers
335 
337  // Remove false alarms consiting of a single column
339 
340 #ifndef GRAPHICS_DISABLED
341  if (textord_show_tables) {
342  ScrollView* table_win = MakeWindow(1200, 300, "Detected Table Locations");
344  DisplayColSegments(table_win, &table_columns, ScrollView::KHAKI);
345  table_grid_.DisplayBoxes(table_win);
346  }
347 #endif // GRAPHICS_DISABLED
348 
349  // Find table grid structure and reject tables that are malformed.
350  RecognizeTables();
352  RecognizeTables();
353 
354 #ifndef GRAPHICS_DISABLED
355  if (textord_show_tables) {
356  ScrollView* table_win = MakeWindow(1400, 600, "Recognized Tables");
359  table_grid_.DisplayBoxes(table_win);
360  }
361 #endif // GRAPHICS_DISABLED
362  } else {
363  // Remove false alarms consiting of a single column
364  // TODO(nbeato): verify this is a NOP after structured table rejection.
365  // Right now it isn't. If the recognize function is doing what it is
366  // supposed to do, this function is obsolete.
368 
369 #ifndef GRAPHICS_DISABLED
370  if (textord_show_tables) {
371  ScrollView* table_win = MakeWindow(1500, 300, "Detected Tables");
374  table_grid_.DisplayBoxes(table_win);
375  }
376 #endif // GRAPHICS_DISABLED
377  }
378 
380  WriteToPix(reskew);
381 
382  // Merge all colpartitions in table regions to make them a single
383  // colpartition and revert types of isolated table cells not
384  // assigned to any table to their original types.
385  MakeTableBlocks(grid, all_columns, width_cb);
386 }
387 // All grids have the same dimensions. The clean_part_grid_ sizes are set from
388 // the part_grid_ that is passed to InsertCleanPartitions, which was the same as
389 // the grid that is the base of ColumnFinder. Just return the clean_part_grid_
390 // dimensions instead of duplicated memory.
392  return clean_part_grid_.gridsize();
393 }
395  return clean_part_grid_.gridwidth();
396 }
398  return clean_part_grid_.gridheight();
399 }
400 const ICOORD& TableFinder::bleft() const {
401  return clean_part_grid_.bleft();
402 }
403 const ICOORD& TableFinder::tright() const {
404  return clean_part_grid_.tright();
405 }
406 
408  ASSERT_HOST(part != NULL);
409  if (AllowTextPartition(*part)) {
410  clean_part_grid_.InsertBBox(true, true, part);
411  } else {
412  delete part;
413  }
414 }
416  ASSERT_HOST(part != NULL);
417  if (AllowTextPartition(*part)) {
418  fragmented_text_grid_.InsertBBox(true, true, part);
419  } else {
420  delete part;
421  }
422 }
424  ASSERT_HOST(part != NULL);
425  if (!part->IsEmpty() && part->bounding_box().area() > 0) {
426  leader_and_ruling_grid_.InsertBBox(true, true, part);
427  } else {
428  delete part;
429  }
430 }
432  leader_and_ruling_grid_.InsertBBox(true, true, part);
433 }
435  // NOTE: If images are placed into a different grid in the future,
436  // the function SetPartitionSpacings needs to be updated. It should
437  // be the only thing that cares about image partitions.
438  clean_part_grid_.InsertBBox(true, true, part);
439 }
440 
441 // Splits a partition into its "words". The splits happen
442 // at locations with wide inter-blob spacing. This is useful
443 // because it allows the table recognize to "cut through" the
444 // text lines on the page. The assumption is that a table
445 // will have several lines with similar overlapping whitespace
446 // whereas text will not have this type of property.
447 // Note: The code Assumes that blobs are sorted by the left side x!
448 // This will not work (as well) if the blobs are sorted by center/right.
450  ASSERT_HOST(part != NULL);
451  // Bye bye empty partitions!
452  if (part->boxes()->empty()) {
453  delete part;
454  return;
455  }
456 
457  // The AllowBlob function prevents this.
458  ASSERT_HOST(part->median_width() > 0);
459  const double kThreshold = part->median_width() * kSplitPartitionSize;
460 
461  ColPartition* right_part = part;
462  bool found_split = true;
463  while (found_split) {
464  found_split = false;
465  BLOBNBOX_C_IT box_it(right_part->boxes());
466  // Blobs are sorted left side first. If blobs overlap,
467  // the previous blob may have a "more right" right side.
468  // Account for this by always keeping the largest "right"
469  // so far.
470  int previous_right = MIN_INT32;
471 
472  // Look for the next split in the partition.
473  for (box_it.mark_cycle_pt(); !box_it.cycled_list(); box_it.forward()) {
474  const TBOX& box = box_it.data()->bounding_box();
475  if (previous_right != MIN_INT32 &&
476  box.left() - previous_right > kThreshold) {
477  // We have a split position. Split the partition in two pieces.
478  // Insert the left piece in the grid and keep processing the right.
479  int mid_x = (box.left() + previous_right) / 2;
480  ColPartition* left_part = right_part;
481  right_part = left_part->SplitAt(mid_x);
482 
484  found_split = true;
485  break;
486  }
487 
488  // The right side of the previous blobs.
489  previous_right = MAX(previous_right, box.right());
490  }
491  }
492  // When a split is not found, the right part is minimized
493  // as much as possible, so process it.
494  InsertFragmentedTextPartition(right_part);
495 }
496 
497 // Some simple criteria to filter out now. We want to make sure the
498 // average blob size in the partition is consistent with the
499 // global page stats.
500 // The area metric will almost always pass for multi-blob partitions.
501 // It is useful when filtering out noise caused by an isolated blob.
503  const double kHeightRequired = global_median_xheight_ * kAllowTextHeight;
504  const double kWidthRequired = global_median_blob_width_ * kAllowTextWidth;
505  const int median_area = global_median_xheight_ * global_median_blob_width_;
506  const double kAreaPerBlobRequired = median_area * kAllowTextArea;
507  // Keep comparisons strictly greater to disallow 0!
508  return part.median_size() > kHeightRequired &&
509  part.median_width() > kWidthRequired &&
510  part.bounding_box().area() > kAreaPerBlobRequired * part.boxes_count();
511 }
512 
513 // Same as above, applied to blobs. Keep in mind that
514 // leaders, commas, and periods are important in tables.
515 bool TableFinder::AllowBlob(const BLOBNBOX& blob) const {
516  const TBOX& box = blob.bounding_box();
517  const double kHeightRequired = global_median_xheight_ * kAllowBlobHeight;
518  const double kWidthRequired = global_median_blob_width_ * kAllowBlobWidth;
519  const int median_area = global_median_xheight_ * global_median_blob_width_;
520  const double kAreaRequired = median_area * kAllowBlobArea;
521  // Keep comparisons strictly greater to disallow 0!
522  return box.height() > kHeightRequired &&
523  box.width() > kWidthRequired &&
524  box.area() > kAreaRequired;
525 }
526 
527 // TODO(nbeato): The grid that makes the window doesn't seem to matter.
528 // The only downside is that window messages will be caught by
529 // clean_part_grid_ instead of a useful object. This is a temporary solution
530 // for the debug windows created by the TableFinder.
531 ScrollView* TableFinder::MakeWindow(int x, int y, const char* window_name) {
532  return clean_part_grid_.MakeWindow(x, y, window_name);
533 }
534 
535 // Make single-column blocks from good_columns_ partitions.
537  ColSegment_LIST* column_blocks) {
538  for (int i = 0; i < gridheight(); ++i) {
539  ColPartitionSet* columns = all_columns[i];
540  if (columns != NULL) {
541  ColSegment_LIST new_blocks;
542  // Get boxes from the current vertical position on the grid
543  columns->GetColumnBoxes(i * gridsize(), (i+1) * gridsize(), &new_blocks);
544  // Merge the new_blocks boxes into column_blocks if they are well-aligned
545  GroupColumnBlocks(&new_blocks, column_blocks);
546  }
547  }
548 }
549 
550 // Merge column segments into the current list if they are well aligned.
551 void TableFinder::GroupColumnBlocks(ColSegment_LIST* new_blocks,
552  ColSegment_LIST* column_blocks) {
553  ColSegment_IT src_it(new_blocks);
554  ColSegment_IT dest_it(column_blocks);
555  // iterate through the source list
556  for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) {
557  ColSegment* src_seg = src_it.data();
558  TBOX src_box = src_seg->bounding_box();
559  bool match_found = false;
560  // iterate through the destination list to find a matching column block
561  for (dest_it.mark_cycle_pt(); !dest_it.cycled_list(); dest_it.forward()) {
562  ColSegment* dest_seg = dest_it.data();
563  TBOX dest_box = dest_seg->bounding_box();
564  if (ConsecutiveBoxes(src_box, dest_box)) {
565  // If matching block is found, insert the current block into it
566  // and delete the soure block
567  dest_seg->InsertBox(src_box);
568  match_found = true;
569  delete src_it.extract();
570  break;
571  }
572  }
573  // If no match is found, just append the source block to column_blocks
574  if (!match_found) {
575  dest_it.add_after_then_move(src_it.extract());
576  }
577  }
578 }
579 
580 // are the two boxes immediate neighbors along the vertical direction
581 bool TableFinder::ConsecutiveBoxes(const TBOX &b1, const TBOX &b2) {
582  int x_margin = 20;
583  int y_margin = 5;
584  return (abs(b1.left() - b2.left()) < x_margin) &&
585  (abs(b1.right() - b2.right()) < x_margin) &&
586  (abs(b1.top()-b2.bottom()) < y_margin ||
587  abs(b2.top()-b1.bottom()) < y_margin);
588 }
589 
590 // Set up info for clean_part_grid_ partitions to be valid during detection
591 // code.
593  FindNeighbors();
594  SetPartitionSpacings(&clean_part_grid_, all_columns);
596 }
597 
598 // Set left, right and top, bottom spacings of each colpartition.
600  ColPartitionSet** all_columns) {
601  // Iterate the ColPartitions in the grid.
602  ColPartitionGridSearch gsearch(grid);
603  gsearch.StartFullSearch();
604  ColPartition* part = NULL;
605  while ((part = gsearch.NextFullSearch()) != NULL) {
606  ColPartitionSet* columns = all_columns[gsearch.GridY()];
607  TBOX box = part->bounding_box();
608  int y = part->MidY();
609  ColPartition* left_column = columns->ColumnContaining(box.left(), y);
610  ColPartition* right_column = columns->ColumnContaining(box.right(), y);
611  // set distance from left column as space to the left
612  if (left_column) {
613  int left_space = MAX(0, box.left() - left_column->LeftAtY(y));
614  part->set_space_to_left(left_space);
615  }
616  // set distance from right column as space to the right
617  if (right_column) {
618  int right_space = MAX(0, right_column->RightAtY(y) - box.right());
619  part->set_space_to_right(right_space);
620  }
621 
622  // Look for images that may be closer.
623  // NOTE: used to be part_grid_, might cause issues now
624  ColPartitionGridSearch hsearch(grid);
625  hsearch.StartSideSearch(box.left(), box.bottom(), box.top());
626  ColPartition* neighbor = NULL;
627  while ((neighbor = hsearch.NextSideSearch(true)) != NULL) {
628  if (neighbor->type() == PT_PULLOUT_IMAGE ||
629  neighbor->type() == PT_FLOWING_IMAGE ||
630  neighbor->type() == PT_HEADING_IMAGE) {
631  int right = neighbor->bounding_box().right();
632  if (right < box.left()) {
633  int space = MIN(box.left() - right, part->space_to_left());
634  part->set_space_to_left(space);
635  }
636  }
637  }
638  hsearch.StartSideSearch(box.left(), box.bottom(), box.top());
639  neighbor = NULL;
640  while ((neighbor = hsearch.NextSideSearch(false)) != NULL) {
641  if (neighbor->type() == PT_PULLOUT_IMAGE ||
642  neighbor->type() == PT_FLOWING_IMAGE ||
643  neighbor->type() == PT_HEADING_IMAGE) {
644  int left = neighbor->bounding_box().left();
645  if (left > box.right()) {
646  int space = MIN(left - box.right(), part->space_to_right());
647  part->set_space_to_right(space);
648  }
649  }
650  }
651 
652  ColPartition* upper_part = part->SingletonPartner(true);
653  if (upper_part) {
654  int space = MAX(0, upper_part->bounding_box().bottom() -
655  part->bounding_box().bottom());
656  part->set_space_above(space);
657  } else {
658  // TODO(nbeato): What constitutes a good value?
659  // 0 is the default value when not set, explicitly noting it needs to
660  // be something else.
661  part->set_space_above(MAX_INT32);
662  }
663 
664  ColPartition* lower_part = part->SingletonPartner(false);
665  if (lower_part) {
666  int space = MAX(0, part->bounding_box().bottom() -
667  lower_part->bounding_box().bottom());
668  part->set_space_below(space);
669  } else {
670  // TODO(nbeato): What constitutes a good value?
671  // 0 is the default value when not set, explicitly noting it needs to
672  // be something else.
673  part->set_space_below(MAX_INT32);
674  }
675  }
676 }
677 
678 // Set spacing and closest neighbors above and below a given colpartition.
680  TBOX box = part->bounding_box();
681  int top_range = MIN(box.top() + kMaxVerticalSpacing, tright().y());
682  int bottom_range = MAX(box.bottom() - kMaxVerticalSpacing, bleft().y());
683  box.set_top(top_range);
684  box.set_bottom(bottom_range);
685 
686  TBOX part_box = part->bounding_box();
687  // Start a rect search
689  rectsearch(&clean_part_grid_);
690  rectsearch.StartRectSearch(box);
691  ColPartition* neighbor;
692  int min_space_above = kMaxVerticalSpacing;
693  int min_space_below = kMaxVerticalSpacing;
694  ColPartition* above_neighbor = NULL;
695  ColPartition* below_neighbor = NULL;
696  while ((neighbor = rectsearch.NextRectSearch()) != NULL) {
697  if (neighbor == part)
698  continue;
699  TBOX neighbor_box = neighbor->bounding_box();
700  if (neighbor_box.major_x_overlap(part_box)) {
701  int gap = abs(part->median_bottom() - neighbor->median_bottom());
702  // If neighbor is below current partition
703  if (neighbor_box.top() < part_box.bottom() &&
704  gap < min_space_below) {
705  min_space_below = gap;
706  below_neighbor = neighbor;
707  } // If neighbor is above current partition
708  else if (part_box.top() < neighbor_box.bottom() &&
709  gap < min_space_above) {
710  min_space_above = gap;
711  above_neighbor = neighbor;
712  }
713  }
714  }
715  part->set_space_above(min_space_above);
716  part->set_space_below(min_space_below);
717  part->set_nearest_neighbor_above(above_neighbor);
718  part->set_nearest_neighbor_below(below_neighbor);
719 }
720 
721 // Set global spacing and x-height estimates
723  STATS xheight_stats(0, kMaxVerticalSpacing + 1);
724  STATS width_stats(0, kMaxBlobWidth + 1);
725  STATS ledding_stats(0, kMaxVerticalSpacing + 1);
726  // Iterate the ColPartitions in the grid.
727  ColPartitionGridSearch gsearch(grid);
728  gsearch.SetUniqueMode(true);
729  gsearch.StartFullSearch();
730  ColPartition* part = NULL;
731  while ((part = gsearch.NextFullSearch()) != NULL) {
732  // TODO(nbeato): HACK HACK HACK! medians are equal to partition length.
733  // ComputeLimits needs to get called somewhere outside of TableFinder
734  // to make sure the partitions are properly initialized.
735  // When this is called, SmoothPartitionPartners dies in an assert after
736  // table find runs. Alternative solution.
737  // part->ComputeLimits();
738  if (part->IsTextType()) {
739  // xheight_stats.add(part->median_size(), part->boxes_count());
740  // width_stats.add(part->median_width(), part->boxes_count());
741 
742  // This loop can be removed when above issues are fixed.
743  // Replace it with the 2 lines commented out above.
744  BLOBNBOX_C_IT it(part->boxes());
745  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
746  xheight_stats.add(it.data()->bounding_box().height(), 1);
747  width_stats.add(it.data()->bounding_box().width(), 1);
748  }
749 
750  ledding_stats.add(part->space_above(), 1);
751  ledding_stats.add(part->space_below(), 1);
752  }
753  }
754  // Set estimates based on median of statistics obtained
755  set_global_median_xheight(static_cast<int>(xheight_stats.median() + 0.5));
756  set_global_median_blob_width(static_cast<int>(width_stats.median() + 0.5));
757  set_global_median_ledding(static_cast<int>(ledding_stats.median() + 0.5));
758  #ifndef GRAPHICS_DISABLED
760  const char* kWindowName = "X-height (R), X-width (G), and ledding (B)";
761  ScrollView* stats_win = MakeWindow(500, 10, kWindowName);
762  xheight_stats.plot(stats_win, 10, 200, 2, 15, ScrollView::RED);
763  width_stats.plot(stats_win, 10, 200, 2, 15, ScrollView::GREEN);
764  ledding_stats.plot(stats_win, 10, 200, 2, 15, ScrollView::BLUE);
765  }
766  #endif // GRAPHICS_DISABLED
767 }
768 
770  global_median_xheight_ = xheight;
771 }
774 }
776  global_median_ledding_ = ledding;
777 }
778 
781  gsearch.StartFullSearch();
782  ColPartition* part = NULL;
783  while ((part = gsearch.NextFullSearch()) != NULL) {
784  // TODO(nbeato): Rename this function, meaning is different now.
785  // IT is finding nearest neighbors its own way
786  //SetVerticalSpacing(part);
787 
788  ColPartition* upper = part->SingletonPartner(true);
789  if (upper)
790  part->set_nearest_neighbor_above(upper);
791 
792  ColPartition* lower = part->SingletonPartner(false);
793  if (lower)
794  part->set_nearest_neighbor_below(lower);
795  }
796 }
797 
798 // High level interface. Input is an unmarked ColPartitionGrid
799 // (namely, clean_part_grid_). Partitions are identified using local
800 // information and filter/smoothed. The function exit should contain
801 // a good sampling of the table partitions.
805  ScrollView* table_win = MakeWindow(300, 300, "Initial Table Partitions");
809  }
812  ScrollView* table_win = MakeWindow(600, 300, "Filtered Table Partitions");
816  }
819  ScrollView* table_win = MakeWindow(900, 300, "Smoothed Table Partitions");
823  }
826  ScrollView* table_win = MakeWindow(900, 300, "Final Table Partitions");
830  }
831 }
832 
833 // These types of partitions are marked as table partitions:
834 // 1- Partitions that have at lease one large gap between words
835 // 2- Partitions that consist of only one word (no significant gap
836 // between components)
837 // 3- Partitions that vertically overlap with other partitions within the
838 // same column.
839 // 4- Partitions with leaders before/after them.
841  // Iterate the ColPartitions in the grid.
843  gsearch(&clean_part_grid_);
844  gsearch.StartFullSearch();
845  ColPartition* part = NULL;
846  while ((part = gsearch.NextFullSearch()) != NULL) {
847  if (!part->IsTextType()) // Only consider text partitions
848  continue;
849  // Only consider partitions in dominant font size or smaller
850  if (part->median_size() > kMaxTableCellXheight * global_median_xheight_)
851  continue;
852  // Mark partitions with a large gap, or no significant gap as
853  // table partitions.
854  // Comments: It produces several false alarms at:
855  // - last line of a paragraph (fixed)
856  // - single word section headings
857  // - page headers and footers
858  // - numbered equations
859  // - line drawing regions
860  // TODO(faisal): detect and fix above-mentioned cases
861  if (HasWideOrNoInterWordGap(part) ||
862  HasLeaderAdjacent(*part)) {
863  part->set_table_type();
864  }
865  }
866 }
867 
868 // Check if the partition has at least one large gap between words or no
869 // significant gap at all
871  // Should only get text partitions.
872  ASSERT_HOST(part->IsTextType());
873  // Blob access
874  BLOBNBOX_CLIST* part_boxes = part->boxes();
875  BLOBNBOX_C_IT it(part_boxes);
876  // Check if this is a relatively small partition (such as a single word)
877  if (part->bounding_box().width() <
878  kMinBoxesInTextPartition * part->median_size() &&
879  part_boxes->length() < kMinBoxesInTextPartition)
880  return true;
881 
882  // Variables used to compute inter-blob spacing.
883  int current_x0 = -1;
884  int current_x1 = -1;
885  int previous_x1 = -1;
886  // Stores the maximum gap detected.
887  int largest_partition_gap_found = -1;
888  // Text partition gap limits. If this is text (and not a table),
889  // there should be at least one gap larger than min_gap and no gap
890  // larger than max_gap.
891  const double max_gap = kMaxGapInTextPartition * part->median_size();
892  const double min_gap = kMinMaxGapInTextPartition * part->median_size();
893 
894  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
895  BLOBNBOX* blob = it.data();
896  current_x0 = blob->bounding_box().left();
897  current_x1 = blob->bounding_box().right();
898  if (previous_x1 != -1) {
899  int gap = current_x0 - previous_x1;
900 
901  // TODO(nbeato): Boxes may overlap? Huh?
902  // For example, mag.3B 8003_033.3B.tif in UNLV data. The titles/authors
903  // on the top right of the page are filtered out with this line.
904  // Note 2: Iterating over blobs in a partition, so we are looking for
905  // spacing between the words.
906  if (gap < 0) {
907  // More likely case, the blobs slightly overlap. This can happen
908  // with diacritics (accents) or broken alphabet symbols (characters).
909  // Merge boxes together by taking max of right sides.
910  if (-gap < part->median_size() * kMaxBlobOverlapFactor) {
911  previous_x1 = MAX(previous_x1, current_x1);
912  continue;
913  }
914  // Extreme case, blobs overlap significantly in the same partition...
915  // This should not happen often (if at all), but it does.
916  // TODO(nbeato): investigate cases when this happens.
917  else {
918  // The behavior before was to completely ignore this case.
919  }
920  }
921 
922  // If a large enough gap is found, mark it as a table cell (return true)
923  if (gap > max_gap)
924  return true;
925  if (gap > largest_partition_gap_found)
926  largest_partition_gap_found = gap;
927  }
928  previous_x1 = current_x1;
929  }
930  // Since no large gap was found, return false if the partition is too
931  // long to be a data cell
932  if (part->bounding_box().width() >
933  kMaxBoxesInDataPartition * part->median_size() ||
934  part_boxes->length() > kMaxBoxesInDataPartition)
935  return false;
936 
937  // A partition may be a single blob. In this case, it's an isolated symbol
938  // or non-text (such as a ruling or image).
939  // Detect these as table partitions? Shouldn't this be case by case?
940  // The behavior before was to ignore this, making max_partition_gap < 0
941  // and implicitly return true. Just making it explicit.
942  if (largest_partition_gap_found == -1)
943  return true;
944 
945  // return true if the maximum gap found is smaller than the minimum allowed
946  // max_gap in a text partition. This indicates that there is no signficant
947  // space in the partition, hence it is likely a single word.
948  return largest_partition_gap_found < min_gap;
949 }
950 
951 // A criteria for possible tables is that a table may have leaders
952 // between data cells. An aggressive solution to find such tables is to
953 // explicitly mark partitions that have adjacent leaders.
954 // Note that this includes overlapping leaders. However, it does not
955 // include leaders in different columns on the page.
956 // Possible false-positive will include lists, such as a table of contents.
957 // As these arise, the agressive nature of this search may need to be
958 // trimmed down.
960  if (part.flow() == BTFT_LEADER)
961  return true;
962  // Search range is left and right bounded by an offset of the
963  // median xheight. This offset is to allow some tolerance to the
964  // the leaders on the page in the event that the alignment is still
965  // a bit off.
966  const TBOX& box = part.bounding_box();
967  const int search_size = kAdjacentLeaderSearchPadding * global_median_xheight_;
968  const int top = box.top() + search_size;
969  const int bottom = box.bottom() - search_size;
971  for (int direction = 0; direction < 2; ++direction) {
972  bool right_to_left = (direction == 0);
973  int x = right_to_left ? box.right() : box.left();
974  hsearch.StartSideSearch(x, bottom, top);
975  ColPartition* leader = NULL;
976  while ((leader = hsearch.NextSideSearch(right_to_left)) != NULL) {
977  // The leader could be a horizontal ruling in the grid.
978  // Make sure it is actually a leader.
979  if (leader->flow() != BTFT_LEADER)
980  continue;
981  // This should not happen, they are in different grids.
982  ASSERT_HOST(&part != leader);
983  // Make sure the leader shares a page column with the partition,
984  // otherwise we are spreading across columns.
985  if (!part.IsInSameColumnAs(*leader))
986  break;
987  // There should be a significant vertical overlap
988  if (!leader->VSignificantCoreOverlap(part))
989  continue;
990  // Leader passed all tests, so it is adjacent.
991  return true;
992  }
993  }
994  // No leaders are adjacent to the given partition.
995  return false;
996 }
997 
998 // Filter individual text partitions marked as table partitions
999 // consisting of paragraph endings, small section headings, and
1000 // headers and footers.
1004  // TODO(nbeato): Fully justified text as non-table?
1005 }
1006 
1008  // Detect last line of paragraph
1009  // Iterate the ColPartitions in the grid.
1011  gsearch.StartFullSearch();
1012  ColPartition* part = NULL;
1013  while ((part = gsearch.NextFullSearch()) != NULL) {
1014  if (part->type() != PT_TABLE)
1015  continue; // Consider only table partitions
1016 
1017  // Paragraph ending should have flowing text above it.
1018  ColPartition* upper_part = part->nearest_neighbor_above();
1019  if (!upper_part)
1020  continue;
1021  if (upper_part->type() != PT_FLOWING_TEXT)
1022  continue;
1023  if (upper_part->bounding_box().width() <
1024  2 * part->bounding_box().width())
1025  continue;
1026  // Check if its the last line of a paragraph.
1027  // In most cases, a paragraph ending should be left-aligned to text line
1028  // above it. Sometimes, it could be a 2 line paragraph, in which case
1029  // the line above it is indented.
1030  // To account for that, check if the partition center is to
1031  // the left of the one above it.
1032  int mid = (part->bounding_box().left() + part->bounding_box().right()) / 2;
1033  int upper_mid = (upper_part->bounding_box().left() +
1034  upper_part->bounding_box().right()) / 2;
1035  int current_spacing = 0; // spacing of the current line to margin
1036  int upper_spacing = 0; // spacing of the previous line to the margin
1038  // Left to right languages, use mid - left to figure out the distance
1039  // the middle is from the left margin.
1040  int left = MIN(part->bounding_box().left(),
1041  upper_part->bounding_box().left());
1042  current_spacing = mid - left;
1043  upper_spacing = upper_mid - left;
1044  } else {
1045  // Right to left languages, use right - mid to figure out the distance
1046  // the middle is from the right margin.
1047  int right = MAX(part->bounding_box().right(),
1048  upper_part->bounding_box().right());
1049  current_spacing = right - mid;
1050  upper_spacing = right - upper_mid;
1051  }
1052  if (current_spacing * kParagraphEndingPreviousLineRatio > upper_spacing)
1053  continue;
1054 
1055  // Paragraphs should have similar fonts.
1056  if (!part->MatchingSizes(*upper_part) ||
1057  !part->MatchingStrokeWidth(*upper_part, kStrokeWidthFractionalTolerance,
1058  kStrokeWidthConstantTolerance)) {
1059  continue;
1060  }
1061 
1062  // The last line of a paragraph should be left aligned.
1063  // TODO(nbeato): This would be untrue if the text was right aligned.
1064  // How often is that?
1065  if (part->space_to_left() >
1066  kMaxParagraphEndingLeftSpaceMultiple * part->median_size())
1067  continue;
1068  // The line above it should be right aligned (assuming justified format).
1069  // Since we can't assume justified text, we compare whitespace to text.
1070  // The above line should have majority spanning text (or the current
1071  // line could have fit on the previous line). So compare
1072  // whitespace to text.
1073  if (upper_part->bounding_box().width() <
1074  kMinParagraphEndingTextToWhitespaceRatio * upper_part->space_to_right())
1075  continue;
1076 
1077  // Ledding above the line should be less than ledding below
1078  if (part->space_above() >= part->space_below() ||
1079  part->space_above() > 2 * global_median_ledding_)
1080  continue;
1081 
1082  // If all checks failed, it is probably text.
1083  part->clear_table_type();
1084  }
1085 }
1086 
1088  // Consider top-most text colpartition as header and bottom most as footer
1089  ColPartition* header = NULL;
1090  ColPartition* footer = NULL;
1091  int max_top = MIN_INT32;
1092  int min_bottom = MAX_INT32;
1094  gsearch.StartFullSearch();
1095  ColPartition* part = NULL;
1096  while ((part = gsearch.NextFullSearch()) != NULL) {
1097  if (!part->IsTextType())
1098  continue; // Consider only text partitions
1099  int top = part->bounding_box().top();
1100  int bottom = part->bounding_box().bottom();
1101  if (top > max_top) {
1102  max_top = top;
1103  header = part;
1104  }
1105  if (bottom < min_bottom) {
1106  min_bottom = bottom;
1107  footer = part;
1108  }
1109  }
1110  if (header)
1111  header->clear_table_type();
1112  if (footer)
1113  footer->clear_table_type();
1114 }
1115 
1116 // Mark all ColPartitions as table cells that have a table cell above
1117 // and below them
1118 // TODO(faisal): This is too aggressive at the moment. The method needs to
1119 // consider spacing and alignment as well. Detection of false alarm table cells
1120 // should also be done as part of it.
1122  // Iterate the ColPartitions in the grid.
1124  gsearch.StartFullSearch();
1125  ColPartition* part = NULL;
1126  while ((part = gsearch.NextFullSearch()) != NULL) {
1127  if (part->type() >= PT_TABLE || part->type() == PT_UNKNOWN)
1128  continue; // Consider only text partitions
1129  ColPartition* upper_part = part->nearest_neighbor_above();
1130  ColPartition* lower_part = part->nearest_neighbor_below();
1131  if (!upper_part || !lower_part)
1132  continue;
1133  if (upper_part->type() == PT_TABLE && lower_part->type() == PT_TABLE)
1134  part->set_table_type();
1135  }
1136 
1137  // Pass 2, do the opposite. If both the upper and lower neighbors
1138  // exist and are not tables, this probably shouldn't be a table.
1139  gsearch.StartFullSearch();
1140  part = NULL;
1141  while ((part = gsearch.NextFullSearch()) != NULL) {
1142  if (part->type() != PT_TABLE)
1143  continue; // Consider only text partitions
1144  ColPartition* upper_part = part->nearest_neighbor_above();
1145  ColPartition* lower_part = part->nearest_neighbor_below();
1146 
1147  // table can't be by itself
1148  if ((upper_part && upper_part->type() != PT_TABLE) &&
1149  (lower_part && lower_part->type() != PT_TABLE)) {
1150  part->clear_table_type();
1151  }
1152  }
1153 }
1154 
1155 // Set the type of a column segment based on the ratio of table to text cells
1156 void TableFinder::SetColumnsType(ColSegment_LIST* column_blocks) {
1157  ColSegment_IT it(column_blocks);
1158  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
1159  ColSegment* seg = it.data();
1160  TBOX box = seg->bounding_box();
1161  int num_table_cells = 0;
1162  int num_text_cells = 0;
1164  rsearch(&clean_part_grid_);
1165  rsearch.SetUniqueMode(true);
1166  rsearch.StartRectSearch(box);
1167  ColPartition* part = NULL;
1168  while ((part = rsearch.NextRectSearch()) != NULL) {
1169  if (part->type() == PT_TABLE) {
1170  num_table_cells++;
1171  } else if (part->type() == PT_FLOWING_TEXT) {
1172  num_text_cells++;
1173  }
1174  }
1175  // If a column block has no text or table partition in it, it is not needed
1176  // for table detection.
1177  if (!num_table_cells && !num_text_cells) {
1178  delete it.extract();
1179  } else {
1180  seg->set_num_table_cells(num_table_cells);
1181  seg->set_num_text_cells(num_text_cells);
1182  // set column type based on the ratio of table to text cells
1183  seg->set_type();
1184  }
1185  }
1186 }
1187 
1188 // Move column blocks to grid
1189 void TableFinder::MoveColSegmentsToGrid(ColSegment_LIST *segments,
1190  ColSegmentGrid *col_seg_grid) {
1191  ColSegment_IT it(segments);
1192  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
1193  ColSegment* seg = it.extract();
1194  col_seg_grid->InsertBBox(true, true, seg);
1195  }
1196 }
1197 
1198 // Merge column blocks if a split is detected due to the presence of a
1199 // table. A text block is considered split if it has multiple
1200 // neighboring blocks above/below it, and at least one of the
1201 // neighboring blocks is of table type (has a high density of table
1202 // partitions). In this case neighboring blocks in the direction
1203 // (above/below) of the table block are merged with the text block.
1204 
1205 // Comment: This method does not handle split due to a full page table
1206 // since table columns in this case do not have a text column on which
1207 // split decision can be based.
1209  int margin = gridsize();
1210 
1211  // Iterate the Column Blocks in the grid.
1213  gsearch(&col_seg_grid_);
1214  gsearch.StartFullSearch();
1215  ColSegment* seg;
1216  while ((seg = gsearch.NextFullSearch()) != NULL) {
1217  if (seg->type() != COL_TEXT)
1218  continue; // only consider text blocks for split detection
1219  bool neighbor_found = false;
1220  bool modified = false; // Modified at least once
1221  // keep expanding current box as long as neighboring table columns
1222  // are found above or below it.
1223  do {
1224  TBOX box = seg->bounding_box();
1225  // slightly expand the search region vertically
1226  int top_range = MIN(box.top() + margin, tright().y());
1227  int bottom_range = MAX(box.bottom() - margin, bleft().y());
1228  box.set_top(top_range);
1229  box.set_bottom(bottom_range);
1230  neighbor_found = false;
1232  rectsearch(&col_seg_grid_);
1233  rectsearch.StartRectSearch(box);
1234  ColSegment* neighbor = NULL;
1235  while ((neighbor = rectsearch.NextRectSearch()) != NULL) {
1236  if (neighbor == seg)
1237  continue;
1238  const TBOX& neighbor_box = neighbor->bounding_box();
1239  // If the neighbor box significantly overlaps with the current
1240  // box (due to the expansion of the current box in the
1241  // previous iteration of this loop), remove the neighbor box
1242  // and expand the current box to include it.
1243  if (neighbor_box.overlap_fraction(box) >= 0.9) {
1244  seg->InsertBox(neighbor_box);
1245  modified = true;
1246  rectsearch.RemoveBBox();
1247  gsearch.RepositionIterator();
1248  delete neighbor;
1249  continue;
1250  }
1251  // Only expand if the neighbor box is of table type
1252  if (neighbor->type() != COL_TABLE)
1253  continue;
1254  // Insert the neighbor box into the current column block
1255  if (neighbor_box.major_x_overlap(box) &&
1256  !box.contains(neighbor_box)) {
1257  seg->InsertBox(neighbor_box);
1258  neighbor_found = true;
1259  modified = true;
1260  rectsearch.RemoveBBox();
1261  gsearch.RepositionIterator();
1262  delete neighbor;
1263  }
1264  }
1265  } while (neighbor_found);
1266  if (modified) {
1267  // Because the box has changed, it has to be removed first.
1268  gsearch.RemoveBBox();
1269  col_seg_grid_.InsertBBox(true, true, seg);
1270  gsearch.RepositionIterator();
1271  }
1272  }
1273 }
1274 
1275 // Group horizontally overlapping table partitions into table columns.
1276 // TODO(faisal): This is too aggressive at the moment. The method should
1277 // consider more attributes to group table partitions together. Some common
1278 // errors are:
1279 // 1- page number is merged with a table column above it even
1280 // if there is a large vertical gap between them.
1281 // 2- column headers go on to catch one of the columns arbitrarily
1282 // 3- an isolated noise blob near page top or bottom merges with the table
1283 // column below/above it
1284 // 4- cells from two vertically adjacent tables merge together to make a
1285 // single column resulting in merging of the two tables
1286 void TableFinder::GetTableColumns(ColSegment_LIST *table_columns) {
1287  ColSegment_IT it(table_columns);
1288  // Iterate the ColPartitions in the grid.
1290  gsearch(&clean_part_grid_);
1291  gsearch.StartFullSearch();
1292  ColPartition* part;
1293  while ((part = gsearch.NextFullSearch()) != NULL) {
1294  if (part->inside_table_column() || part->type() != PT_TABLE)
1295  continue; // prevent a partition to be assigned to multiple columns
1296  const TBOX& box = part->bounding_box();
1297  ColSegment* col = new ColSegment();
1298  col->InsertBox(box);
1299  part->set_inside_table_column(true);
1300  // Start a search below the current cell to find bottom neighbours
1301  // Note: a full search will always process things above it first, so
1302  // this should be starting at the highest cell and working its way down.
1304  vsearch(&clean_part_grid_);
1305  vsearch.StartVerticalSearch(box.left(), box.right(), box.bottom());
1306  ColPartition* neighbor = NULL;
1307  bool found_neighbours = false;
1308  while ((neighbor = vsearch.NextVerticalSearch(true)) != NULL) {
1309  // only consider neighbors not assigned to any column yet
1310  if (neighbor->inside_table_column())
1311  continue;
1312  // Horizontal lines should not break the flow
1313  if (neighbor->IsHorizontalLine())
1314  continue;
1315  // presence of a non-table neighbor marks the end of current
1316  // table column
1317  if (neighbor->type() != PT_TABLE)
1318  break;
1319  // add the neighbor partition to the table column
1320  const TBOX& neighbor_box = neighbor->bounding_box();
1321  col->InsertBox(neighbor_box);
1322  neighbor->set_inside_table_column(true);
1323  found_neighbours = true;
1324  }
1325  if (found_neighbours) {
1326  it.add_after_then_move(col);
1327  } else {
1328  part->set_inside_table_column(false);
1329  delete col;
1330  }
1331  }
1332 }
1333 
1334 // Mark regions in a column that are x-bounded by the column boundaries and
1335 // y-bounded by the table columns' projection on the y-axis as table regions
1336 void TableFinder::GetTableRegions(ColSegment_LIST* table_columns,
1337  ColSegment_LIST* table_regions) {
1338  ColSegment_IT cit(table_columns);
1339  ColSegment_IT rit(table_regions);
1340  // Iterate through column blocks
1342  gsearch(&col_seg_grid_);
1343  gsearch.StartFullSearch();
1344  ColSegment* part;
1345  int page_height = tright().y() - bleft().y();
1346  ASSERT_HOST(page_height > 0);
1347  // create a bool array to hold projection on y-axis
1348  bool* table_region = new bool[page_height];
1349  while ((part = gsearch.NextFullSearch()) != NULL) {
1350  TBOX part_box = part->bounding_box();
1351  // reset the projection array
1352  for (int i = 0; i < page_height; i++) {
1353  table_region[i] = false;
1354  }
1355  // iterate through all table columns to find regions in the current
1356  // page column block
1357  cit.move_to_first();
1358  for (cit.mark_cycle_pt(); !cit.cycled_list(); cit.forward()) {
1359  TBOX col_box = cit.data()->bounding_box();
1360  // find intersection region of table column and page column
1361  TBOX intersection_box = col_box.intersection(part_box);
1362  // project table column on the y-axis
1363  for (int i = intersection_box.bottom(); i < intersection_box.top(); i++) {
1364  table_region[i - bleft().y()] = true;
1365  }
1366  }
1367  // set x-limits of table regions to page column width
1368  TBOX current_table_box;
1369  current_table_box.set_left(part_box.left());
1370  current_table_box.set_right(part_box.right());
1371  // go through the y-axis projection to find runs of table
1372  // regions. Each run makes one table region.
1373  for (int i = 1; i < page_height; i++) {
1374  // detect start of a table region
1375  if (!table_region[i - 1] && table_region[i]) {
1376  current_table_box.set_bottom(i + bleft().y());
1377  }
1378  // TODO(nbeato): Is it guaranteed that the last row is not a table region?
1379  // detect end of a table region
1380  if (table_region[i - 1] && !table_region[i]) {
1381  current_table_box.set_top(i + bleft().y());
1382  if (!current_table_box.null_box()) {
1383  ColSegment* seg = new ColSegment();
1384  seg->InsertBox(current_table_box);
1385  rit.add_after_then_move(seg);
1386  }
1387  }
1388  }
1389  }
1390  delete[] table_region;
1391 }
1392 
1393 // Merge table regions corresponding to tables spanning multiple columns if
1394 // there is a colpartition (horizontal ruling line or normal text) that
1395 // touches both regions.
1396 // TODO(faisal): A rare error occurs if there are two horizontally adjacent
1397 // tables with aligned ruling lines. In this case, line finder returns a
1398 // single line and hence the tables get merged together
1400  // Iterate the table regions in the grid.
1402  gsearch(&table_grid_);
1403  gsearch.StartFullSearch();
1404  ColSegment* seg = NULL;
1405  while ((seg = gsearch.NextFullSearch()) != NULL) {
1406  bool neighbor_found = false;
1407  bool modified = false; // Modified at least once
1408  do {
1409  // Start a rectangle search x-bounded by the image and y by the table
1410  const TBOX& box = seg->bounding_box();
1411  TBOX search_region(box);
1412  search_region.set_left(bleft().x());
1413  search_region.set_right(tright().x());
1414  neighbor_found = false;
1416  rectsearch(&table_grid_);
1417  rectsearch.StartRectSearch(search_region);
1418  ColSegment* neighbor = NULL;
1419  while ((neighbor = rectsearch.NextRectSearch()) != NULL) {
1420  if (neighbor == seg)
1421  continue;
1422  const TBOX& neighbor_box = neighbor->bounding_box();
1423  // Check if a neighbor box has a large overlap with the table
1424  // region. This may happen as a result of merging two table
1425  // regions in the previous iteration.
1426  if (neighbor_box.overlap_fraction(box) >= 0.9) {
1427  seg->InsertBox(neighbor_box);
1428  rectsearch.RemoveBBox();
1429  gsearch.RepositionIterator();
1430  delete neighbor;
1431  modified = true;
1432  continue;
1433  }
1434  // Check if two table regions belong together based on a common
1435  // horizontal ruling line
1436  if (BelongToOneTable(box, neighbor_box)) {
1437  seg->InsertBox(neighbor_box);
1438  neighbor_found = true;
1439  modified = true;
1440  rectsearch.RemoveBBox();
1441  gsearch.RepositionIterator();
1442  delete neighbor;
1443  }
1444  }
1445  } while (neighbor_found);
1446  if (modified) {
1447  // Because the box has changed, it has to be removed first.
1448  gsearch.RemoveBBox();
1449  table_grid_.InsertBBox(true, true, seg);
1450  gsearch.RepositionIterator();
1451  }
1452  }
1453 }
1454 
1455 // Decide if two table regions belong to one table based on a common
1456 // horizontal ruling line or another colpartition
1457 bool TableFinder::BelongToOneTable(const TBOX &box1, const TBOX &box2) {
1458  // Check the obvious case. Most likely not true because overlapping boxes
1459  // should already be merged, but seems like a good thing to do in case things
1460  // change.
1461  if (box1.overlap(box2))
1462  return true;
1463  // Check for ColPartitions spanning both table regions
1464  TBOX bbox = box1.bounding_union(box2);
1465  // Start a rect search on bbox
1467  rectsearch(&clean_part_grid_);
1468  rectsearch.StartRectSearch(bbox);
1469  ColPartition* part = NULL;
1470  while ((part = rectsearch.NextRectSearch()) != NULL) {
1471  const TBOX& part_box = part->bounding_box();
1472  // return true if a colpartition spanning both table regions is found
1473  if (part_box.overlap(box1) && part_box.overlap(box2) &&
1474  !part->IsImageType())
1475  return true;
1476  }
1477  return false;
1478 }
1479 
1480 // Adjust table boundaries by:
1481 // - building a tight bounding box around all ColPartitions contained in it.
1482 // - expanding table boundaries to include all colpartitions that overlap the
1483 // table by more than half of their area
1484 // - expanding table boundaries to include nearby horizontal rule lines
1485 // - expanding table vertically to include left out column headers
1486 // TODO(faisal): Expansion of table boundaries is quite aggressive. It usually
1487 // makes following errors:
1488 // 1- horizontal lines consisting of underlines are included in the table if
1489 // they are close enough
1490 // 2- horizontal lines originating from noise tend to get merged with a table
1491 // near the top of the page
1492 // 3- the criteria for including horizontal lines is very generous. Many times
1493 // horizontal lines separating headers and footers get merged with a
1494 // single-column table in a multi-column page thereby including text
1495 // from the neighboring column inside the table
1496 // 4- the criteria for including left out column headers also tends to
1497 // occasionally include text-lines above the tables, typically from
1498 // table caption
1500  // Iterate the table regions in the grid
1501  ColSegment_CLIST adjusted_tables;
1502  ColSegment_C_IT it(&adjusted_tables);
1504  gsearch.StartFullSearch();
1505  ColSegment* table = NULL;
1506  while ((table = gsearch.NextFullSearch()) != NULL) {
1507  const TBOX& table_box = table->bounding_box();
1508  TBOX grown_box = table_box;
1509  GrowTableBox(table_box, &grown_box);
1510  // To prevent a table from expanding again, do not insert the
1511  // modified box back to the grid. Instead move it to a list and
1512  // and remove it from the grid. The list is moved later back to the grid.
1513  if (!grown_box.null_box()) {
1514  ColSegment* col = new ColSegment();
1515  col->InsertBox(grown_box);
1516  it.add_after_then_move(col);
1517  }
1518  gsearch.RemoveBBox();
1519  delete table;
1520  }
1521  // clear table grid to move final tables in it
1522  // TODO(nbeato): table_grid_ should already be empty. The above loop
1523  // removed everything. Maybe just assert it is empty?
1524  table_grid_.Clear();
1525  it.move_to_first();
1526  // move back final tables to table_grid_
1527  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
1528  ColSegment* seg = it.extract();
1529  table_grid_.InsertBBox(true, true, seg);
1530  }
1531 }
1532 
1533 void TableFinder::GrowTableBox(const TBOX& table_box, TBOX* result_box) {
1534  // TODO(nbeato): The growing code is a bit excessive right now.
1535  // By removing these lines, the partitions considered need
1536  // to have some overlap or be special cases. These lines could
1537  // be added again once a check is put in place to make sure that
1538  // growing tables don't stomp on a lot of non-table partitions.
1539 
1540  // search for horizontal ruling lines within the vertical margin
1541  // int vertical_margin = kRulingVerticalMargin * gridsize();
1542  TBOX search_box = table_box;
1543  // int top = MIN(search_box.top() + vertical_margin, tright().y());
1544  // int bottom = MAX(search_box.bottom() - vertical_margin, bleft().y());
1545  // search_box.set_top(top);
1546  // search_box.set_bottom(bottom);
1547 
1548  GrowTableToIncludePartials(table_box, search_box, result_box);
1549  GrowTableToIncludeLines(table_box, search_box, result_box);
1550  IncludeLeftOutColumnHeaders(result_box);
1551 }
1552 
1553 // Grow a table by increasing the size of the box to include
1554 // partitions with significant overlap with the table.
1556  const TBOX& search_range,
1557  TBOX* result_box) {
1558  // Rulings are in a different grid, so search 2 grids for rulings, text,
1559  // and table partitions that are not entirely within the new box.
1560  for (int i = 0; i < 2; ++i) {
1561  ColPartitionGrid* grid = (i == 0) ? &fragmented_text_grid_ :
1563  ColPartitionGridSearch rectsearch(grid);
1564  rectsearch.StartRectSearch(search_range);
1565  ColPartition* part = NULL;
1566  while ((part = rectsearch.NextRectSearch()) != NULL) {
1567  // Only include text and table types.
1568  if (part->IsImageType())
1569  continue;
1570  const TBOX& part_box = part->bounding_box();
1571  // Include partition in the table if more than half of it
1572  // is covered by the table
1573  if (part_box.overlap_fraction(table_box) > kMinOverlapWithTable) {
1574  *result_box = result_box->bounding_union(part_box);
1575  continue;
1576  }
1577  }
1578  }
1579 }
1580 
1581 // Grow a table by expanding to the extents of significantly
1582 // overlapping lines.
1584  const TBOX& search_range,
1585  TBOX* result_box) {
1587  rsearch.SetUniqueMode(true);
1588  rsearch.StartRectSearch(search_range);
1589  ColPartition* part = NULL;
1590  while ((part = rsearch.NextRectSearch()) != NULL) {
1591  // TODO(nbeato) This should also do vertical, but column
1592  // boundaries are breaking things. This function needs to be
1593  // updated to allow vertical lines as well.
1594  if (!part->IsLineType())
1595  continue;
1596  // Avoid the following function call if the result of the
1597  // function is irrelevant.
1598  const TBOX& part_box = part->bounding_box();
1599  if (result_box->contains(part_box))
1600  continue;
1601  // Include a partially overlapping horizontal line only if the
1602  // extra ColPartitions that will be included due to expansion
1603  // have large side spacing w.r.t. columns containing them.
1604  if (HLineBelongsToTable(*part, table_box))
1605  *result_box = result_box->bounding_union(part_box);
1606  // TODO(nbeato): Vertical
1607  }
1608 }
1609 
1610 // Checks whether the horizontal line belong to the table by looking at the
1611 // side spacing of extra ColParitions that will be included in the table
1612 // due to expansion
1614  const TBOX& table_box) {
1615  if (!part.IsHorizontalLine())
1616  return false;
1617  const TBOX& part_box = part.bounding_box();
1618  if (!part_box.major_x_overlap(table_box))
1619  return false;
1620  // Do not consider top-most horizontal line since it usually
1621  // originates from noise.
1622  // TODO(nbeato): I had to comment this out because the ruling grid doesn't
1623  // have neighbors solved.
1624  // if (!part.nearest_neighbor_above())
1625  // return false;
1626  const TBOX bbox = part_box.bounding_union(table_box);
1627  // In the "unioned table" box (the table extents expanded by the line),
1628  // keep track of how many partitions have significant padding to the left
1629  // and right. If more than half of the partitions covered by the new table
1630  // have significant spacing, the line belongs to the table and the table
1631  // grows to include all of the partitions.
1632  int num_extra_partitions = 0;
1633  int extra_space_to_right = 0;
1634  int extra_space_to_left = 0;
1635  // Rulings are in a different grid, so search 2 grids for rulings, text,
1636  // and table partitions that are introduced by the new box.
1637  for (int i = 0; i < 2; ++i) {
1638  ColPartitionGrid* grid = (i == 0) ? &clean_part_grid_ :
1640  // Start a rect search on bbox
1641  ColPartitionGridSearch rectsearch(grid);
1642  rectsearch.SetUniqueMode(true);
1643  rectsearch.StartRectSearch(bbox);
1644  ColPartition* extra_part = NULL;
1645  while ((extra_part = rectsearch.NextRectSearch()) != NULL) {
1646  // ColPartition already in table
1647  const TBOX& extra_part_box = extra_part->bounding_box();
1648  if (extra_part_box.overlap_fraction(table_box) > kMinOverlapWithTable)
1649  continue;
1650  // Non-text ColPartitions do not contribute
1651  if (extra_part->IsImageType())
1652  continue;
1653  // Consider this partition.
1654  num_extra_partitions++;
1655  // presence of a table cell is a strong hint, so just increment the scores
1656  // without looking at the spacing.
1657  if (extra_part->type() == PT_TABLE || extra_part->IsLineType()) {
1658  extra_space_to_right++;
1659  extra_space_to_left++;
1660  continue;
1661  }
1662  int space_threshold = kSideSpaceMargin * part.median_size();
1663  if (extra_part->space_to_right() > space_threshold)
1664  extra_space_to_right++;
1665  if (extra_part->space_to_left() > space_threshold)
1666  extra_space_to_left++;
1667  }
1668  }
1669  // tprintf("%d %d %d\n",
1670  // num_extra_partitions,extra_space_to_right,extra_space_to_left);
1671  return (extra_space_to_right > num_extra_partitions / 2) ||
1672  (extra_space_to_left > num_extra_partitions / 2);
1673 }
1674 
1675 // Look for isolated column headers above the given table box and
1676 // include them in the table
1678  // Start a search above the current table to look for column headers
1680  vsearch.StartVerticalSearch(table_box->left(), table_box->right(),
1681  table_box->top());
1682  ColPartition* neighbor = NULL;
1683  ColPartition* previous_neighbor = NULL;
1684  while ((neighbor = vsearch.NextVerticalSearch(false)) != NULL) {
1685  // Max distance to find a table heading.
1686  const int max_distance = kMaxColumnHeaderDistance *
1687  neighbor->median_size();
1688  int table_top = table_box->top();
1689  const TBOX& box = neighbor->bounding_box();
1690  // Do not continue if the next box is way above
1691  if (box.bottom() - table_top > max_distance)
1692  break;
1693  // Unconditionally include partitions of type TABLE or LINE
1694  // TODO(faisal): add some reasonable conditions here
1695  if (neighbor->type() == PT_TABLE || neighbor->IsLineType()) {
1696  table_box->set_top(box.top());
1697  previous_neighbor = NULL;
1698  continue;
1699  }
1700  // If there are two text partitions, one above the other, without a table
1701  // cell on their left or right side, consider them a barrier and quit
1702  if (previous_neighbor == NULL) {
1703  previous_neighbor = neighbor;
1704  } else {
1705  const TBOX& previous_box = previous_neighbor->bounding_box();
1706  if (!box.major_y_overlap(previous_box))
1707  break;
1708  }
1709  }
1710 }
1711 
1712 // Remove false alarms consiting of a single column based on their
1713 // projection on the x-axis. Projection of a real table on the x-axis
1714 // should have at least one zero-valley larger than the global median
1715 // x-height of the page.
1717  int page_width = tright().x() - bleft().x();
1718  ASSERT_HOST(page_width > 0);
1719  // create an integer array to hold projection on x-axis
1720  int* table_xprojection = new int[page_width];
1721  // Iterate through all tables in the table grid
1723  table_search(&table_grid_);
1724  table_search.StartFullSearch();
1725  ColSegment* table;
1726  while ((table = table_search.NextFullSearch()) != NULL) {
1727  TBOX table_box = table->bounding_box();
1728  // reset the projection array
1729  for (int i = 0; i < page_width; i++) {
1730  table_xprojection[i] = 0;
1731  }
1732  // Start a rect search on table_box
1734  rectsearch(&clean_part_grid_);
1735  rectsearch.SetUniqueMode(true);
1736  rectsearch.StartRectSearch(table_box);
1737  ColPartition* part;
1738  while ((part = rectsearch.NextRectSearch()) != NULL) {
1739  if (!part->IsTextType())
1740  continue; // Do not consider non-text partitions
1741  if (part->flow() == BTFT_LEADER)
1742  continue; // Assume leaders are in tables
1743  TBOX part_box = part->bounding_box();
1744  // Do not consider partitions partially covered by the table
1745  if (part_box.overlap_fraction(table_box) < kMinOverlapWithTable)
1746  continue;
1747  BLOBNBOX_CLIST* part_boxes = part->boxes();
1748  BLOBNBOX_C_IT pit(part_boxes);
1749 
1750  // Make sure overlapping blobs don't artificially inflate the number
1751  // of rows in the table. This happens frequently with things such as
1752  // decimals and split characters. Do this by assuming the column
1753  // partition is sorted mostly left to right and just clip
1754  // bounding boxes by the previous box's extent.
1755  int next_position_to_write = 0;
1756 
1757  for (pit.mark_cycle_pt(); !pit.cycled_list(); pit.forward()) {
1758  BLOBNBOX *pblob = pit.data();
1759  // ignore blob height for the purpose of projection since we
1760  // are only interested in finding valleys
1761  int xstart = pblob->bounding_box().left();
1762  int xend = pblob->bounding_box().right();
1763 
1764  xstart = MAX(xstart, next_position_to_write);
1765  for (int i = xstart; i < xend; i++)
1766  table_xprojection[i - bleft().x()]++;
1767  next_position_to_write = xend;
1768  }
1769  }
1770  // Find largest valley between two reasonable peaks in the table
1771  if (!GapInXProjection(table_xprojection, page_width)) {
1772  table_search.RemoveBBox();
1773  delete table;
1774  }
1775  }
1776  delete[] table_xprojection;
1777 }
1778 
1779 // Return true if at least one gap larger than the global x-height
1780 // exists in the horizontal projection
1781 bool TableFinder::GapInXProjection(int* xprojection, int length) {
1782  // Find peak value of the histogram
1783  int peak_value = 0;
1784  for (int i = 0; i < length; i++) {
1785  if (xprojection[i] > peak_value) {
1786  peak_value = xprojection[i];
1787  }
1788  }
1789  // Peak value represents the maximum number of horizontally
1790  // overlapping colpartitions, so this can be considered as the
1791  // number of rows in the table
1792  if (peak_value < kMinRowsInTable)
1793  return false;
1794  double projection_threshold = kSmallTableProjectionThreshold * peak_value;
1795  if (peak_value >= kLargeTableRowCount)
1796  projection_threshold = kLargeTableProjectionThreshold * peak_value;
1797  // Threshold the histogram
1798  for (int i = 0; i < length; i++) {
1799  xprojection[i] = (xprojection[i] >= projection_threshold) ? 1 : 0;
1800  }
1801  // Find the largest run of zeros between two ones
1802  int largest_gap = 0;
1803  int run_start = -1;
1804  for (int i = 1; i < length; i++) {
1805  // detect start of a run of zeros
1806  if (xprojection[i - 1] && !xprojection[i]) {
1807  run_start = i;
1808  }
1809  // detect end of a run of zeros and update the value of largest gap
1810  if (run_start != -1 && !xprojection[i - 1] && xprojection[i]) {
1811  int gap = i - run_start;
1812  if (gap > largest_gap)
1813  largest_gap = gap;
1814  run_start = -1;
1815  }
1816  }
1817  return largest_gap > kMaxXProjectionGapFactor * global_median_xheight_;
1818 }
1819 
1820 // Given the location of a table "guess", try to overlay a cellular
1821 // grid in the location, adjusting the boundaries.
1822 // TODO(nbeato): Falsely introduces:
1823 // -headers/footers (not any worse, too much overlap destroys cells)
1824 // -page numbers (not worse, included because maximize margins)
1825 // -equations (nicely fit into a celluar grid, but more sparsely)
1826 // -figures (random text box, also sparse)
1827 // -small left-aligned text areas with overlapping positioned whitespace
1828 // (rejected before)
1829 // Overall, this just needs some more work.
1831  ScrollView* table_win = NULL;
1832  if (textord_show_tables) {
1833  table_win = MakeWindow(0, 0, "Table Structure");
1836  // table_grid_.DisplayBoxes(table_win);
1837  }
1838 
1839 
1840  TableRecognizer recognizer;
1841  recognizer.Init();
1843  recognizer.set_text_grid(&fragmented_text_grid_);
1844  recognizer.set_max_text_height(global_median_xheight_ * 2.0);
1845  recognizer.set_min_height(1.5 * gridheight());
1846  // Loop over all of the tables and try to fit them.
1847  // Store the good tables here.
1848  ColSegment_CLIST good_tables;
1849  ColSegment_C_IT good_it(&good_tables);
1850 
1852  gsearch.StartFullSearch();
1853  ColSegment* found_table = NULL;
1854  while ((found_table = gsearch.NextFullSearch()) != NULL) {
1855  gsearch.RemoveBBox();
1856 
1857  // The goal is to make the tables persistent in a list.
1858  // When that happens, this will move into the search loop.
1859  const TBOX& found_box = found_table->bounding_box();
1860  StructuredTable* table_structure = recognizer.RecognizeTable(found_box);
1861 
1862  // Process a table. Good tables are inserted into the grid again later on
1863  // We can't change boxes in the grid while it is running a search.
1864  if (table_structure != NULL) {
1865  if (textord_show_tables) {
1866  table_structure->Display(table_win, ScrollView::LIME_GREEN);
1867  }
1868  found_table->set_bounding_box(table_structure->bounding_box());
1869  delete table_structure;
1870  good_it.add_after_then_move(found_table);
1871  } else {
1872  delete found_table;
1873  }
1874  }
1875  // TODO(nbeato): MERGE!! There is awesome info now available for merging.
1876 
1877  // At this point, the grid is empty. We can safely insert the good tables
1878  // back into grid.
1879  for (good_it.mark_cycle_pt(); !good_it.cycled_list(); good_it.forward())
1880  table_grid_.InsertBBox(true, true, good_it.extract());
1881 }
1882 
1883 // Displays the column segments in some window.
1885  ColSegment_LIST *segments,
1886  ScrollView::Color color) {
1887 #ifndef GRAPHICS_DISABLED
1888  win->Pen(color);
1889  win->Brush(ScrollView::NONE);
1890  ColSegment_IT it(segments);
1891  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
1892  ColSegment* col = it.data();
1893  const TBOX& box = col->bounding_box();
1894  int left_x = box.left();
1895  int right_x = box.right();
1896  int top_y = box.top();
1897  int bottom_y = box.bottom();
1898  win->Rectangle(left_x, bottom_y, right_x, top_y);
1899  }
1900  win->UpdateWindow();
1901 #endif
1902 }
1903 
1905  ScrollView::Color color) {
1906 #ifndef GRAPHICS_DISABLED
1907  // Iterate the ColPartitions in the grid.
1909  gsearch(grid);
1910  gsearch.StartFullSearch();
1911  ColSegment* seg = NULL;
1912  while ((seg = gsearch.NextFullSearch()) != NULL) {
1913  const TBOX& box = seg->bounding_box();
1914  int left_x = box.left();
1915  int right_x = box.right();
1916  int top_y = box.top();
1917  int bottom_y = box.bottom();
1918  win->Brush(ScrollView::NONE);
1919  win->Pen(color);
1920  win->Rectangle(left_x, bottom_y, right_x, top_y);
1921  }
1922  win->UpdateWindow();
1923 #endif
1924 }
1925 
1926 // Displays the colpartitions using a new coloring on an existing window.
1927 // Note: This method is only for debug purpose during development and
1928 // would not be part of checked in code
1930  ColPartitionGrid* grid,
1931  ScrollView::Color default_color,
1932  ScrollView::Color table_color) {
1933 #ifndef GRAPHICS_DISABLED
1934  ScrollView::Color color = default_color;
1935  // Iterate the ColPartitions in the grid.
1937  gsearch(grid);
1938  gsearch.StartFullSearch();
1939  ColPartition* part = NULL;
1940  while ((part = gsearch.NextFullSearch()) != NULL) {
1941  color = default_color;
1942  if (part->type() == PT_TABLE)
1943  color = table_color;
1944 
1945  const TBOX& box = part->bounding_box();
1946  int left_x = box.left();
1947  int right_x = box.right();
1948  int top_y = box.top();
1949  int bottom_y = box.bottom();
1950  win->Brush(ScrollView::NONE);
1951  win->Pen(color);
1952  win->Rectangle(left_x, bottom_y, right_x, top_y);
1953  }
1954  win->UpdateWindow();
1955 #endif
1956 }
1958  ColPartitionGrid* grid,
1959  ScrollView::Color default_color) {
1960  DisplayColPartitions(win, grid, default_color, ScrollView::YELLOW);
1961 }
1962 
1964  ScrollView* win,
1965  ColPartitionGrid* grid,
1966  ScrollView::Color color) {
1967 #ifndef GRAPHICS_DISABLED
1968  // Iterate the ColPartitions in the grid.
1970  gsearch(grid);
1971  gsearch.StartFullSearch();
1972  ColPartition* part = NULL;
1973  while ((part = gsearch.NextFullSearch()) != NULL) {
1974  const TBOX& box = part->bounding_box();
1975  int left_x = box.left();
1976  int right_x = box.right();
1977  int top_y = box.top();
1978  int bottom_y = box.bottom();
1979 
1980  ColPartition* upper_part = part->nearest_neighbor_above();
1981  if (upper_part) {
1982  TBOX upper_box = upper_part->bounding_box();
1983  int mid_x = (left_x + right_x) / 2;
1984  int mid_y = (top_y + bottom_y) / 2;
1985  int other_x = (upper_box.left() + upper_box.right()) / 2;
1986  int other_y = (upper_box.top() + upper_box.bottom()) / 2;
1987  win->Brush(ScrollView::NONE);
1988  win->Pen(color);
1989  win->Line(mid_x, mid_y, other_x, other_y);
1990  }
1991  ColPartition* lower_part = part->nearest_neighbor_below();
1992  if (lower_part) {
1993  TBOX lower_box = lower_part->bounding_box();
1994  int mid_x = (left_x + right_x) / 2;
1995  int mid_y = (top_y + bottom_y) / 2;
1996  int other_x = (lower_box.left() + lower_box.right()) / 2;
1997  int other_y = (lower_box.top() + lower_box.bottom()) / 2;
1998  win->Brush(ScrollView::NONE);
1999  win->Pen(color);
2000  win->Line(mid_x, mid_y, other_x, other_y);
2001  }
2002  }
2003  win->UpdateWindow();
2004 #endif
2005 }
2006 
2007 
2008 // Write debug image and text file.
2009 // Note: This method is only for debug purpose during development and
2010 // would not be part of checked in code
2011 void TableFinder::WriteToPix(const FCOORD& reskew) {
2012  // Input file must be named test1.tif
2013  PIX* pix = pixRead("test1.tif");
2014  if (!pix) {
2015  tprintf("Input file test1.tif not found.\n");
2016  return;
2017  }
2018  int img_height = pixGetHeight(pix);
2019  int img_width = pixGetWidth(pix);
2020  // Maximum number of text or table partitions
2021  int num_boxes = 10;
2022  BOXA* text_box_array = boxaCreate(num_boxes);
2023  BOXA* table_box_array = boxaCreate(num_boxes);
2025  gsearch(&clean_part_grid_);
2026  gsearch.StartFullSearch();
2027  ColPartition* part;
2028  // load colpartitions into text_box_array and table_box_array
2029  while ((part = gsearch.NextFullSearch()) != NULL) {
2030  TBOX box = part->bounding_box();
2031  box.rotate_large(reskew);
2032  BOX* lept_box = boxCreate(box.left(), img_height - box.top(),
2033  box.right() - box.left(),
2034  box.top() - box.bottom());
2035  if (part->type() == PT_TABLE)
2036  boxaAddBox(table_box_array, lept_box, L_INSERT);
2037  else
2038  boxaAddBox(text_box_array, lept_box, L_INSERT);
2039  }
2040  // draw colpartitions on the output image
2041  PIX* out = pixDrawBoxa(pix, text_box_array, 3, 0xff000000);
2042  out = pixDrawBoxa(out, table_box_array, 3, 0x0000ff00);
2043 
2044  BOXA* table_array = boxaCreate(num_boxes);
2045  // text file containing detected table bounding boxes
2046  FILE* fptr = fopen("tess-table.txt", "wb");
2048  table_search(&table_grid_);
2049  table_search.StartFullSearch();
2050  ColSegment* table;
2051  // load table boxes to table_array and write them to text file as well
2052  while ((table = table_search.NextFullSearch()) != NULL) {
2053  TBOX box = table->bounding_box();
2054  box.rotate_large(reskew);
2055  // Since deskewing introduces negative coordinates, reskewing
2056  // might not completely recover from that since both steps enlarge
2057  // the actual box. Hence a box that undergoes deskewing/reskewing
2058  // may go out of image boundaries. Crop a table box if needed to
2059  // contain it inside the image dimensions.
2060  box = box.intersection(TBOX(0, 0, img_width - 1, img_height - 1));
2061  BOX* lept_box = boxCreate(box.left(), img_height - box.top(),
2062  box.right() - box.left(),
2063  box.top() - box.bottom());
2064  boxaAddBox(table_array, lept_box, L_INSERT);
2065  fprintf(fptr, "%d %d %d %d TABLE\n", box.left(),
2066  img_height - box.top(), box.right(), img_height - box.bottom());
2067  }
2068  fclose(fptr);
2069  // paint table boxes on the debug image
2070  out = pixDrawBoxa(out, table_array, 5, 0x7fff0000);
2071 
2072  pixWrite("out.png", out, IFF_PNG);
2073  // memory cleanup
2074  boxaDestroy(&text_box_array);
2075  boxaDestroy(&table_box_array);
2076  boxaDestroy(&table_array);
2077  pixDestroy(&pix);
2078  pixDestroy(&out);
2079 }
2080 
2081 // Merge all colpartitions in table regions to make them a single
2082 // colpartition and revert types of isolated table cells not
2083 // assigned to any table to their original types.
2085  ColPartitionSet** all_columns,
2086  WidthCallback* width_cb) {
2087  // Since we have table blocks already, remove table tags from all
2088  // colpartitions
2090  gsearch(grid);
2091  gsearch.StartFullSearch();
2092  ColPartition* part = NULL;
2093 
2094  while ((part = gsearch.NextFullSearch()) != NULL) {
2095  if (part->type() == PT_TABLE) {
2096  part->clear_table_type();
2097  }
2098  }
2099  // Now make a single colpartition out of each table block and remove
2100  // all colpartitions contained within a table
2102  table_search(&table_grid_);
2103  table_search.StartFullSearch();
2104  ColSegment* table;
2105  while ((table = table_search.NextFullSearch()) != NULL) {
2106  TBOX table_box = table->bounding_box();
2107  // Start a rect search on table_box
2109  rectsearch(grid);
2110  rectsearch.StartRectSearch(table_box);
2111  ColPartition* part;
2112  ColPartition* table_partition = NULL;
2113  while ((part = rectsearch.NextRectSearch()) != NULL) {
2114  // Do not consider image partitions
2115  if (!part->IsTextType())
2116  continue;
2117  TBOX part_box = part->bounding_box();
2118  // Include partition in the table if more than half of it
2119  // is covered by the table
2120  if (part_box.overlap_fraction(table_box) > kMinOverlapWithTable) {
2121  rectsearch.RemoveBBox();
2122  if (table_partition) {
2123  table_partition->Absorb(part, width_cb);
2124  } else {
2125  table_partition = part;
2126  }
2127  }
2128  }
2129  // Insert table colpartition back to part_grid_
2130  if (table_partition) {
2131  // To match the columns used when transforming to blocks, the new table
2132  // partition must have its first and last column set at the grid y that
2133  // corresponds to its bottom.
2134  const TBOX& table_box = table_partition->bounding_box();
2135  int grid_x, grid_y;
2136  grid->GridCoords(table_box.left(), table_box.bottom(), &grid_x, &grid_y);
2137  table_partition->SetPartitionType(resolution_, all_columns[grid_y]);
2138  table_partition->set_table_type();
2139  table_partition->set_blob_type(BRT_TEXT);
2140  table_partition->set_flow(BTFT_CHAIN);
2141  table_partition->SetBlobTypes();
2142  grid->InsertBBox(true, true, table_partition);
2143  }
2144  }
2145 }
2146 
2150  : ELIST_LINK(),
2151  num_table_cells_(0),
2152  num_text_cells_(0),
2153  type_(COL_UNKNOWN) {
2154 }
2156 }
2157 
2158 // Provides a color for BBGrid to draw the rectangle.
2160  const ScrollView::Color kBoxColors[PT_COUNT] = {
2165  };
2166  return kBoxColors[type_];
2167 }
2168 
2169 // Insert a box into this column segment
2170 void ColSegment::InsertBox(const TBOX& other) {
2171  bounding_box_ = bounding_box_.bounding_union(other);
2172 }
2173 
2174 // Set column segment type based on the ratio of text and table partitions
2175 // in it.
2177  if (num_table_cells_ > kTableColumnThreshold * num_text_cells_)
2178  type_ = COL_TABLE;
2179  else if (num_text_cells_ > num_table_cells_)
2180  type_ = COL_TEXT;
2181  else
2182  type_ = COL_MIXED;
2183 }
2184 
2185 } // namespace tesseract.
void GetColumnBlocks(ColPartitionSet **columns, ColSegment_LIST *col_segments)
Definition: tablefind.cpp:536
void InsertRulingPartition(ColPartition *part)
Definition: tablefind.cpp:431
bool textord_tablefind_recognize_tables
Definition: tablefind.cpp:158
void DisplayColPartitionConnections(ScrollView *win, ColPartitionGrid *grid, ScrollView::Color default_color)
Definition: tablefind.cpp:1963
const double kAllowBlobWidth
Definition: tablefind.cpp:60
void set_global_median_ledding(int ledding)
Definition: tablefind.cpp:775
BBC * NextRectSearch()
Definition: bbgrid.h:845
BLOBNBOX_CLIST * boxes()
Definition: colpartition.h:187
const double kMinOverlapWithTable
Definition: tablefind.cpp:100
void set_space_below(int space)
Definition: colpartition.h:270
bool IsLineType() const
Definition: colpartition.h:419
void Pen(Color color)
Definition: scrollview.cpp:726
void Init(int gridsize, const ICOORD &bleft, const ICOORD &tright)
Definition: bbgrid.h:447
const int kLargeTableRowCount
Definition: tablefind.cpp:112
ColPartitionGrid clean_part_grid_
Definition: tablefind.h:418
#define MAX(x, y)
Definition: ndminx.h:24
void SetVerticalSpacing(ColPartition *part)
Definition: tablefind.cpp:679
const double kMaxBlobOverlapFactor
Definition: tablefind.cpp:80
ColPartition * ShallowCopy() const
const double kAllowTextHeight
Definition: tablefind.cpp:52
int space_to_left() const
Definition: colpartition.h:273
int space_above() const
Definition: colpartition.h:261
int gridheight() const
Definition: bbgrid.h:69
bool textord_dump_table_images
Definition: tablefind.cpp:151
BBC * NextVerticalSearch(bool top_to_bottom)
Definition: bbgrid.h:805
int GridY() const
Definition: bbgrid.h:246
const TBOX & bounding_box() const
Definition: colpartition.h:109
void DeleteObject(T *object)
Definition: tablefind.cpp:165
#define tprintf(...)
Definition: tprintf.h:31
#define MIN(x, y)
Definition: ndminx.h:28
const double kSplitPartitionSize
Definition: tablefind.cpp:47
bool AllowTextPartition(const ColPartition &part) const
Definition: tablefind.cpp:502
int direction(EDGEPT *point)
Definition: vecfuncs.cpp:43
void set_nearest_neighbor_below(ColPartition *part)
Definition: colpartition.h:258
Definition: statistc.h:33
void set_right(int x)
Definition: rect.h:78
bool IsInSameColumnAs(const ColPartition &part) const
void set_space_to_right(int space)
Definition: colpartition.h:282
int median_width() const
Definition: colpartition.h:142
int gridheight() const
Definition: tablefind.cpp:397
void add(inT32 value, inT32 count)
Definition: statistc.cpp:104
void RepositionIterator()
Definition: bbgrid.h:895
#define BOOL_VAR(name, val, comment)
Definition: params.h:280
int gridsize() const
Definition: tablefind.cpp:391
BlobRegionType blob_type() const
Definition: colpartition.h:148
const double kMinMaxGapInTextPartition
Definition: tablefind.cpp:76
void AddBox(BLOBNBOX *box)
ColPartition * nearest_neighbor_above() const
Definition: colpartition.h:249
void GrowTableBox(const TBOX &table_box, TBOX *result_box)
Definition: tablefind.cpp:1533
void Clear()
Definition: bbgrid.h:458
void SetColumnsType(ColSegment_LIST *col_segments)
Definition: tablefind.cpp:1156
const ICOORD & bleft() const
Definition: tablefind.cpp:400
void set_global_median_xheight(int xheight)
Definition: tablefind.cpp:769
ColPartition * SplitAt(int split_x)
int LeftAtY(int y) const
Definition: colpartition.h:340
inT16 right() const
Definition: rect.h:75
bool null_box() const
Definition: rect.h:46
BBC * NextSideSearch(bool right_to_left)
Definition: bbgrid.h:764
int median_size() const
Definition: colpartition.h:136
bool textord_show_tables
Definition: tablefind.cpp:152
void set_left(int x)
Definition: rect.h:71
const double kStrokeWidthFractionalTolerance
Definition: tablefind.cpp:148
#define MIN_INT32
Definition: host.h:128
Definition: capi.h:78
void MoveColSegmentsToGrid(ColSegment_LIST *segments, ColSegmentGrid *col_seg_grid)
Definition: tablefind.cpp:1189
void rotate_large(const FCOORD &vec)
Definition: rect.cpp:72
const double kParagraphEndingPreviousLineRatio
Definition: tablefind.cpp:130
#define ASSERT_HOST(x)
Definition: errcode.h:84
bool textord_tablefind_show_stats
Definition: tablefind.cpp:156
void SplitAndInsertFragmentedTextPartition(ColPartition *part)
Definition: tablefind.cpp:449
void InsertImagePartition(ColPartition *part)
Definition: tablefind.cpp:434
ColSegType type() const
Definition: tablefind.h:94
void StartFullSearch()
Definition: bbgrid.h:668
const ICOORD & tright() const
Definition: tablefind.cpp:403
Definition: capi.h:79
int gridwidth() const
Definition: bbgrid.h:66
void set_max_text_height(int height)
Definition: tablerecog.cpp:732
void DisplayColSegments(ScrollView *win, ColSegment_LIST *cols, ScrollView::Color color)
Definition: tablefind.cpp:1884
void set_space_to_left(int space)
Definition: colpartition.h:276
void set_space_above(int space)
Definition: colpartition.h:264
int median_bottom() const
Definition: colpartition.h:127
void InsertBBox(bool h_spread, bool v_spread, BBC *bbox)
Definition: bbgrid.h:489
bool BelongToOneTable(const TBOX &box1, const TBOX &box2)
Definition: tablefind.cpp:1457
void InsertCleanPartitions(ColPartitionGrid *grid, TO_BLOCK *block)
Definition: tablefind.cpp:203
void DisplayBoxes(ScrollView *window)
Definition: bbgrid.h:616
double median() const
Definition: statistc.cpp:243
bool IsTextType() const
Definition: colpartition.h:427
const double kAllowTextWidth
Definition: tablefind.cpp:53
inT32 area() const
Definition: rect.h:118
const TBOX & bounding_box() const
Definition: tablerecog.cpp:110
void SetPartitionType(int resolution, ColPartitionSet *columns)
const int kRulingVerticalMargin
Definition: tablefind.cpp:96
bool textord_tablefind_show_mark
Definition: tablefind.cpp:154
void WriteToPix(const FCOORD &reskew)
Definition: tablefind.cpp:2011
const double kAllowBlobHeight
Definition: tablefind.cpp:59
bool HasWideOrNoInterWordGap(ColPartition *part) const
Definition: tablefind.cpp:870
static void SetPartitionSpacings(ColPartitionGrid *grid, ColPartitionSet **all_columns)
Definition: tablefind.cpp:599
#define CLISTIZE(CLASSNAME)
Definition: clst.h:958
void SetUniqueMode(bool mode)
Definition: bbgrid.h:254
bool IsImageType() const
Definition: colpartition.h:423
void set_bottom(int y)
Definition: rect.h:64
inT16 y() const
access_function
Definition: points.h:56
inT16 left() const
Definition: rect.h:68
void InsertFragmentedTextPartition(ColPartition *part)
Definition: tablefind.cpp:415
ColPartition * SingletonPartner(bool upper)
bool HLineBelongsToTable(const ColPartition &part, const TBOX &table_box)
Definition: tablefind.cpp:1613
const double kSmallTableProjectionThreshold
Definition: tablefind.cpp:109
ColPartition * ColumnContaining(int x, int y)
bool HasLeaderAdjacent(const ColPartition &part)
Definition: tablefind.cpp:959
ScrollView::Color BoxColor() const
Definition: tablefind.cpp:2159
void set_line_grid(ColPartitionGrid *lines)
Definition: tablerecog.cpp:723
int gridsize() const
Definition: bbgrid.h:63
BlobTextFlowType flow() const
Definition: colpartition.h:154
void set_num_table_cells(int n)
Definition: tablefind.h:81
BlobRegionType region_type() const
Definition: blobbox.h:268
void InitializePartitions(ColPartitionSet **all_columns)
Definition: tablefind.cpp:592
void Brush(Color color)
Definition: scrollview.cpp:732
ScrollView * MakeWindow(int x, int y, const char *window_name)
Definition: tablefind.cpp:531
TBOX bounding_union(const TBOX &box) const
Definition: rect.cpp:129
void InsertBox(const TBOX &other)
Definition: tablefind.cpp:2170
bool IsHorizontalLine() const
Definition: colpartition.h:453
void set_inside_table_column(bool val)
Definition: colpartition.h:246
const double kAllowTextArea
Definition: tablefind.cpp:54
bool AllowBlob(const BLOBNBOX &blob) const
Definition: tablefind.cpp:515
const double kMaxXProjectionGapFactor
Definition: tablefind.cpp:144
int gridwidth() const
Definition: tablefind.cpp:394
const double kMaxGapInTextPartition
Definition: tablefind.cpp:72
void UpdateWindow()
Definition: scrollview.cpp:710
ColPartitionGrid leader_and_ruling_grid_
Definition: tablefind.h:420
const int kMaxColumnHeaderDistance
Definition: tablefind.cpp:88
void SetGlobalSpacings(ColPartitionGrid *grid)
Definition: tablefind.cpp:722
ColSegmentGrid col_seg_grid_
Definition: tablefind.h:426
const ICOORD & bleft() const
Definition: bbgrid.h:72
void GetTableRegions(ColSegment_LIST *table_columns, ColSegment_LIST *table_regions)
Definition: tablefind.cpp:1336
bool MatchingSizes(const ColPartition &other) const
void InsertLeaderPartition(ColPartition *part)
Definition: tablefind.cpp:423
int space_to_right() const
Definition: colpartition.h:279
#define MAX_INT32
Definition: host.h:120
const int kMinRowsInTable
Definition: tablefind.cpp:115
void set_min_height(int height)
Definition: tablerecog.cpp:726
void set_blob_type(BlobRegionType t)
Definition: colpartition.h:151
integer coordinate
Definition: points.h:30
bool major_x_overlap(const TBOX &box) const
Definition: rect.h:402
inT16 bottom() const
Definition: rect.h:61
int RightAtY(int y) const
Definition: colpartition.h:344
ColPartition * CopyButDontOwnBlobs()
PolyBlockType type() const
Definition: colpartition.h:181
ELISTIZE(AmbigSpec)
inT16 height() const
Definition: rect.h:104
void StartSideSearch(int x, int ymin, int ymax)
Definition: bbgrid.h:749
void GroupColumnBlocks(ColSegment_LIST *current_segments, ColSegment_LIST *col_segments)
Definition: tablefind.cpp:551
const double kRequiredFullJustifiedSpacing
Definition: tablefind.cpp:120
void set_text_grid(ColPartitionGrid *text)
Definition: tablerecog.cpp:720
void Absorb(ColPartition *other, WidthCallback *cb)
void StartRectSearch(const TBOX &rect)
Definition: bbgrid.h:833
double overlap_fraction(const TBOX &box) const
Definition: rect.h:378
TBOX intersection(const TBOX &box) const
Definition: rect.cpp:87
inT16 width() const
Definition: rect.h:111
BBC * NextFullSearch()
Definition: bbgrid.h:678
void set_flow(BlobTextFlowType f)
Definition: colpartition.h:157
ScrollView * MakeWindow(int x, int y, const char *window_name)
Definition: bbgrid.h:592
void GrowTableToIncludeLines(const TBOX &table_box, const TBOX &search_range, TBOX *result_box)
Definition: tablefind.cpp:1583
bool MatchingStrokeWidth(const ColPartition &other, double fractional_tolerance, double constant_tolerance) const
bool major_y_overlap(const TBOX &box) const
Definition: rect.h:429
const double kTableColumnThreshold
Definition: tablefind.cpp:92
void GrowTableToIncludePartials(const TBOX &table_box, const TBOX &search_range, TBOX *result_box)
Definition: tablefind.cpp:1555
void LocateTables(ColPartitionGrid *grid, ColPartitionSet **columns, WidthCallback *width_cb, const FCOORD &reskew)
Definition: tablefind.cpp:269
inT16 x() const
access function
Definition: points.h:52
void set_global_median_blob_width(int width)
Definition: tablefind.cpp:772
StructuredTable * RecognizeTable(const TBOX &guess_box)
Definition: tablerecog.cpp:736
const int kMaxVerticalSpacing
Definition: tablefind.cpp:41
Definition: rect.h:30
void DisplayColSegmentGrid(ScrollView *win, ColSegmentGrid *grid, ScrollView::Color color)
Definition: tablefind.cpp:1904
void GetColumnBoxes(int y_bottom, int y_top, ColSegment_LIST *segments)
const double kAllowBlobArea
Definition: tablefind.cpp:61
void Rectangle(int x1, int y1, int x2, int y2)
Definition: scrollview.cpp:606
ColSegmentGrid table_grid_
Definition: tablefind.h:428
void MakeTableBlocks(ColPartitionGrid *grid, ColPartitionSet **columns, WidthCallback *width_cb)
Definition: tablefind.cpp:2084
bool contains(const FCOORD pt) const
Definition: rect.h:323
const ICOORD & tright() const
Definition: bbgrid.h:75
const TBOX & bounding_box() const
Definition: tablefind.h:52
void DisplayColPartitions(ScrollView *win, ColPartitionGrid *grid, ScrollView::Color text_color, ScrollView::Color table_color)
Definition: tablefind.cpp:1929
void set_left_to_right_language(bool order)
Definition: tablefind.cpp:187
const int kMaxBoxesInDataPartition
Definition: tablefind.cpp:69
const double kMinParagraphEndingTextToWhitespaceRatio
Definition: tablefind.cpp:140
const int kAdjacentLeaderSearchPadding
Definition: tablefind.cpp:125
#define NULL
Definition: host.h:144
ColPartition * nearest_neighbor_below() const
Definition: colpartition.h:255
void ClearGridData(void(*free_method)(BBC *))
Definition: bbgrid.h:467
const TBOX & bounding_box() const
Definition: blobbox.h:215
void set_top(int y)
Definition: rect.h:57
void Init(int grid_size, const ICOORD &bottom_left, const ICOORD &top_right)
Definition: tablefind.cpp:191
void set_num_text_cells(int n)
Definition: tablefind.h:90
bool GapInXProjection(int *xprojection, int length)
Definition: tablefind.cpp:1781
int boxes_count() const
Definition: colpartition.h:190
void plot(ScrollView *window, float xorigin, float yorigin, float xscale, float yscale, ScrollView::Color colour) const
Definition: statistc.cpp:589
inT16 top() const
Definition: rect.h:54
const int kMaxBlobWidth
Definition: tablefind.cpp:43
bool ConsecutiveBoxes(const TBOX &b1, const TBOX &b2)
Definition: tablefind.cpp:581
BlobTextFlowType flow() const
Definition: blobbox.h:280
void set_nearest_neighbor_above(ColPartition *part)
Definition: colpartition.h:252
void Line(int x1, int y1, int x2, int y2)
Definition: scrollview.cpp:538
ColPartitionGrid fragmented_text_grid_
Definition: tablefind.h:424
void Display(ScrollView *window, ScrollView::Color color)
Definition: tablerecog.cpp:290
void set_bounding_box(const TBOX &other)
Definition: tablefind.h:72
void InsertTextPartition(ColPartition *part)
Definition: tablefind.cpp:407
const double kMaxParagraphEndingLeftSpaceMultiple
Definition: tablefind.cpp:134
Definition: points.h:189
void IncludeLeftOutColumnHeaders(TBOX *table_box)
Definition: tablefind.cpp:1677
bool overlap(const TBOX &box) const
Definition: rect.h:345
bool VSignificantCoreOverlap(const ColPartition &other) const
Definition: colpartition.h:387
void GetTableColumns(ColSegment_LIST *table_columns)
Definition: tablefind.cpp:1286
const double kLargeTableProjectionThreshold
Definition: tablefind.cpp:110
const int kSideSpaceMargin
Definition: tablefind.cpp:105
void GridCoords(int x, int y, int *grid_x, int *grid_y) const
Definition: bbgrid.cpp:54
const double kStrokeWidthConstantTolerance
const double kMaxTableCellXheight
Definition: tablefind.cpp:84
void MarkPartitionsUsingLocalInformation()
Definition: tablefind.cpp:840
void RefinePartitionPartners(bool get_desperate)
void StartVerticalSearch(int xmin, int xmax, int y)
Definition: bbgrid.h:791
const int kMinBoxesInTextPartition
Definition: tablefind.cpp:66
int space_below() const
Definition: colpartition.h:267