diff mupdf-source/thirdparty/tesseract/src/ccmain/equationdetect.cpp @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/thirdparty/tesseract/src/ccmain/equationdetect.cpp	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1452 @@
+///////////////////////////////////////////////////////////////////////
+// File:        equationdetect.cpp
+// Description: Helper classes to detect equations.
+// Author:      Zongyi (Joe) Liu (joeliu@google.com)
+//
+// (C) Copyright 2011, Google Inc.
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+// http://www.apache.org/licenses/LICENSE-2.0
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+///////////////////////////////////////////////////////////////////////
+
+// Include automatically generated configuration file if running autoconf.
+#ifdef HAVE_CONFIG_H
+#  include "config_auto.h"
+#endif
+
+#include "equationdetect.h"
+
+#include "bbgrid.h"
+#include "classify.h"
+#include "colpartition.h"
+#include "colpartitiongrid.h"
+#include "colpartitionset.h"
+#include "ratngs.h"
+#include "tesseractclass.h"
+
+#include "helpers.h"
+
+#include <algorithm>
+#include <cfloat>
+#include <cmath>
+#include <limits>
+#include <memory>
+
+namespace tesseract {
+
+// Config variables.
+static BOOL_VAR(equationdetect_save_bi_image, false, "Save input bi image");
+static BOOL_VAR(equationdetect_save_spt_image, false, "Save special character image");
+static BOOL_VAR(equationdetect_save_seed_image, false, "Save the seed image");
+static BOOL_VAR(equationdetect_save_merged_image, false, "Save the merged image");
+
+///////////////////////////////////////////////////////////////////////////
+// Utility ColParition sort functions.
+///////////////////////////////////////////////////////////////////////////
+static int SortCPByTopReverse(const void *p1, const void *p2) {
+  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
+  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
+  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
+  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
+  return box2.top() - box1.top();
+}
+
+static int SortCPByBottom(const void *p1, const void *p2) {
+  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
+  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
+  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
+  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
+  return box1.bottom() - box2.bottom();
+}
+
+static int SortCPByHeight(const void *p1, const void *p2) {
+  const ColPartition *cp1 = *static_cast<ColPartition *const *>(p1);
+  const ColPartition *cp2 = *static_cast<ColPartition *const *>(p2);
+  ASSERT_HOST(cp1 != nullptr && cp2 != nullptr);
+  const TBOX &box1(cp1->bounding_box()), &box2(cp2->bounding_box());
+  return box1.height() - box2.height();
+}
+
+// TODO(joeliu): we may want to parameterize these constants.
+const float kMathDigitDensityTh1 = 0.25;
+const float kMathDigitDensityTh2 = 0.1;
+const float kMathItalicDensityTh = 0.5;
+const float kUnclearDensityTh = 0.25;
+const int kSeedBlobsCountTh = 10;
+const int kLeftIndentAlignmentCountTh = 1;
+
+// Returns true if PolyBlockType is of text type or equation type.
+inline bool IsTextOrEquationType(PolyBlockType type) {
+  return PTIsTextType(type) || type == PT_EQUATION;
+}
+
+inline bool IsLeftIndented(const EquationDetect::IndentType type) {
+  return type == EquationDetect::LEFT_INDENT || type == EquationDetect::BOTH_INDENT;
+}
+
+inline bool IsRightIndented(const EquationDetect::IndentType type) {
+  return type == EquationDetect::RIGHT_INDENT || type == EquationDetect::BOTH_INDENT;
+}
+
+EquationDetect::EquationDetect(const char *equ_datapath, const char *equ_name) {
+  const char *default_name = "equ";
+  if (equ_name == nullptr) {
+    equ_name = default_name;
+  }
+  lang_tesseract_ = nullptr;
+  resolution_ = 0;
+  page_count_ = 0;
+
+  if (equ_tesseract_.init_tesseract(equ_datapath, equ_name, OEM_TESSERACT_ONLY)) {
+    tprintf(
+        "Warning: equation region detection requested,"
+        " but %s failed to load from %s\n",
+        equ_name, equ_datapath);
+  }
+
+  cps_super_bbox_ = nullptr;
+}
+
+EquationDetect::~EquationDetect() {
+  delete (cps_super_bbox_);
+}
+
+void EquationDetect::SetLangTesseract(Tesseract *lang_tesseract) {
+  lang_tesseract_ = lang_tesseract;
+}
+
+void EquationDetect::SetResolution(const int resolution) {
+  resolution_ = resolution;
+}
+
+int EquationDetect::LabelSpecialText(TO_BLOCK *to_block) {
+  if (to_block == nullptr) {
+    tprintf("Warning: input to_block is nullptr!\n");
+    return -1;
+  }
+
+  std::vector<BLOBNBOX_LIST *> blob_lists;
+  blob_lists.push_back(&(to_block->blobs));
+  blob_lists.push_back(&(to_block->large_blobs));
+  for (auto &blob_list : blob_lists) {
+    BLOBNBOX_IT bbox_it(blob_list);
+    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
+      bbox_it.data()->set_special_text_type(BSTT_NONE);
+    }
+  }
+
+  return 0;
+}
+
+void EquationDetect::IdentifySpecialText(BLOBNBOX *blobnbox, const int height_th) {
+  ASSERT_HOST(blobnbox != nullptr);
+  if (blobnbox->bounding_box().height() < height_th && height_th > 0) {
+    // For small blob, we simply set to BSTT_NONE.
+    blobnbox->set_special_text_type(BSTT_NONE);
+    return;
+  }
+
+  BLOB_CHOICE_LIST ratings_equ, ratings_lang;
+  C_BLOB *blob = blobnbox->cblob();
+  // TODO(joeliu/rays) Fix this. We may have to normalize separately for
+  // each classifier here, as they may require different PolygonalCopy.
+  TBLOB *tblob = TBLOB::PolygonalCopy(false, blob);
+  const TBOX &box = tblob->bounding_box();
+
+  // Normalize the blob. Set the origin to the place we want to be the
+  // bottom-middle, and scaling is to make the height the x-height.
+  const float scaling = static_cast<float>(kBlnXHeight) / box.height();
+  const float x_orig = (box.left() + box.right()) / 2.0f, y_orig = box.bottom();
+  std::unique_ptr<TBLOB> normed_blob(new TBLOB(*tblob));
+  normed_blob->Normalize(nullptr, nullptr, nullptr, x_orig, y_orig, scaling, scaling, 0.0f,
+                         static_cast<float>(kBlnBaselineOffset), false, nullptr);
+  equ_tesseract_.AdaptiveClassifier(normed_blob.get(), &ratings_equ);
+  lang_tesseract_->AdaptiveClassifier(normed_blob.get(), &ratings_lang);
+  delete tblob;
+
+  // Get the best choice from ratings_lang and rating_equ. As the choice in the
+  // list has already been sorted by the certainty, we simply use the first
+  // choice.
+  BLOB_CHOICE *lang_choice = nullptr, *equ_choice = nullptr;
+  if (ratings_lang.length() > 0) {
+    BLOB_CHOICE_IT choice_it(&ratings_lang);
+    lang_choice = choice_it.data();
+  }
+  if (ratings_equ.length() > 0) {
+    BLOB_CHOICE_IT choice_it(&ratings_equ);
+    equ_choice = choice_it.data();
+  }
+
+  const float lang_score = lang_choice ? lang_choice->certainty() : -FLT_MAX;
+  const float equ_score = equ_choice ? equ_choice->certainty() : -FLT_MAX;
+
+  const float kConfScoreTh = -5.0f, kConfDiffTh = 1.8;
+  // The scores here are negative, so the max/min == fabs(min/max).
+  // float ratio = fmax(lang_score, equ_score) / fmin(lang_score, equ_score);
+  const float diff = std::fabs(lang_score - equ_score);
+  BlobSpecialTextType type = BSTT_NONE;
+
+  // Classification.
+  if (std::fmax(lang_score, equ_score) < kConfScoreTh) {
+    // If both score are very small, then mark it as unclear.
+    type = BSTT_UNCLEAR;
+  } else if (diff > kConfDiffTh && equ_score > lang_score) {
+    // If equ_score is significantly higher, then we classify this character as
+    // math symbol.
+    type = BSTT_MATH;
+  } else if (lang_choice) {
+    // For other cases: lang_score is similar or significantly higher.
+    type = EstimateTypeForUnichar(lang_tesseract_->unicharset, lang_choice->unichar_id());
+  }
+
+  if (type == BSTT_NONE &&
+      lang_tesseract_->get_fontinfo_table().at(lang_choice->fontinfo_id()).is_italic()) {
+    // For text symbol, we still check if it is italic.
+    blobnbox->set_special_text_type(BSTT_ITALIC);
+  } else {
+    blobnbox->set_special_text_type(type);
+  }
+}
+
+BlobSpecialTextType EquationDetect::EstimateTypeForUnichar(const UNICHARSET &unicharset,
+                                                           const UNICHAR_ID id) const {
+  const std::string s = unicharset.id_to_unichar(id);
+  if (unicharset.get_isalpha(id)) {
+    return BSTT_NONE;
+  }
+
+  if (unicharset.get_ispunctuation(id)) {
+    // Exclude some special texts that are likely to be confused as math symbol.
+    static std::vector<UNICHAR_ID> ids_to_exclude;
+    if (ids_to_exclude.empty()) {
+      static const char *kCharsToEx[] = {"'",  "`",  "\"", "\\", ",",  ".",
+                                         "〈", "〉", "《", "》", "」", "「"};
+      for (auto &i : kCharsToEx) {
+        ids_to_exclude.push_back(unicharset.unichar_to_id(i));
+      }
+      std::sort(ids_to_exclude.begin(), ids_to_exclude.end());
+    }
+    auto found = std::binary_search(ids_to_exclude.begin(), ids_to_exclude.end(), id);
+    return found ? BSTT_NONE : BSTT_MATH;
+  }
+
+  // Check if it is digit. In addition to the isdigit attribute, we also check
+  // if this character belongs to those likely to be confused with a digit.
+  static const char kDigitsChars[] = "|";
+  if (unicharset.get_isdigit(id) || (s.length() == 1 && strchr(kDigitsChars, s[0]) != nullptr)) {
+    return BSTT_DIGIT;
+  } else {
+    return BSTT_MATH;
+  }
+}
+
+void EquationDetect::IdentifySpecialText() {
+  // Set configuration for Tesseract::AdaptiveClassifier.
+  equ_tesseract_.tess_cn_matching.set_value(true); // turn it on
+  equ_tesseract_.tess_bn_matching.set_value(false);
+
+  // Set the multiplier to zero for lang_tesseract_ to improve the accuracy.
+  const int classify_class_pruner = lang_tesseract_->classify_class_pruner_multiplier;
+  const int classify_integer_matcher = lang_tesseract_->classify_integer_matcher_multiplier;
+  lang_tesseract_->classify_class_pruner_multiplier.set_value(0);
+  lang_tesseract_->classify_integer_matcher_multiplier.set_value(0);
+
+  ColPartitionGridSearch gsearch(part_grid_);
+  ColPartition *part = nullptr;
+  gsearch.StartFullSearch();
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    if (!IsTextOrEquationType(part->type())) {
+      continue;
+    }
+    IdentifyBlobsToSkip(part);
+    BLOBNBOX_C_IT bbox_it(part->boxes());
+    // Compute the height threshold.
+    std::vector<int> blob_heights;
+    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
+      if (bbox_it.data()->special_text_type() != BSTT_SKIP) {
+        blob_heights.push_back(bbox_it.data()->bounding_box().height());
+      }
+    }
+    std::sort(blob_heights.begin(), blob_heights.end());
+    const int height_th = blob_heights[blob_heights.size() / 2] / 3 * 2;
+    for (bbox_it.mark_cycle_pt(); !bbox_it.cycled_list(); bbox_it.forward()) {
+      if (bbox_it.data()->special_text_type() != BSTT_SKIP) {
+        IdentifySpecialText(bbox_it.data(), height_th);
+      }
+    }
+  }
+
+  // Set the multiplier values back.
+  lang_tesseract_->classify_class_pruner_multiplier.set_value(classify_class_pruner);
+  lang_tesseract_->classify_integer_matcher_multiplier.set_value(classify_integer_matcher);
+
+  if (equationdetect_save_spt_image) { // For debug.
+    std::string outfile;
+    GetOutputTiffName("_spt", outfile);
+    PaintSpecialTexts(outfile);
+  }
+}
+
+void EquationDetect::IdentifyBlobsToSkip(ColPartition *part) {
+  ASSERT_HOST(part);
+  BLOBNBOX_C_IT blob_it(part->boxes());
+
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    // At this moment, no blob should have been joined.
+    ASSERT_HOST(!blob_it.data()->joined_to_prev());
+  }
+  for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+    BLOBNBOX *blob = blob_it.data();
+    if (blob->joined_to_prev() || blob->special_text_type() == BSTT_SKIP) {
+      continue;
+    }
+    TBOX blob_box = blob->bounding_box();
+
+    // Search if any blob can be merged into blob. If found, then we mark all
+    // these blobs as BSTT_SKIP.
+    BLOBNBOX_C_IT blob_it2 = blob_it;
+    bool found = false;
+    while (!blob_it2.at_last()) {
+      BLOBNBOX *nextblob = blob_it2.forward();
+      const TBOX &nextblob_box = nextblob->bounding_box();
+      if (nextblob_box.left() >= blob_box.right()) {
+        break;
+      }
+      const float kWidthR = 0.4, kHeightR = 0.3;
+      const bool xoverlap = blob_box.major_x_overlap(nextblob_box),
+                 yoverlap = blob_box.y_overlap(nextblob_box);
+      const float widthR = static_cast<float>(std::min(nextblob_box.width(), blob_box.width())) /
+                           std::max(nextblob_box.width(), blob_box.width());
+      const float heightR = static_cast<float>(std::min(nextblob_box.height(), blob_box.height())) /
+                            std::max(nextblob_box.height(), blob_box.height());
+
+      if (xoverlap && yoverlap && widthR > kWidthR && heightR > kHeightR) {
+        // Found one, set nextblob type and recompute blob_box.
+        found = true;
+        nextblob->set_special_text_type(BSTT_SKIP);
+        blob_box += nextblob_box;
+      }
+    }
+    if (found) {
+      blob->set_special_text_type(BSTT_SKIP);
+    }
+  }
+}
+
+int EquationDetect::FindEquationParts(ColPartitionGrid *part_grid, ColPartitionSet **best_columns) {
+  if (!lang_tesseract_) {
+    tprintf("Warning: lang_tesseract_ is nullptr!\n");
+    return -1;
+  }
+  if (!part_grid || !best_columns) {
+    tprintf("part_grid/best_columns is nullptr!!\n");
+    return -1;
+  }
+  cp_seeds_.clear();
+  part_grid_ = part_grid;
+  best_columns_ = best_columns;
+  resolution_ = lang_tesseract_->source_resolution();
+  std::string outfile;
+  page_count_++;
+
+  if (equationdetect_save_bi_image) {
+    GetOutputTiffName("_bi", outfile);
+    pixWrite(outfile.c_str(), lang_tesseract_->pix_binary(), IFF_TIFF_G4);
+  }
+
+  // Pass 0: Compute special text type for blobs.
+  IdentifySpecialText();
+
+  // Pass 1: Merge parts by overlap.
+  MergePartsByLocation();
+
+  // Pass 2: compute the math blob density and find the seed partition.
+  IdentifySeedParts();
+  // We still need separate seed into block seed and inline seed partition.
+  IdentifyInlineParts();
+
+  if (equationdetect_save_seed_image) {
+    GetOutputTiffName("_seed", outfile);
+    PaintColParts(outfile);
+  }
+
+  // Pass 3: expand block equation seeds.
+  while (!cp_seeds_.empty()) {
+    std::vector<ColPartition *> seeds_expanded;
+    for (auto &cp_seed : cp_seeds_) {
+      if (ExpandSeed(cp_seed)) {
+        // If this seed is expanded, then we add it into seeds_expanded. Note
+        // this seed has been removed from part_grid_ if it is expanded.
+        seeds_expanded.push_back(cp_seed);
+      }
+    }
+    // Add seeds_expanded back into part_grid_ and reset cp_seeds_.
+    for (auto &i : seeds_expanded) {
+      InsertPartAfterAbsorb(i);
+    }
+    cp_seeds_ = std::move(seeds_expanded);
+  }
+
+  // Pass 4: find math block satellite text partitions and merge them.
+  ProcessMathBlockSatelliteParts();
+
+  if (equationdetect_save_merged_image) { // For debug.
+    GetOutputTiffName("_merged", outfile);
+    PaintColParts(outfile);
+  }
+
+  return 0;
+}
+
+void EquationDetect::MergePartsByLocation() {
+  while (true) {
+    ColPartition *part = nullptr;
+    // partitions that have been updated.
+    std::vector<ColPartition *> parts_updated;
+    ColPartitionGridSearch gsearch(part_grid_);
+    gsearch.StartFullSearch();
+    while ((part = gsearch.NextFullSearch()) != nullptr) {
+      if (!IsTextOrEquationType(part->type())) {
+        continue;
+      }
+      std::vector<ColPartition *> parts_to_merge;
+      SearchByOverlap(part, &parts_to_merge);
+      if (parts_to_merge.empty()) {
+        continue;
+      }
+
+      // Merge parts_to_merge with part, and remove them from part_grid_.
+      part_grid_->RemoveBBox(part);
+      for (auto &i : parts_to_merge) {
+        ASSERT_HOST(i != nullptr && i != part);
+        part->Absorb(i, nullptr);
+      }
+      gsearch.RepositionIterator();
+
+      parts_updated.push_back(part);
+    }
+
+    if (parts_updated.empty()) { // Exit the loop
+      break;
+    }
+
+    // Re-insert parts_updated into part_grid_.
+    for (auto &i : parts_updated) {
+      InsertPartAfterAbsorb(i);
+    }
+  }
+}
+
+void EquationDetect::SearchByOverlap(ColPartition *seed,
+                                     std::vector<ColPartition *> *parts_overlap) {
+  ASSERT_HOST(seed != nullptr && parts_overlap != nullptr);
+  if (!IsTextOrEquationType(seed->type())) {
+    return;
+  }
+  ColPartitionGridSearch search(part_grid_);
+  const TBOX &seed_box(seed->bounding_box());
+  const int kRadNeighborCells = 30;
+  search.StartRadSearch((seed_box.left() + seed_box.right()) / 2,
+                        (seed_box.top() + seed_box.bottom()) / 2, kRadNeighborCells);
+  search.SetUniqueMode(true);
+
+  // Search iteratively.
+  ColPartition *part;
+  std::vector<ColPartition *> parts;
+  const float kLargeOverlapTh = 0.95;
+  const float kEquXOverlap = 0.4, kEquYOverlap = 0.5;
+  while ((part = search.NextRadSearch()) != nullptr) {
+    if (part == seed || !IsTextOrEquationType(part->type())) {
+      continue;
+    }
+    const TBOX &part_box(part->bounding_box());
+    bool merge = false;
+
+    const float x_overlap_fraction = part_box.x_overlap_fraction(seed_box),
+                y_overlap_fraction = part_box.y_overlap_fraction(seed_box);
+
+    // If part is large overlapped with seed, then set merge to true.
+    if (x_overlap_fraction >= kLargeOverlapTh && y_overlap_fraction >= kLargeOverlapTh) {
+      merge = true;
+    } else if (seed->type() == PT_EQUATION && IsTextOrEquationType(part->type())) {
+      if ((x_overlap_fraction > kEquXOverlap && y_overlap_fraction > 0.0) ||
+          (x_overlap_fraction > 0.0 && y_overlap_fraction > kEquYOverlap)) {
+        merge = true;
+      }
+    }
+
+    if (merge) { // Remove the part from search and put it into parts.
+      search.RemoveBBox();
+      parts_overlap->push_back(part);
+    }
+  }
+}
+
+void EquationDetect::InsertPartAfterAbsorb(ColPartition *part) {
+  ASSERT_HOST(part);
+
+  // Before insert part back into part_grid_, we will need re-compute some
+  // of its attributes such as first_column_, last_column_. However, we still
+  // want to preserve its type.
+  BlobTextFlowType flow_type = part->flow();
+  PolyBlockType part_type = part->type();
+  BlobRegionType blob_type = part->blob_type();
+
+  // Call SetPartitionType to re-compute the attributes of part.
+  const TBOX &part_box(part->bounding_box());
+  int grid_x, grid_y;
+  part_grid_->GridCoords(part_box.left(), part_box.bottom(), &grid_x, &grid_y);
+  part->SetPartitionType(resolution_, best_columns_[grid_y]);
+
+  // Reset the types back.
+  part->set_type(part_type);
+  part->set_blob_type(blob_type);
+  part->set_flow(flow_type);
+  part->SetBlobTypes();
+
+  // Insert into part_grid_.
+  part_grid_->InsertBBox(true, true, part);
+}
+
+void EquationDetect::IdentifySeedParts() {
+  ColPartitionGridSearch gsearch(part_grid_);
+  ColPartition *part = nullptr;
+  gsearch.StartFullSearch();
+
+  std::vector<ColPartition *> seeds1, seeds2;
+  // The left coordinates of indented text partitions.
+  std::vector<int> indented_texts_left;
+  // The foreground density of text partitions.
+  std::vector<float> texts_foreground_density;
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    if (!IsTextOrEquationType(part->type())) {
+      continue;
+    }
+    part->ComputeSpecialBlobsDensity();
+    const bool blobs_check = CheckSeedBlobsCount(part);
+    const int kTextBlobsTh = 20;
+
+    if (CheckSeedDensity(kMathDigitDensityTh1, kMathDigitDensityTh2, part) && blobs_check) {
+      // Passed high density threshold test, save into seeds1.
+      seeds1.push_back(part);
+    } else {
+      IndentType indent = IsIndented(part);
+      if (IsLeftIndented(indent) && blobs_check &&
+          CheckSeedDensity(kMathDigitDensityTh2, kMathDigitDensityTh2, part)) {
+        // Passed low density threshold test and is indented, save into seeds2.
+        seeds2.push_back(part);
+      } else if (!IsRightIndented(indent) && part->boxes_count() > kTextBlobsTh) {
+        // This is likely to be a text part, save the features.
+        const TBOX &box = part->bounding_box();
+        if (IsLeftIndented(indent)) {
+          indented_texts_left.push_back(box.left());
+        }
+        texts_foreground_density.push_back(ComputeForegroundDensity(box));
+      }
+    }
+  }
+
+  // Sort the features collected from text regions.
+  std::sort(indented_texts_left.begin(), indented_texts_left.end());
+  std::sort(texts_foreground_density.begin(), texts_foreground_density.end());
+  float foreground_density_th = 0.15; // Default value.
+  if (!texts_foreground_density.empty()) {
+    // Use the median of the texts_foreground_density.
+    foreground_density_th = 0.8 * texts_foreground_density[texts_foreground_density.size() / 2];
+  }
+
+  for (auto &i : seeds1) {
+    const TBOX &box = i->bounding_box();
+    if (CheckSeedFgDensity(foreground_density_th, i) &&
+        !(IsLeftIndented(IsIndented(i)) &&
+          CountAlignment(indented_texts_left, box.left()) >= kLeftIndentAlignmentCountTh)) {
+      // Mark as PT_EQUATION type.
+      i->set_type(PT_EQUATION);
+      cp_seeds_.push_back(i);
+    } else { // Mark as PT_INLINE_EQUATION type.
+      i->set_type(PT_INLINE_EQUATION);
+    }
+  }
+
+  for (auto &i : seeds2) {
+    if (CheckForSeed2(indented_texts_left, foreground_density_th, i)) {
+      i->set_type(PT_EQUATION);
+      cp_seeds_.push_back(i);
+    }
+  }
+}
+
+float EquationDetect::ComputeForegroundDensity(const TBOX &tbox) {
+  Image pix_bi = lang_tesseract_->pix_binary();
+  const int pix_height = pixGetHeight(pix_bi);
+  Box *box = boxCreate(tbox.left(), pix_height - tbox.top(), tbox.width(), tbox.height());
+  Image pix_sub = pixClipRectangle(pix_bi, box, nullptr);
+  l_float32 fract;
+  pixForegroundFraction(pix_sub, &fract);
+  pix_sub.destroy();
+  boxDestroy(&box);
+
+  return fract;
+}
+
+bool EquationDetect::CheckSeedFgDensity(const float density_th, ColPartition *part) {
+  ASSERT_HOST(part);
+
+  // Split part horizontall, and check for each sub part.
+  std::vector<TBOX> sub_boxes;
+  SplitCPHorLite(part, &sub_boxes);
+  float parts_passed = 0.0;
+  for (auto &sub_boxe : sub_boxes) {
+    const float density = ComputeForegroundDensity(sub_boxe);
+    if (density < density_th) {
+      parts_passed++;
+    }
+  }
+
+  // If most sub parts passed, then we return true.
+  const float kSeedPartRatioTh = 0.3;
+  bool retval = (parts_passed / sub_boxes.size() >= kSeedPartRatioTh);
+
+  return retval;
+}
+
+void EquationDetect::SplitCPHor(ColPartition *part, std::vector<ColPartition *> *parts_splitted) {
+  ASSERT_HOST(part && parts_splitted);
+  if (part->median_width() == 0 || part->boxes_count() == 0) {
+    return;
+  }
+
+  // Make a copy of part, and reset parts_splitted.
+  ColPartition *right_part = part->CopyButDontOwnBlobs();
+  for (auto data : *parts_splitted) {
+    delete data;
+  }
+  parts_splitted->clear();
+
+  const double kThreshold = part->median_width() * 3.0;
+  bool found_split = true;
+  while (found_split) {
+    found_split = false;
+    BLOBNBOX_C_IT box_it(right_part->boxes());
+    // Blobs are sorted left side first. If blobs overlap,
+    // the previous blob may have a "more right" right side.
+    // Account for this by always keeping the largest "right"
+    // so far.
+    int previous_right = INT32_MIN;
+
+    // Look for the next split in the partition.
+    for (box_it.mark_cycle_pt(); !box_it.cycled_list(); box_it.forward()) {
+      const TBOX &box = box_it.data()->bounding_box();
+      if (previous_right != INT32_MIN && box.left() - previous_right > kThreshold) {
+        // We have a split position. Split the partition in two pieces.
+        // Insert the left piece in the grid and keep processing the right.
+        const int mid_x = (box.left() + previous_right) / 2;
+        ColPartition *left_part = right_part;
+        right_part = left_part->SplitAt(mid_x);
+
+        parts_splitted->push_back(left_part);
+        left_part->ComputeSpecialBlobsDensity();
+        found_split = true;
+        break;
+      }
+
+      // The right side of the previous blobs.
+      previous_right = std::max(previous_right, static_cast<int>(box.right()));
+    }
+  }
+
+  // Add the last piece.
+  right_part->ComputeSpecialBlobsDensity();
+  parts_splitted->push_back(right_part);
+}
+
+void EquationDetect::SplitCPHorLite(ColPartition *part, std::vector<TBOX> *splitted_boxes) {
+  ASSERT_HOST(part && splitted_boxes);
+  splitted_boxes->clear();
+  if (part->median_width() == 0) {
+    return;
+  }
+
+  const double kThreshold = part->median_width() * 3.0;
+
+  // Blobs are sorted left side first. If blobs overlap,
+  // the previous blob may have a "more right" right side.
+  // Account for this by always keeping the largest "right"
+  // so far.
+  TBOX union_box;
+  int previous_right = INT32_MIN;
+  BLOBNBOX_C_IT box_it(part->boxes());
+  for (box_it.mark_cycle_pt(); !box_it.cycled_list(); box_it.forward()) {
+    const TBOX &box = box_it.data()->bounding_box();
+    if (previous_right != INT32_MIN && box.left() - previous_right > kThreshold) {
+      // We have a split position.
+      splitted_boxes->push_back(union_box);
+      previous_right = INT32_MIN;
+    }
+    if (previous_right == INT32_MIN) {
+      union_box = box;
+    } else {
+      union_box += box;
+    }
+    // The right side of the previous blobs.
+    previous_right = std::max(previous_right, static_cast<int>(box.right()));
+  }
+
+  // Add the last piece.
+  if (previous_right != INT32_MIN) {
+    splitted_boxes->push_back(union_box);
+  }
+}
+
+bool EquationDetect::CheckForSeed2(const std::vector<int> &indented_texts_left,
+                                   const float foreground_density_th, ColPartition *part) {
+  ASSERT_HOST(part);
+  const TBOX &box = part->bounding_box();
+
+  // Check if it is aligned with any indented_texts_left.
+  if (!indented_texts_left.empty() &&
+      CountAlignment(indented_texts_left, box.left()) >= kLeftIndentAlignmentCountTh) {
+    return false;
+  }
+
+  // Check the foreground density.
+  if (ComputeForegroundDensity(box) > foreground_density_th) {
+    return false;
+  }
+
+  return true;
+}
+
+int EquationDetect::CountAlignment(const std::vector<int> &sorted_vec, const int val) const {
+  if (sorted_vec.empty()) {
+    return 0;
+  }
+  const int kDistTh = static_cast<int>(std::round(0.03f * resolution_));
+  auto pos = std::upper_bound(sorted_vec.begin(), sorted_vec.end(), val);
+  if (pos > sorted_vec.begin()) {
+    --pos;
+  }
+  int count = 0;
+
+  // Search left side.
+  auto index = pos - sorted_vec.begin();
+  while (index >= 0 && abs(val - sorted_vec[index--]) < kDistTh) {
+    count++;
+  }
+
+  // Search right side.
+  index = pos + 1 - sorted_vec.begin();
+  while (static_cast<size_t>(index) < sorted_vec.size() && sorted_vec[index++] - val < kDistTh) {
+    count++;
+  }
+
+  return count;
+}
+
+void EquationDetect::IdentifyInlineParts() {
+  ComputeCPsSuperBBox();
+  IdentifyInlinePartsHorizontal();
+  const int textparts_linespacing = EstimateTextPartLineSpacing();
+  IdentifyInlinePartsVertical(true, textparts_linespacing);
+  IdentifyInlinePartsVertical(false, textparts_linespacing);
+}
+
+void EquationDetect::ComputeCPsSuperBBox() {
+  ColPartitionGridSearch gsearch(part_grid_);
+  ColPartition *part = nullptr;
+  gsearch.StartFullSearch();
+  delete cps_super_bbox_;
+  cps_super_bbox_ = new TBOX();
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    (*cps_super_bbox_) += part->bounding_box();
+  }
+}
+
+void EquationDetect::IdentifyInlinePartsHorizontal() {
+  ASSERT_HOST(cps_super_bbox_);
+  std::vector<ColPartition *> new_seeds;
+  const int kMarginDiffTh = IntCastRounded(0.5 * lang_tesseract_->source_resolution());
+  const int kGapTh = static_cast<int>(std::round(1.0f * lang_tesseract_->source_resolution()));
+  ColPartitionGridSearch search(part_grid_);
+  search.SetUniqueMode(true);
+  // The center x coordinate of the cp_super_bbox_.
+  const int cps_cx = cps_super_bbox_->left() + cps_super_bbox_->width() / 2;
+  for (auto part : cp_seeds_) {
+    const TBOX &part_box(part->bounding_box());
+    const int left_margin = part_box.left() - cps_super_bbox_->left(),
+              right_margin = cps_super_bbox_->right() - part_box.right();
+    bool right_to_left;
+    if (left_margin + kMarginDiffTh < right_margin && left_margin < kMarginDiffTh) {
+      // part is left aligned, so we search if it has any right neighbor.
+      search.StartSideSearch(part_box.right(), part_box.top(), part_box.bottom());
+      right_to_left = false;
+    } else if (left_margin > cps_cx) {
+      // part locates on the right half on image, so search if it has any left
+      // neighbor.
+      search.StartSideSearch(part_box.left(), part_box.top(), part_box.bottom());
+      right_to_left = true;
+    } else { // part is not an inline equation.
+      new_seeds.push_back(part);
+      continue;
+    }
+    ColPartition *neighbor = nullptr;
+    bool side_neighbor_found = false;
+    while ((neighbor = search.NextSideSearch(right_to_left)) != nullptr) {
+      const TBOX &neighbor_box(neighbor->bounding_box());
+      if (!IsTextOrEquationType(neighbor->type()) || part_box.x_gap(neighbor_box) > kGapTh ||
+          !part_box.major_y_overlap(neighbor_box) || part_box.major_x_overlap(neighbor_box)) {
+        continue;
+      }
+      // We have found one. Set the side_neighbor_found flag.
+      side_neighbor_found = true;
+      break;
+    }
+    if (!side_neighbor_found) { // Mark part as PT_INLINE_EQUATION.
+      part->set_type(PT_INLINE_EQUATION);
+    } else {
+      // Check the geometric feature of neighbor.
+      const TBOX &neighbor_box(neighbor->bounding_box());
+      if (neighbor_box.width() > part_box.width() &&
+          neighbor->type() != PT_EQUATION) { // Mark as PT_INLINE_EQUATION.
+        part->set_type(PT_INLINE_EQUATION);
+      } else { // part is not an inline equation type.
+        new_seeds.push_back(part);
+      }
+    }
+  }
+
+  // Reset the cp_seeds_ using the new_seeds.
+  cp_seeds_ = std::move(new_seeds);
+}
+
+int EquationDetect::EstimateTextPartLineSpacing() {
+  ColPartitionGridSearch gsearch(part_grid_);
+
+  // Get the y gap between text partitions;
+  ColPartition *current = nullptr, *prev = nullptr;
+  gsearch.StartFullSearch();
+  std::vector<int> ygaps;
+  while ((current = gsearch.NextFullSearch()) != nullptr) {
+    if (!PTIsTextType(current->type())) {
+      continue;
+    }
+    if (prev != nullptr) {
+      const TBOX &current_box = current->bounding_box();
+      const TBOX &prev_box = prev->bounding_box();
+      // prev and current should be x major overlap and non y overlap.
+      if (current_box.major_x_overlap(prev_box) && !current_box.y_overlap(prev_box)) {
+        int gap = current_box.y_gap(prev_box);
+        if (gap < std::min(current_box.height(), prev_box.height())) {
+          // The gap should be smaller than the height of the bounding boxes.
+          ygaps.push_back(gap);
+        }
+      }
+    }
+    prev = current;
+  }
+
+  if (ygaps.size() < 8) { // We do not have enough data.
+    return -1;
+  }
+
+  // Compute the line spacing from ygaps: use the mean of the first half.
+  std::sort(ygaps.begin(), ygaps.end());
+  int spacing = 0;
+  unsigned count;
+  for (count = 0; count < ygaps.size() / 2; count++) {
+    spacing += ygaps[count];
+  }
+  return spacing / count;
+}
+
+void EquationDetect::IdentifyInlinePartsVertical(const bool top_to_bottom,
+                                                 const int textparts_linespacing) {
+  if (cp_seeds_.empty()) {
+    return;
+  }
+
+  // Sort cp_seeds_.
+  if (top_to_bottom) { // From top to bottom.
+    std::sort(cp_seeds_.begin(), cp_seeds_.end(), &SortCPByTopReverse);
+  } else { // From bottom to top.
+    std::sort(cp_seeds_.begin(), cp_seeds_.end(), &SortCPByBottom);
+  }
+
+  std::vector<ColPartition *> new_seeds;
+  for (auto part : cp_seeds_) {
+    // If we sort cp_seeds_ from top to bottom, then for each cp_seeds_, we look
+    // for its top neighbors, so that if two/more inline regions are connected
+    // to each other, then we will identify the top one, and then use it to
+    // identify the bottom one.
+    if (IsInline(!top_to_bottom, textparts_linespacing, part)) {
+      part->set_type(PT_INLINE_EQUATION);
+    } else {
+      new_seeds.push_back(part);
+    }
+  }
+  cp_seeds_ = std::move(new_seeds);
+}
+
+bool EquationDetect::IsInline(const bool search_bottom, const int textparts_linespacing,
+                              ColPartition *part) {
+  ASSERT_HOST(part != nullptr);
+  // Look for its nearest vertical neighbor that hardly overlaps in y but
+  // largely overlaps in x.
+  ColPartitionGridSearch search(part_grid_);
+  ColPartition *neighbor = nullptr;
+  const TBOX &part_box(part->bounding_box());
+  const float kYGapRatioTh = 1.0;
+
+  if (search_bottom) {
+    search.StartVerticalSearch(part_box.left(), part_box.right(), part_box.bottom());
+  } else {
+    search.StartVerticalSearch(part_box.left(), part_box.right(), part_box.top());
+  }
+  search.SetUniqueMode(true);
+  while ((neighbor = search.NextVerticalSearch(search_bottom)) != nullptr) {
+    const TBOX &neighbor_box(neighbor->bounding_box());
+    if (part_box.y_gap(neighbor_box) >
+        kYGapRatioTh * std::min(part_box.height(), neighbor_box.height())) {
+      // Finished searching.
+      break;
+    }
+    if (!PTIsTextType(neighbor->type())) {
+      continue;
+    }
+
+    // Check if neighbor and part is inline similar.
+    const float kHeightRatioTh = 0.5;
+    const int kYGapTh = textparts_linespacing > 0
+                            ? textparts_linespacing + static_cast<int>(std::round(0.02f * resolution_))
+                            : static_cast<int>(std::round(0.05f * resolution_)); // Default value.
+    if (part_box.x_overlap(neighbor_box) &&                                 // Location feature.
+        part_box.y_gap(neighbor_box) <= kYGapTh &&                          // Line spacing.
+        // Geo feature.
+        static_cast<float>(std::min(part_box.height(), neighbor_box.height())) /
+                std::max(part_box.height(), neighbor_box.height()) >
+            kHeightRatioTh) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+bool EquationDetect::CheckSeedBlobsCount(ColPartition *part) {
+  if (!part) {
+    return false;
+  }
+  const int kSeedMathBlobsCount = 2;
+  const int kSeedMathDigitBlobsCount = 5;
+
+  const int blobs = part->boxes_count(), math_blobs = part->SpecialBlobsCount(BSTT_MATH),
+            digit_blobs = part->SpecialBlobsCount(BSTT_DIGIT);
+  if (blobs < kSeedBlobsCountTh || math_blobs <= kSeedMathBlobsCount ||
+      math_blobs + digit_blobs <= kSeedMathDigitBlobsCount) {
+    return false;
+  }
+
+  return true;
+}
+
+bool EquationDetect::CheckSeedDensity(const float math_density_high, const float math_density_low,
+                                      const ColPartition *part) const {
+  ASSERT_HOST(part);
+  float math_digit_density =
+      part->SpecialBlobsDensity(BSTT_MATH) + part->SpecialBlobsDensity(BSTT_DIGIT);
+  float italic_density = part->SpecialBlobsDensity(BSTT_ITALIC);
+  if (math_digit_density > math_density_high) {
+    return true;
+  }
+  if (math_digit_density + italic_density > kMathItalicDensityTh &&
+      math_digit_density > math_density_low) {
+    return true;
+  }
+
+  return false;
+}
+
+EquationDetect::IndentType EquationDetect::IsIndented(ColPartition *part) {
+  ASSERT_HOST(part);
+
+  ColPartitionGridSearch search(part_grid_);
+  ColPartition *neighbor = nullptr;
+  const TBOX &part_box(part->bounding_box());
+  const int kXGapTh = static_cast<int>(std::round(0.5f * resolution_));
+  const int kRadiusTh = static_cast<int>(std::round(3.0f * resolution_));
+  const int kYGapTh = static_cast<int>(std::round(0.5f * resolution_));
+
+  // Here we use a simple approximation algorithm: from the center of part, We
+  // perform the radius search, and check if we can find a neighboring partition
+  // that locates on the top/bottom left of part.
+  search.StartRadSearch((part_box.left() + part_box.right()) / 2,
+                        (part_box.top() + part_box.bottom()) / 2, kRadiusTh);
+  search.SetUniqueMode(true);
+  bool left_indented = false, right_indented = false;
+  while ((neighbor = search.NextRadSearch()) != nullptr && (!left_indented || !right_indented)) {
+    if (neighbor == part) {
+      continue;
+    }
+    const TBOX &neighbor_box(neighbor->bounding_box());
+
+    if (part_box.major_y_overlap(neighbor_box) && part_box.x_gap(neighbor_box) < kXGapTh) {
+      // When this happens, it is likely part is a fragment of an
+      // over-segmented colpartition. So we return false.
+      return NO_INDENT;
+    }
+
+    if (!IsTextOrEquationType(neighbor->type())) {
+      continue;
+    }
+
+    // The neighbor should be above/below part, and overlap in x direction.
+    if (!part_box.x_overlap(neighbor_box) || part_box.y_overlap(neighbor_box)) {
+      continue;
+    }
+
+    if (part_box.y_gap(neighbor_box) < kYGapTh) {
+      const int left_gap = part_box.left() - neighbor_box.left();
+      const int right_gap = neighbor_box.right() - part_box.right();
+      if (left_gap > kXGapTh) {
+        left_indented = true;
+      }
+      if (right_gap > kXGapTh) {
+        right_indented = true;
+      }
+    }
+  }
+
+  if (left_indented && right_indented) {
+    return BOTH_INDENT;
+  }
+  if (left_indented) {
+    return LEFT_INDENT;
+  }
+  if (right_indented) {
+    return RIGHT_INDENT;
+  }
+  return NO_INDENT;
+}
+
+bool EquationDetect::ExpandSeed(ColPartition *seed) {
+  if (seed == nullptr ||        // This seed has been absorbed by other seeds.
+      seed->IsVerticalType()) { // We skip vertical type right now.
+    return false;
+  }
+
+  // Expand in four directions.
+  std::vector<ColPartition *> parts_to_merge;
+  ExpandSeedHorizontal(true, seed, &parts_to_merge);
+  ExpandSeedHorizontal(false, seed, &parts_to_merge);
+  ExpandSeedVertical(true, seed, &parts_to_merge);
+  ExpandSeedVertical(false, seed, &parts_to_merge);
+  SearchByOverlap(seed, &parts_to_merge);
+
+  if (parts_to_merge.empty()) { // We don't find any partition to merge.
+    return false;
+  }
+
+  // Merge all partitions in parts_to_merge with seed. We first remove seed
+  // from part_grid_ as its bounding box is going to expand. Then we add it
+  // back after it absorbs all parts_to_merge partitions.
+  part_grid_->RemoveBBox(seed);
+  for (auto part : parts_to_merge) {
+    if (part->type() == PT_EQUATION) {
+      // If part is in cp_seeds_, then we mark it as nullptr so that we won't
+      // process it again.
+      for (auto &cp_seed : cp_seeds_) {
+        if (part == cp_seed) {
+          cp_seed = nullptr;
+          break;
+        }
+      }
+    }
+
+    // part has already been removed from part_grid_ in function
+    // ExpandSeedHorizontal/ExpandSeedVertical.
+    seed->Absorb(part, nullptr);
+  }
+
+  return true;
+}
+
+void EquationDetect::ExpandSeedHorizontal(const bool search_left, ColPartition *seed,
+                                          std::vector<ColPartition *> *parts_to_merge) {
+  ASSERT_HOST(seed != nullptr && parts_to_merge != nullptr);
+  const float kYOverlapTh = 0.6;
+  const int kXGapTh = static_cast<int>(std::round(0.2f * resolution_));
+
+  ColPartitionGridSearch search(part_grid_);
+  const TBOX &seed_box(seed->bounding_box());
+  const int x = search_left ? seed_box.left() : seed_box.right();
+  search.StartSideSearch(x, seed_box.bottom(), seed_box.top());
+  search.SetUniqueMode(true);
+
+  // Search iteratively.
+  ColPartition *part = nullptr;
+  while ((part = search.NextSideSearch(search_left)) != nullptr) {
+    if (part == seed) {
+      continue;
+    }
+    const TBOX &part_box(part->bounding_box());
+    if (part_box.x_gap(seed_box) > kXGapTh) { // Out of scope.
+      break;
+    }
+
+    // Check part location.
+    if ((part_box.left() >= seed_box.left() && search_left) ||
+        (part_box.right() <= seed_box.right() && !search_left)) {
+      continue;
+    }
+
+    if (part->type() != PT_EQUATION) { // Non-equation type.
+      // Skip PT_LINLINE_EQUATION and non text type.
+      if (part->type() == PT_INLINE_EQUATION ||
+          (!IsTextOrEquationType(part->type()) && part->blob_type() != BRT_HLINE)) {
+        continue;
+      }
+      // For other types, it should be the near small neighbor of seed.
+      if (!IsNearSmallNeighbor(seed_box, part_box) || !CheckSeedNeighborDensity(part)) {
+        continue;
+      }
+    } else { // Equation type, check the y overlap.
+      if (part_box.y_overlap_fraction(seed_box) < kYOverlapTh &&
+          seed_box.y_overlap_fraction(part_box) < kYOverlapTh) {
+        continue;
+      }
+    }
+
+    // Passed the check, delete it from search and add into parts_to_merge.
+    search.RemoveBBox();
+    parts_to_merge->push_back(part);
+  }
+}
+
+void EquationDetect::ExpandSeedVertical(const bool search_bottom, ColPartition *seed,
+                                        std::vector<ColPartition *> *parts_to_merge) {
+  ASSERT_HOST(seed != nullptr && parts_to_merge != nullptr && cps_super_bbox_ != nullptr);
+  const float kXOverlapTh = 0.4;
+  const int kYGapTh = static_cast<int>(std::round(0.2f * resolution_));
+
+  ColPartitionGridSearch search(part_grid_);
+  const TBOX &seed_box(seed->bounding_box());
+  const int y = search_bottom ? seed_box.bottom() : seed_box.top();
+  search.StartVerticalSearch(cps_super_bbox_->left(), cps_super_bbox_->right(), y);
+  search.SetUniqueMode(true);
+
+  // Search iteratively.
+  ColPartition *part = nullptr;
+  std::vector<ColPartition *> parts;
+  int skipped_min_top = std::numeric_limits<int>::max(), skipped_max_bottom = -1;
+  while ((part = search.NextVerticalSearch(search_bottom)) != nullptr) {
+    if (part == seed) {
+      continue;
+    }
+    const TBOX &part_box(part->bounding_box());
+
+    if (part_box.y_gap(seed_box) > kYGapTh) { // Out of scope.
+      break;
+    }
+
+    // Check part location.
+    if ((part_box.bottom() >= seed_box.bottom() && search_bottom) ||
+        (part_box.top() <= seed_box.top() && !search_bottom)) {
+      continue;
+    }
+
+    bool skip_part = false;
+    if (part->type() != PT_EQUATION) { // Non-equation type.
+      // Skip PT_LINLINE_EQUATION and non text type.
+      if (part->type() == PT_INLINE_EQUATION ||
+          (!IsTextOrEquationType(part->type()) && part->blob_type() != BRT_HLINE)) {
+        skip_part = true;
+      } else if (!IsNearSmallNeighbor(seed_box, part_box) || !CheckSeedNeighborDensity(part)) {
+        // For other types, it should be the near small neighbor of seed.
+        skip_part = true;
+      }
+    } else { // Equation type, check the x overlap.
+      if (part_box.x_overlap_fraction(seed_box) < kXOverlapTh &&
+          seed_box.x_overlap_fraction(part_box) < kXOverlapTh) {
+        skip_part = true;
+      }
+    }
+    if (skip_part) {
+      if (part->type() != PT_EQUATION) {
+        if (skipped_min_top > part_box.top()) {
+          skipped_min_top = part_box.top();
+        }
+        if (skipped_max_bottom < part_box.bottom()) {
+          skipped_max_bottom = part_box.bottom();
+        }
+      }
+    } else {
+      parts.push_back(part);
+    }
+  }
+
+  // For every part in parts, we need verify it is not above skipped_min_top
+  // when search top, or not below skipped_max_bottom when search bottom. I.e.,
+  // we will skip a part if it looks like:
+  //             search bottom      |         search top
+  // seed:     ******************   | part:    **********
+  // skipped: xxx                   | skipped:  xxx
+  // part:       **********         | seed:    ***********
+  for (auto &part : parts) {
+    const TBOX &part_box(part->bounding_box());
+    if ((search_bottom && part_box.top() <= skipped_max_bottom) ||
+        (!search_bottom && part_box.bottom() >= skipped_min_top)) {
+      continue;
+    }
+    // Add parts[i] into parts_to_merge, and delete it from part_grid_.
+    parts_to_merge->push_back(part);
+    part_grid_->RemoveBBox(part);
+  }
+}
+
+bool EquationDetect::IsNearSmallNeighbor(const TBOX &seed_box, const TBOX &part_box) const {
+  const int kXGapTh = static_cast<int>(std::round(0.25f * resolution_));
+  const int kYGapTh = static_cast<int>(std::round(0.05f * resolution_));
+
+  // Check geometric feature.
+  if (part_box.height() > seed_box.height() || part_box.width() > seed_box.width()) {
+    return false;
+  }
+
+  // Check overlap and distance.
+  if ((!part_box.major_x_overlap(seed_box) || part_box.y_gap(seed_box) > kYGapTh) &&
+      (!part_box.major_y_overlap(seed_box) || part_box.x_gap(seed_box) > kXGapTh)) {
+    return false;
+  }
+
+  return true;
+}
+
+bool EquationDetect::CheckSeedNeighborDensity(const ColPartition *part) const {
+  ASSERT_HOST(part);
+  if (part->boxes_count() < kSeedBlobsCountTh) {
+    // Too few blobs, skip the check.
+    return true;
+  }
+
+  // We check the math blobs density and the unclear blobs density.
+  if (part->SpecialBlobsDensity(BSTT_MATH) + part->SpecialBlobsDensity(BSTT_DIGIT) >
+          kMathDigitDensityTh1 ||
+      part->SpecialBlobsDensity(BSTT_UNCLEAR) > kUnclearDensityTh) {
+    return true;
+  }
+
+  return false;
+}
+
+void EquationDetect::ProcessMathBlockSatelliteParts() {
+  // Iterate over part_grid_, and find all parts that are text type but not
+  // equation type.
+  ColPartition *part = nullptr;
+  std::vector<ColPartition *> text_parts;
+  ColPartitionGridSearch gsearch(part_grid_);
+  gsearch.StartFullSearch();
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    if (part->type() == PT_FLOWING_TEXT || part->type() == PT_HEADING_TEXT) {
+      text_parts.push_back(part);
+    }
+  }
+  if (text_parts.empty()) {
+    return;
+  }
+
+  // Compute the medium height of the text_parts.
+  std::sort(text_parts.begin(), text_parts.end(), &SortCPByHeight);
+  const TBOX &text_box = text_parts[text_parts.size() / 2]->bounding_box();
+  int med_height = text_box.height();
+  if (text_parts.size() % 2 == 0 && text_parts.size() > 1) {
+    const TBOX &text_box = text_parts[text_parts.size() / 2 - 1]->bounding_box();
+    med_height = static_cast<int>(std::round(0.5f * (text_box.height() + med_height)));
+  }
+
+  // Iterate every text_parts and check if it is a math block satellite.
+  for (auto &text_part : text_parts) {
+    const TBOX &text_box(text_part->bounding_box());
+    if (text_box.height() > med_height) {
+      continue;
+    }
+    std::vector<ColPartition *> math_blocks;
+    if (!IsMathBlockSatellite(text_part, &math_blocks)) {
+      continue;
+    }
+
+    // Found. merge text_parts[i] with math_blocks.
+    part_grid_->RemoveBBox(text_part);
+    text_part->set_type(PT_EQUATION);
+    for (auto &math_block : math_blocks) {
+      part_grid_->RemoveBBox(math_block);
+      text_part->Absorb(math_block, nullptr);
+    }
+    InsertPartAfterAbsorb(text_part);
+  }
+}
+
+bool EquationDetect::IsMathBlockSatellite(ColPartition *part,
+                                          std::vector<ColPartition *> *math_blocks) {
+  ASSERT_HOST(part != nullptr && math_blocks != nullptr);
+  math_blocks->clear();
+  const TBOX &part_box(part->bounding_box());
+  // Find the top/bottom nearest neighbor of part.
+  ColPartition *neighbors[2];
+  int y_gaps[2] = {std::numeric_limits<int>::max(), std::numeric_limits<int>::max()};
+  // The horizontal boundary of the neighbors.
+  int neighbors_left = std::numeric_limits<int>::max(), neighbors_right = 0;
+  for (int i = 0; i < 2; ++i) {
+    neighbors[i] = SearchNNVertical(i != 0, part);
+    if (neighbors[i]) {
+      const TBOX &neighbor_box = neighbors[i]->bounding_box();
+      y_gaps[i] = neighbor_box.y_gap(part_box);
+      if (neighbor_box.left() < neighbors_left) {
+        neighbors_left = neighbor_box.left();
+      }
+      if (neighbor_box.right() > neighbors_right) {
+        neighbors_right = neighbor_box.right();
+      }
+    }
+  }
+  if (neighbors[0] == neighbors[1]) {
+    // This happens when part is inside neighbor.
+    neighbors[1] = nullptr;
+    y_gaps[1] = std::numeric_limits<int>::max();
+  }
+
+  // Check if part is within [neighbors_left, neighbors_right].
+  if (part_box.left() < neighbors_left || part_box.right() > neighbors_right) {
+    return false;
+  }
+
+  // Get the index of the near one in neighbors.
+  int index = y_gaps[0] < y_gaps[1] ? 0 : 1;
+
+  // Check the near one.
+  if (IsNearMathNeighbor(y_gaps[index], neighbors[index])) {
+    math_blocks->push_back(neighbors[index]);
+  } else {
+    // If the near one failed the check, then we skip checking the far one.
+    return false;
+  }
+
+  // Check the far one.
+  index = 1 - index;
+  if (IsNearMathNeighbor(y_gaps[index], neighbors[index])) {
+    math_blocks->push_back(neighbors[index]);
+  }
+
+  return true;
+}
+
+ColPartition *EquationDetect::SearchNNVertical(const bool search_bottom, const ColPartition *part) {
+  ASSERT_HOST(part);
+  ColPartition *nearest_neighbor = nullptr, *neighbor = nullptr;
+  const int kYGapTh = static_cast<int>(std::round(resolution_ * 0.5f));
+
+  ColPartitionGridSearch search(part_grid_);
+  search.SetUniqueMode(true);
+  const TBOX &part_box(part->bounding_box());
+  int y = search_bottom ? part_box.bottom() : part_box.top();
+  search.StartVerticalSearch(part_box.left(), part_box.right(), y);
+  int min_y_gap = std::numeric_limits<int>::max();
+  while ((neighbor = search.NextVerticalSearch(search_bottom)) != nullptr) {
+    if (neighbor == part || !IsTextOrEquationType(neighbor->type())) {
+      continue;
+    }
+    const TBOX &neighbor_box(neighbor->bounding_box());
+    int y_gap = neighbor_box.y_gap(part_box);
+    if (y_gap > kYGapTh) { // Out of scope.
+      break;
+    }
+    if (!neighbor_box.major_x_overlap(part_box) ||
+        (search_bottom && neighbor_box.bottom() > part_box.bottom()) ||
+        (!search_bottom && neighbor_box.top() < part_box.top())) {
+      continue;
+    }
+    if (y_gap < min_y_gap) {
+      min_y_gap = y_gap;
+      nearest_neighbor = neighbor;
+    }
+  }
+
+  return nearest_neighbor;
+}
+
+bool EquationDetect::IsNearMathNeighbor(const int y_gap, const ColPartition *neighbor) const {
+  if (!neighbor) {
+    return false;
+  }
+  const int kYGapTh = static_cast<int>(std::round(resolution_ * 0.1f));
+  return neighbor->type() == PT_EQUATION && y_gap <= kYGapTh;
+}
+
+void EquationDetect::GetOutputTiffName(const char *name, std::string &image_name) const {
+  ASSERT_HOST(name);
+  char page[50];
+  snprintf(page, sizeof(page), "%04d", page_count_);
+  image_name = (lang_tesseract_->imagebasename) + page + name + ".tif";
+}
+
+void EquationDetect::PaintSpecialTexts(const std::string &outfile) const {
+  Image pix = nullptr, pixBi = lang_tesseract_->pix_binary();
+  pix = pixConvertTo32(pixBi);
+  ColPartitionGridSearch gsearch(part_grid_);
+  ColPartition *part = nullptr;
+  gsearch.StartFullSearch();
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    BLOBNBOX_C_IT blob_it(part->boxes());
+    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
+      RenderSpecialText(pix, blob_it.data());
+    }
+  }
+
+  pixWrite(outfile.c_str(), pix, IFF_TIFF_LZW);
+  pix.destroy();
+}
+
+void EquationDetect::PaintColParts(const std::string &outfile) const {
+  Image pix = pixConvertTo32(lang_tesseract_->BestPix());
+  ColPartitionGridSearch gsearch(part_grid_);
+  gsearch.StartFullSearch();
+  ColPartition *part = nullptr;
+  while ((part = gsearch.NextFullSearch()) != nullptr) {
+    const TBOX &tbox = part->bounding_box();
+    Box *box = boxCreate(tbox.left(), pixGetHeight(pix) - tbox.top(), tbox.width(), tbox.height());
+    if (part->type() == PT_EQUATION) {
+      pixRenderBoxArb(pix, box, 5, 255, 0, 0);
+    } else if (part->type() == PT_INLINE_EQUATION) {
+      pixRenderBoxArb(pix, box, 5, 0, 255, 0);
+    } else {
+      pixRenderBoxArb(pix, box, 5, 0, 0, 255);
+    }
+    boxDestroy(&box);
+  }
+
+  pixWrite(outfile.c_str(), pix, IFF_TIFF_LZW);
+  pix.destroy();
+}
+
+void EquationDetect::PrintSpecialBlobsDensity(const ColPartition *part) const {
+  ASSERT_HOST(part);
+  TBOX box(part->bounding_box());
+  int h = pixGetHeight(lang_tesseract_->BestPix());
+  tprintf("Printing special blobs density values for ColParition (t=%d,b=%d) ", h - box.top(),
+          h - box.bottom());
+  box.print();
+  tprintf("blobs count = %d, density = ", part->boxes_count());
+  for (int i = 0; i < BSTT_COUNT; ++i) {
+    auto type = static_cast<BlobSpecialTextType>(i);
+    tprintf("%d:%f ", i, part->SpecialBlobsDensity(type));
+  }
+  tprintf("\n");
+}
+
+} // namespace tesseract