Coverage Report

Created: 2025-06-13 07:15

/src/tesseract/src/textord/colpartitiongrid.cpp
Line
Count
Source (jump to first uncovered line)
1
///////////////////////////////////////////////////////////////////////
2
// File:        colpartitiongrid.cpp
3
// Description: Class collecting code that acts on a BBGrid of ColPartitions.
4
// Author:      Ray Smith
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 "colpartitiongrid.h"
24
#include "colpartitionset.h"
25
#include "imagefind.h"
26
27
#include <algorithm>
28
#include <utility>
29
30
namespace tesseract {
31
32
// Max pad factor used to search the neighbourhood of a partition to smooth
33
// partition types.
34
const int kMaxPadFactor = 6;
35
// Max multiple of size (min(height, width)) for the distance of the nearest
36
// neighbour for the change of type to be used.
37
const int kMaxNeighbourDistFactor = 4;
38
// Maximum number of lines in a credible figure caption.
39
const int kMaxCaptionLines = 7;
40
// Min ratio between biggest and smallest gap to bound a caption.
41
const double kMinCaptionGapRatio = 2.0;
42
// Min ratio between biggest gap and mean line height to bound a caption.
43
const double kMinCaptionGapHeightRatio = 0.5;
44
// Min fraction of ColPartition height to be overlapping for margin purposes.
45
const double kMarginOverlapFraction = 0.25;
46
// Size ratio required to consider an unmerged overlapping partition to be big.
47
const double kBigPartSizeRatio = 1.75;
48
// Fraction of gridsize to allow arbitrary overlap between partitions.
49
const double kTinyEnoughTextlineOverlapFraction = 0.25;
50
// Max vertical distance of neighbouring ColPartition as a multiple of
51
// partition height for it to be a partner.
52
// TODO(rays) fix the problem that causes a larger number to not work well.
53
// The value needs to be larger as sparse text blocks in a page that gets
54
// marked as single column will not find adjacent lines as partners, and
55
// will merge horizontally distant, but aligned lines. See rep.4B3 p5.
56
// The value needs to be small because double-spaced legal docs written
57
// in a single column, but justified courier have widely spaced lines
58
// that need to get merged before they partner-up with the lines above
59
// and below. See legal.3B5 p13/17. Neither of these should depend on
60
// the value of kMaxPartitionSpacing to be successful, and ColPartition
61
// merging needs attention to fix this problem.
62
const double kMaxPartitionSpacing = 1.75;
63
// Margin by which text has to beat image or vice-versa to make a firm
64
// decision in GridSmoothNeighbour.
65
const int kSmoothDecisionMargin = 4;
66
67
ColPartitionGrid::ColPartitionGrid(int gridsize, const ICOORD &bleft,
68
                                   const ICOORD &tright)
69
0
    : BBGrid<ColPartition, ColPartition_CLIST, ColPartition_C_IT>(
70
0
          gridsize, bleft, tright) {}
71
72
// Handles a click event in a display window.
73
0
void ColPartitionGrid::HandleClick(int x, int y) {
74
0
  BBGrid<ColPartition, ColPartition_CLIST, ColPartition_C_IT>::HandleClick(x,
75
0
                                                                           y);
76
  // Run a radial search for partitions that overlap.
77
0
  ColPartitionGridSearch radsearch(this);
78
0
  radsearch.SetUniqueMode(true);
79
0
  radsearch.StartRadSearch(x, y, 1);
80
0
  ColPartition *neighbour;
81
0
  FCOORD click(x, y);
82
0
  while ((neighbour = radsearch.NextRadSearch()) != nullptr) {
83
0
    const TBOX &nbox = neighbour->bounding_box();
84
0
    if (nbox.contains(click)) {
85
0
      tprintf("Block box:");
86
0
      neighbour->bounding_box().print();
87
0
      neighbour->Print();
88
0
    }
89
0
  }
90
0
}
91
92
// Merges ColPartitions in the grid that look like they belong in the same
93
// textline.
94
// For all partitions in the grid, calls the box_cb permanent callback
95
// to compute the search box, searches the box, and if a candidate is found,
96
// calls the confirm_cb to check any more rules. If the confirm_cb returns
97
// true, then the partitions are merged.
98
// Both callbacks are deleted before returning.
99
void ColPartitionGrid::Merges(
100
    const std::function<bool(ColPartition *, TBOX *)> &box_cb,
101
    const std::function<bool(const ColPartition *, const ColPartition *)>
102
0
        &confirm_cb) {
103
  // Iterate the ColPartitions in the grid.
104
0
  ColPartitionGridSearch gsearch(this);
105
0
  gsearch.StartFullSearch();
106
0
  ColPartition *part;
107
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
108
0
    if (MergePart(box_cb, confirm_cb, part)) {
109
0
      gsearch.RepositionIterator();
110
0
    }
111
0
  }
112
0
}
113
114
// For the given partition, calls the box_cb permanent callback
115
// to compute the search box, searches the box, and if a candidate is found,
116
// calls the confirm_cb to check any more rules. If the confirm_cb returns
117
// true, then the partitions are merged.
118
// Returns true if the partition is consumed by one or more merges.
119
bool ColPartitionGrid::MergePart(
120
    const std::function<bool(ColPartition *, TBOX *)> &box_cb,
121
    const std::function<bool(const ColPartition *, const ColPartition *)>
122
        &confirm_cb,
123
0
    ColPartition *part) {
124
0
  if (part->IsUnMergeableType()) {
125
0
    return false;
126
0
  }
127
0
  bool any_done = false;
128
  // Repeatedly merge part while we find a best merge candidate that works.
129
0
  bool merge_done = false;
130
0
  do {
131
0
    merge_done = false;
132
0
    TBOX box = part->bounding_box();
133
0
    bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());
134
0
    if (debug) {
135
0
      tprintf("Merge candidate:");
136
0
      box.print();
137
0
    }
138
    // Set up a rectangle search bounded by the part.
139
0
    if (!box_cb(part, &box)) {
140
0
      continue;
141
0
    }
142
    // Create a list of merge candidates.
143
0
    ColPartition_CLIST merge_candidates;
144
0
    FindMergeCandidates(part, box, debug, &merge_candidates);
145
    // Find the best merge candidate based on minimal overlap increase.
146
0
    int overlap_increase;
147
0
    ColPartition *neighbour = BestMergeCandidate(part, &merge_candidates, debug,
148
0
                                                 confirm_cb, &overlap_increase);
149
0
    if (neighbour != nullptr && overlap_increase <= 0) {
150
0
      if (debug) {
151
0
        tprintf("Merging:hoverlap=%d, voverlap=%d, OLI=%d\n",
152
0
                part->HCoreOverlap(*neighbour), part->VCoreOverlap(*neighbour),
153
0
                overlap_increase);
154
0
      }
155
      // Looks like a good candidate so merge it.
156
0
      RemoveBBox(neighbour);
157
      // We will modify the box of part, so remove it from the grid, merge
158
      // it and then re-insert it into the grid.
159
0
      RemoveBBox(part);
160
0
      part->Absorb(neighbour, nullptr);
161
0
      InsertBBox(true, true, part);
162
0
      merge_done = true;
163
0
      any_done = true;
164
0
    } else if (neighbour != nullptr) {
165
0
      if (debug) {
166
0
        tprintf("Overlapped when merged with increase %d: ", overlap_increase);
167
0
        neighbour->bounding_box().print();
168
0
      }
169
0
    } else if (debug) {
170
0
      tprintf("No candidate neighbour returned\n");
171
0
    }
172
0
  } while (merge_done);
173
0
  return any_done;
174
0
}
175
176
// Returns true if the given part and merge candidate might believably
177
// be part of a single text line according to the default rules.
178
// In general we only want to merge partitions that look like they
179
// are on the same text line, ie their median limits overlap, but we have
180
// to make exceptions for diacritics and stray punctuation.
181
static bool OKMergeCandidate(const ColPartition *part,
182
0
                             const ColPartition *candidate, bool debug) {
183
0
  const TBOX &part_box = part->bounding_box();
184
0
  if (candidate == part) {
185
0
    return false; // Ignore itself.
186
0
  }
187
0
  if (!part->TypesMatch(*candidate) || candidate->IsUnMergeableType()) {
188
0
    return false; // Don't mix inappropriate types.
189
0
  }
190
191
0
  const TBOX &c_box = candidate->bounding_box();
192
0
  if (debug) {
193
0
    tprintf("Examining merge candidate:");
194
0
    c_box.print();
195
0
  }
196
  // Candidates must be within a reasonable distance.
197
0
  if (candidate->IsVerticalType() || part->IsVerticalType()) {
198
0
    int h_dist = -part->HCoreOverlap(*candidate);
199
0
    if (h_dist >= std::max(part_box.width(), c_box.width()) / 2) {
200
0
      if (debug) {
201
0
        tprintf("Too far away: h_dist = %d\n", h_dist);
202
0
      }
203
0
      return false;
204
0
    }
205
0
  } else {
206
    // Coarse filter by vertical distance between partitions.
207
0
    int v_dist = -part->VCoreOverlap(*candidate);
208
0
    if (v_dist >= std::max(part_box.height(), c_box.height()) / 2) {
209
0
      if (debug) {
210
0
        tprintf("Too far away: v_dist = %d\n", v_dist);
211
0
      }
212
0
      return false;
213
0
    }
214
    // Candidates must either overlap in median y,
215
    // or part or candidate must be an acceptable diacritic.
216
0
    if (!part->VSignificantCoreOverlap(*candidate) &&
217
0
        !part->OKDiacriticMerge(*candidate, debug) &&
218
0
        !candidate->OKDiacriticMerge(*part, debug)) {
219
0
      if (debug) {
220
0
        tprintf("Candidate fails overlap and diacritic tests!\n");
221
0
      }
222
0
      return false;
223
0
    }
224
0
  }
225
0
  return true;
226
0
}
227
228
// Helper function to compute the increase in overlap of the parts list of
229
// Colpartitions with the combination of merge1 and merge2, compared to
230
// the overlap with them uncombined.
231
// An overlap is not counted if passes the OKMergeOverlap test with ok_overlap
232
// as the pixel overlap limit. merge1 and merge2 must both be non-nullptr.
233
static int IncreaseInOverlap(const ColPartition *merge1,
234
                             const ColPartition *merge2, int ok_overlap,
235
0
                             ColPartition_CLIST *parts) {
236
0
  ASSERT_HOST(merge1 != nullptr && merge2 != nullptr);
237
0
  int total_area = 0;
238
0
  ColPartition_C_IT it(parts);
239
0
  TBOX merged_box(merge1->bounding_box());
240
0
  merged_box += merge2->bounding_box();
241
0
  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
242
0
    ColPartition *part = it.data();
243
0
    if (part == merge1 || part == merge2) {
244
0
      continue;
245
0
    }
246
0
    TBOX part_box = part->bounding_box();
247
    // Compute the overlap of the merged box with part.
248
0
    int overlap_area = part_box.intersection(merged_box).area();
249
0
    if (overlap_area > 0 &&
250
0
        !part->OKMergeOverlap(*merge1, *merge2, ok_overlap, false)) {
251
0
      total_area += overlap_area;
252
      // Subtract the overlap of merge1 and merge2 individually.
253
0
      overlap_area = part_box.intersection(merge1->bounding_box()).area();
254
0
      if (overlap_area > 0) {
255
0
        total_area -= overlap_area;
256
0
      }
257
0
      TBOX intersection_box = part_box.intersection(merge2->bounding_box());
258
0
      overlap_area = intersection_box.area();
259
0
      if (overlap_area > 0) {
260
0
        total_area -= overlap_area;
261
        // Add back the 3-way area.
262
0
        intersection_box &= merge1->bounding_box(); // In-place intersection.
263
0
        overlap_area = intersection_box.area();
264
0
        if (overlap_area > 0) {
265
0
          total_area += overlap_area;
266
0
        }
267
0
      }
268
0
    }
269
0
  }
