Coverage Report

Created: 2025-09-27 07:10

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tesseract/src/ccmain/equationdetect.cpp
Line
Count
Source
1
///////////////////////////////////////////////////////////////////////
2
// File:        equationdetect.cpp
3
// Description: Helper classes to detect equations.
4
// Author:      Zongyi (Joe) Liu (joeliu@google.com)
5
//
6
// (C) Copyright 2011, 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
// Include automatically generated configuration file if running autoconf.
20
#ifdef HAVE_CONFIG_H
21
#  include "config_auto.h"
22
#endif
23
24
#include "equationdetect.h"
25
26
#include "bbgrid.h"
27
#include "classify.h"
28
#include "colpartition.h"
29
#include "colpartitiongrid.h"
30
#include "colpartitionset.h"
31
#include "ratngs.h"
32
#include "tesseractclass.h"
33
34
#include "helpers.h"
35
36
#include <algorithm>
37
#include <cfloat>
38
#include <cmath>
39
#include <limits>
40
#include <memory>
41
42
namespace tesseract {
43
44
// Config variables.
45
static BOOL_VAR(equationdetect_save_bi_image, false, "Save input bi image");
46
static BOOL_VAR(equationdetect_save_spt_image, false, "Save special character image");
47
static BOOL_VAR(equationdetect_save_seed_image, false, "Save the seed image");
48
static BOOL_VAR(equationdetect_save_merged_image, false, "Save the merged image");
49
50
///////////////////////////////////////////////////////////////////////////
51
// Utility ColParition sort functions.
52
///////////////////////////////////////////////////////////////////////////
53
0
static int SortCPByTopReverse(const void *p1, const void *p2) {
54
0
  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
55
0
  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
56
0
  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
57
0
  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
58
0
  return box2.top() - box1.top();
59
0
}
60
61
0
static int SortCPByBottom(const void *p1, const void *p2) {
62
0
  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
63
0
  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
64
0
  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
65
0
  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
66
0
  return box1.bottom() - box2.bottom();
67
0
}
68
69
0
static int SortCPByHeight(const void *p1, const void *p2) {
70
0
  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
71
0
  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
72
0
  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
73
0
  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
74
0
  return box1.height() - box2.height();
75
0
}
76
77
// TODO(joeliu): we may want to parameterize these constants.
78
const float kMathDigitDensityTh1 = 0.25;
79
const float kMathDigitDensityTh2 = 0.1;
80
const float kMathItalicDensityTh = 0.5;
81
const float kUnclearDensityTh = 0.25;
82
const int kSeedBlobsCountTh = 10;
83
const int kLeftIndentAlignmentCountTh = 1;
84
85
// Returns true if PolyBlockType is of text type or equation type.
86
0
inline bool IsTextOrEquationType(PolyBlockType type) {
87
0
  return PTIsTextType(type) || type == PT_EQUATION;
88
0
}
89
90
0
inline bool IsLeftIndented(const EquationDetect::IndentType type) {
91
0
  return type == EquationDetect::LEFT_INDENT || type == EquationDetect::BOTH_INDENT;
92
0
}
93
94
0
inline bool IsRightIndented(const EquationDetect::IndentType type) {
95
0
  return type == EquationDetect::RIGHT_INDENT || type == EquationDetect::BOTH_INDENT;
96
0
}
97
98
0
EquationDetect::EquationDetect(const char *equ_datapath, const char *equ_name) {
99
0
  const char *default_name = "equ";
100
0
  if (equ_name == nullptr) {
101
0
    equ_name = default_name;
102
0
  }
103
0
  lang_tesseract_ = nullptr;
104
0
  resolution_ = 0;
105
0
  page_count_ = 0;
106
107
0
  if (equ_tesseract_.init_tesseract(equ_datapath, equ_name, OEM_TESSERACT_ONLY)) {
108
0
    tprintf(
109
0
        "Warning: equation region detection requested,"
110
0
        " but %s failed to load from %s\n",
111
0
        equ_name, equ_datapath);
112
0
  }
113
114
0
  cps_super_bbox_ = nullptr;
115
0
}
116
117
0
EquationDetect::~EquationDetect() {
118
0
  delete (cps_super_bbox_);
119
0
}
120
121
0
void EquationDetect::SetLangTesseract(Tesseract *lang_tesseract) {
122
0
  lang_tesseract_ = lang_tesseract;
123
0
}
124
125
0
void EquationDetect::SetResolution(const int resolution) {
126
0
  resolution_ = resolution;
127
0
}
128
129
0
int EquationDetect::LabelSpecialText(TO_BLOCK *to_block) {
130
0
  if (to_block == nullptr) {
131
0
    tprintf("Warning: input to_block is nullptr!\n");
132
0
    return -1;
133
0
  }
134
135
0
  std::vector<BLOBNBOX_LIST *> blob_lists;
136
0
  blob_lists.push_back(&(to_block->blobs));
137
0
  blob_lists.push_back(&(to_block->large_blobs));
138
0
  for (auto &blob_list : blob_lists) {
139
0
    BLOBNBOX_IT bbox_it(blob_list);
140
0
    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
141
0
      bbox_it.data()->set_special_text_type(BSTT_NONE);
142
0
    }
143
0
  }
144
145
0
  return 0;
146
0
}
147
148
0
void EquationDetect::IdentifySpecialText(BLOBNBOX *blobnbox, const int height_th) {
149
0
  ASSERT_HOST(blobnbox != nullptr);
150
0
  if (blobnbox->bounding_box().height() < height_th && height_th > 0) {
151
    // For small blob, we simply set to BSTT_NONE.
152
0
    blobnbox->set_special_text_type(BSTT_NONE);
153
0
    return;
154
0
  }
155
156
0
  BLOB_CHOICE_LIST ratings_equ, ratings_lang;
157
0
  C_BLOB *blob = blobnbox->cblob();
158
  // TODO(joeliu/rays) Fix this. We may have to normalize separately for
159
  // each classifier here, as they may require different PolygonalCopy.
160
0
  TBLOB *tblob = TBLOB::PolygonalCopy(false, blob);
161
0
  const TBOX &box = tblob->bounding_box();
162
163
  // Normalize the blob. Set the origin to the place we want to be the
164
  // bottom-middle, and scaling is to make the height the x-height.
165
0
  const float scaling = static_cast<float>(kBlnXHeight) / box.height();
166
0
  const float x_orig = (box.left() + box.right()) / 2.0f, y_orig = box.bottom();
167
0
  std::unique_ptr<TBLOB> normed_blob(new TBLOB(*tblob));
168
0
  normed_blob->Normalize(nullptr, nullptr, nullptr, x_orig, y_orig, scaling, scaling, 0.0f,
169
0
                         static_cast<float>(kBlnBaselineOffset), false, nullptr);
170
0
  equ_tesseract_.AdaptiveClassifier(normed_blob.get(), &ratings_equ);
171
0
  lang_tesseract_->AdaptiveClassifier(normed_blob.get(), &ratings_lang);
172
0
  delete tblob;
173
174
  // Get the best choice from ratings_lang and rating_equ. As the choice in the
175
  // list has already been sorted by the certainty, we simply use the first
176
  // choice.
177
0
  BLOB_CHOICE *lang_choice = nullptr, *equ_choice = nullptr;
178
0
  if (ratings_lang.length() > 0) {
179
0
    BLOB_CHOICE_IT choice_it(&ratings_lang);
180
0
    lang_choice = choice_it.data();
181
0
  }
182
0
  if (ratings_equ.length() > 0) {
183
0
    BLOB_CHOICE_IT choice_it(&ratings_equ);
184
0
    equ_choice = choice_it.data();
185
0
  }
186
187
0
  const float lang_score = lang_choice ? lang_choice->certainty() : -FLT_MAX;
188
0
  const float equ_score = equ_choice ? equ_choice->certainty() : -FLT_MAX;
189
190
0
  const float kConfScoreTh = -5.0f, kConfDiffTh = 1.8;
191
  // The scores here are negative, so the max/min == fabs(min/max).
192
  // float ratio = fmax(lang_score, equ_score) / fmin(lang_score, equ_score);
193
0
  const float diff = std::fabs(lang_score - equ_score);
194
0
  BlobSpecialTextType type = BSTT_NONE;
195
196
  // Classification.
197
0
  if (std::fmax(lang_score, equ_score) < kConfScoreTh) {
198
    // If both score are very small, then mark it as unclear.
199
0
    type = BSTT_UNCLEAR;
200
0
  } else if (diff > kConfDiffTh && equ_score > lang_score) {
201
    // If equ_score is significantly higher, then we classify this character as
202
    // math symbol.
203
0
    type = BSTT_MATH;
204
0
  } else if (lang_choice) {
205
    // For other cases: lang_score is similar or significantly higher.
206
0
    type = EstimateTypeForUnichar(lang_tesseract_->unicharset, lang_choice->unichar_id());
207
0
  }
208
209
0
  if (type == BSTT_NONE &&
210
0
      lang_tesseract_->get_fontinfo_table().at(lang_choice->fontinfo_id()).is_italic()) {
211
    // For text symbol, we still check if it is italic.
212
0
    blobnbox->set_special_text_type(BSTT_ITALIC);
213
0
  } else {
214
0
    blobnbox->set_special_text_type(type);
215
0
  }
216
0
}
217
218
BlobSpecialTextType EquationDetect::EstimateTypeForUnichar(const UNICHARSET &unicharset,
219
0
                                                           const UNICHAR_ID id) const {
220
0
  const std::string s = unicharset.id_to_unichar(id);
221
0
  if (unicharset.get_isalpha(id)) {
222
0
    return BSTT_NONE;
223
0
  }
224
225
0
  if (unicharset.get_ispunctuation(id)) {
226
    // Exclude some special texts that are likely to be confused as math symbol.
227
0
    static std::vector<UNICHAR_ID> ids_to_exclude;
228
0
    if (ids_to_exclude.empty()) {
229
0
      static const char *kCharsToEx[] = {"'",  "`",  "\"", "\\", ",",  ".",
230
0
                                         "〈", "〉", "《", "》", "」", "「"};
231
0
      for (auto &i : kCharsToEx) {
232
0
        ids_to_exclude.push_back(unicharset.unichar_to_id(i));
233
0
      }
234
0
      std::sort(ids_to_exclude.begin(), ids_to_exclude.end());
235
0
    }
236
0
    auto found = std::binary_search(ids_to_exclude.begin(), ids_to_exclude.end(), id);
237
0
    return found ? BSTT_NONE : BSTT_MATH;
238
0
  }
239
240
  // Check if it is digit. In addition to the isdigit attribute, we also check
241
  // if this character belongs to those likely to be confused with a digit.
242
0
  static const char kDigitsChars[] = "|";
243
0
  if (unicharset.get_isdigit(id) || (s.length() == 1 && strchr(kDigitsChars, s[0]) != nullptr)) {
244
0
    return BSTT_DIGIT;
245
0
  } else {
246
0
    return BSTT_MATH;
247
0
  }
248
0
}
249
250
0
void EquationDetect::IdentifySpecialText() {
251
  // Set configuration for Tesseract::AdaptiveClassifier.
252
0
  equ_tesseract_.tess_cn_matching.set_value(true); // turn it on
253
0
  equ_tesseract_.tess_bn_matching.set_value(false);
254
255
  // Set the multiplier to zero for lang_tesseract_ to improve the accuracy.
256
0
  const int classify_class_pruner = lang_tesseract_->classify_class_pruner_multiplier;
257
0
  const int classify_integer_matcher = lang_tesseract_->classify_integer_matcher_multiplier;
258
0
  lang_tesseract_->classify_class_pruner_multiplier.set_value(0);
259
0
  lang_tesseract_->classify_integer_matcher_multiplier.set_value(0);
260
261
0
  ColPartitionGridSearch gsearch(part_grid_);
262
0
  ColPartition *part = nullptr;
263
0
  gsearch.StartFullSearch();
264
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
265
0
    if (!IsTextOrEquationType(part->type())) {
266
0
      continue;
267
0
    }
268
0
    IdentifyBlobsToSkip(part);
269
0
    BLOBNBOX_C_IT bbox_it(part->boxes());
270
    // Compute the height threshold.
271
0
    std::vector<int> blob_heights;
272
0
    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
273
0
      if (bbox_it.data()->special_text_type() != BSTT_SKIP) {
274
0
        blob_heights.push_back(bbox_it.data()->bounding_box().height());
275
0
      }
276
0
    }
