/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 ¤t_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 |