270
0
  return total_area;
271
0
}
272
273
// Helper function to test that each partition in candidates is either a
274
// good diacritic merge with part or an OK merge candidate with all others
275
// in the candidates list.
276
// ASCII Art Scenario:
277
// We sometimes get text such as "join-this" where the - is actually a long
278
// dash culled from a standard set of extra characters that don't match the
279
// font of the text. This makes its strokewidth not match and forms a broken
280
// set of 3 partitions for "join", "-" and "this" and the dash may slightly
281
// overlap BOTH words.
282
// -------  -------
283
// |     ====     |
284
// -------  -------
285
// The standard merge rule: "you can merge 2 partitions as long as there is
286
// no increase in overlap elsewhere" fails miserably here. Merge any pair
287
// of partitions and the combined box overlaps more with the third than
288
// before. To allow the merge, we need to consider whether it is safe to
289
// merge everything, without merging separate text lines. For that we need
290
// everything to be an OKMergeCandidate (which is supposed to prevent
291
// separate text lines merging), but this is hard for diacritics to satisfy,
292
// so an alternative to being OKMergeCandidate with everything is to be an
293
// OKDiacriticMerge with part as the base character.
294
static bool TestCompatibleCandidates(const ColPartition &part, bool debug,
295
0
                                     ColPartition_CLIST *candidates) {
296
0
  ColPartition_C_IT it(candidates);
297
0
  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
298
0
    ColPartition *candidate = it.data();
299
0
    if (!candidate->OKDiacriticMerge(part, false)) {
300
0
      ColPartition_C_IT it2(it);
301
0
      for (it2.mark_cycle_pt(); !it2.cycled_list(); it2.forward()) {
302
0
        ColPartition *candidate2 = it2.data();
303
0
        if (candidate2 != candidate &&
304
0
            !OKMergeCandidate(candidate, candidate2, false)) {
305
0
          if (debug) {
306
0
            tprintf("NC overlap failed:Candidate:");
307
0
            candidate2->bounding_box().print();
308
0
            tprintf("fails to be a good merge with:");
309
0
            candidate->bounding_box().print();
310
0
          }
311
0
          return false;
312
0
        }
313
0
      }
314
0
    }
315
0
  }
316
0
  return true;
317
0
}
318
319
// Computes and returns the total overlap of all partitions in the grid.
320
// If overlap_grid is non-null, it is filled with a grid that holds empty
321
// partitions representing the union of all overlapped partitions.
322
0
int ColPartitionGrid::ComputeTotalOverlap(ColPartitionGrid **overlap_grid) {
323
0
  int total_overlap = 0;
324
  // Iterate the ColPartitions in the grid.
325
0
  ColPartitionGridSearch gsearch(this);
326
0
  gsearch.StartFullSearch();
327
0
  ColPartition *part;
328
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
329
0
    ColPartition_CLIST neighbors;
330
0
    const TBOX &part_box = part->bounding_box();
331
0
    FindOverlappingPartitions(part_box, part, &neighbors);
332
0
    ColPartition_C_IT n_it(&neighbors);
333
0
    bool any_part_overlap = false;
334
0
    for (n_it.mark_cycle_pt(); !n_it.cycled_list(); n_it.forward()) {
335
0
      const TBOX &n_box = n_it.data()->bounding_box();
336
0
      int overlap = n_box.intersection(part_box).area();
337
0
      if (overlap > 0 && overlap_grid != nullptr) {
338
0
        if (*overlap_grid == nullptr) {
339
0
          *overlap_grid = new ColPartitionGrid(gridsize(), bleft(), tright());
340
0
        }
341
0
        (*overlap_grid)->InsertBBox(true, true, n_it.data()->ShallowCopy());
342
0
        if (!any_part_overlap) {
343
0
          (*overlap_grid)->InsertBBox(true, true, part->ShallowCopy());
344
0
        }
345
0
      }
346
0
      any_part_overlap = true;
347
0
      total_overlap += overlap;
348
0
    }
349
0
  }
350
0
  return total_overlap;
351
0
}
352
353
// Finds all the ColPartitions in the grid that overlap with the given
354
// box and returns them SortByBoxLeft(ed) and uniqued in the given list.
355
// Any partition equal to not_this (may be nullptr) is excluded.
356
void ColPartitionGrid::FindOverlappingPartitions(const TBOX &box,
357
                                                 const ColPartition *not_this,
358
0
                                                 ColPartition_CLIST *parts) {
359
0
  ColPartitionGridSearch rsearch(this);
360
0
  rsearch.StartRectSearch(box);
361
0
  ColPartition *part;
362
0
  while ((part = rsearch.NextRectSearch()) != nullptr) {
363
0
    if (part != not_this) {
364
0
      parts->add_sorted(SortByBoxLeft<ColPartition>, true, part);
365
0
    }
366
0
  }
367
0
}
368
369
// Finds and returns the best candidate ColPartition to merge with part,
370
// selected from the candidates list, based on the minimum increase in
371
// pairwise overlap among all the partitions overlapped by the combined box.
372
// If overlap_increase is not nullptr then it returns the increase in overlap
373
// that would result from the merge.
374
// confirm_cb is a permanent callback that (if non-null) will be used to
375
// confirm the validity of a proposed merge candidate before selecting it.
376
//
377
// ======HOW MERGING WORKS======
378
// The problem:
379
// We want to merge all the parts of a textline together, but avoid merging
380
// separate textlines. Diacritics, i dots, punctuation, and broken characters
381
// are examples of small bits that need merging with the main textline.
382
// Drop-caps and descenders in one line that touch ascenders in the one below
383
// are examples of cases where we don't want to merge.
384
//
385
// The solution:
386
// Merges that increase overlap among other partitions are generally bad.
387
// Those that don't increase overlap (much) and minimize the total area
388
// seem to be good.
389
//
390
// Ascii art example:
391
// The text:
392
// groggy descenders
393
// minimum ascenders
394
// The boxes: The === represents a small box near or overlapping the lower box.
395
// -----------------
396
// |               |
397
// -----------------
398
// -===-------------
399
// |               |
400
// -----------------
401
// In considering what to do with the small === box, we find the 2 larger
402
// boxes as neighbours and possible merge candidates, but merging with the
403
// upper box increases overlap with the lower box, whereas merging with the
404
// lower box does not increase overlap.
405
// If the small === box didn't overlap either to start with, total area
406
// would be minimized by merging with the nearer (lower) box.
407
//
408
// This is a simple example. In reality, we have to allow some increase
409
// in overlap, or tightly spaced text would end up in bits.
410
ColPartition *ColPartitionGrid::BestMergeCandidate(
411
    const ColPartition *part, ColPartition_CLIST *candidates, bool debug,
412
    const std::function<bool(const ColPartition *, const ColPartition *)>
413
        &confirm_cb,
414
0
    int *overlap_increase) {
415
0
  if (overlap_increase != nullptr) {
416
0
    *overlap_increase = 0;
417
0
  }
418
0
  if (candidates->empty()) {
419
0
    return nullptr;
420
0
  }
421
0
  int ok_overlap =
422
0
      static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);
423
  // The best neighbour to merge with is the one that causes least
424
  // total pairwise overlap among all the neighbours.
425
  // If more than one offers the same total overlap, choose the one
426
  // with the least total area.
427
0
  const TBOX &part_box = part->bounding_box();
428
0
  ColPartition_C_IT it(candidates);
429
0
  ColPartition *best_candidate = nullptr;
430
  // Find the total combined box of all candidates and the original.
431
0
  TBOX full_box(part_box);
432
0
  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
433
0
    ColPartition *candidate = it.data();
434
0
    full_box += candidate->bounding_box();
435
0
  }
436
  // Keep valid neighbours in a list.
437
0
  ColPartition_CLIST neighbours;
438
  // Now run a rect search of the merged box for overlapping neighbours, as
439
  // we need anything that might be overlapped by the merged box.
440
0
  FindOverlappingPartitions(full_box, part, &neighbours);
441
0
  if (debug) {
442
0
    tprintf("Finding best merge candidate from %d, %d neighbours for box:",
443
0
            candidates->length(), neighbours.length());
444
0
    part_box.print();
445
0
  }
446
  // If the best increase in overlap is positive, then we also check the
447
  // worst non-candidate overlap. This catches the case of multiple good
448
  // candidates that overlap each other when merged. If the worst
449
  // non-candidate overlap is better than the best overlap, then return
450
  // the worst non-candidate overlap instead.
451
0
  ColPartition_CLIST non_candidate_neighbours;
452
0
  non_candidate_neighbours.set_subtract(SortByBoxLeft<ColPartition>, true,
453
0
                                        &neighbours, candidates);
454
0
  int worst_nc_increase = 0;
455
0
  int best_increase = INT32_MAX;
456
0
  int best_area = 0;
457
0
  for (it.mark_cycle_pt(); !it.cycled_list(); it.forward()) {
458
0
    ColPartition *candidate = it.data();
459
0
    if (confirm_cb != nullptr && !confirm_cb(part, candidate)) {
460
0
      if (debug) {
461
0
        tprintf("Candidate not confirmed:");
462
0
        candidate->bounding_box().print();
463
0
      }
464
0
      continue;
465
0
    }
466
0
    int increase = IncreaseInOverlap(part, candidate, ok_overlap, &neighbours);
467
0
    const TBOX &cand_box = candidate->bounding_box();
468
0
    if (best_candidate == nullptr || increase < best_increase) {
469
0
      best_candidate = candidate;
470
0
      best_increase = increase;
471
0
      best_area = cand_box.bounding_union(part_box).area() - cand_box.area();
472
0
      if (debug) {
473
0
        tprintf("New best merge candidate has increase %d, area %d, over box:",
474
0
                increase, best_area);
475
0
        full_box.print();
476
0
        candidate->Print();
477
0
      }
478
0
    } else if (increase == best_increase) {
479
0
      int area = cand_box.bounding_union(part_box).area() - cand_box.area();
480
0
      if (area < best_area) {
481
0
        best_area = area;
482
0
        best_candidate = candidate;
483
0
      }
484
0
    }
485
0
    increase = IncreaseInOverlap(part, candidate, ok_overlap,
486
0
                                 &non_candidate_neighbours);
487
0
    if (increase > worst_nc_increase) {
488
0
      worst_nc_increase = increase;
489
0
    }
490
0
  }