277
0
    std::sort(blob_heights.begin(), blob_heights.end());
278
0
    const int height_th = blob_heights[blob_heights.size() / 2] / 3 * 2;
279
0
    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
280
0
      if (bbox_it.data()->special_text_type() != BSTT_SKIP) {
281
0
        IdentifySpecialText(bbox_it.data(), height_th);
282
0
      }
283
0
    }
284
0
  }
285
286
  // Set the multiplier values back.
287
0
  lang_tesseract_->classify_class_pruner_multiplier.set_value(classify_class_pruner);
288
0
  lang_tesseract_->classify_integer_matcher_multiplier.set_value(classify_integer_matcher);
289
290
0
  if (equationdetect_save_spt_image) { // For debug.
291
0
    std::string outfile;
292
0
    GetOutputTiffName("_spt", outfile);
293
0
    PaintSpecialTexts(outfile);
294
0
  }
295
0
}
296
297
0
void EquationDetect::IdentifyBlobsToSkip(ColPartition *part) {
298
0
  ASSERT_HOST(part);
299
0
  BLOBNBOX_C_IT blob_it(part->boxes());
300
301
0
  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
302
    // At this moment, no blob should have been joined.
303
0
    ASSERT_HOST(!blob_it.data()->joined_to_prev());
304
0
  }
305
0
  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
306
0
    BLOBNBOX *blob = blob_it.data();
307
0
    if (blob->joined_to_prev() || blob->special_text_type() == BSTT_SKIP) {
308
0
      continue;
309
0
    }
310
0
    TBOX blob_box = blob->bounding_box();
311
312
    // Search if any blob can be merged into blob. If found, then we mark all
313
    // these blobs as BSTT_SKIP.
314
0
    BLOBNBOX_C_IT blob_it2 = blob_it;
315
0
    bool found = false;
316
0
    while (!blob_it2.at_last()) {
317
0
      BLOBNBOX *nextblob = blob_it2.forward();
318
0
      const TBOX &nextblob_box = nextblob->bounding_box();
319
0
      if (nextblob_box.left() >= blob_box.right()) {
320
0
        break;
321
0
      }
322
0
      const float kWidthR = 0.4, kHeightR = 0.3;
323
0
      const bool xoverlap = blob_box.major_x_overlap(nextblob_box),
324
0
                 yoverlap = blob_box.y_overlap(nextblob_box);
325
0
      const float widthR = static_cast<float>(std::min(nextblob_box.width(), blob_box.width())) /
326
0
                           std::max(nextblob_box.width(), blob_box.width());
327
0
      const float heightR = static_cast<float>(std::min(nextblob_box.height(), blob_box.height())) /
328
0
                            std::max(nextblob_box.height(), blob_box.height());
329
330
0
      if (xoverlap && yoverlap && widthR > kWidthR && heightR > kHeightR) {
331
        // Found one, set nextblob type and recompute blob_box.
332
0
        found = true;
333
0
        nextblob->set_special_text_type(BSTT_SKIP);
334
0
        blob_box += nextblob_box;
335
0
      }
336
0
    }
337
0
    if (found) {
338
0
      blob->set_special_text_type(BSTT_SKIP);
339
0
    }
340
0
  }
