Coverage Report

Created: 2026-09-14 06:59

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