491
0
  if (best_increase > 0) {
492
    // If the worst non-candidate increase is less than the best increase
493
    // including the candidates, then all the candidates can merge together
494
    // and the increase in outside overlap would be less, so use that result,
495
    // but only if each candidate is either a good diacritic merge with part,
496
    // or an ok merge candidate with all the others.
497
    // See TestCompatibleCandidates for more explanation and a picture.
498
0
    if (worst_nc_increase < best_increase &&
499
0
        TestCompatibleCandidates(*part, debug, candidates)) {
500
0
      best_increase = worst_nc_increase;
501
0
    }
502
0
  }
503
0
  if (overlap_increase != nullptr) {
504
0
    *overlap_increase = best_increase;
505
0
  }
506
0
  return best_candidate;
507
0
}
508
509
// Helper to remove the given box from the given partition, put it in its
510
// own partition, and add to the partition list.
511
static void RemoveBadBox(BLOBNBOX *box, ColPartition *part,
512
0
                         ColPartition_LIST *part_list) {
513
0
  part->RemoveBox(box);
514
0
  ColPartition::MakeBigPartition(box, part_list);
515
0
}
516
517
// Split partitions where it reduces overlap between their bounding boxes.
518
// ColPartitions are after all supposed to be a partitioning of the blobs
519
// AND of the space on the page!
520
// Blobs that cause overlaps get removed, put in individual partitions
521
// and added to the big_parts list. They are most likely characters on
522
// 2 textlines that touch, or something big like a dropcap.
523
void ColPartitionGrid::SplitOverlappingPartitions(
524
0
    ColPartition_LIST *big_parts) {
525
0
  int ok_overlap =
526
0
      static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);
527
  // Iterate the ColPartitions in the grid.
528
0
  ColPartitionGridSearch gsearch(this);
529
0
  gsearch.StartFullSearch();
530
0
  ColPartition *part;
531
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
532
    // Set up a rectangle search bounded by the part.
533
0
    const TBOX &box = part->bounding_box();
534
0
    ColPartitionGridSearch rsearch(this);
535
0
    rsearch.SetUniqueMode(true);
536
0
    rsearch.StartRectSearch(box);
537
0
    int unresolved_overlaps = 0;
538
539
0
    ColPartition *neighbour;
540
0
    while ((neighbour = rsearch.NextRectSearch()) != nullptr) {
541
0
      if (neighbour == part) {
542
0
        continue;
543
0
      }
544
0
      const TBOX &neighbour_box = neighbour->bounding_box();
545
0
      if (neighbour->OKMergeOverlap(*part, *part, ok_overlap, false) &&
546
0
          part->OKMergeOverlap(*neighbour, *neighbour, ok_overlap, false)) {
547
0
        continue; // The overlap is OK both ways.
548
0
      }
549
550
      // If removal of the biggest box from either partition eliminates the
551
      // overlap, and it is much bigger than the box left behind, then
552
      // it is either a drop-cap, an inter-line join, or some junk that
553
      // we don't want anyway, so put it in the big_parts list.
554
0
      if (!part->IsSingleton()) {
555
0
        BLOBNBOX *excluded = part->BiggestBox();
556
0
        TBOX shrunken = part->BoundsWithoutBox(excluded);
557
0
        if (!shrunken.overlap(neighbour_box) &&
558
0
            excluded->bounding_box().height() >
559
0
                kBigPartSizeRatio * shrunken.height()) {
560
          // Removing the biggest box fixes the overlap, so do it!
561
0
          gsearch.RemoveBBox();
562
0
          RemoveBadBox(excluded, part, big_parts);
563
0
          InsertBBox(true, true, part);
564
0
          gsearch.RepositionIterator();
565
0
          break;
566
0
        }
567
0
      } else if (box.contains(neighbour_box)) {
568
0
        ++unresolved_overlaps;
569
0
        continue; // No amount of splitting will fix it.
570
0
      }
571
0
      if (!neighbour->IsSingleton()) {
572
0
        BLOBNBOX *excluded = neighbour->BiggestBox();
573
0
        TBOX shrunken = neighbour->BoundsWithoutBox(excluded);
574
0
        if (!shrunken.overlap(box) &&
575
0
            excluded->bounding_box().height() >
576
0
                kBigPartSizeRatio * shrunken.height()) {
577
          // Removing the biggest box fixes the overlap, so do it!
578
0
          rsearch.RemoveBBox();
579
0
          RemoveBadBox(excluded, neighbour, big_parts);
580
0
          InsertBBox(true, true, neighbour);
581
0
          gsearch.RepositionIterator();
582
0
          break;
583
0
        }
584
0
      }
585
0
      int part_overlap_count = part->CountOverlappingBoxes(neighbour_box);
586
0
      int neighbour_overlap_count = neighbour->CountOverlappingBoxes(box);
587
0
      ColPartition *right_part = nullptr;
588
0
      if (neighbour_overlap_count <= part_overlap_count ||
589
0
          part->IsSingleton()) {
590
        // Try to split the neighbour to reduce overlap.
591
0
        BLOBNBOX *split_blob = neighbour->OverlapSplitBlob(box);
592
0
        if (split_blob != nullptr) {
593
0
          rsearch.RemoveBBox();
594
0
          right_part = neighbour->SplitAtBlob(split_blob);
595
0
          InsertBBox(true, true, neighbour);
596
0
          ASSERT_HOST(right_part != nullptr);
597
0
        }
598
0
      } else {
599
        // Try to split part to reduce overlap.
600
0
        BLOBNBOX *split_blob = part->OverlapSplitBlob(neighbour_box);
601
0
        if (split_blob != nullptr) {
602
0
          gsearch.RemoveBBox();
603
0
          right_part = part->SplitAtBlob(split_blob);
604
0
          InsertBBox(true, true, part);
605
0
          ASSERT_HOST(right_part != nullptr);
606
0
        }
607
0
      }
608
0
      if (right_part != nullptr) {
609
0
        InsertBBox(true, true, right_part);
610
0
        gsearch.RepositionIterator();
611
0
        rsearch.RepositionIterator();
612
0
        break;
613
0
      }
614
0
    }
615
0
    if (unresolved_overlaps > 2 && part->IsSingleton()) {
616
      // This part is no good so just add to big_parts.
617
0
      RemoveBBox(part);
618
0
      ColPartition_IT big_it(big_parts);
619
0
      part->set_block_owned(true);
620
0
      big_it.add_to_end(part);
621
0
      gsearch.RepositionIterator();
622
0
    }
623
0
  }
624
0
}
625
626
// Filters partitions of source_type by looking at local neighbours.
627
// Where a majority of neighbours have a text type, the partitions are
628
// changed to text, where the neighbours have image type, they are changed
629
// to image, and partitions that have no definite neighbourhood type are
630
// left unchanged.
631
// im_box and rerotation are used to map blob coordinates onto the
632
// nontext_map, which is used to prevent the spread of text neighbourhoods
633
// into images.
634
// Returns true if anything was changed.
635
bool ColPartitionGrid::GridSmoothNeighbours(BlobTextFlowType source_type,
636
                                            Image nontext_map,
637
                                            const TBOX &im_box,
638
0
                                            const FCOORD &rotation) {
639
  // Iterate the ColPartitions in the grid.
640
0
  ColPartitionGridSearch gsearch(this);
641
0
  gsearch.StartFullSearch();
642
0
  ColPartition *part;
643
0
  bool any_changed = false;
644
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
645
0
    if (part->flow() != source_type ||
646
0
        BLOBNBOX::IsLineType(part->blob_type())) {
647
0
      continue;
648
0
    }
649
0
    const TBOX &box = part->bounding_box();
650
0
    bool debug = AlignedBlob::WithinTestRegion(2, box.left(), box.bottom());
651
0
    if (SmoothRegionType(nontext_map, im_box, rotation, debug, part)) {
652
0
      any_changed = true;
653
0
    }
654
0
  }
655
0
  return any_changed;
656
0
}
657
658
// Reflects the grid and its colpartitions in the y-axis, assuming that
659
// all blob boxes have already been done.
660
0
void ColPartitionGrid::ReflectInYAxis() {
661
0
  ColPartition_LIST parts;
662
0
  ColPartition_IT part_it(&parts);
663
  // Iterate the ColPartitions in the grid to extract them.
664
0
  ColPartitionGridSearch gsearch(this);
665
0
  gsearch.StartFullSearch();
666
0
  ColPartition *part;
667
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
668
0
    part_it.add_after_then_move(part);
669
0
  }
670
0
  ICOORD bot_left(-tright().x(), bleft().y());
671
0
  ICOORD top_right(-bleft().x(), tright().y());
672
  // Reinitializing the grid with reflected coords also clears all the
673
  // pointers, so parts will now own the ColPartitions. (Briefly).
674
0
  Init(gridsize(), bot_left, top_right);
675
0
  for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) {
676
0
    part = part_it.extract();
677
0
    part->ReflectInYAxis();
678
0
    InsertBBox(true, true, part);
679
0
  }