341
0
}
342
343
0
int EquationDetect::FindEquationParts(ColPartitionGrid *part_grid, ColPartitionSet **best_columns) {
344
0
  if (!lang_tesseract_) {
345
0
    tprintf("Warning: lang_tesseract_ is nullptr!\n");
346
0
    return -1;
347
0
  }
348
0
  if (!part_grid || !best_columns) {
349
0
    tprintf("part_grid/best_columns is nullptr!!\n");
350
0
    return -1;
351
0
  }
352
0
  cp_seeds_.clear();
353
0
  part_grid_ = part_grid;
354
0
  best_columns_ = best_columns;
355
0
  resolution_ = lang_tesseract_->source_resolution();
356
0
  std::string outfile;
357
0
  page_count_++;
358
359
0
  if (equationdetect_save_bi_image) {
360
0
    GetOutputTiffName("_bi", outfile);
361
0
    pixWrite(outfile.c_str(), lang_tesseract_->pix_binary(), IFF_TIFF_G4);
362
0
  }
363
364
  // Pass 0: Compute special text type for blobs.
365
0
  IdentifySpecialText();
366
367
  // Pass 1: Merge parts by overlap.
368
0
  MergePartsByLocation();
369
370
  // Pass 2: compute the math blob density and find the seed partition.
371
0
  IdentifySeedParts();
372
  // We still need separate seed into block seed and inline seed partition.
373
0
  IdentifyInlineParts();
374
375
0
  if (equationdetect_save_seed_image) {
376
0
    GetOutputTiffName("_seed", outfile);
377
0
    PaintColParts(outfile);
378
0
  }
379
380
  // Pass 3: expand block equation seeds.
381
0
  while (!cp_seeds_.empty()) {
382
0
    std::vector<ColPartition *> seeds_expanded;
383
0
    for (auto &cp_seed : cp_seeds_) {
384
0
      if (ExpandSeed(cp_seed)) {
385
        // If this seed is expanded, then we add it into seeds_expanded. Note
386
        // this seed has been removed from part_grid_ if it is expanded.
387
0
        seeds_expanded.push_back(cp_seed);
388
0
      }
389
0
    }
390
    // Add seeds_expanded back into part_grid_ and reset cp_seeds_.
391
0
    for (auto &i : seeds_expanded) {
392
0
      InsertPartAfterAbsorb(i);
393
0
    }
394
0
    cp_seeds_ = std::move(seeds_expanded);
395
0
  }
396
397
  // Pass 4: find math block satellite text partitions and merge them.
398
0
  ProcessMathBlockSatelliteParts();
399
400
0
  if (equationdetect_save_merged_image) { // For debug.
401
0
    GetOutputTiffName("_merged", outfile);
402
0
    PaintColParts(outfile);
403
0
  }
404
405
0
  return 0;
406
0
}
407
408
0
void EquationDetect::MergePartsByLocation() {
409
0
  while (true) {
410
0
    ColPartition *part = nullptr;
411
    // partitions that have been updated.
412
0
    std::vector<ColPartition *> parts_updated;
413
0
    ColPartitionGridSearch gsearch(part_grid_);
414
0
    gsearch.StartFullSearch();
415
0
    while ((part = gsearch.NextFullSearch()) != nullptr) {
416
0
      if (!IsTextOrEquationType(part->type())) {
417
0
        continue;
418
0
      }
419
0
      std::vector<ColPartition *> parts_to_merge;
420
0
      SearchByOverlap(part, &parts_to_merge);
421
0
      if (parts_to_merge.empty()) {
422
0
        continue;
423
0
      }
424
425
      // Merge parts_to_merge with part, and remove them from part_grid_.
426
0
      part_grid_->RemoveBBox(part);
427
0
      for (auto &i : parts_to_merge) {
428
0
        ASSERT_HOST(i != nullptr && i != part);
429
0
        part->Absorb(i, nullptr);
430
0
      }
431
0
      gsearch.RepositionIterator();
432
433
0
      parts_updated.push_back(part);
434
0
    }
435
436
0
    if (parts_updated.empty()) { // Exit the loop
437
0
      break;
438
0
    }
439
440
    // Re-insert parts_updated into part_grid_.
441
0
    for (auto &i : parts_updated) {
442
0
      InsertPartAfterAbsorb(i);
443
0
    }
444
0
  }
445
0
}
446
447
void EquationDetect::SearchByOverlap(ColPartition *seed,
448
0
                                     std::vector<ColPartition *> *parts_overlap) {
449
0
  ASSERT_HOST(seed != nullptr && parts_overlap != nullptr);
450
0
  if (!IsTextOrEquationType(seed->type())) {
451
0
    return;
452
0
  }
453
0
  ColPartitionGridSearch search(part_grid_);
454
0
  const TBOX &seed_box(seed->bounding_box());
455
0
  const int kRadNeighborCells = 30;
456
0
  search.StartRadSearch((seed_box.left() + seed_box.right()) / 2,
457
0
                        (seed_box.top() + seed_box.bottom()) / 2, kRadNeighborCells);
458
0
  search.SetUniqueMode(true);
459
460
  // Search iteratively.
461
0
  ColPartition *part;
462
0
  std::vector<ColPartition *> parts;
463
0
  const float kLargeOverlapTh = 0.95;
464
0
  const float kEquXOverlap = 0.4, kEquYOverlap = 0.5;
465
0
  while ((part = search.NextRadSearch()) != nullptr) {
466
0
    if (part == seed || !IsTextOrEquationType(part->type())) {
467
0
      continue;
468
0
    }
469
0
    const TBOX &part_box(part->bounding_box());
470
0
    bool merge = false;
471
472
0
    const float x_overlap_fraction = part_box.x_overlap_fraction(seed_box),
473
0
                y_overlap_fraction = part_box.y_overlap_fraction(seed_box);
474
475
    // If part is large overlapped with seed, then set merge to true.
476
0
    if (x_overlap_fraction >= kLargeOverlapTh && y_overlap_fraction >= kLargeOverlapTh) {
477
0
      merge = true;
478
0
    } else if (seed->type() == PT_EQUATION && IsTextOrEquationType(part->type())) {
479
0
      if ((x_overlap_fraction > kEquXOverlap && y_overlap_fraction > 0.0) ||
480
0
          (x_overlap_fraction > 0.0 && y_overlap_fraction > kEquYOverlap)) {
481
0
        merge = true;
482
0
      }
483
0
    }
484
485
0
    if (merge) { // Remove the part from search and put it into parts.
486
0
      search.RemoveBBox();
487
0
      parts_overlap->push_back(part);
488
0
    }
489
0
  }
490
0
}
491
492
0
void EquationDetect::InsertPartAfterAbsorb(ColPartition *part) {
493
0
  ASSERT_HOST(part);
494
495
  // Before insert part back into part_grid_, we will need re-compute some
496
  // of its attributes such as first_column_, last_column_. However, we still
497
  // want to preserve its type.
498
0
  BlobTextFlowType flow_type = part->flow();
499
0
  PolyBlockType part_type = part->type();
500
0
  BlobRegionType blob_type = part->blob_type();
501
502
  // Call SetPartitionType to re-compute the attributes of part.
503
0
  const TBOX &part_box(part->bounding_box());
504
0
  int grid_x, grid_y;
505
0
  part_grid_->GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);
506
0
  part->SetPartitionType(resolution_, best_columns_[grid_y]);
507
508
  // Reset the types back.
509
0
  part->set_type(part_type);
510
0
  part->set_blob_type(blob_type);
511
0
  part->set_flow(flow_type);
512
0
  part->SetBlobTypes();
513
514
  // Insert into part_grid_.
515
0
  part_grid_->InsertBBox(true, true, part);
516
0
}
517
518
0
void EquationDetect::IdentifySeedParts() {
519
0
  ColPartitionGridSearch gsearch(part_grid_);
520
0
  ColPartition *part = nullptr;
521
0
  gsearch.StartFullSearch();
522
523
0
  std::vector<ColPartition *> seeds1, seeds2;
524
  // The left coordinates of indented text partitions.
525
0
  std::vector<int> indented_texts_left;
526
  // The foreground density of text partitions.
527
0
  std::vector<float> texts_foreground_density;
528
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
529
0
    if (!IsTextOrEquationType(part->type())) {
530
0
      continue;
531
0
    }
532
0
    part->ComputeSpecialBlobsDensity();
533
0
    const bool blobs_check = CheckSeedBlobsCount(part);
534
0
    const int kTextBlobsTh = 20;
535
536
0
    if (CheckSeedDensity(kMathDigitDensityTh1, kMathDigitDensityTh2, part) && blobs_check) {
537
      // Passed high density threshold test, save into seeds1.
538
0
      seeds1.push_back(part);
539
0
    } else {
540
0
      IndentType indent = IsIndented(part);
541
0
      if (IsLeftIndented(indent) && blobs_check &&
542
0
          CheckSeedDensity(kMathDigitDensityTh2, kMathDigitDensityTh2, part)) {
543
        // Passed low density threshold test and is indented, save into seeds2.
544
0
        seeds2.push_back(part);
545
0
      } else if (!IsRightIndented(indent) && part->boxes_count() > kTextBlobsTh) {
546
        // This is likely to be a text part, save the features.
547
0
        const TBOX &box = part->bounding_box();
548
0
        if (IsLeftIndented(indent)) {
549
0
          indented_texts_left.push_back(box.left());
550
0
        }
551
0
        texts_foreground_density.push_back(ComputeForegroundDensity(box));
552
0
      }
553
0
    }
554
0
  }
555
556
  // Sort the features collected from text regions.
557
0
  std::sort(indented_texts_left.begin(), indented_texts_left.end());
558
0
  std::sort(texts_foreground_density.begin(), texts_foreground_density.end());
559
0
  float foreground_density_th = 0.15; // Default value.
560
0
  if (!texts_foreground_density.empty()) {
561
    // Use the median of the texts_foreground_density.
562
0
    foreground_density_th = 0.8 * texts_foreground_density[texts_foreground_density.size() / 2];
563
0
  }