680
0
}
681
682
// Transforms the grid of partitions to the output blocks, putting each
683
// partition into a separate block. We don't really care about the order,
684
// as we just want to get as much text as possible without trying to organize
685
// it into proper blocks or columns.
686
// TODO(rays) some kind of sort function would be useful and probably better
687
// than the default here, which is to sort by order of the grid search.
688
void ColPartitionGrid::ExtractPartitionsAsBlocks(BLOCK_LIST *blocks,
689
0
                                                 TO_BLOCK_LIST *to_blocks) {
690
0
  TO_BLOCK_IT to_block_it(to_blocks);
691
0
  BLOCK_IT block_it(blocks);
692
  // All partitions will be put on this list and deleted on return.
693
0
  ColPartition_LIST parts;
694
0
  ColPartition_IT part_it(&parts);
695
  // Iterate the ColPartitions in the grid to extract them.
696
0
  ColPartitionGridSearch gsearch(this);
697
0
  gsearch.StartFullSearch();
698
0
  ColPartition *part;
699
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
700
0
    part_it.add_after_then_move(part);
701
    // The partition has to be at least vaguely like text.
702
0
    BlobRegionType blob_type = part->blob_type();
703
0
    if (BLOBNBOX::IsTextType(blob_type) ||
704
0
        (blob_type == BRT_UNKNOWN && part->boxes_count() > 1)) {
705
0
      PolyBlockType type =
706
0
          blob_type == BRT_VERT_TEXT ? PT_VERTICAL_TEXT : PT_FLOWING_TEXT;
707
      // Get metrics from the row that will be used for the block.
708
0
      TBOX box = part->bounding_box();
709
0
      int median_width = part->median_width();
710
0
      int median_height = part->median_height();
711
      // Turn the partition into a TO_ROW.
712
0
      TO_ROW *row = part->MakeToRow();
713
0
      if (row == nullptr) {
714
        // This partition is dead.
715
0
        part->DeleteBoxes();
716
0
        continue;
717
0
      }
718
0
      auto *block = new BLOCK("", true, 0, 0, box.left(), box.bottom(),
719
0
                              box.right(), box.top());
720
0
      block->pdblk.set_poly_block(new POLY_BLOCK(box, type));
721
0
      auto *to_block = new TO_BLOCK(block);
722
0
      TO_ROW_IT row_it(to_block->get_rows());
723
0
      row_it.add_after_then_move(row);
724
      // We haven't differentially rotated vertical and horizontal text at
725
      // this point, so use width or height as appropriate.
726
0
      if (blob_type == BRT_VERT_TEXT) {
727
0
        to_block->line_size = static_cast<float>(median_width);
728
0
        to_block->line_spacing = static_cast<float>(box.width());
729
0
        to_block->max_blob_size = static_cast<float>(box.width() + 1);
730
0
      } else {
731
0
        to_block->line_size = static_cast<float>(median_height);
732
0
        to_block->line_spacing = static_cast<float>(box.height());
733
0
        to_block->max_blob_size = static_cast<float>(box.height() + 1);
734
0
      }
735
0
      if (to_block->line_size == 0) {
736
0
        to_block->line_size = 1;
737
0
      }
738
0
      block_it.add_to_end(block);
739
0
      to_block_it.add_to_end(to_block);
740
0
    } else {
741
      // This partition is dead.
742
0
      part->DeleteBoxes();
743
0
    }
744
0
  }
745
0
  Clear();
746
  // Now it is safe to delete the ColPartitions as parts goes out of scope.
747
0
}
748
749
// Rotates the grid and its colpartitions by the given angle, assuming that
750
// all blob boxes have already been done.
751
0
void ColPartitionGrid::Deskew(const FCOORD &deskew) {
752
0
  ColPartition_LIST parts;
753
0
  ColPartition_IT part_it(&parts);
754
  // Iterate the ColPartitions in the grid to extract them.
755
0
  ColPartitionGridSearch gsearch(this);
756
0
  gsearch.StartFullSearch();
757
0
  ColPartition *part;
758
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
759
0
    part_it.add_after_then_move(part);
760
0
  }
761
  // Rebuild the grid to the new size.
762
0
  TBOX grid_box(bleft_, tright_);
763
0
  grid_box.rotate_large(deskew);
764
0
  Init(gridsize(), grid_box.botleft(), grid_box.topright());
765
  // Reinitializing the grid with rotated coords also clears all the
766
  // pointers, so parts will now own the ColPartitions. (Briefly).
767
0
  for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) {
768
0
    part = part_it.extract();
769
0
    part->ComputeLimits();
770
0
    InsertBBox(true, true, part);
771
0
  }
772
0
}
773
774
// Sets the left and right tabs of the partitions in the grid.
775
0
void ColPartitionGrid::SetTabStops(TabFind *tabgrid) {
776
  // Iterate the ColPartitions in the grid.
777
0
  ColPartitionGridSearch gsearch(this);
778
0
  gsearch.StartFullSearch();
779
0
  ColPartition *part;
780
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
781
0
    const TBOX &part_box = part->bounding_box();
782
0
    TabVector *left_line = tabgrid->LeftTabForBox(part_box, true, false);
783
    // If the overlapping line is not a left tab, try for non-overlapping.
784
0
    if (left_line != nullptr && !left_line->IsLeftTab()) {
785
0
      left_line = tabgrid->LeftTabForBox(part_box, false, false);
786
0
    }
787
0
    if (left_line != nullptr && left_line->IsLeftTab()) {
788
0
      part->SetLeftTab(left_line);
789
0
    }
790
0
    TabVector *right_line = tabgrid->RightTabForBox(part_box, true, false);
791
0
    if (right_line != nullptr && !right_line->IsRightTab()) {
792
0
      right_line = tabgrid->RightTabForBox(part_box, false, false);
793
0
    }
794
0
    if (right_line != nullptr && right_line->IsRightTab()) {
795
0
      part->SetRightTab(right_line);
796
0
    }
797
0
    part->SetColumnGoodness(tabgrid->WidthCB());
798
0
  }
799
0
}
800
801
// Makes the ColPartSets and puts them in the PartSetVector ready
802
// for finding column bounds. Returns false if no partitions were found.
803
0
bool ColPartitionGrid::MakeColPartSets(PartSetVector *part_sets) {
804
0
  auto *part_lists = new ColPartition_LIST[gridheight()];
805
0
  part_sets->reserve(gridheight());
806
  // Iterate the ColPartitions in the grid to get parts onto lists for the
807
  // y bottom of each.
808
0
  ColPartitionGridSearch gsearch(this);
809
0
  gsearch.StartFullSearch();
810
0
  ColPartition *part;
811
0
  bool any_parts_found = false;
812
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
813
0
    BlobRegionType blob_type = part->blob_type();
814
0
    if (blob_type != BRT_NOISE &&
815
0
        (blob_type != BRT_UNKNOWN || !part->boxes()->singleton())) {
816
0
      int grid_x, grid_y;
817
0
      const TBOX &part_box = part->bounding_box();
818
0
      GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);
819
0
      ColPartition_IT part_it(&part_lists[grid_y]);
820
0
      part_it.add_to_end(part);
821
0
      any_parts_found = true;
822
0
    }
823
0
  }
824
0
  if (any_parts_found) {
825
0
    for (int grid_y = 0; grid_y < gridheight(); ++grid_y) {
826
0
      ColPartitionSet *line_set = nullptr;
827
0
      if (!part_lists[grid_y].empty()) {
828
0
        line_set = new ColPartitionSet(&part_lists[grid_y]);
829
0
      }
830
0
      part_sets->push_back(line_set);
831
0
    }
832
0
  }
833
0
  delete[] part_lists;
834
0
  return any_parts_found;
835
0
}
836
837
// Makes a single ColPartitionSet consisting of a single ColPartition that
838
// represents the total horizontal extent of the significant content on the
839
// page. Used for the single column setting in place of automatic detection.
840
// Returns nullptr if the page is empty of significant content.
841
0
ColPartitionSet *ColPartitionGrid::MakeSingleColumnSet(WidthCallback cb) {
842
0
  ColPartition *single_column_part = nullptr;
843
  // Iterate the ColPartitions in the grid to get parts onto lists for the
844
  // y bottom of each.
845
0
  ColPartitionGridSearch gsearch(this);
846
0
  gsearch.StartFullSearch();
847
0
  ColPartition *part;
848
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
849
0
    BlobRegionType blob_type = part->blob_type();
850
0
    if (blob_type != BRT_NOISE &&
851
0
        (blob_type != BRT_UNKNOWN || !part->boxes()->singleton())) {
852
      // Consider for single column.
853
0
      BlobTextFlowType flow = part->flow();
854
0
      if ((blob_type == BRT_TEXT &&
855
0
           (flow == BTFT_STRONG_CHAIN || flow == BTFT_CHAIN ||
856
0
            flow == BTFT_LEADER || flow == BTFT_TEXT_ON_IMAGE)) ||
857
0
          blob_type == BRT_RECTIMAGE || blob_type == BRT_POLYIMAGE) {
858
0
        if (single_column_part == nullptr) {
859
0
          single_column_part = part->ShallowCopy();
860
0
          single_column_part->set_blob_type(BRT_TEXT);
861
          // Copy the tabs from itself to properly setup the margins.
862
0
          single_column_part->CopyLeftTab(*single_column_part, false);
863
0
          single_column_part->CopyRightTab(*single_column_part, false);
864
0
        } else {
865
0
          if (part->left_key() < single_column_part->left_key()) {
866
0
            single_column_part->CopyLeftTab(*part, false);
867
0
          }
868
0
          if (part->right_key() > single_column_part->right_key()) {
869
0
            single_column_part->CopyRightTab(*part, false);
870
0
          }
871
0
        }
872
0
      }
873
0
    }
874
0
  }
875
0
  if (single_column_part != nullptr) {
876
    // Make a ColPartitionSet out of the single_column_part as a candidate
877
    // for the single column case.
878
0
    single_column_part->SetColumnGoodness(cb);
879
0
    return new ColPartitionSet(single_column_part);
880
0
  }
881
0
  return nullptr;
882
0
}
883
884
// Mark the BLOBNBOXes in each partition as being owned by that partition.
885
0
void ColPartitionGrid::ClaimBoxes() {
886
  // Iterate the ColPartitions in the grid.
887
0
  ColPartitionGridSearch gsearch(this);
888
0
  gsearch.StartFullSearch();
889
0
  ColPartition *part;
890
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
891
0
    part->ClaimBoxes();
892
0
  }
893
0
}
894
895
// Retypes all the blobs referenced by the partitions in the grid.
896
// Image blobs are found and returned in the im_blobs list, as they are not
897
// owned by the block.
898
0
void ColPartitionGrid::ReTypeBlobs(BLOBNBOX_LIST *im_blobs) {
899
0
  BLOBNBOX_IT im_blob_it(im_blobs);
900
0
  ColPartition_LIST dead_parts;
901
0
  ColPartition_IT dead_part_it(&dead_parts);
902
  // Iterate the ColPartitions in the grid.
903
0
  ColPartitionGridSearch gsearch(this);
904
0
  gsearch.StartFullSearch();
905
0
  ColPartition *part;
906
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
907
0
    BlobRegionType blob_type = part->blob_type();
908
0
    BlobTextFlowType flow = part->flow();
909
0
    bool any_blobs_moved = false;
910
0
    if (blob_type == BRT_POLYIMAGE || blob_type == BRT_RECTIMAGE) {
911
0
      BLOBNBOX_C_IT blob_it(part->boxes());
912
0
      for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
913
0
        BLOBNBOX *blob = blob_it.data();
914
0
        im_blob_it.add_after_then_move(blob);
915
0
      }
916
0
    } else if (blob_type != BRT_NOISE) {
917
      // Make sure the blobs are marked with the correct type and flow.
918
0
      BLOBNBOX_C_IT blob_it(part->boxes());
919
0
      for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
920
0
        BLOBNBOX *blob = blob_it.data();
921
0
        if (blob->region_type() == BRT_NOISE) {
922
          // TODO(rays) Deprecated. Change this section to an assert to verify
923
          // and then delete.
924
0
          ASSERT_HOST(blob->cblob()->area() != 0);
925
0
          blob->set_owner(nullptr);
926
0
          blob_it.extract();
927
0
          any_blobs_moved = true;
928
0
        } else {
929
0
          blob->set_region_type(blob_type);
930
0
          if (blob->flow() != BTFT_LEADER) {
931
0
            blob->set_flow(flow);
932
0
          }
933
0
        }
934
0
      }