564
565
0
  for (auto &i : seeds1) {
566
0
    const TBOX &box = i->bounding_box();
567
0
    if (CheckSeedFgDensity(foreground_density_th, i) &&
568
0
        !(IsLeftIndented(IsIndented(i)) &&
569
0
          CountAlignment(indented_texts_left, box.left()) >= kLeftIndentAlignmentCountTh)) {
570
      // Mark as PT_EQUATION type.
571
0
      i->set_type(PT_EQUATION);
572
0
      cp_seeds_.push_back(i);
573
0
    } else { // Mark as PT_INLINE_EQUATION type.
574
0
      i->set_type(PT_INLINE_EQUATION);
575
0
    }
576
0
  }
577
578
0
  for (auto &i : seeds2) {
579
0
    if (CheckForSeed2(indented_texts_left, foreground_density_th, i)) {
580
0
      i->set_type(PT_EQUATION);
581
0
      cp_seeds_.push_back(i);
582
0
    }
583
0
  }
584
0
}
585
586
0
float EquationDetect::ComputeForegroundDensity(const TBOX &tbox) {
587
0
  Image pix_bi = lang_tesseract_->pix_binary();
588
0
  const int pix_height = pixGetHeight(pix_bi);
589
0
  Box *box = boxCreate(tbox.left(), pix_height - tbox.top(), tbox.width(), tbox.height());
590
0
  Image pix_sub = pixClipRectangle(pix_bi, box, nullptr);
591
0
  l_float32 fract;
592
0
  pixForegroundFraction(pix_sub, &fract);
593
0
  pix_sub.destroy();
594
0
  boxDestroy(&box);
595
596
0
  return fract;
597
0
}
598
599
0
bool EquationDetect::CheckSeedFgDensity(const float density_th, ColPartition *part) {
600
0
  ASSERT_HOST(part);
601
602
  // Split part horizontall, and check for each sub part.
603
0
  std::vector<TBOX> sub_boxes;
604
0
  SplitCPHorLite(part, &sub_boxes);
605
0
  float parts_passed = 0.0;
606
0
  for (auto &sub_boxe : sub_boxes) {
607
0
    const float density = ComputeForegroundDensity(sub_boxe);
608
0
    if (density < density_th) {
609
0
      parts_passed++;
610
0
    }
611
0
  }
612
613
  // If most sub parts passed, then we return true.
614
0
  const float kSeedPartRatioTh = 0.3;
615
0
  bool retval = (parts_passed / sub_boxes.size() >= kSeedPartRatioTh);
616
617
0
  return retval;
618
0
}
619
620
0
void EquationDetect::SplitCPHor(ColPartition *part, std::vector<ColPartition *> *parts_splitted) {
621
0
  ASSERT_HOST(part && parts_splitted);
622
0
  if (part->median_width() == 0 || part->boxes_count() == 0) {
623
0
    return;
624
0
  }
625
626
  // Make a copy of part, and reset parts_splitted.
627
0
  ColPartition *right_part = part->CopyButDontOwnBlobs();
628
0
  for (auto data : *parts_splitted) {
629
0
    delete data;
630
0
  }
631
0
  parts_splitted->clear();
632
633
0
  const double kThreshold = part->median_width() * 3.0;
634
0
  bool found_split = true;
635
0
  while (found_split) {
636
0
    found_split = false;
637
0
    BLOBNBOX_C_IT box_it(right_part->boxes());
638
    // Blobs are sorted left side first. If blobs overlap,
639
    // the previous blob may have a "more right" right side.
640
    // Account for this by always keeping the largest "right"
641
    // so far.
642
0
    int previous_right = INT32_MIN;
643
644
    // Look for the next split in the partition.
645
0
    for (box_it.mark_cycle_pt(); !box_it.cycled_list(); box_it.forward()) {
646
0
      const TBOX &box = box_it.data()->bounding_box();
647
0
      if (previous_right != INT32_MIN && box.left() - previous_right > kThreshold) {
648
        // We have a split position. Split the partition in two pieces.
649
        // Insert the left piece in the grid and keep processing the right.
650
0
        const int mid_x = (box.left() + previous_right) / 2;
651
0
        ColPartition *left_part = right_part;
652
0
        right_part = left_part->SplitAt(mid_x);
653
654
0
        parts_splitted->push_back(left_part);
655
0
        left_part->ComputeSpecialBlobsDensity();
656
0
        found_split = true;
657
0
        break;
658
0
      }
659
660
      // The right side of the previous blobs.
661
0
      previous_right = std::max(previous_right, static_cast<int>(box.right()));
662
0
    }
663
0
  }
664
665
  // Add the last piece.
666
0
  right_part->ComputeSpecialBlobsDensity();
667
0
  parts_splitted->push_back(right_part);
668
0
}
669
670
0
void EquationDetect::SplitCPHorLite(ColPartition *part, std::vector<TBOX> *splitted_boxes) {
671
0
  ASSERT_HOST(part && splitted_boxes);
672
0
  splitted_boxes->clear();
673
0
  if (part->median_width() == 0) {
674
0
    return;
675
0
  }
676
677
0
  const double kThreshold = part->median_width() * 3.0;
678
679
  // Blobs are sorted left side first. If blobs overlap,
680
  // the previous blob may have a "more right" right side.
681
  // Account for this by always keeping the largest "right"
682
  // so far.
683
0
  TBOX union_box;
684
0
  int previous_right = INT32_MIN;
685
0
  BLOBNBOX_C_IT box_it(part->boxes());
686
0
  for (box_it.mark_cycle_pt(); !box_it.cycled_list(); box_it.forward()) {
687
0
    const TBOX &box = box_it.data()->bounding_box();
688
0
    if (previous_right != INT32_MIN && box.left() - previous_right > kThreshold) {
689
      // We have a split position.
690
0
      splitted_boxes->push_back(union_box);
691
0
      previous_right = INT32_MIN;
692
0
    }
693
0
    if (previous_right == INT32_MIN) {
694
0
      union_box = box;
695
0
    } else {
696
0
      union_box += box;
697
0
    }
698
    // The right side of the previous blobs.
699
0
    previous_right = std::max(previous_right, static_cast<int>(box.right()));
700
0
  }
701
702
  // Add the last piece.
703
0
  if (previous_right != INT32_MIN) {
704
0
    splitted_boxes->push_back(union_box);
705
0
  }
706
0
}
707
708
bool EquationDetect::CheckForSeed2(const std::vector<int> &indented_texts_left,
709
0
                                   const float foreground_density_th, ColPartition *part) {
710
0
  ASSERT_HOST(part);
711
0
  const TBOX &box = part->bounding_box();
712
713
  // Check if it is aligned with any indented_texts_left.
714
0
  if (!indented_texts_left.empty() &&
715
0
      CountAlignment(indented_texts_left, box.left()) >= kLeftIndentAlignmentCountTh) {
716
0
    return false;
717
0
  }
718
719
  // Check the foreground density.
720
0
  if (ComputeForegroundDensity(box) > foreground_density_th) {
721
0
    return false;
722
0
  }
723
724
0
  return true;
725
0
}
726
727
0
int EquationDetect::CountAlignment(const std::vector<int> &sorted_vec, const int val) const {
728
0
  if (sorted_vec.empty()) {
729
0
    return 0;
730
0
  }
731
0
  const int kDistTh = static_cast<int>(std::round(0.03f * resolution_));
732
0
  auto pos = std::upper_bound(sorted_vec.begin(), sorted_vec.end(), val);
733
0
  if (pos > sorted_vec.begin()) {
734
0
    --pos;
735
0
  }
736
0
  int count = 0;
737
738
  // Search left side.
739
0
  auto index = pos - sorted_vec.begin();
740
0
  while (index >= 0 && abs(val - sorted_vec[index--]) < kDistTh) {
741
0
    count++;
742
0
  }
743
744
  // Search right side.
745
0
  index = pos + 1 - sorted_vec.begin();
746
0
  while (static_cast<size_t>(index) < sorted_vec.size() && sorted_vec[index++] - val < kDistTh) {
747
0
    count++;
748
0
  }
749
750
0
  return count;
751
0
}
752
753
0
void EquationDetect::IdentifyInlineParts() {
754
0
  ComputeCPsSuperBBox();
755
0
  IdentifyInlinePartsHorizontal();
756
0
  const int textparts_linespacing = EstimateTextPartLineSpacing();
757
0
  IdentifyInlinePartsVertical(true, textparts_linespacing);
758
0
  IdentifyInlinePartsVertical(false, textparts_linespacing);
759
0
}
760
761
0
void EquationDetect::ComputeCPsSuperBBox() {
762
0
  ColPartitionGridSearch gsearch(part_grid_);
763
0
  ColPartition *part = nullptr;
764
0
  gsearch.StartFullSearch();
765
0
  delete cps_super_bbox_;
766
0
  cps_super_bbox_ = new TBOX();
767
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
768
0
    (*cps_super_bbox_) += part->bounding_box();
769
0
  }
770
0
}
771
772
0
void EquationDetect::IdentifyInlinePartsHorizontal() {
773
0
  ASSERT_HOST(cps_super_bbox_);
774
0
  std::vector<ColPartition *> new_seeds;
775
0
  const int kMarginDiffTh = IntCastRounded(0.5 * lang_tesseract_->source_resolution());
776
0
  const int kGapTh = static_cast<int>(std::round(1.0f * lang_tesseract_->source_resolution()));
777
0
  ColPartitionGridSearch search(part_grid_);
778
0
  search.SetUniqueMode(true);
779
  // The center x coordinate of the cp_super_bbox_.
780
0
  const int cps_cx = cps_super_bbox_->left() + cps_super_bbox_->width() / 2;
781
0
  for (auto part : cp_seeds_) {
782
0
    const TBOX &part_box(part->bounding_box());
783
0
    const int left_margin = part_box.left() - cps_super_bbox_->left(),
784
0
              right_margin = cps_super_bbox_->right() - part_box.right();
785
0
    bool right_to_left;
786
0
    if (left_margin + kMarginDiffTh < right_margin && left_margin < kMarginDiffTh) {
787
      // part is left aligned, so we search if it has any right neighbor.
788
0
      search.StartSideSearch(part_box.right(), part_box.top(), part_box.bottom());
789
0
      right_to_left = false;
790
0
    } else if (left_margin > cps_cx) {
791
      // part locates on the right half on image, so search if it has any left
792
      // neighbor.
793
0
      search.StartSideSearch(part_box.left(), part_box.top(), part_box.bottom());
794
0
      right_to_left = true;
795
0
    } else { // part is not an inline equation.
796
0
      new_seeds.push_back(part);
797
0
      continue;
798
0
    }
799
0
    ColPartition *neighbor = nullptr;
800
0
    bool side_neighbor_found = false;
801
0
    while ((neighbor = search.NextSideSearch(right_to_left)) != nullptr) {
802
0
      const TBOX &neighbor_box(neighbor->bounding_box());
803
0
      if (!IsTextOrEquationType(neighbor->type()) || part_box.x_gap(neighbor_box) > kGapTh ||
804
0
          !part_box.major_y_overlap(neighbor_box) || part_box.major_x_overlap(neighbor_box)) {
805
0
        continue;
806
0
      }
807
      // We have found one. Set the side_neighbor_found flag.
808
0
      side_neighbor_found = true;
809
0
      break;
810
0
    }
811
0
    if (!side_neighbor_found) { // Mark part as PT_INLINE_EQUATION.
812
0
      part->set_type(PT_INLINE_EQUATION);
813
0
    } else {
814
      // Check the geometric feature of neighbor.
815
0
      const TBOX &neighbor_box(neighbor->bounding_box());
816
0
      if (neighbor_box.width() > part_box.width() &&
817
0
          neighbor->type() != PT_EQUATION) { // Mark as PT_INLINE_EQUATION.
818
0
        part->set_type(PT_INLINE_EQUATION);
819
0
      } else { // part is not an inline equation type.
820
0
        new_seeds.push_back(part);
821
0
      }
822
0
    }
823
0
  }
824
825
  // Reset the cp_seeds_ using the new_seeds.
826
0
  cp_seeds_ = std::move(new_seeds);
827
0
}
828
829
0
int EquationDetect::EstimateTextPartLineSpacing() {
830
0
  ColPartitionGridSearch gsearch(part_grid_);
831
832
  // Get the y gap between text partitions;
833
0
  ColPartition *current = nullptr, *prev = nullptr;
834
0
  gsearch.StartFullSearch();
835
0
  std::vector<int> ygaps;
836
0
  while ((current = gsearch.NextFullSearch()) != nullptr) {
837
0
    if (!PTIsTextType(current->type())) {
838
0
      continue;
839
0
    }
840
0
    if (prev != nullptr) {
841
0
      const TBOX &current_box = current->bounding_box();
842
0
      const TBOX &prev_box = prev->bounding_box();
843
      // prev and current should be x major overlap and non y overlap.
844
0
      if (current_box.major_x_overlap(prev_box) && !current_box.y_overlap(prev_box)) {
845
0
        int gap = current_box.y_gap(prev_box);
846
0
        if (gap < std::min(current_box.height(), prev_box.height())) {
847
          // The gap should be smaller than the height of the bounding boxes.
848
0
          ygaps.push_back(gap);
849
0
        }
850
0
      }
851
0
    }
852
0
    prev = current;
853
0
  }
854
855
0
  if (ygaps.size() < 8) { // We do not have enough data.
856
0
    return -1;
857
0
  }
858
859
  // Compute the line spacing from ygaps: use the mean of the first half.
860
0
  std::sort(ygaps.begin(), ygaps.end());
861
0
  int spacing = 0;
862
0
  unsigned count;
863
0
  for (count = 0; count < ygaps.size() / 2; count++) {
864
0
    spacing += ygaps[count];
865
0
  }
866
0
  return spacing / count;
867
0
}
868
869
void EquationDetect::IdentifyInlinePartsVertical(const bool top_to_bottom,
870
0
                                                 const int textparts_linespacing) {
871
0
  if (cp_seeds_.empty()) {
872
0
    return;
873
0
  }
874
875
  // Sort cp_seeds_.
876
0
  if (top_to_bottom) { // From top to bottom.
877
0
    std::sort(cp_seeds_.begin(), cp_seeds_.end(), &SortCPByTopReverse);
878
0
  } else { // From bottom to top.
879
0
    std::sort(cp_seeds_.begin(), cp_seeds_.end(), &SortCPByBottom);
880
0
  }
881
882
0
  std::vector<ColPartition *> new_seeds;
883
0
  for (auto part : cp_seeds_) {
884
    // If we sort cp_seeds_ from top to bottom, then for each cp_seeds_, we look
885
    // for its top neighbors, so that if two/more inline regions are connected
886
    // to each other, then we will identify the top one, and then use it to
887
    // identify the bottom one.
888
0
    if (IsInline(!top_to_bottom, textparts_linespacing, part)) {
889
0
      part->set_type(PT_INLINE_EQUATION);
890
0
    } else {
891
0
      new_seeds.push_back(part);
892
0
    }
893
0
  }
894
0
  cp_seeds_ = std::move(new_seeds);
895
0
}
896
897
bool EquationDetect::IsInline(const bool search_bottom, const int textparts_linespacing,
898
0
                              ColPartition *part) {
899
0
  ASSERT_HOST(part != nullptr);
900
  // Look for its nearest vertical neighbor that hardly overlaps in y but
901
  // largely overlaps in x.
902
0
  ColPartitionGridSearch search(part_grid_);
903
0
  ColPartition *neighbor = nullptr;
904
0
  const TBOX &part_box(part->bounding_box());
905
0
  const float kYGapRatioTh = 1.0;
906
907
0
  if (search_bottom) {
908
0
    search.StartVerticalSearch(part_box.left(), part_box.right(), part_box.bottom());
909
0
  } else {
910
0
    search.StartVerticalSearch(part_box.left(), part_box.right(), part_box.top());
911
0
  }
912
0
  search.SetUniqueMode(true);
913
0
  while ((neighbor = search.NextVerticalSearch(search_bottom)) != nullptr) {
914
0
    const TBOX &neighbor_box(neighbor->bounding_box());
915
0
    if (part_box.y_gap(neighbor_box) >
916
0
        kYGapRatioTh * std::min(part_box.height(), neighbor_box.height())) {
917
      // Finished searching.
918
0
      break;
919
0
    }
920
0
    if (!PTIsTextType(neighbor->type())) {
921
0
      continue;
922
0
    }
923
924
    // Check if neighbor and part is inline similar.
925
0
    const float kHeightRatioTh = 0.5;
926
0
    const int kYGapTh = textparts_linespacing > 0
927
0
                            ? textparts_linespacing + static_cast<int>(std::round(0.02f * resolution_))
928
0
                            : static_cast<int>(std::round(0.05f * resolution_)); // Default value.
929
0
    if (part_box.x_overlap(neighbor_box) &&                                 // Location feature.
930
0
        part_box.y_gap(neighbor_box) <= kYGapTh &&                          // Line spacing.
931
        // Geo feature.
932
0
        static_cast<float>(std::min(part_box.height(), neighbor_box.height())) /
933
0
                std::max(part_box.height(), neighbor_box.height()) >
934
0
            kHeightRatioTh) {
935
0
      return true;
936
0
    }
937
0
  }
938
939
0
  return false;
940
0
}
941
942
0
bool EquationDetect::CheckSeedBlobsCount(ColPartition *part) {
943
0
  if (!part) {
944
0
    return false;
945
0
  }
946
0
  const int kSeedMathBlobsCount = 2;
947
0
  const int kSeedMathDigitBlobsCount = 5;
948
949
0
  const int blobs = part->boxes_count(), math_blobs = part->SpecialBlobsCount(BSTT_MATH),
950
0
            digit_blobs = part->SpecialBlobsCount(BSTT_DIGIT);
951
0
  if (blobs < kSeedBlobsCountTh || math_blobs <= kSeedMathBlobsCount ||
952
0
      math_blobs + digit_blobs <= kSeedMathDigitBlobsCount) {
953
0
    return false;
954
0
  }
955
956
0
  return true;
957
0
}
958
959
bool EquationDetect::CheckSeedDensity(const float math_density_high, const float math_density_low,
960
0
                                      const ColPartition *part) const {
961
0
  ASSERT_HOST(part);
962
0
  float math_digit_density =
963
0
      part->SpecialBlobsDensity(BSTT_MATH) + part->SpecialBlobsDensity(BSTT_DIGIT);
964
0
  float italic_density = part->SpecialBlobsDensity(BSTT_ITALIC);
965
0
  if (math_digit_density > math_density_high) {
966
0
    return true;
967
0
  }
968
0
  if (math_digit_density + italic_density > kMathItalicDensityTh &&
969
0
      math_digit_density > math_density_low) {
970
0
    return true;
971
0
  }
972
973
0
  return false;
974
0
}
975
976
0
EquationDetect::IndentType EquationDetect::IsIndented(ColPartition *part) {
977
0
  ASSERT_HOST(part);
978
979
0
  ColPartitionGridSearch search(part_grid_);
980
0
  ColPartition *neighbor = nullptr;
981
0
  const TBOX &part_box(part->bounding_box());
982
0
  const int kXGapTh = static_cast<int>(std::round(0.5f * resolution_));
983
0
  const int kRadiusTh = static_cast<int>(std::round(3.0f * resolution_));
984
0
  const int kYGapTh = static_cast<int>(std::round(0.5f * resolution_));
985
986
  // Here we use a simple approximation algorithm: from the center of part, We
987
  // perform the radius search, and check if we can find a neighboring partition
988
  // that locates on the top/bottom left of part.
989
0
  search.StartRadSearch((part_box.left() + part_box.right()) / 2,
990
0
                        (part_box.top() + part_box.bottom()) / 2, kRadiusTh);
991
0
  search.SetUniqueMode(true);
992
0
  bool left_indented = false, right_indented = false;
993
0
  while ((neighbor = search.NextRadSearch()) != nullptr && (!left_indented || !right_indented)) {
994
0
    if (neighbor == part) {
995
0
      continue;
996
0
    }
997
0
    const TBOX &neighbor_box(neighbor->bounding_box());
998
999
0
    if (part_box.major_y_overlap(neighbor_box) && part_box.x_gap(neighbor_box) < kXGapTh) {
1000
      // When this happens, it is likely part is a fragment of an
1001
      // over-segmented colpartition. So we return false.
1002
0
      return NO_INDENT;
1003
0
    }
1004
1005
0
    if (!IsTextOrEquationType(neighbor->type())) {
1006
0
      continue;
1007
0
    }
1008
1009
    // The neighbor should be above/below part, and overlap in x direction.
1010
0
    if (!part_box.x_overlap(neighbor_box) || part_box.y_overlap(neighbor_box)) {
1011
0
      continue;
1012
0
    }
1013
1014
0
    if (part_box.y_gap(neighbor_box) < kYGapTh) {
1015
0
      const int left_gap = part_box.left() - neighbor_box.left();
1016
0
      const int right_gap = neighbor_box.right() - part_box.right();
1017
0
      if (left_gap > kXGapTh) {
1018
0
        left_indented = true;
1019
0
      }
1020
0
      if (right_gap > kXGapTh) {
1021
0
        right_indented = true;
1022
0
      }
1023
0
    }
1024
0
  }
1025
1026
0
  if (left_indented && right_indented) {
1027
0
    return BOTH_INDENT;
1028
0
  }
1029
0
  if (left_indented) {
1030
0
    return LEFT_INDENT;
1031
0
  }
1032
0
  if (right_indented) {
1033
0
    return RIGHT_INDENT;
1034
0
  }
1035
0
  return NO_INDENT;
1036
0
}
1037
1038
0
bool EquationDetect::ExpandSeed(ColPartition *seed) {
1039
0
  if (seed == nullptr ||        // This seed has been absorbed by other seeds.
1040
0
      seed->IsVerticalType()) { // We skip vertical type right now.
1041
0
    return false;
1042
0
  }
1043
1044
  // Expand in four directions.
1045
0
  std::vector<ColPartition *> parts_to_merge;
1046
0
  ExpandSeedHorizontal(true, seed, &parts_to_merge);
1047
0
  ExpandSeedHorizontal(false, seed, &parts_to_merge);
1048
0
  ExpandSeedVertical(true, seed, &parts_to_merge);
1049
0
  ExpandSeedVertical(false, seed, &parts_to_merge);
1050
0
  SearchByOverlap(seed, &parts_to_merge);
1051
1052
0
  if (parts_to_merge.empty()) { // We don't find any partition to merge.
1053
0
    return false;
1054
0
  }
1055
1056
  // Merge all partitions in parts_to_merge with seed. We first remove seed
1057
  // from part_grid_ as its bounding box is going to expand. Then we add it
1058
  // back after it absorbs all parts_to_merge partitions.
1059
0
  part_grid_->RemoveBBox(seed);
1060
0
  for (auto part : parts_to_merge) {
1061
0
    if (part->type() == PT_EQUATION) {
1062
      // If part is in cp_seeds_, then we mark it as nullptr so that we won't
1063
      // process it again.
1064
0
      for (auto &cp_seed : cp_seeds_) {
1065
0
        if (part == cp_seed) {
1066
0
          cp_seed = nullptr;
1067
0
          break;
1068
0
        }
1069
0
      }
1070
0
    }
1071
1072
    // part has already been removed from part_grid_ in function
1073
    // ExpandSeedHorizontal/ExpandSeedVertical.
1074
0
    seed->Absorb(part, nullptr);
1075
0
  }
1076
1077
0
  return true;
1078
0
}
1079
1080
void EquationDetect::ExpandSeedHorizontal(const bool search_left, ColPartition *seed,
1081
0
                                          std::vector<ColPartition *> *parts_to_merge) {
1082
0
  ASSERT_HOST(seed != nullptr && parts_to_merge != nullptr);
1083
0
  const float kYOverlapTh = 0.6;
1084
0
  const int kXGapTh = static_cast<int>(std::round(0.2f * resolution_));
1085
1086
0
  ColPartitionGridSearch search(part_grid_);
1087
0
  const TBOX &seed_box(seed->bounding_box());
1088
0
  const int x = search_left ? seed_box.left() : seed_box.right();
1089
0
  search.StartSideSearch(x, seed_box.bottom(), seed_box.top());
1090
0
  search.SetUniqueMode(true);
1091
1092
  // Search iteratively.
1093
0
  ColPartition *part = nullptr;
1094
0
  while ((part = search.NextSideSearch(search_left)) != nullptr) {
1095
0
    if (part == seed) {
1096
0
      continue;
1097
0
    }
1098
0
    const TBOX &part_box(part->bounding_box());
1099
0
    if (part_box.x_gap(seed_box) > kXGapTh) { // Out of scope.
1100
0
      break;
1101
0
    }
1102
1103
    // Check part location.
1104
0
    if ((part_box.left() >= seed_box.left() && search_left) ||
1105
0
        (part_box.right() <= seed_box.right() && !search_left)) {
1106
0
      continue;
1107
0
    }
1108
1109
0
    if (part->type() != PT_EQUATION) { // Non-equation type.
1110
      // Skip PT_LINLINE_EQUATION and non text type.
1111
0
      if (part->type() == PT_INLINE_EQUATION ||
1112
0
          (!IsTextOrEquationType(part->type()) && part->blob_type() != BRT_HLINE)) {
1113
0
        continue;
1114
0
      }
1115
      // For other types, it should be the near small neighbor of seed.
1116
0
      if (!IsNearSmallNeighbor(seed_box, part_box) || !CheckSeedNeighborDensity(part)) {
1117
0
        continue;
1118
0
      }
1119
0
    } else { // Equation type, check the y overlap.
1120
0
      if (part_box.y_overlap_fraction(seed_box) < kYOverlapTh &&
1121
0
          seed_box.y_overlap_fraction(part_box) < kYOverlapTh) {
1122
0
        continue;
1123
0
      }
1124
0
    }
1125
1126
    // Passed the check, delete it from search and add into parts_to_merge.
1127
0
    search.RemoveBBox();
1128
0
    parts_to_merge->push_back(part);
1129
0
  }
1130
0
}
1131
1132
void EquationDetect::ExpandSeedVertical(const bool search_bottom, ColPartition *seed,
1133
0
                                        std::vector<ColPartition *> *parts_to_merge) {
1134
0
  ASSERT_HOST(seed != nullptr && parts_to_merge != nullptr && cps_super_bbox_ != nullptr);
1135
0
  const float kXOverlapTh = 0.4;
1136
0
  const int kYGapTh = static_cast<int>(std::round(0.2f * resolution_));
1137
1138
0
  ColPartitionGridSearch search(part_grid_);
1139
0
  const TBOX &seed_box(seed->bounding_box());
1140
0
  const int y = search_bottom ? seed_box.bottom() : seed_box.top();
1141
0
  search.StartVerticalSearch(cps_super_bbox_->left(), cps_super_bbox_->right(), y);
1142
0
  search.SetUniqueMode(true);
1143
1144
  // Search iteratively.
1145
0
  ColPartition *part = nullptr;
1146
0
  std::vector<ColPartition *> parts;
1147
0
  int skipped_min_top = std::numeric_limits<int>::max(), skipped_max_bottom = -1;
1148
0
  while ((part = search.NextVerticalSearch(search_bottom)) != nullptr) {
1149
0
    if (part == seed) {
1150
0
      continue;
1151
0
    }
1152
0
    const TBOX &part_box(part->bounding_box());
1153
1154
0
    if (part_box.y_gap(seed_box) > kYGapTh) { // Out of scope.
1155
0
      break;
1156
0
    }
1157
1158
    // Check part location.
1159
0
    if ((part_box.bottom() >= seed_box.bottom() && search_bottom) ||
1160
0
        (part_box.top() <= seed_box.top() && !search_bottom)) {
1161
0
      continue;
1162
0
    }
1163
1164
0
    bool skip_part = false;
1165
0
    if (part->type() != PT_EQUATION) { // Non-equation type.
1166
      // Skip PT_LINLINE_EQUATION and non text type.
1167
0
      if (part->type() == PT_INLINE_EQUATION ||
1168
0
          (!IsTextOrEquationType(part->type()) && part->blob_type() != BRT_HLINE)) {
1169
0
        skip_part = true;
1170
0
      } else if (!IsNearSmallNeighbor(seed_box, part_box) || !CheckSeedNeighborDensity(part)) {
1171
        // For other types, it should be the near small neighbor of seed.
1172
0
        skip_part = true;
1173
0
      }
1174
0
    } else { // Equation type, check the x overlap.
1175
0
      if (part_box.x_overlap_fraction(seed_box) < kXOverlapTh &&
1176
0
          seed_box.x_overlap_fraction(part_box) < kXOverlapTh) {
1177
0
        skip_part = true;
1178
0
      }
1179
0
    }
1180
0
    if (skip_part) {
1181
0
      if (part->type() != PT_EQUATION) {
1182
0
        if (skipped_min_top > part_box.top()) {
1183
0
          skipped_min_top = part_box.top();
1184
0
        }
1185
0
        if (skipped_max_bottom < part_box.bottom()) {
1186
0
          skipped_max_bottom = part_box.bottom();
1187
0
        }
1188
0
      }
1189
0
    } else {
1190
0
      parts.push_back(part);
1191
0
    }
1192
0
  }
1193
1194
  // For every part in parts, we need verify it is not above skipped_min_top
1195
  // when search top, or not below skipped_max_bottom when search bottom. I.e.,
1196
  // we will skip a part if it looks like:
1197
  //             search bottom      |         search top
1198
  // seed:     ******************   | part:    **********
1199
  // skipped: xxx                   | skipped:  xxx
1200
  // part:       **********         | seed:    ***********
1201
0
  for (auto &part : parts) {
1202
0
    const TBOX &part_box(part->bounding_box());
1203
0
    if ((search_bottom && part_box.top() <= skipped_max_bottom) ||
1204
0
        (!search_bottom && part_box.bottom() >= skipped_min_top)) {
1205
0
      continue;
1206
0
    }
1207
    // Add parts[i] into parts_to_merge, and delete it from part_grid_.
1208
0
    parts_to_merge->push_back(part);
1209
0
    part_grid_->RemoveBBox(part);
1210
0
  }
1211
0
}
1212
1213
0
bool EquationDetect::IsNearSmallNeighbor(const TBOX &seed_box, const TBOX &part_box) const {
1214
0
  const int kXGapTh = static_cast<int>(std::round(0.25f * resolution_));
1215
0
  const int kYGapTh = static_cast<int>(std::round(0.05f * resolution_));
1216
1217
  // Check geometric feature.
1218
0
  if (part_box.height() > seed_box.height() || part_box.width() > seed_box.width()) {
1219
0
    return false;
1220
0
  }
1221
1222
  // Check overlap and distance.
1223
0
  if ((!part_box.major_x_overlap(seed_box) || part_box.y_gap(seed_box) > kYGapTh) &&
1224
0
      (!part_box.major_y_overlap(seed_box) || part_box.x_gap(seed_box) > kXGapTh)) {
1225
0
    return false;
1226
0
  }
1227
1228
0
  return true;
1229
0
}
1230
1231
0
bool EquationDetect::CheckSeedNeighborDensity(const ColPartition *part) const {
1232
0
  ASSERT_HOST(part);
1233
0
  if (part->boxes_count() < kSeedBlobsCountTh) {
1234
    // Too few blobs, skip the check.
1235
0
    return true;
1236
0
  }
1237
1238
  // We check the math blobs density and the unclear blobs density.
1239
0
  if (part->SpecialBlobsDensity(BSTT_MATH) + part->SpecialBlobsDensity(BSTT_DIGIT) >
1240
0
          kMathDigitDensityTh1 ||
1241
0
      part->SpecialBlobsDensity(BSTT_UNCLEAR) > kUnclearDensityTh) {
1242
0
    return true;
1243
0
  }
1244
1245
0
  return false;
1246
0
}
1247
1248
0
void EquationDetect::ProcessMathBlockSatelliteParts() {
1249
  // Iterate over part_grid_, and find all parts that are text type but not
1250
  // equation type.
1251
0
  ColPartition *part = nullptr;
1252
0
  std::vector<ColPartition *> text_parts;
1253
0
  ColPartitionGridSearch gsearch(part_grid_);
1254
0
  gsearch.StartFullSearch();
1255
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1256
0
    if (part->type() == PT_FLOWING_TEXT || part->type() == PT_HEADING_TEXT) {
1257
0
      text_parts.push_back(part);
1258
0
    }
1259
0
  }