935
0
    }
936
0
    if (blob_type == BRT_NOISE || part->boxes()->empty()) {
937
0
      BLOBNBOX_C_IT blob_it(part->boxes());
938
0
      part->DisownBoxes();
939
0
      dead_part_it.add_to_end(part);
940
0
      gsearch.RemoveBBox();
941
0
      for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
942
0
        BLOBNBOX *blob = blob_it.data();
943
0
        if (blob->cblob()->area() == 0) {
944
          // Any blob with zero area is a fake image blob and should be deleted.
945
0
          delete blob->cblob();
946
0
          delete blob;
947
0
        }
948
0
      }
949
0
    } else if (any_blobs_moved) {
950
0
      gsearch.RemoveBBox();
951
0
      part->ComputeLimits();
952
0
      InsertBBox(true, true, part);
953
0
      gsearch.RepositionIterator();
954
0
    }
955
0
  }
956
0
}
957
958
// The boxes within the partitions have changed (by deskew) so recompute
959
// the bounds of all the partitions and reinsert them into the grid.
960
void ColPartitionGrid::RecomputeBounds(int gridsize, const ICOORD &bleft,
961
                                       const ICOORD &tright,
962
0
                                       const ICOORD &vertical) {
963
0
  ColPartition_LIST saved_parts;
964
0
  ColPartition_IT part_it(&saved_parts);
965
  // Iterate the ColPartitions in the grid to get parts onto a list.
966
0
  ColPartitionGridSearch gsearch(this);
967
0
  gsearch.StartFullSearch();
968
0
  ColPartition *part;
969
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
970
0
    part_it.add_to_end(part);
971
0
  }
972
  // Reinitialize grid to the new size.
973
0
  Init(gridsize, bleft, tright);
974
  // Recompute the bounds of the parts and put them back in the new grid.
975
0
  for (part_it.move_to_first(); !part_it.empty(); part_it.forward()) {
976
0
    part = part_it.extract();
977
0
    part->set_vertical(vertical);
978
0
    part->ComputeLimits();
979
0
    InsertBBox(true, true, part);
980
0
  }
981
0
}
982
983
// Improves the margins of the ColPartitions in the grid by calling
984
// FindPartitionMargins on each.
985
// best_columns, which may be nullptr, is an array of pointers indicating the
986
// column set at each y-coordinate in the grid.
987
// best_columns is usually the best_columns_ member of ColumnFinder.
988
0
void ColPartitionGrid::GridFindMargins(ColPartitionSet **best_columns) {
989
  // Iterate the ColPartitions in the grid.
990
0
  ColPartitionGridSearch gsearch(this);
991
0
  gsearch.StartFullSearch();
992
0
  ColPartition *part;
993
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
994
    // Set up a rectangle search x-bounded by the column and y by the part.
995
0
    ColPartitionSet *columns =
996
0
        best_columns != nullptr ? best_columns[gsearch.GridY()] : nullptr;
997
0
    FindPartitionMargins(columns, part);
998
0
    const TBOX &box = part->bounding_box();
999
0
    if (AlignedBlob::WithinTestRegion(2, box.left(), box.bottom())) {
1000
0
      tprintf("Computed margins for part:");
1001
0
      part->Print();
1002
0
    }
1003
0
  }
1004
0
}
1005
1006
// Improves the margins of the ColPartitions in the list by calling
1007
// FindPartitionMargins on each.
1008
// best_columns, which may be nullptr, is an array of pointers indicating the
1009
// column set at each y-coordinate in the grid.
1010
// best_columns is usually the best_columns_ member of ColumnFinder.
1011
void ColPartitionGrid::ListFindMargins(ColPartitionSet **best_columns,
1012
0
                                       ColPartition_LIST *parts) {
1013
0
  ColPartition_IT part_it(parts);
1014
0
  for (part_it.mark_cycle_pt(); !part_it.cycled_list(); part_it.forward()) {
1015
0
    ColPartition *part = part_it.data();
1016
0
    ColPartitionSet *columns = nullptr;
1017
0
    if (best_columns != nullptr) {
1018
0
      const TBOX &part_box = part->bounding_box();
1019
      // Get the columns from the y grid coord.
1020
0
      int grid_x, grid_y;
1021
0
      GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);
1022
0
      columns = best_columns[grid_y];
1023
0
    }
1024
0
    FindPartitionMargins(columns, part);
1025
0
  }
1026
0
}
1027
1028
// Deletes all the partitions in the grid after disowning all the blobs.
1029
0
void ColPartitionGrid::DeleteParts() {
1030
0
  ColPartition_LIST dead_parts;
1031
0
  ColPartition_IT dead_it(&dead_parts);
1032
0
  ColPartitionGridSearch gsearch(this);
1033
0
  gsearch.StartFullSearch();
1034
0
  ColPartition *part;
1035
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1036
0
    part->DisownBoxes();
1037
0
    dead_it.add_to_end(part); // Parts will be deleted on return.
1038
0
  }
1039
0
  Clear();
1040
0
}
1041
1042
// Deletes all the partitions in the grid that are of type BRT_UNKNOWN and
1043
// all the blobs in them.
1044
0
void ColPartitionGrid::DeleteUnknownParts(TO_BLOCK *block) {
1045
0
  ColPartitionGridSearch gsearch(this);
1046
0
  gsearch.StartFullSearch();
1047
0
  ColPartition *part;
1048
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1049
0
    if (part->blob_type() == BRT_UNKNOWN) {
1050
0
      gsearch.RemoveBBox();
1051
      // Once marked, the blobs will be swept up by DeleteUnownedNoise.
1052
0
      part->set_flow(BTFT_NONTEXT);
1053
0
      part->set_blob_type(BRT_NOISE);
1054
0
      part->SetBlobTypes();
1055
0
      part->DisownBoxes();
1056
0
      delete part;
1057
0
    }
1058
0
  }
1059
0
  block->DeleteUnownedNoise();
1060
0
}
1061
1062
// Deletes all the partitions in the grid that are NOT of flow type BTFT_LEADER.
1063
0
void ColPartitionGrid::DeleteNonLeaderParts() {
1064
0
  ColPartitionGridSearch gsearch(this);
1065
0
  gsearch.StartFullSearch();
1066
0
  ColPartition *part;
1067
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1068
0
    if (part->flow() != BTFT_LEADER) {
1069
0
      gsearch.RemoveBBox();
1070
0
      if (part->ReleaseNonLeaderBoxes()) {
1071
0
        InsertBBox(true, true, part);
1072
0
        gsearch.RepositionIterator();
1073
0
      } else {
1074
0
        delete part;
1075
0
      }
1076
0
    }
1077
0
  }
1078
0
}
1079
1080
// Finds and marks text partitions that represent figure captions.
1081
0
void ColPartitionGrid::FindFigureCaptions() {
1082
  // For each image region find its best candidate text caption region,
1083
  // if any and mark it as such.
1084
0
  ColPartitionGridSearch gsearch(this);
1085
0
  gsearch.StartFullSearch();
1086
0
  ColPartition *part;
1087
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1088
0
    if (part->IsImageType()) {
1089
0
      const TBOX &part_box = part->bounding_box();
1090
0
      bool debug =
1091
0
          AlignedBlob::WithinTestRegion(2, part_box.left(), part_box.bottom());
1092
0
      ColPartition *best_caption = nullptr;
1093
0
      int best_dist = 0;  // Distance to best_caption.
1094
0
      int best_upper = 0; // Direction of best_caption.
1095
      // Handle both lower and upper directions.
1096
0
      for (int upper = 0; upper < 2; ++upper) {
1097
0
        ColPartition_C_IT partner_it(upper ? part->upper_partners()
1098
0
                                           : part->lower_partners());
1099
        // If there are no image partners, then this direction is ok.
1100
0
        for (partner_it.mark_cycle_pt(); !partner_it.cycled_list();
1101
0
             partner_it.forward()) {
1102
0
          ColPartition *partner = partner_it.data();
1103
0
          if (partner->IsImageType()) {
1104
0
            break;
1105
0
          }
1106
0
        }
1107
0
        if (!partner_it.cycled_list()) {
1108
0
          continue;
1109
0
        }
1110
        // Find the nearest totally overlapping text partner.
1111
0
        for (partner_it.mark_cycle_pt(); !partner_it.cycled_list();
1112
0
             partner_it.forward()) {
1113
0
          ColPartition *partner = partner_it.data();
1114
0
          if (!partner->IsTextType() || partner->type() == PT_TABLE) {
1115
0
            continue;
1116
0
          }
1117
0
          const TBOX &partner_box = partner->bounding_box();
1118
0
          if (debug) {
1119
0
            tprintf("Finding figure captions for image part:");
1120
0
            part_box.print();
1121
0
            tprintf("Considering partner:");
1122
0
            partner_box.print();
1123
0
          }
1124
0
          if (partner_box.left() >= part_box.left() &&
1125
0
              partner_box.right() <= part_box.right()) {
1126
0
            int dist = partner_box.y_gap(part_box);
1127
0
            if (best_caption == nullptr || dist < best_dist) {
1128
0
              best_dist = dist;
1129
0
              best_caption = partner;
1130
0
              best_upper = upper;
1131
0
            }
1132
0
          }
1133
0
        }
1134
0
      }
1135
0
      if (best_caption != nullptr) {
1136
0
        if (debug) {
1137
0
          tprintf("Best caption candidate:");
1138
0
          best_caption->bounding_box().print();
1139
0
        }
1140
        // We have a candidate caption. Qualify it as being separable from
1141
        // any body text. We are looking for either a small number of lines
1142
        // or a big gap that indicates a separation from the body text.
1143
0
        int line_count = 0;
1144
0
        int biggest_gap = 0;
1145
0
        int smallest_gap = INT16_MAX;
1146
0
        int total_height = 0;
1147
0
        int mean_height = 0;
1148
0
        ColPartition *end_partner = nullptr;
1149
0
        ColPartition *next_partner = nullptr;
1150
0
        for (ColPartition *partner = best_caption;
1151
0
             partner != nullptr && line_count <= kMaxCaptionLines;
1152
0
             partner = next_partner) {
1153
0
          if (!partner->IsTextType()) {
1154
0
            end_partner = partner;
1155
0
            break;
1156
0
          }
1157
0
          ++line_count;
1158
0
          total_height += partner->bounding_box().height();
1159
0
          next_partner = partner->SingletonPartner(best_upper);
1160
0
          if (next_partner != nullptr) {
1161
0
            int gap =
1162
0
                partner->bounding_box().y_gap(next_partner->bounding_box());
1163
0
            if (gap > biggest_gap) {
1164
0
              biggest_gap = gap;
1165
0
              end_partner = next_partner;
1166
0
              mean_height = total_height / line_count;
1167
0
            } else if (gap < smallest_gap) {
1168
0
              smallest_gap = gap;
1169
0
            }
1170
            // If the gap looks big compared to the text size and the smallest
1171
            // gap seen so far, then we can stop.
1172
0
            if (biggest_gap > mean_height * kMinCaptionGapHeightRatio &&
1173
0
                biggest_gap > smallest_gap * kMinCaptionGapRatio) {
1174
0
              break;
1175
0
            }
1176
0
          }
1177
0
        }
1178
0
        if (debug) {
1179
0
          tprintf("Line count=%d, biggest gap %d, smallest%d, mean height %d\n",
1180
0
                  line_count, biggest_gap, smallest_gap, mean_height);
1181
0
          if (end_partner != nullptr) {
1182
0
            tprintf("End partner:");
1183
0
            end_partner->bounding_box().print();
1184
0
          }
1185
0
        }
1186
0
        if (next_partner == nullptr && line_count <= kMaxCaptionLines) {
1187
0
          end_partner = nullptr; // No gap, but line count is small.
1188
0
        }
1189
0
        if (line_count <= kMaxCaptionLines) {
1190
          // This is a qualified caption. Mark the text as caption.
1191
0
          for (ColPartition *partner = best_caption;
1192
0
               partner != nullptr && partner != end_partner;
1193
0
               partner = next_partner) {
1194
0
            partner->set_type(PT_CAPTION_TEXT);
1195
0
            partner->SetBlobTypes();
1196
0
            if (debug) {
1197
0
              tprintf("Set caption type for partition:");
1198
0
              partner->bounding_box().print();
1199
0
            }
1200
0
            next_partner = partner->SingletonPartner(best_upper);
1201
0
          }
1202
0
        }
1203
0
      }
1204
0
    }
1205
0
  }