1260
0
  if (text_parts.empty()) {
1261
0
    return;
1262
0
  }
1263
1264
  // Compute the medium height of the text_parts.
1265
0
  std::sort(text_parts.begin(), text_parts.end(), &SortCPByHeight);
1266
0
  const TBOX &text_box = text_parts[text_parts.size() / 2]->bounding_box();
1267
0
  int med_height = text_box.height();
1268
0
  if (text_parts.size() % 2 == 0 && text_parts.size() > 1) {
1269
0
    const TBOX &text_box = text_parts[text_parts.size() / 2 - 1]->bounding_box();
1270
0
    med_height = static_cast<int>(std::round(0.5f * (text_box.height() + med_height)));
1271
0
  }
1272
1273
  // Iterate every text_parts and check if it is a math block satellite.
1274
0
  for (auto &text_part : text_parts) {
1275
0
    const TBOX &text_box(text_part->bounding_box());
1276
0
    if (text_box.height() > med_height) {
1277
0
      continue;
1278
0
    }
1279
0
    std::vector<ColPartition *> math_blocks;
1280
0
    if (!IsMathBlockSatellite(text_part, &math_blocks)) {
1281
0
      continue;
1282
0
    }
1283
1284
    // Found. merge text_parts[i] with math_blocks.
1285
0
    part_grid_->RemoveBBox(text_part);
1286
0
    text_part->set_type(PT_EQUATION);
1287
0
    for (auto &math_block : math_blocks) {
1288
0
      part_grid_->RemoveBBox(math_block);
1289
0
      text_part->Absorb(math_block, nullptr);
1290
0
    }
1291
0
    InsertPartAfterAbsorb(text_part);
1292
0
  }
1293
0
}
1294
1295
bool EquationDetect::IsMathBlockSatellite(ColPartition *part,
1296
0
                                          std::vector<ColPartition *> *math_blocks) {
1297
0
  ASSERT_HOST(part != nullptr && math_blocks != nullptr);
1298
0
  math_blocks->clear();
1299
0
  const TBOX &part_box(part->bounding_box());
1300
  // Find the top/bottom nearest neighbor of part.
1301
0
  ColPartition *neighbors[2];
1302
0
  int y_gaps[2] = {std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
1303
  // The horizontal boundary of the neighbors.
1304
0
  int neighbors_left = std::numeric_limits<int>::max(), neighbors_right = 0;
1305
0
  for (int i = 0; i < 2; ++i) {
1306
0
    neighbors[i] = SearchNNVertical(i != 0, part);
1307
0
    if (neighbors[i]) {
1308
0
      const TBOX &neighbor_box = neighbors[i]->bounding_box();
1309
0
      y_gaps[i] = neighbor_box.y_gap(part_box);
1310
0
      if (neighbor_box.left() < neighbors_left) {
1311
0
        neighbors_left = neighbor_box.left();
1312
0
      }
1313
0
      if (neighbor_box.right() > neighbors_right) {
1314
0
        neighbors_right = neighbor_box.right();
1315
0
      }
1316
0
    }
1317
0
  }
1318
0
  if (neighbors[0] == neighbors[1]) {
1319
    // This happens when part is inside neighbor.
1320
0
    neighbors[1] = nullptr;
1321
0
    y_gaps[1] = std::numeric_limits<int>::max();
1322
0
  }
1323
1324
  // Check if part is within [neighbors_left, neighbors_right].
1325
0
  if (part_box.left() < neighbors_left || part_box.right() > neighbors_right) {
1326
0
    return false;
1327
0
  }
1328
1329
  // Get the index of the near one in neighbors.
1330
0
  int index = y_gaps[0] < y_gaps[1] ? 0 : 1;
1331
1332
  // Check the near one.
1333
0
  if (IsNearMathNeighbor(y_gaps[index], neighbors[index])) {
1334
0
    math_blocks->push_back(neighbors[index]);
1335
0
  } else {
1336
    // If the near one failed the check, then we skip checking the far one.
1337
0
    return false;
1338
0
  }
1339
1340
  // Check the far one.
1341
0
  index = 1 - index;
1342
0
  if (IsNearMathNeighbor(y_gaps[index], neighbors[index])) {
1343
0
    math_blocks->push_back(neighbors[index]);
1344
0
  }
1345
1346
0
  return true;
1347
0
}
1348
1349
0
ColPartition *EquationDetect::SearchNNVertical(const bool search_bottom, const ColPartition *part) {
1350
0
  ASSERT_HOST(part);
1351
0
  ColPartition *nearest_neighbor = nullptr, *neighbor = nullptr;
1352
0
  const int kYGapTh = static_cast<int>(std::round(resolution_ * 0.5f));
1353
1354
0
  ColPartitionGridSearch search(part_grid_);
1355
0
  search.SetUniqueMode(true);
1356
0
  const TBOX &part_box(part->bounding_box());
1357
0
  int y = search_bottom ? part_box.bottom() : part_box.top();
1358
0
  search.StartVerticalSearch(part_box.left(), part_box.right(), y);
1359
0
  int min_y_gap = std::numeric_limits<int>::max();
1360
0
  while ((neighbor = search.NextVerticalSearch(search_bottom)) != nullptr) {
1361
0
    if (neighbor == part || !IsTextOrEquationType(neighbor->type())) {
1362
0
      continue;
1363
0
    }
1364
0
    const TBOX &neighbor_box(neighbor->bounding_box());
1365
0
    int y_gap = neighbor_box.y_gap(part_box);
1366
0
    if (y_gap > kYGapTh) { // Out of scope.
1367
0
      break;
1368
0
    }
1369
0
    if (!neighbor_box.major_x_overlap(part_box) ||
1370
0
        (search_bottom && neighbor_box.bottom() > part_box.bottom()) ||
1371
0
        (!search_bottom && neighbor_box.top() < part_box.top())) {
1372
0
      continue;
1373
0
    }
1374
0
    if (y_gap < min_y_gap) {
1375
0
      min_y_gap = y_gap;
1376
0
      nearest_neighbor = neighbor;
1377
0
    }
1378
0
  }
1379
1380
0
  return nearest_neighbor;
1381
0
}
1382
1383
0
bool EquationDetect::IsNearMathNeighbor(const int y_gap, const ColPartition *neighbor) const {
1384
0
  if (!neighbor) {
1385
0
    return false;
1386
0
  }
1387
0
  const int kYGapTh = static_cast<int>(std::round(resolution_ * 0.1f));
1388
0
  return neighbor->type() == PT_EQUATION && y_gap <= kYGapTh;
1389
0
}
1390
1391
0
void EquationDetect::GetOutputTiffName(const char *name, std::string &image_name) const {
1392
0
  ASSERT_HOST(name);
1393
0
  char page[50];
1394
0
  snprintf(page, sizeof(page), "%04d", page_count_);
1395
0
  image_name = (lang_tesseract_->imagebasename) + page + name + ".tif";
1396
0
}
1397
1398
0
void EquationDetect::PaintSpecialTexts(const std::string &outfile) const {
1399
0
  Image pix = nullptr, pixBi = lang_tesseract_->pix_binary();
1400
0
  pix = pixConvertTo32(pixBi);
1401
0
  ColPartitionGridSearch gsearch(part_grid_);
1402
0
  ColPartition *part = nullptr;
1403
0
  gsearch.StartFullSearch();
1404
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1405
0
    BLOBNBOX_C_IT blob_it(part->boxes());
1406
0
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1407
0
      RenderSpecialText(pix, blob_it.data());
1408
0
    }
1409
0
  }