1206
0
}
1207
1208
//////// Functions that manipulate ColPartitions in the part_grid_ /////
1209
//////// to find chains of partner partitions of the same type.  ///////
1210
1211
// For every ColPartition in the grid, finds its upper and lower neighbours.
1212
0
void ColPartitionGrid::FindPartitionPartners() {
1213
0
  ColPartitionGridSearch gsearch(this);
1214
0
  gsearch.StartFullSearch();
1215
0
  ColPartition *part;
1216
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1217
0
    if (part->IsVerticalType()) {
1218
0
      FindVPartitionPartners(true, part);
1219
0
      FindVPartitionPartners(false, part);
1220
0
    } else {
1221
0
      FindPartitionPartners(true, part);
1222
0
      FindPartitionPartners(false, part);
1223
0
    }
1224
0
  }
1225
0
}
1226
1227
// Finds the best partner in the given direction for the given partition.
1228
// Stores the result with AddPartner.
1229
0
void ColPartitionGrid::FindPartitionPartners(bool upper, ColPartition *part) {
1230
0
  if (part->type() == PT_NOISE) {
1231
0
    return; // Noise is not allowed to partner anything.
1232
0
  }
1233
0
  const TBOX &box = part->bounding_box();
1234
0
  int top = part->median_top();
1235
0
  int bottom = part->median_bottom();
1236
0
  int height = top - bottom;
1237
0
  int mid_y = (bottom + top) / 2;
1238
0
  ColPartitionGridSearch vsearch(this);
1239
  // Search down for neighbour below
1240
0
  vsearch.StartVerticalSearch(box.left(), box.right(), part->MidY());
1241
0
  ColPartition *neighbour;
1242
0
  ColPartition *best_neighbour = nullptr;
1243
0
  int best_dist = INT32_MAX;
1244
0
  while ((neighbour = vsearch.NextVerticalSearch(!upper)) != nullptr) {
1245
0
    if (neighbour == part || neighbour->type() == PT_NOISE) {
1246
0
      continue; // Noise is not allowed to partner anything.
1247
0
    }
1248
0
    int neighbour_bottom = neighbour->median_bottom();
1249
0
    int neighbour_top = neighbour->median_top();
1250
0
    int neighbour_y = (neighbour_bottom + neighbour_top) / 2;
1251
0
    if (upper != (neighbour_y > mid_y)) {
1252
0
      continue;
1253
0
    }
1254
0
    if (!part->HOverlaps(*neighbour) && !part->WithinSameMargins(*neighbour)) {
1255
0
      continue;
1256
0
    }
1257
0
    if (!part->TypesMatch(*neighbour)) {
1258
0
      if (best_neighbour == nullptr) {
1259
0
        best_neighbour = neighbour;
1260
0
      }
1261
0
      continue;
1262
0
    }
1263
0
    int dist = upper ? neighbour_bottom - top : bottom - neighbour_top;
1264
0
    if (dist <= kMaxPartitionSpacing * height) {
1265
0
      if (dist < best_dist) {
1266
0
        best_dist = dist;
1267
0
        best_neighbour = neighbour;
1268
0
      }
1269
0
    } else {
1270
0
      break;
1271
0
    }
1272
0
  }
1273
0
  if (best_neighbour != nullptr) {
1274
0
    part->AddPartner(upper, best_neighbour);
1275
0
  }
1276
0
}
1277
1278
// Finds the best partner in the given direction for the given partition.
1279
// Stores the result with AddPartner.
1280
void ColPartitionGrid::FindVPartitionPartners(bool to_the_left,
1281
0
                                              ColPartition *part) {
1282
0
  if (part->type() == PT_NOISE) {
1283
0
    return; // Noise is not allowed to partner anything.
1284
0
  }
1285
0
  const TBOX &box = part->bounding_box();
1286
0
  int left = part->median_left();
1287
0
  int right = part->median_right();
1288
0
  int width = right >= left ? right - left : -1;
1289
0
  int mid_x = (left + right) / 2;
1290
0
  ColPartitionGridSearch hsearch(this);
1291
  // Search left for neighbour to_the_left
1292
0
  hsearch.StartSideSearch(mid_x, box.bottom(), box.top());
1293
0
  ColPartition *neighbour;
1294
0
  ColPartition *best_neighbour = nullptr;
1295
0
  int best_dist = INT32_MAX;
1296
0
  while ((neighbour = hsearch.NextSideSearch(to_the_left)) != nullptr) {
1297
0
    if (neighbour == part || neighbour->type() == PT_NOISE) {
1298
0
      continue; // Noise is not allowed to partner anything.
1299
0
    }
1300
0
    int neighbour_left = neighbour->median_left();
1301
0
    int neighbour_right = neighbour->median_right();
1302
0
    int neighbour_x = (neighbour_left + neighbour_right) / 2;
1303
0
    if (to_the_left != (neighbour_x < mid_x)) {
1304
0
      continue;
1305
0
    }
1306
0
    if (!part->VOverlaps(*neighbour)) {
1307
0
      continue;
1308
0
    }
1309
0
    if (!part->TypesMatch(*neighbour)) {
1310
0
      continue; // Only match to other vertical text.
1311
0
    }
1312
0
    int dist = to_the_left ? left - neighbour_right : neighbour_left - right;
1313
0
    if (dist <= kMaxPartitionSpacing * width) {
1314
0
      if (dist < best_dist || best_neighbour == nullptr) {
1315
0
        best_dist = dist;
1316
0
        best_neighbour = neighbour;
1317
0
      }
1318
0
    } else {
1319
0
      break;
1320
0
    }
1321
0
  }
1322
  // For vertical partitions, the upper partner is to the left, and lower is
1323
  // to the right.
1324
0
  if (best_neighbour != nullptr) {
1325
0
    part->AddPartner(to_the_left, best_neighbour);
1326
0
  }
1327
0
}
1328
1329
// For every ColPartition with multiple partners in the grid, reduces the
1330
// number of partners to 0 or 1. If get_desperate is true, goes to more
1331
// desperate merge methods to merge flowing text before breaking partnerships.
1332
0
void ColPartitionGrid::RefinePartitionPartners(bool get_desperate) {
1333
0
  ColPartitionGridSearch gsearch(this);
1334
  // Refine in type order so that chasing multiple partners can be done
1335
  // before eliminating type mis-matching partners.
1336
0
  for (int type = PT_UNKNOWN + 1; type <= PT_COUNT; type++) {
1337
    // Iterate the ColPartitions in the grid.
1338
0
    gsearch.StartFullSearch();
1339
0
    ColPartition *part;
1340
0
    while ((part = gsearch.NextFullSearch()) != nullptr) {
1341
0
      part->RefinePartners(static_cast<PolyBlockType>(type), get_desperate,
1342
0
                           this);
1343
      // Iterator may have been messed up by a merge.
1344
0
      gsearch.RepositionIterator();
1345
0
    }
1346
0
  }
1347
0
}
1348
1349
// ========================== PRIVATE CODE ========================
1350
1351
// Finds and returns a list of candidate ColPartitions to merge with part.
1352
// The candidates must overlap search_box, and when merged must not
1353
// overlap any other partitions that are not overlapped by each individually.
1354
void ColPartitionGrid::FindMergeCandidates(const ColPartition *part,
1355
                                           const TBOX &search_box, bool debug,
1356
0
                                           ColPartition_CLIST *candidates) {
1357
0
  int ok_overlap =
1358
0
      static_cast<int>(kTinyEnoughTextlineOverlapFraction * gridsize() + 0.5);
1359
0
  const TBOX &part_box = part->bounding_box();
1360
  // Now run the rect search.
1361
0
  ColPartitionGridSearch rsearch(this);
1362
0
  rsearch.SetUniqueMode(true);
1363
0
  rsearch.StartRectSearch(search_box);
1364
0
  ColPartition *candidate;
1365
0
  while ((candidate = rsearch.NextRectSearch()) != nullptr) {
1366
0
    if (!OKMergeCandidate(part, candidate, debug)) {
1367
0
      continue;
1368
0
    }
1369
0
    const TBOX &c_box = candidate->bounding_box();
1370
    // Candidate seems to be a potential merge with part. If one contains
1371
    // the other, then the merge is a no-brainer. Otherwise, search the
1372
    // combined box to see if anything else is inappropriately overlapped.
1373
0
    if (!part_box.contains(c_box) && !c_box.contains(part_box)) {
1374
      // Search the combined rectangle to see if anything new is overlapped.
1375
      // This is a preliminary test designed to quickly weed-out poor
1376
      // merge candidates that would create a big list of overlapped objects
1377
      // for the squared-order overlap analysis. Eg. vertical and horizontal
1378
      // line-like objects that overlap real text when merged:
1379
      // || ==========================
1380
      // ||
1381
      // ||  r e a l  t e x t
1382
      // ||
1383
      // ||
1384
0
      TBOX merged_box(part_box);
1385
0
      merged_box += c_box;
1386
0
      ColPartitionGridSearch msearch(this);
1387
0
      msearch.SetUniqueMode(true);
1388
0
      msearch.StartRectSearch(merged_box);
1389
0
      ColPartition *neighbour;
1390
0
      while ((neighbour = msearch.NextRectSearch()) != nullptr) {
1391
0
        if (neighbour == part || neighbour == candidate) {
1392
0
          continue; // Ignore itself.
1393
0
        }
1394
0
        if (neighbour->OKMergeOverlap(*part, *candidate, ok_overlap, false)) {
1395
0
          continue; // This kind of merge overlap is OK.
1396
0
        }
1397
0
        TBOX n_box = neighbour->bounding_box();
1398
        // The overlap is OK if:
1399
        // * the n_box already overlapped the part or the candidate OR
1400
        // * the n_box is a suitable merge with either part or candidate
1401
0
        if (!n_box.overlap(part_box) && !n_box.overlap(c_box) &&
1402
0
            !OKMergeCandidate(part, neighbour, false) &&
1403
0
            !OKMergeCandidate(candidate, neighbour, false)) {
1404
0
          break;
1405
0
        }
1406
0
      }
1407
0
      if (neighbour != nullptr) {
1408
0
        if (debug) {
1409
0
          tprintf(
1410
0
              "Combined box overlaps another that is not OK despite"
1411
0
              " allowance of %d:",
1412
0
              ok_overlap);
1413
0
          neighbour->bounding_box().print();
1414
0
          tprintf("Reason:");
1415
0
          OKMergeCandidate(part, neighbour, true);
1416
0
          tprintf("...and:");
1417
0
          OKMergeCandidate(candidate, neighbour, true);
1418
0
          tprintf("Overlap:");
1419
0
          neighbour->OKMergeOverlap(*part, *candidate, ok_overlap, true);
1420
0
        }
1421
0
        continue;
1422
0
      }
1423
0
    }
1424
0
    if (debug) {
1425
0
      tprintf("Adding candidate:");
1426
0
      candidate->bounding_box().print();
1427
0
    }
1428
    // Unique elements as they arrive.
1429
0
    candidates->add_sorted(SortByBoxLeft<ColPartition>, true, candidate);
1430
0
  }
1431
0
}
1432
1433
// Smoothes the region type/flow type of the given part by looking at local
1434
// neighbours and the given image mask. Searches a padded rectangle with the
1435
// padding truncated on one size of the part's box in turn for each side,
1436
// using the result (if any) that has the least distance to all neighbours
1437
// that contribute to the decision. This biases in favor of rectangular
1438
// regions without completely enforcing them.
1439
// If a good decision cannot be reached, the part is left unchanged.
1440
// im_box and rerotation are used to map blob coordinates onto the
1441
// nontext_map, which is used to prevent the spread of text neighbourhoods
1442
// into images.
1443
// Returns true if the partition was changed.
1444
bool ColPartitionGrid::SmoothRegionType(Image nontext_map, const TBOX &im_box,
1445
                                        const FCOORD &rerotation, bool debug,
1446
0
                                        ColPartition *part) {
1447
0
  const TBOX &part_box = part->bounding_box();
1448
0
  if (debug) {
1449
0
    tprintf("Smooothing part at:");
1450
0
    part_box.print();
1451
0
  }
1452
0
  BlobRegionType best_type = BRT_UNKNOWN;
1453
0
  int best_dist = INT32_MAX;
1454
0
  int max_dist = std::min(part_box.width(), part_box.height());
1455
0
  max_dist = std::max(max_dist * kMaxNeighbourDistFactor, gridsize() * 2);
1456
  // Search with the pad truncated on each side of the box in turn.
1457
0
  bool any_image = false;
1458
0
  bool all_image = true;
1459
0
  for (int d = 0; d < BND_COUNT; ++d) {
1460
0
    int dist;
1461
0
    auto dir = static_cast<BlobNeighbourDir>(d);
1462
0
    BlobRegionType type = SmoothInOneDirection(dir, nontext_map, im_box,
1463
0
                                               rerotation, debug, *part, &dist);
1464
0
    if (debug) {
1465
0
      tprintf("Result in dir %d = %d at dist %d\n", dir, type, dist);
1466
0
    }
1467
0
    if (type != BRT_UNKNOWN && dist < best_dist) {
1468
0
      best_dist = dist;
1469
0
      best_type = type;
1470
0
    }
1471
0
    if (type == BRT_POLYIMAGE) {
1472
0
      any_image = true;
1473
0
    } else {
1474
0
      all_image = false;
1475
0
    }
1476
0
  }
1477
0
  if (best_dist > max_dist) {
1478
0
    return false; // Too far away to set the type with it.
1479
0
  }
1480
0
  if (part->flow() == BTFT_STRONG_CHAIN && !all_image) {
1481
0
    return false; // We are not modifying it.
1482
0
  }
1483
0
  BlobRegionType new_type = part->blob_type();
1484
0
  BlobTextFlowType new_flow = part->flow();
1485
0
  if (best_type == BRT_TEXT && !any_image) {
1486
0
    new_flow = BTFT_STRONG_CHAIN;
1487
0
    new_type = BRT_TEXT;
1488
0
  } else if (best_type == BRT_VERT_TEXT && !any_image) {
1489
0
    new_flow = BTFT_STRONG_CHAIN;
1490
0
    new_type = BRT_VERT_TEXT;
1491
0
  } else if (best_type == BRT_POLYIMAGE) {
1492
0
    new_flow = BTFT_NONTEXT;
1493
0
    new_type = BRT_UNKNOWN;
1494
0
  }
1495
0
  if (new_type != part->blob_type() || new_flow != part->flow()) {
1496
0
    part->set_flow(new_flow);
1497
0
    part->set_blob_type(new_type);
1498
0
    part->SetBlobTypes();
1499
0
    if (debug) {
1500
0
      tprintf("Modified part:");
1501
0
      part->Print();
1502
0
    }
1503
0
    return true;
1504
0
  } else {
1505
0
    return false;
1506
0
  }
1507
0
}
1508
1509
// Sets up a search box based on the part_box, padded in all directions
1510
// except direction. Also setup dist_scaling to weight x,y distances according
1511
// to the given direction.
1512
static void ComputeSearchBoxAndScaling(BlobNeighbourDir direction,
1513
                                       const TBOX &part_box, int min_padding,
1514
0
                                       TBOX *search_box, ICOORD *dist_scaling) {
1515
0
  *search_box = part_box;
1516
  // Generate a pad value based on the min dimension of part_box, but at least
1517
  // min_padding and then scaled by kMaxPadFactor.
1518
0
  int padding = std::min(part_box.height(), part_box.width());
1519
0
  padding = std::max(padding, min_padding);
1520
0
  padding *= kMaxPadFactor;
1521
0
  search_box->pad(padding, padding);
1522
  // Truncate the box in the appropriate direction and make the distance
1523
  // metric slightly biased in the truncated direction.
1524
0
  switch (direction) {
1525
0
    case BND_LEFT:
1526
0
      search_box->set_left(part_box.left());
1527
0
      *dist_scaling = ICOORD(2, 1);
1528
0
      break;
1529
0
    case BND_BELOW:
1530
0
      search_box->set_bottom(part_box.bottom());
1531
0
      *dist_scaling = ICOORD(1, 2);
1532
0
      break;
1533
0
    case BND_RIGHT:
1534
0
      search_box->set_right(part_box.right());
1535
0
      *dist_scaling = ICOORD(2, 1);
1536
0
      break;
1537
0
    case BND_ABOVE:
1538
0
      search_box->set_top(part_box.top());
1539
0
      *dist_scaling = ICOORD(1, 2);
1540
0
      break;
1541
0
    default:
1542
0
      ASSERT_HOST(false);
1543
0
  }
1544
0
}
1545
1546
// Local enum used by SmoothInOneDirection and AccumulatePartDistances
1547
// for the different types of partition neighbour.
1548
enum NeighbourPartitionType {
1549
  NPT_HTEXT,      // Definite horizontal text.
1550
  NPT_VTEXT,      // Definite vertical text.
1551
  NPT_WEAK_HTEXT, // Weakly horizontal text. Counts as HTEXT for HTEXT, but
1552
                  // image for image and VTEXT.
1553
  NPT_WEAK_VTEXT, // Weakly vertical text. Counts as VTEXT for VTEXT, but
1554
                  // image for image and HTEXT.
1555
  NPT_IMAGE,      // Defininte non-text.
1556
  NPT_COUNT       // Number of array elements.
1557
};
1558
1559
// Executes the search for SmoothRegionType in a single direction.
1560
// Creates a bounding box that is padded in all directions except direction,
1561
// and searches it for other partitions. Finds the nearest collection of
1562
// partitions that makes a decisive result (if any) and returns the type
1563
// and the distance of the collection. If there are any pixels in the
1564
// nontext_map, then the decision is biased towards image.
1565
BlobRegionType ColPartitionGrid::SmoothInOneDirection(
1566
    BlobNeighbourDir direction, Image nontext_map, const TBOX &im_box,
1567
    const FCOORD &rerotation, bool debug, const ColPartition &part,
1568
0
    int *best_distance) {
1569
  // Set up a rectangle search bounded by the part.
1570
0
  const TBOX &part_box = part.bounding_box();
1571
0
  TBOX search_box;
1572
0
  ICOORD dist_scaling;
1573
0
  ComputeSearchBoxAndScaling(direction, part_box, gridsize(), &search_box,
1574
0
                             &dist_scaling);
1575
0
  bool image_region = ImageFind::CountPixelsInRotatedBox(
1576
0
                          search_box, im_box, rerotation, nontext_map) > 0;
1577
0
  std::vector<int> dists[NPT_COUNT];
1578
0
  AccumulatePartDistances(part, dist_scaling, search_box, nontext_map, im_box,
1579
0
                          rerotation, debug, dists);
1580
  // By iteratively including the next smallest distance across the vectors,
1581
  // (as in a merge sort) we can use the vector indices as counts of each type
1582
  // and find the nearest set of objects that give us a definite decision.
1583
0
  unsigned counts[NPT_COUNT];
1584
0
  memset(counts, 0, sizeof(counts));
1585
  // If there is image in the search box, tip the balance in image's favor.
1586
0
  int image_bias = image_region ? kSmoothDecisionMargin / 2 : 0;
1587
0
  BlobRegionType text_dir = part.blob_type();
1588
0
  BlobTextFlowType flow_type = part.flow();
1589
0
  int min_dist = 0;
1590
0
  do {
1591
    // Find the minimum new entry across the vectors
1592
0
    min_dist = INT32_MAX;
1593
0
    for (int i = 0; i < NPT_COUNT; ++i) {
1594
0
      if (counts[i] < dists[i].size() && dists[i][counts[i]] < min_dist) {
1595
0
        min_dist = dists[i][counts[i]];
1596
0
      }
1597
0
    }
1598
    // Step all the indices/counts forward to include min_dist.
1599
0
    for (int i = 0; i < NPT_COUNT; ++i) {
1600
0
      while (counts[i] < dists[i].size() && dists[i][counts[i]] <= min_dist) {
1601
0
        ++counts[i];
1602
0
      }
1603
0
    }
1604
0
    *best_distance = min_dist;
1605
0
    if (debug) {
1606
0
      tprintf("Totals: htext=%u+%u, vtext=%u+%u, image=%u+%u, at dist=%d\n",
1607
0
              counts[NPT_HTEXT], counts[NPT_WEAK_HTEXT], counts[NPT_VTEXT],
1608
0
              counts[NPT_WEAK_VTEXT], counts[NPT_IMAGE], image_bias, min_dist);
1609
0
    }
1610
    // See if we have a decision yet.
1611
0
    auto image_count = counts[NPT_IMAGE];
1612
0
    int htext_score = counts[NPT_HTEXT] + counts[NPT_WEAK_HTEXT] -
1613
0
                      (image_count + counts[NPT_WEAK_VTEXT]);
1614
0
    int vtext_score = counts[NPT_VTEXT] + counts[NPT_WEAK_VTEXT] -
1615
0
                      (image_count + counts[NPT_WEAK_HTEXT]);
1616
0
    if (image_count > 0 && image_bias - htext_score >= kSmoothDecisionMargin &&
1617
0
        image_bias - vtext_score >= kSmoothDecisionMargin) {
1618
0
      *best_distance = dists[NPT_IMAGE][0];
1619
0
      if (!dists[NPT_WEAK_VTEXT].empty() &&
1620
0
          *best_distance > dists[NPT_WEAK_VTEXT][0]) {
1621
0
        *best_distance = dists[NPT_WEAK_VTEXT][0];
1622
0
      }
1623
0
      if (!dists[NPT_WEAK_HTEXT].empty() &&
1624
0
          *best_distance > dists[NPT_WEAK_HTEXT][0]) {
1625
0
        *best_distance = dists[NPT_WEAK_HTEXT][0];
1626
0
      }
1627
0
      return BRT_POLYIMAGE;
1628
0
    }
1629
0
    if ((text_dir != BRT_VERT_TEXT || flow_type != BTFT_CHAIN) &&
1630
0
        counts[NPT_HTEXT] > 0 && htext_score >= kSmoothDecisionMargin) {
1631
0
      *best_distance = dists[NPT_HTEXT][0];
1632
0
      return BRT_TEXT;
1633
0
    } else if ((text_dir != BRT_TEXT || flow_type != BTFT_CHAIN) &&
1634
0
               counts[NPT_VTEXT] > 0 && vtext_score >= kSmoothDecisionMargin) {
1635
0
      *best_distance = dists[NPT_VTEXT][0];
1636
0
      return BRT_VERT_TEXT;
1637
0
    }
1638
0
  } while (min_dist < INT32_MAX);
1639
0
  return BRT_UNKNOWN;
1640
0
}
1641
1642
// Counts the partitions in the given search_box by appending the gap
1643
// distance (scaled by dist_scaling) of the part from the base_part to the
1644
// vector of the appropriate type for the partition. Prior to return, the
1645
// vectors in the dists array are sorted in increasing order.
1646
// The nontext_map (+im_box, rerotation) is used to make text invisible if
1647
// there is non-text in between.
1648
// dists must be an array of vectors of size NPT_COUNT.
1649
void ColPartitionGrid::AccumulatePartDistances(
1650
    const ColPartition &base_part, const ICOORD &dist_scaling,
1651
    const TBOX &search_box, Image nontext_map, const TBOX &im_box,
1652
0
    const FCOORD &rerotation, bool debug, std::vector<int> *dists) {
1653
0
  const TBOX &part_box = base_part.bounding_box();
1654
0
  ColPartitionGridSearch rsearch(this);
1655
0
  rsearch.SetUniqueMode(true);
1656
0
  rsearch.StartRectSearch(search_box);
1657
0
  ColPartition *neighbour;
1658
  // Search for compatible neighbours with a similar strokewidth, but not
1659
  // on the other side of a tab vector.
1660
0
  while ((neighbour = rsearch.NextRectSearch()) != nullptr) {
1661
0
    if (neighbour->IsUnMergeableType() ||
1662
0
        !base_part.ConfirmNoTabViolation(*neighbour) ||
1663
0
        neighbour == &base_part) {
1664
0
      continue;
1665
0
    }
1666
0
    TBOX nbox = neighbour->bounding_box();
1667
0
    BlobRegionType n_type = neighbour->blob_type();
1668
0
    if ((n_type == BRT_TEXT || n_type == BRT_VERT_TEXT) &&
1669
0
        !ImageFind::BlankImageInBetween(part_box, nbox, im_box, rerotation,
1670
0
                                        nontext_map)) {
1671
0
      continue; // Text not visible the other side of image.
1672
0
    }
1673
0
    if (BLOBNBOX::IsLineType(n_type)) {
1674
0
      continue; // Don't use horizontal lines as neighbours.
1675
0
    }
1676
0
    int x_gap = std::max(part_box.x_gap(nbox), 0);
1677
0
    int y_gap = std::max(part_box.y_gap(nbox), 0);
1678
0
    int n_dist = x_gap * dist_scaling.x() + y_gap * dist_scaling.y();
1679
0
    if (debug) {
1680
0
      tprintf("Part has x-gap=%d, y=%d, dist=%d at:", x_gap, y_gap, n_dist);
1681
0
      nbox.print();
1682
0
    }
1683
    // Truncate the number of boxes, so text doesn't get too much advantage.
1684
0
    int n_boxes = std::min(neighbour->boxes_count(), kSmoothDecisionMargin);
1685
0
    BlobTextFlowType n_flow = neighbour->flow();
1686
0
    std::vector<int> *count_vector = nullptr;
1687
0
    if (n_flow == BTFT_STRONG_CHAIN) {
1688
0
      if (n_type == BRT_TEXT) {
1689
0
        count_vector = &dists[NPT_HTEXT];
1690
0
      } else {
1691
0
        count_vector = &dists[NPT_VTEXT];
1692
0
      }
1693
0
      if (debug) {
1694
0
        tprintf("%s %d\n", n_type == BRT_TEXT ? "Htext" : "Vtext", n_boxes);
1695
0
      }
1696
0
    } else if ((n_type == BRT_TEXT || n_type == BRT_VERT_TEXT) &&
1697
0
               (n_flow == BTFT_CHAIN || n_flow == BTFT_NEIGHBOURS)) {
1698
      // Medium text counts as weak, and all else counts as image.
1699
0
      if (n_type == BRT_TEXT) {
1700
0
        count_vector = &dists[NPT_WEAK_HTEXT];
1701
0
      } else {
1702
0
        count_vector = &dists[NPT_WEAK_VTEXT];
1703
0
      }
1704
0
      if (debug) {
1705
0
        tprintf("Weak %d\n", n_boxes);
1706
0
      }
1707
0
    } else {
1708
0
      count_vector = &dists[NPT_IMAGE];
1709
0
      if (debug) {
1710
0
        tprintf("Image %d\n", n_boxes);
1711
0
      }
1712
0
    }
1713
0
    if (count_vector != nullptr) {
1714
0
      for (int i = 0; i < n_boxes; ++i) {
1715
0
        count_vector->push_back(n_dist);
1716
0
      }
1717
0
    }
1718
0
    if (debug) {
1719
0
      neighbour->Print();
1720
0
    }
1721
0
  }
1722
0
  for (int i = 0; i < NPT_COUNT; ++i) {
1723
0
    std::sort(dists[i].begin(), dists[i].end());
1724
0
  }
1725
0
}
1726
1727
// Improves the margins of the part ColPartition by searching for
1728
// neighbours that vertically overlap significantly.
1729
// columns may be nullptr, and indicates the assigned column structure this
1730
// is applicable to part.
1731
void ColPartitionGrid::FindPartitionMargins(ColPartitionSet *columns,
1732
0
                                            ColPartition *part) {
1733
  // Set up a rectangle search x-bounded by the column and y by the part.
1734
0
  TBOX box = part->bounding_box();
1735
0
  int y = part->MidY();
1736
  // Initial left margin is based on the column, if there is one.
1737
0
  int left_margin = bleft().x();
1738
0
  int right_margin = tright().x();
1739
0
  if (columns != nullptr) {
1740
0
    ColPartition *column = columns->ColumnContaining(box.left(), y);
1741
0
    if (column != nullptr) {
1742
0
      left_margin = column->LeftAtY(y);
1743
0
    }
1744
0
    column = columns->ColumnContaining(box.right(), y);
1745
0
    if (column != nullptr) {
1746
0
      right_margin = column->RightAtY(y);
1747
0
    }
1748
0
  }
1749
0
  left_margin -= kColumnWidthFactor;
1750
0
  right_margin += kColumnWidthFactor;
1751
  // Search for ColPartitions that reduce the margin.
1752
0
  left_margin = FindMargin(box.left() + box.height(), true, left_margin,
1753
0
                           box.bottom(), box.top(), part);
1754
0
  part->set_left_margin(left_margin);
1755
  // Search for ColPartitions that reduce the margin.
1756
0
  right_margin = FindMargin(box.right() - box.height(), false, right_margin,
1757
0
                            box.bottom(), box.top(), part);
1758
0
  part->set_right_margin(right_margin);
1759
0
}
1760
1761
// Starting at x, and going in the specified direction, up to x_limit, finds
1762
// the margin for the given y range by searching sideways,
1763
// and ignoring not_this.
1764
int ColPartitionGrid::FindMargin(int x, bool right_to_left, int x_limit,
1765
                                 int y_bottom, int y_top,
1766
0
                                 const ColPartition *not_this) {
1767
0
  int height = y_top - y_bottom;
1768
  // Iterate the ColPartitions in the grid.
1769
0
  ColPartitionGridSearch side_search(this);
1770
0
  side_search.SetUniqueMode(true);
1771
0
  side_search.StartSideSearch(x, y_bottom, y_top);
1772
0
  ColPartition *part;
1773
0
  while ((part = side_search.NextSideSearch(right_to_left)) != nullptr) {
1774
    // Ignore itself.
1775
0
    if (part == not_this) { // || part->IsLineType())
1776
0
      continue;
1777
0
    }
1778
    // Must overlap by enough, based on the min of the heights, so
1779
    // large partitions can't smash through small ones.
1780
0
    TBOX box = part->bounding_box();
1781
0
    int min_overlap = std::min(height, static_cast<int>(box.height()));
1782
0
    min_overlap = static_cast<int>(min_overlap * kMarginOverlapFraction + 0.5);
1783
0
    int y_overlap = std::min(y_top, static_cast<int>(box.top())) -
1784
0
                    std::max(y_bottom, static_cast<int>(box.bottom()));
1785
0
    if (y_overlap < min_overlap) {
1786
0
      continue;
1787
0
    }
1788
    // Must be going the right way.
1789
0
    int x_edge = right_to_left ? box.right() : box.left();
1790
0
    if ((x_edge < x) != right_to_left) {
1791
0
      continue;
1792
0
    }
1793
    // If we have gone past x_limit, then x_limit will do.
1794
0
    if ((x_edge < x_limit) == right_to_left) {
1795
0
      break;
1796
0
    }
1797
    // It reduces x limit, so save the new one.
1798
0
    x_limit = x_edge;
1799
0
  }
1800
0
  return x_limit;
1801
0
}
1802
1803
} // namespace tesseract.