1410
1411
0
  pixWrite(outfile.c_str(), pix, IFF_TIFF_LZW);
1412
0
  pix.destroy();
1413
0
}
1414
1415
0
void EquationDetect::PaintColParts(const std::string &outfile) const {
1416
0
  Image pix = pixConvertTo32(lang_tesseract_->BestPix());
1417
0
  ColPartitionGridSearch gsearch(part_grid_);
1418
0
  gsearch.StartFullSearch();
1419
0
  ColPartition *part = nullptr;
1420
0
  while ((part = gsearch.NextFullSearch()) != nullptr) {
1421
0
    const TBOX &tbox = part->bounding_box();
1422
0
    Box *box = boxCreate(tbox.left(), pixGetHeight(pix) - tbox.top(), tbox.width(), tbox.height());
1423
0
    if (part->type() == PT_EQUATION) {
1424
0
      pixRenderBoxArb(pix, box, 5, 255, 0, 0);
1425
0
    } else if (part->type() == PT_INLINE_EQUATION) {
1426
0
      pixRenderBoxArb(pix, box, 5, 0, 255, 0);
1427
0
    } else {
1428
0
      pixRenderBoxArb(pix, box, 5, 0, 0, 255);
1429
0
    }
1430
0
    boxDestroy(&box);
1431
0
  }
1432
1433
0
  pixWrite(outfile.c_str(), pix, IFF_TIFF_LZW);
1434
0
  pix.destroy();
1435
0
}
1436
1437
0
void EquationDetect::PrintSpecialBlobsDensity(const ColPartition *part) const {
1438
0
  ASSERT_HOST(part);
1439
0
  TBOX box(part->bounding_box());
1440
0
  int h = pixGetHeight(lang_tesseract_->BestPix());
1441
0
  tprintf("Printing special blobs density values for ColParition (t=%d,b=%d) ", h - box.top(),
1442
0
          h - box.bottom());
1443
0
  box.print();
1444
0
  tprintf("blobs count = %d, density = ", part->boxes_count());
1445
0
  for (int i = 0; i < BSTT_COUNT; ++i) {
1446
0
    auto type = static_cast<BlobSpecialTextType>(i);
1447
0
    tprintf("%d:%f ", i, part->SpecialBlobsDensity(type));
1448
0
  }
1449
0
  tprintf("\n");
1450
0
}
1451
1452
} // namespace tesseract