1 /**********************************************************************
2 * File: makerow.cpp (Formerly makerows.c)
3 * Description: Code to arrange blobs into rows of text.
4 * Author: Ray Smith
5 *
6 * (C) Copyright 1992, Hewlett-Packard Ltd.
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 "makerow.h"
25
26 #include "blkocc.h"
27 #include "blobbox.h"
28 #include "ccstruct.h"
29 #include "detlinefit.h"
30 #include "drawtord.h"
31 #include "oldbasel.h"
32 #include "sortflts.h"
33 #include "statistc.h"
34 #include "textord.h"
35 #include "tordmain.h"
36 #include "tovars.h"
37 #include "tprintf.h"
38 #include "underlin.h"
39
40 #include <algorithm>
41 #include <cmath>
42 #include <vector> // for std::vector
43
44 namespace tesseract {
45
46 BOOL_VAR(textord_heavy_nr, false, "Vigorously remove noise");
47 BOOL_VAR(textord_show_initial_rows, false, "Display row accumulation");
48 BOOL_VAR(textord_show_parallel_rows, false, "Display page correlated rows");
49 BOOL_VAR(textord_show_expanded_rows, false, "Display rows after expanding");
50 BOOL_VAR(textord_show_final_rows, false, "Display rows after final fitting");
51 BOOL_VAR(textord_show_final_blobs, false, "Display blob bounds after pre-ass");
52 BOOL_VAR(textord_test_landscape, false, "Tests refer to land/port");
53 BOOL_VAR(textord_parallel_baselines, true, "Force parallel baselines");
54 BOOL_VAR(textord_straight_baselines, false, "Force straight baselines");
55 BOOL_VAR(textord_old_baselines, true, "Use old baseline algorithm");
56 BOOL_VAR(textord_old_xheight, false, "Use old xheight algorithm");
57 BOOL_VAR(textord_fix_xheight_bug, true, "Use spline baseline");
58 BOOL_VAR(textord_fix_makerow_bug, true, "Prevent multiple baselines");
59 BOOL_VAR(textord_debug_xheights, false, "Test xheight algorithms");
60 static BOOL_VAR(textord_biased_skewcalc, true, "Bias skew estimates with line length");
61 static BOOL_VAR(textord_interpolating_skew, true, "Interpolate across gaps");
62 static INT_VAR(textord_skewsmooth_offset, 4, "For smooth factor");
63 static INT_VAR(textord_skewsmooth_offset2, 1, "For smooth factor");
64 INT_VAR(textord_test_x, -INT32_MAX, "coord of test pt");
65 INT_VAR(textord_test_y, -INT32_MAX, "coord of test pt");
66 INT_VAR(textord_min_blobs_in_row, 4, "Min blobs before gradient counted");
67 INT_VAR(textord_spline_minblobs, 8, "Min blobs in each spline segment");
68 INT_VAR(textord_spline_medianwin, 6, "Size of window for spline segmentation");
69 static INT_VAR(textord_max_blob_overlaps, 4, "Max number of blobs a big blob can overlap");
70 INT_VAR(textord_min_xheight, 10, "Min credible pixel xheight");
71 double_VAR(textord_spline_shift_fraction, 0.02, "Fraction of line spacing for quad");
72 double_VAR(textord_skew_ile, 0.5, "Ile of gradients for page skew");
73 double_VAR(textord_skew_lag, 0.02, "Lag for skew on row accumulation");
74 double_VAR(textord_linespace_iqrlimit, 0.2, "Max iqr/median for linespace");
75 double_VAR(textord_width_limit, 8, "Max width of blobs to make rows");
76 double_VAR(textord_chop_width, 1.5, "Max width before chopping");
77 static double_VAR(textord_expansion_factor, 1.0, "Factor to expand rows by in expand_rows");
78 static double_VAR(textord_overlap_x, 0.375, "Fraction of linespace for good overlap");
79 double_VAR(textord_minxh, 0.25, "fraction of linesize for min xheight");
80 double_VAR(textord_min_linesize, 1.25, "* blob height for initial linesize");
81 double_VAR(textord_excess_blobsize, 1.3, "New row made if blob makes row this big");
82 double_VAR(textord_occupancy_threshold, 0.4, "Fraction of neighbourhood");
83 double_VAR(textord_underline_width, 2.0, "Multiple of line_size for underline");
84 double_VAR(textord_min_blob_height_fraction, 0.75,
85 "Min blob height/top to include blob top into xheight stats");
86 double_VAR(textord_xheight_mode_fraction, 0.4, "Min pile height to make xheight");
87 double_VAR(textord_ascheight_mode_fraction, 0.08, "Min pile height to make ascheight");
88 static double_VAR(textord_descheight_mode_fraction, 0.08, "Min pile height to make descheight");
89 double_VAR(textord_ascx_ratio_min, 1.25, "Min cap/xheight");
90 double_VAR(textord_ascx_ratio_max, 1.8, "Max cap/xheight");
91 double_VAR(textord_descx_ratio_min, 0.25, "Min desc/xheight");
92 double_VAR(textord_descx_ratio_max, 0.6, "Max desc/xheight");
93 double_VAR(textord_xheight_error_margin, 0.1, "Accepted variation");
94 INT_VAR(textord_lms_line_trials, 12, "Number of linew fits to do");
95 BOOL_VAR(textord_new_initial_xheight, true, "Use test xheight mechanism");
96 BOOL_VAR(textord_debug_blob, false, "Print test blob information");
97
98 #define MAX_HEIGHT_MODES 12
99
100 const int kMinLeaderCount = 5;
101
102 /**
103 * @name row_y_order
104 *
105 * Sort function to sort rows in y from page top.
106 */
row_y_order(const void * item1,const void * item2)107 static int row_y_order( // sort function
108 const void *item1, // items to compare
109 const void *item2) {
110 // converted ptr
111 const TO_ROW *row1 = *reinterpret_cast<const TO_ROW *const *>(item1);
112 // converted ptr
113 const TO_ROW *row2 = *reinterpret_cast<const TO_ROW *const *>(item2);
114
115 if (row1->parallel_c() > row2->parallel_c()) {
116 return -1;
117 } else if (row1->parallel_c() < row2->parallel_c()) {
118 return 1;
119 } else {
120 return 0;
121 }
122 }
123
124 /**
125 * @name row_spacing_order
126 *
127 * Qsort style function to compare 2 TO_ROWS based on their spacing value.
128 */
row_spacing_order(const TO_ROW * row1,const TO_ROW * row2)129 static int row_spacing_order( // sort function
130 const TO_ROW *row1, // items to compare
131 const TO_ROW *row2) {
132 return row1->spacing < row2->spacing;
133 }
134
135 // Factored-out helper to build a single row from a list of blobs.
136 // Returns the mean blob size.
MakeRowFromBlobs(float line_size,BLOBNBOX_IT * blob_it,TO_ROW_IT * row_it)137 static float MakeRowFromBlobs(float line_size, BLOBNBOX_IT *blob_it, TO_ROW_IT *row_it) {
138 blob_it->sort(blob_x_order);
139 blob_it->move_to_first();
140 TO_ROW *row = nullptr;
141 float total_size = 0.0f;
142 int blob_count = 0;
143 // Add all the blobs to a single TO_ROW.
144 for (; !blob_it->empty(); blob_it->forward()) {
145 BLOBNBOX *blob = blob_it->extract();
146 int top = blob->bounding_box().top();
147 int bottom = blob->bounding_box().bottom();
148 if (row == nullptr) {
149 row = new TO_ROW(blob, top, bottom, line_size);
150 row_it->add_before_then_move(row);
151 } else {
152 row->add_blob(blob, top, bottom, line_size);
153 }
154 total_size += top - bottom;
155 ++blob_count;
156 }
157 return blob_count > 0 ? total_size / blob_count : total_size;
158 }
159
160 // Helper to make a row using the children of a single blob.
161 // Returns the mean size of the blobs created.
MakeRowFromSubBlobs(TO_BLOCK * block,C_BLOB * blob,TO_ROW_IT * row_it)162 static float MakeRowFromSubBlobs(TO_BLOCK *block, C_BLOB *blob, TO_ROW_IT *row_it) {
163 // The blobs made from the children will go in the small_blobs list.
164 BLOBNBOX_IT bb_it(&block->small_blobs);
165 C_OUTLINE_IT ol_it(blob->out_list());
166 // Get the children.
167 ol_it.set_to_list(ol_it.data()->child());
168 if (ol_it.empty()) {
169 return 0.0f;
170 }
171 for (ol_it.mark_cycle_pt(); !ol_it.cycled_list(); ol_it.forward()) {
172 // Deep copy the child outline and use that to make a blob.
173 blob = new C_BLOB(C_OUTLINE::deep_copy(ol_it.data()));
174 // Correct direction as needed.
175 blob->CheckInverseFlagAndDirection();
176 auto *bbox = new BLOBNBOX(blob);
177 bb_it.add_after_then_move(bbox);
178 }
179 // Now we can make a row from the blobs.
180 return MakeRowFromBlobs(block->line_size, &bb_it, row_it);
181 }
182
183 /**
184 * @name make_single_row
185 *
186 * Arrange the blobs into a single row... well actually, if there is
187 * only a single blob, it makes 2 rows, in case the top-level blob
188 * is a container of the real blobs to recognize.
189 */
make_single_row(ICOORD page_tr,bool allow_sub_blobs,TO_BLOCK * block,TO_BLOCK_LIST * blocks)190 float make_single_row(ICOORD page_tr, bool allow_sub_blobs, TO_BLOCK *block,
191 TO_BLOCK_LIST *blocks) {
192 BLOBNBOX_IT blob_it = &block->blobs;
193 TO_ROW_IT row_it = block->get_rows();
194
195 // Include all the small blobs and large blobs.
196 blob_it.add_list_after(&block->small_blobs);
197 blob_it.add_list_after(&block->noise_blobs);
198 blob_it.add_list_after(&block->large_blobs);
199 if (block->blobs.singleton() && allow_sub_blobs) {
200 blob_it.move_to_first();
201 float size = MakeRowFromSubBlobs(block, blob_it.data()->cblob(), &row_it);
202 if (size > block->line_size) {
203 block->line_size = size;
204 }
205 } else if (block->blobs.empty()) {
206 // Make a fake blob.
207 C_BLOB *blob = C_BLOB::FakeBlob(block->block->pdblk.bounding_box());
208 // The blobnbox owns the blob.
209 auto *bblob = new BLOBNBOX(blob);
210 blob_it.add_after_then_move(bblob);
211 }
212 MakeRowFromBlobs(block->line_size, &blob_it, &row_it);
213 // Fit an LMS line to the rows.
214 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
215 fit_lms_line(row_it.data());
216 }
217 float gradient;
218 float fit_error;
219 // Compute the skew based on the fitted line.
220 compute_page_skew(blocks, gradient, fit_error);
221 return gradient;
222 }
223
224 /**
225 * @name make_rows
226 *
227 * Arrange the blobs into rows.
228 */
make_rows(ICOORD page_tr,TO_BLOCK_LIST * port_blocks)229 float make_rows(ICOORD page_tr, TO_BLOCK_LIST *port_blocks) {
230 float port_m; // global skew
231 float port_err; // global noise
232 TO_BLOCK_IT block_it; // iterator
233
234 block_it.set_to_list(port_blocks);
235 for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
236 make_initial_textrows(page_tr, block_it.data(), FCOORD(1.0f, 0.0f), !textord_test_landscape);
237 }
238 // compute globally
239 compute_page_skew(port_blocks, port_m, port_err);
240 block_it.set_to_list(port_blocks);
241 for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
242 cleanup_rows_making(page_tr, block_it.data(), port_m, FCOORD(1.0f, 0.0f),
243 block_it.data()->block->pdblk.bounding_box().left(),
244 !textord_test_landscape);
245 }
246 return port_m; // global skew
247 }
248
249 /**
250 * @name make_initial_textrows
251 *
252 * Arrange the good blobs into rows of text.
253 */
make_initial_textrows(ICOORD page_tr,TO_BLOCK * block,FCOORD rotation,bool testing_on)254 void make_initial_textrows( // find lines
255 ICOORD page_tr,
256 TO_BLOCK *block, // block to do
257 FCOORD rotation, // for drawing
258 bool testing_on // correct orientation
259 ) {
260 TO_ROW_IT row_it = block->get_rows();
261
262 #ifndef GRAPHICS_DISABLED
263 ScrollView::Color colour; // of row
264
265 if (textord_show_initial_rows && testing_on) {
266 if (to_win == nullptr) {
267 create_to_win(page_tr);
268 }
269 }
270 #endif
271 // guess skew
272 assign_blobs_to_rows(block, nullptr, 0, true, true, textord_show_initial_rows && testing_on);
273 row_it.move_to_first();
274 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
275 fit_lms_line(row_it.data());
276 }
277 #ifndef GRAPHICS_DISABLED
278 if (textord_show_initial_rows && testing_on) {
279 colour = ScrollView::RED;
280 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
281 plot_to_row(row_it.data(), colour, rotation);
282 colour = static_cast<ScrollView::Color>(colour + 1);
283 if (colour > ScrollView::MAGENTA) {
284 colour = ScrollView::RED;
285 }
286 }
287 }
288 #endif
289 }
290
291 /**
292 * @name fit_lms_line
293 *
294 * Fit an LMS line to a row.
295 */
fit_lms_line(TO_ROW * row)296 void fit_lms_line(TO_ROW *row) {
297 float m, c; // fitted line
298 tesseract::DetLineFit lms;
299 BLOBNBOX_IT blob_it = row->blob_list();
300
301 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
302 const TBOX &box = blob_it.data()->bounding_box();
303 lms.Add(ICOORD((box.left() + box.right()) / 2, box.bottom()));
304 }
305 double error = lms.Fit(&m, &c);
306 row->set_line(m, c, error);
307 }
308
309 /**
310 * @name compute_page_skew
311 *
312 * Compute the skew over a full page by averaging the gradients over
313 * all the lines. Get the error of the same row.
314 */
compute_page_skew(TO_BLOCK_LIST * blocks,float & page_m,float & page_err)315 void compute_page_skew( // get average gradient
316 TO_BLOCK_LIST *blocks, // list of blocks
317 float &page_m, // average gradient
318 float &page_err // average error
319 ) {
320 int32_t row_count; // total rows
321 int32_t blob_count; // total_blobs
322 int32_t row_err; // integer error
323 int32_t row_index; // of total
324 TO_ROW *row; // current row
325 TO_BLOCK_IT block_it = blocks; // iterator
326
327 row_count = 0;
328 blob_count = 0;
329 for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
330 POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
331 if (pb != nullptr && !pb->IsText()) {
332 continue; // Pretend non-text blocks don't exist.
333 }
334 row_count += block_it.data()->get_rows()->length();
335 // count up rows
336 TO_ROW_IT row_it(block_it.data()->get_rows());
337 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
338 blob_count += row_it.data()->blob_list()->length();
339 }
340 }
341 if (row_count == 0) {
342 page_m = 0.0f;
343 page_err = 0.0f;
344 return;
345 }
346 // of rows
347 std::vector<float> gradients(blob_count);
348 // of rows
349 std::vector<float> errors(blob_count);
350
351 row_index = 0;
352 for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
353 POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
354 if (pb != nullptr && !pb->IsText()) {
355 continue; // Pretend non-text blocks don't exist.
356 }
357 TO_ROW_IT row_it(block_it.data()->get_rows());
358 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
359 row = row_it.data();
360 blob_count = row->blob_list()->length();
361 row_err = static_cast<int32_t>(std::ceil(row->line_error()));
362 if (row_err <= 0) {
363 row_err = 1;
364 }
365 if (textord_biased_skewcalc) {
366 blob_count /= row_err;
367 for (blob_count /= row_err; blob_count > 0; blob_count--) {
368 gradients[row_index] = row->line_m();
369 errors[row_index] = row->line_error();
370 row_index++;
371 }
372 } else if (blob_count >= textord_min_blobs_in_row) {
373 // get gradient
374 gradients[row_index] = row->line_m();
375 errors[row_index] = row->line_error();
376 row_index++;
377 }
378 }
379 }
380 if (row_index == 0) {
381 // desperate
382 for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
383 POLY_BLOCK *pb = block_it.data()->block->pdblk.poly_block();
384 if (pb != nullptr && !pb->IsText()) {
385 continue; // Pretend non-text blocks don't exist.
386 }
387 TO_ROW_IT row_it(block_it.data()->get_rows());
388 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
389 row = row_it.data();
390 gradients[row_index] = row->line_m();
391 errors[row_index] = row->line_error();
392 row_index++;
393 }
394 }
395 }
396 row_count = row_index;
397 row_index = static_cast<int32_t>(row_count * textord_skew_ile);
398 gradients.resize(row_count);
399 std::nth_element(gradients.begin(), gradients.begin() + row_index, gradients.end());
400 page_m = gradients[row_index];
401 row_index = static_cast<int32_t>(row_count * textord_skew_ile);
402 errors.resize(row_count);
403 std::nth_element(errors.begin(), errors.begin() + row_index, errors.end());
404 page_err = errors[row_index];
405 }
406
407 const double kNoiseSize = 0.5; // Fraction of xheight.
408 const int kMinSize = 8; // Min pixels to be xheight.
409
410 /**
411 * Return true if the dot looks like it is part of the i.
412 * Doesn't work for any other diacritical.
413 */
dot_of_i(BLOBNBOX * dot,BLOBNBOX * i,TO_ROW * row)414 static bool dot_of_i(BLOBNBOX *dot, BLOBNBOX *i, TO_ROW *row) {
415 const TBOX &ibox = i->bounding_box();
416 const TBOX &dotbox = dot->bounding_box();
417
418 // Must overlap horizontally by enough and be high enough.
419 int overlap = std::min(dotbox.right(), ibox.right()) - std::max(dotbox.left(), ibox.left());
420 if (ibox.height() <= 2 * dotbox.height() ||
421 (overlap * 2 < ibox.width() && overlap < dotbox.width())) {
422 return false;
423 }
424
425 // If the i is tall and thin then it is good.
426 if (ibox.height() > ibox.width() * 2) {
427 return true; // The i or ! must be tall and thin.
428 }
429
430 // It might still be tall and thin, but it might be joined to something.
431 // So search the outline for a piece of large height close to the edges
432 // of the dot.
433 const double kHeightFraction = 0.6;
434 double target_height = std::min(dotbox.bottom(), ibox.top());
435 target_height -= row->line_m() * dotbox.left() + row->line_c();
436 target_height *= kHeightFraction;
437 int left_min = dotbox.left() - dotbox.width();
438 int middle = (dotbox.left() + dotbox.right()) / 2;
439 int right_max = dotbox.right() + dotbox.width();
440 int left_miny = 0;
441 int left_maxy = 0;
442 int right_miny = 0;
443 int right_maxy = 0;
444 bool found_left = false;
445 bool found_right = false;
446 bool in_left = false;
447 bool in_right = false;
448 C_BLOB *blob = i->cblob();
449 C_OUTLINE_IT o_it = blob->out_list();
450 for (o_it.mark_cycle_pt(); !o_it.cycled_list(); o_it.forward()) {
451 C_OUTLINE *outline = o_it.data();
452 int length = outline->pathlength();
453 ICOORD pos = outline->start_pos();
454 for (int step = 0; step < length; pos += outline->step(step++)) {
455 int x = pos.x();
456 int y = pos.y();
457 if (x >= left_min && x < middle && !found_left) {
458 // We are in the left part so find min and max y.
459 if (in_left) {
460 if (y > left_maxy) {
461 left_maxy = y;
462 }
463 if (y < left_miny) {
464 left_miny = y;
465 }
466 } else {
467 left_maxy = left_miny = y;
468 in_left = true;
469 }
470 } else if (in_left) {
471 // We just left the left so look for size.
472 if (left_maxy - left_miny > target_height) {
473 if (found_right) {
474 return true;
475 }
476 found_left = true;
477 }
478 in_left = false;
479 }
480 if (x <= right_max && x > middle && !found_right) {
481 // We are in the right part so find min and max y.
482 if (in_right) {
483 if (y > right_maxy) {
484 right_maxy = y;
485 }
486 if (y < right_miny) {
487 right_miny = y;
488 }
489 } else {
490 right_maxy = right_miny = y;
491 in_right = true;
492 }
493 } else if (in_right) {
494 // We just left the right so look for size.
495 if (right_maxy - right_miny > target_height) {
496 if (found_left) {
497 return true;
498 }
499 found_right = true;
500 }
501 in_right = false;
502 }
503 }
504 }
505 return false;
506 }
507
vigorous_noise_removal(TO_BLOCK * block)508 void vigorous_noise_removal(TO_BLOCK *block) {
509 TO_ROW_IT row_it = block->get_rows();
510 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
511 TO_ROW *row = row_it.data();
512 BLOBNBOX_IT b_it = row->blob_list();
513 // Estimate the xheight on the row.
514 int max_height = 0;
515 for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
516 BLOBNBOX *blob = b_it.data();
517 if (blob->bounding_box().height() > max_height) {
518 max_height = blob->bounding_box().height();
519 }
520 }
521 STATS hstats(0, max_height + 1);
522 for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
523 BLOBNBOX *blob = b_it.data();
524 int height = blob->bounding_box().height();
525 if (height >= kMinSize) {
526 hstats.add(blob->bounding_box().height(), 1);
527 }
528 }
529 float xheight = hstats.median();
530 // Delete small objects.
531 BLOBNBOX *prev = nullptr;
532 for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
533 BLOBNBOX *blob = b_it.data();
534 const TBOX &box = blob->bounding_box();
535 if (box.height() < kNoiseSize * xheight) {
536 // Small so delete unless it looks like an i dot.
537 if (prev != nullptr) {
538 if (dot_of_i(blob, prev, row)) {
539 continue; // Looks OK.
540 }
541 }
542 if (!b_it.at_last()) {
543 BLOBNBOX *next = b_it.data_relative(1);
544 if (dot_of_i(blob, next, row)) {
545 continue; // Looks OK.
546 }
547 }
548 // It might be noise so get rid of it.
549 delete blob->cblob();
550 delete b_it.extract();
551 } else {
552 prev = blob;
553 }
554 }
555 }
556 }
557
558 /**
559 * cleanup_rows_making
560 *
561 * Remove overlapping rows and fit all the blobs to what's left.
562 */
cleanup_rows_making(ICOORD page_tr,TO_BLOCK * block,float gradient,FCOORD rotation,int32_t block_edge,bool testing_on)563 void cleanup_rows_making( // find lines
564 ICOORD page_tr, // top right
565 TO_BLOCK *block, // block to do
566 float gradient, // gradient to fit
567 FCOORD rotation, // for drawing
568 int32_t block_edge, // edge of block
569 bool testing_on // correct orientation
570 ) {
571 // iterators
572 BLOBNBOX_IT blob_it = &block->blobs;
573 TO_ROW_IT row_it = block->get_rows();
574
575 #ifndef GRAPHICS_DISABLED
576 if (textord_show_parallel_rows && testing_on) {
577 if (to_win == nullptr) {
578 create_to_win(page_tr);
579 }
580 }
581 #endif
582 // get row coords
583 fit_parallel_rows(block, gradient, rotation, block_edge,
584 textord_show_parallel_rows && testing_on);
585 delete_non_dropout_rows(block, gradient, rotation, block_edge,
586 textord_show_parallel_rows && testing_on);
587 expand_rows(page_tr, block, gradient, rotation, block_edge, testing_on);
588 blob_it.set_to_list(&block->blobs);
589 row_it.set_to_list(block->get_rows());
590 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
591 blob_it.add_list_after(row_it.data()->blob_list());
592 }
593 // give blobs back
594 assign_blobs_to_rows(block, &gradient, 1, false, false, false);
595 // now new rows must be genuine
596 blob_it.set_to_list(&block->blobs);
597 blob_it.add_list_after(&block->large_blobs);
598 assign_blobs_to_rows(block, &gradient, 2, true, true, false);
599 // safe to use big ones now
600 blob_it.set_to_list(&block->blobs);
601 // throw all blobs in
602 blob_it.add_list_after(&block->noise_blobs);
603 blob_it.add_list_after(&block->small_blobs);
604 assign_blobs_to_rows(block, &gradient, 3, false, false, false);
605 }
606
607 /**
608 * delete_non_dropout_rows
609 *
610 * Compute the linespacing and offset.
611 */
delete_non_dropout_rows(TO_BLOCK * block,float gradient,FCOORD rotation,int32_t block_edge,bool testing_on)612 void delete_non_dropout_rows( // find lines
613 TO_BLOCK *block, // block to do
614 float gradient, // global skew
615 FCOORD rotation, // deskew vector
616 int32_t block_edge, // left edge
617 bool testing_on // correct orientation
618 ) {
619 TBOX block_box; // deskewed block
620 int32_t max_y; // in block
621 int32_t min_y;
622 int32_t line_index; // of scan line
623 int32_t line_count; // no of scan lines
624 int32_t distance; // to drop-out
625 int32_t xleft; // of block
626 int32_t ybottom; // of block
627 TO_ROW *row; // current row
628 TO_ROW_IT row_it = block->get_rows();
629 BLOBNBOX_IT blob_it = &block->blobs;
630
631 if (row_it.empty()) {
632 return; // empty block
633 }
634 block_box = deskew_block_coords(block, gradient);
635 xleft = block->block->pdblk.bounding_box().left();
636 ybottom = block->block->pdblk.bounding_box().bottom();
637 min_y = block_box.bottom() - 1;
638 max_y = block_box.top() + 1;
639 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
640 line_index = static_cast<int32_t>(std::floor(row_it.data()->intercept()));
641 if (line_index <= min_y) {
642 min_y = line_index - 1;
643 }
644 if (line_index >= max_y) {
645 max_y = line_index + 1;
646 }
647 }
648 line_count = max_y - min_y + 1;
649 if (line_count <= 0) {
650 return; // empty block
651 }
652 // change in occupation
653 std::vector<int32_t> deltas(line_count);
654 // of pixel coords
655 std::vector<int32_t> occupation(line_count);
656
657 compute_line_occupation(block, gradient, min_y, max_y, &occupation[0], &deltas[0]);
658 compute_occupation_threshold(
659 static_cast<int32_t>(ceil(block->line_spacing * (tesseract::CCStruct::kDescenderFraction +
660 tesseract::CCStruct::kAscenderFraction))),
661 static_cast<int32_t>(ceil(block->line_spacing * (tesseract::CCStruct::kXHeightFraction +
662 tesseract::CCStruct::kAscenderFraction))),
663 max_y - min_y + 1, &occupation[0], &deltas[0]);
664 #ifndef GRAPHICS_DISABLED
665 if (testing_on) {
666 draw_occupation(xleft, ybottom, min_y, max_y, &occupation[0], &deltas[0]);
667 }
668 #endif
669 compute_dropout_distances(&occupation[0], &deltas[0], line_count);
670 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
671 row = row_it.data();
672 line_index = static_cast<int32_t>(std::floor(row->intercept()));
673 distance = deltas[line_index - min_y];
674 if (find_best_dropout_row(row, distance, block->line_spacing / 2, line_index, &row_it,
675 testing_on)) {
676 #ifndef GRAPHICS_DISABLED
677 if (testing_on) {
678 plot_parallel_row(row, gradient, block_edge, ScrollView::WHITE, rotation);
679 }
680 #endif
681 blob_it.add_list_after(row_it.data()->blob_list());
682 delete row_it.extract(); // too far away
683 }
684 }
685 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
686 blob_it.add_list_after(row_it.data()->blob_list());
687 }
688 }
689
690 /**
691 * @name find_best_dropout_row
692 *
693 * Delete this row if it has a neighbour with better dropout characteristics.
694 * true is returned if the row should be deleted.
695 */
find_best_dropout_row(TO_ROW * row,int32_t distance,float dist_limit,int32_t line_index,TO_ROW_IT * row_it,bool testing_on)696 bool find_best_dropout_row( // find neighbours
697 TO_ROW *row, // row to test
698 int32_t distance, // dropout dist
699 float dist_limit, // threshold distance
700 int32_t line_index, // index of row
701 TO_ROW_IT *row_it, // current position
702 bool testing_on // correct orientation
703 ) {
704 int32_t next_index; // of neighbouring row
705 int32_t row_offset; // from current row
706 int32_t abs_dist; // absolute distance
707 int8_t row_inc; // increment to row_index
708 TO_ROW *next_row; // nextious row
709
710 if (testing_on) {
711 tprintf("Row at %g(%g), dropout dist=%d,", row->intercept(), row->parallel_c(), distance);
712 }
713 if (distance < 0) {
714 row_inc = 1;
715 abs_dist = -distance;
716 } else {
717 row_inc = -1;
718 abs_dist = distance;
719 }
720 if (abs_dist > dist_limit) {
721 if (testing_on) {
722 tprintf(" too far - deleting\n");
723 }
724 return true;
725 }
726 if ((distance < 0 && !row_it->at_last()) || (distance >= 0 && !row_it->at_first())) {
727 row_offset = row_inc;
728 do {
729 next_row = row_it->data_relative(row_offset);
730 next_index = static_cast<int32_t>(std::floor(next_row->intercept()));
731 if ((distance < 0 && next_index < line_index &&
732 next_index > line_index + distance + distance) ||
733 (distance >= 0 && next_index > line_index &&
734 next_index < line_index + distance + distance)) {
735 if (testing_on) {
736 tprintf(" nearer neighbour (%d) at %g\n", line_index + distance - next_index,
737 next_row->intercept());
738 }
739 return true; // other is nearer
740 } else if (next_index == line_index || next_index == line_index + distance + distance) {
741 if (row->believability() <= next_row->believability()) {
742 if (testing_on) {
743 tprintf(" equal but more believable at %g (%g/%g)\n", next_row->intercept(),
744 row->believability(), next_row->believability());
745 }
746 return true; // other is more believable
747 }
748 }
749 row_offset += row_inc;
750 } while ((next_index == line_index || next_index == line_index + distance + distance) &&
751 row_offset < row_it->length());
752 if (testing_on) {
753 tprintf(" keeping\n");
754 }
755 }
756 return false;
757 }
758
759 /**
760 * @name deskew_block_coords
761 *
762 * Compute the bounding box of all the blobs in the block
763 * if they were deskewed without actually doing it.
764 */
deskew_block_coords(TO_BLOCK * block,float gradient)765 TBOX deskew_block_coords( // block box
766 TO_BLOCK *block, // block to do
767 float gradient // global skew
768 ) {
769 TBOX result; // block bounds
770 TBOX blob_box; // of block
771 FCOORD rotation; // deskew vector
772 float length; // of gradient vector
773 TO_ROW_IT row_it = block->get_rows();
774 TO_ROW *row; // current row
775 BLOBNBOX *blob; // current blob
776 BLOBNBOX_IT blob_it; // iterator
777
778 length = std::sqrt(gradient * gradient + 1);
779 rotation = FCOORD(1 / length, -gradient / length);
780 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
781 row = row_it.data();
782 blob_it.set_to_list(row->blob_list());
783 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
784 blob = blob_it.data();
785 blob_box = blob->bounding_box();
786 blob_box.rotate(rotation); // de-skew it
787 result += blob_box;
788 }
789 }
790 return result;
791 }
792
793 /**
794 * @name compute_line_occupation
795 *
796 * Compute the pixel projection back on the y axis given the global
797 * skew. Also compute the 1st derivative.
798 */
compute_line_occupation(TO_BLOCK * block,float gradient,int32_t min_y,int32_t max_y,int32_t * occupation,int32_t * deltas)799 void compute_line_occupation( // project blobs
800 TO_BLOCK *block, // block to do
801 float gradient, // global skew
802 int32_t min_y, // min coord in block
803 int32_t max_y, // in block
804 int32_t *occupation, // output projection
805 int32_t *deltas // derivative
806 ) {
807 int32_t line_count; // maxy-miny+1
808 int32_t line_index; // of scan line
809 int index; // array index for daft compilers
810 TO_ROW *row; // current row
811 TO_ROW_IT row_it = block->get_rows();
812 BLOBNBOX *blob; // current blob
813 BLOBNBOX_IT blob_it; // iterator
814 float length; // of skew vector
815 TBOX blob_box; // bounding box
816 FCOORD rotation; // inverse of skew
817
818 line_count = max_y - min_y + 1;
819 length = std::sqrt(gradient * gradient + 1);
820 rotation = FCOORD(1 / length, -gradient / length);
821 for (line_index = 0; line_index < line_count; line_index++) {
822 deltas[line_index] = 0;
823 }
824 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
825 row = row_it.data();
826 blob_it.set_to_list(row->blob_list());
827 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
828 blob = blob_it.data();
829 blob_box = blob->bounding_box();
830 blob_box.rotate(rotation); // de-skew it
831 int32_t width = blob_box.right() - blob_box.left();
832 index = blob_box.bottom() - min_y;
833 ASSERT_HOST(index >= 0 && index < line_count);
834 // count transitions
835 deltas[index] += width;
836 index = blob_box.top() - min_y;
837 ASSERT_HOST(index >= 0 && index < line_count);
838 deltas[index] -= width;
839 }
840 }
841 occupation[0] = deltas[0];
842 for (line_index = 1; line_index < line_count; line_index++) {
843 occupation[line_index] = occupation[line_index - 1] + deltas[line_index];
844 }
845 }
846
847 /**
848 * compute_occupation_threshold
849 *
850 * Compute thresholds for textline or not for the occupation array.
851 */
compute_occupation_threshold(int32_t low_window,int32_t high_window,int32_t line_count,int32_t * occupation,int32_t * thresholds)852 void compute_occupation_threshold( // project blobs
853 int32_t low_window, // below result point
854 int32_t high_window, // above result point
855 int32_t line_count, // array sizes
856 int32_t *occupation, // input projection
857 int32_t *thresholds // output thresholds
858 ) {
859 int32_t line_index; // of thresholds line
860 int32_t low_index; // in occupation
861 int32_t high_index; // in occupation
862 int32_t sum; // current average
863 int32_t divisor; // to get thresholds
864 int32_t min_index; // of min occ
865 int32_t min_occ; // min in locality
866 int32_t test_index; // for finding min
867
868 divisor = static_cast<int32_t>(ceil((low_window + high_window) / textord_occupancy_threshold));
869 if (low_window + high_window < line_count) {
870 for (sum = 0, high_index = 0; high_index < low_window; high_index++) {
871 sum += occupation[high_index];
872 }
873 for (low_index = 0; low_index < high_window; low_index++, high_index++) {
874 sum += occupation[high_index];
875 }
876 min_occ = occupation[0];
877 min_index = 0;
878 for (test_index = 1; test_index < high_index; test_index++) {
879 if (occupation[test_index] <= min_occ) {
880 min_occ = occupation[test_index];
881 min_index = test_index; // find min in region
882 }
883 }
884 for (line_index = 0; line_index < low_window; line_index++) {
885 thresholds[line_index] = (sum - min_occ) / divisor + min_occ;
886 }
887 // same out to end
888 for (low_index = 0; high_index < line_count; low_index++, high_index++) {
889 sum -= occupation[low_index];
890 sum += occupation[high_index];
891 if (occupation[high_index] <= min_occ) {
892 // find min in region
893 min_occ = occupation[high_index];
894 min_index = high_index;
895 }
896 // lost min from region
897 if (min_index <= low_index) {
898 min_occ = occupation[low_index + 1];
899 min_index = low_index + 1;
900 for (test_index = low_index + 2; test_index <= high_index; test_index++) {
901 if (occupation[test_index] <= min_occ) {
902 min_occ = occupation[test_index];
903 // find min in region
904 min_index = test_index;
905 }
906 }
907 }
908 thresholds[line_index++] = (sum - min_occ) / divisor + min_occ;
909 }
910 } else {
911 min_occ = occupation[0];
912 min_index = 0;
913 for (sum = 0, low_index = 0; low_index < line_count; low_index++) {
914 if (occupation[low_index] < min_occ) {
915 min_occ = occupation[low_index];
916 min_index = low_index;
917 }
918 sum += occupation[low_index];
919 }
920 line_index = 0;
921 }
922 for (; line_index < line_count; line_index++) {
923 thresholds[line_index] = (sum - min_occ) / divisor + min_occ;
924 }
925 // same out to end
926 }
927
928 /**
929 * @name compute_dropout_distances
930 *
931 * Compute the distance from each coordinate to the nearest dropout.
932 */
compute_dropout_distances(int32_t * occupation,int32_t * thresholds,int32_t line_count)933 void compute_dropout_distances( // project blobs
934 int32_t *occupation, // input projection
935 int32_t *thresholds, // output thresholds
936 int32_t line_count // array sizes
937 ) {
938 int32_t line_index; // of thresholds line
939 int32_t distance; // from prev dropout
940 int32_t next_dist; // to next dropout
941 int32_t back_index; // for back filling
942 int32_t prev_threshold; // before overwrite
943
944 distance = -line_count;
945 line_index = 0;
946 do {
947 do {
948 distance--;
949 prev_threshold = thresholds[line_index];
950 // distance from prev
951 thresholds[line_index] = distance;
952 line_index++;
953 } while (line_index < line_count && (occupation[line_index] < thresholds[line_index] ||
954 occupation[line_index - 1] >= prev_threshold));
955 if (line_index < line_count) {
956 back_index = line_index - 1;
957 next_dist = 1;
958 while (next_dist < -distance && back_index >= 0) {
959 thresholds[back_index] = next_dist;
960 back_index--;
961 next_dist++;
962 distance++;
963 }
964 distance = 1;
965 }
966 } while (line_index < line_count);
967 }
968
969 /**
970 * @name expand_rows
971 *
972 * Expand each row to the least of its allowed size and touching its
973 * neighbours. If the expansion would entirely swallow a neighbouring row
974 * then do so.
975 */
expand_rows(ICOORD page_tr,TO_BLOCK * block,float gradient,FCOORD rotation,int32_t block_edge,bool testing_on)976 void expand_rows( // find lines
977 ICOORD page_tr, // top right
978 TO_BLOCK *block, // block to do
979 float gradient, // gradient to fit
980 FCOORD rotation, // for drawing
981 int32_t block_edge, // edge of block
982 bool testing_on // correct orientation
983 ) {
984 bool swallowed_row; // eaten a neighbour
985 float y_max, y_min; // new row limits
986 float y_bottom, y_top; // allowed limits
987 TO_ROW *test_row; // next row
988 TO_ROW *row; // current row
989 // iterators
990 BLOBNBOX_IT blob_it = &block->blobs;
991 TO_ROW_IT row_it = block->get_rows();
992
993 #ifndef GRAPHICS_DISABLED
994 if (textord_show_expanded_rows && testing_on) {
995 if (to_win == nullptr) {
996 create_to_win(page_tr);
997 }
998 }
999 #endif
1000
1001 adjust_row_limits(block); // shift min,max.
1002 if (textord_new_initial_xheight) {
1003 if (block->get_rows()->empty()) {
1004 return;
1005 }
1006 compute_row_stats(block, textord_show_expanded_rows && testing_on);
1007 }
1008 assign_blobs_to_rows(block, &gradient, 4, true, false, false);
1009 // get real membership
1010 if (block->get_rows()->empty()) {
1011 return;
1012 }
1013 fit_parallel_rows(block, gradient, rotation, block_edge,
1014 textord_show_expanded_rows && testing_on);
1015 if (!textord_new_initial_xheight) {
1016 compute_row_stats(block, textord_show_expanded_rows && testing_on);
1017 }
1018 row_it.move_to_last();
1019 do {
1020 row = row_it.data();
1021 y_max = row->max_y(); // get current limits
1022 y_min = row->min_y();
1023 y_bottom = row->intercept() - block->line_size * textord_expansion_factor *
1024 tesseract::CCStruct::kDescenderFraction;
1025 y_top = row->intercept() +
1026 block->line_size * textord_expansion_factor *
1027 (tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction);
1028 if (y_min > y_bottom) { // expansion allowed
1029 if (textord_show_expanded_rows && testing_on) {
1030 tprintf("Expanding bottom of row at %f from %f to %f\n", row->intercept(), y_min, y_bottom);
1031 }
1032 // expandable
1033 swallowed_row = true;
1034 while (swallowed_row && !row_it.at_last()) {
1035 swallowed_row = false;
1036 // get next one
1037 test_row = row_it.data_relative(1);
1038 // overlaps space
1039 if (test_row->max_y() > y_bottom) {
1040 if (test_row->min_y() > y_bottom) {
1041 if (textord_show_expanded_rows && testing_on) {
1042 tprintf("Eating row below at %f\n", test_row->intercept());
1043 }
1044 row_it.forward();
1045 #ifndef GRAPHICS_DISABLED
1046 if (textord_show_expanded_rows && testing_on) {
1047 plot_parallel_row(test_row, gradient, block_edge, ScrollView::WHITE, rotation);
1048 }
1049 #endif
1050 blob_it.set_to_list(row->blob_list());
1051 blob_it.add_list_after(test_row->blob_list());
1052 // swallow complete row
1053 delete row_it.extract();
1054 row_it.backward();
1055 swallowed_row = true;
1056 } else if (test_row->max_y() < y_min) {
1057 // shorter limit
1058 y_bottom = test_row->max_y();
1059 if (textord_show_expanded_rows && testing_on) {
1060 tprintf("Truncating limit to %f due to touching row at %f\n", y_bottom,
1061 test_row->intercept());
1062 }
1063 } else {
1064 y_bottom = y_min; // can't expand it
1065 if (textord_show_expanded_rows && testing_on) {
1066 tprintf("Not expanding limit beyond %f due to touching row at %f\n", y_bottom,
1067 test_row->intercept());
1068 }
1069 }
1070 }
1071 }
1072 y_min = y_bottom; // expand it
1073 }
1074 if (y_max < y_top) { // expansion allowed
1075 if (textord_show_expanded_rows && testing_on) {
1076 tprintf("Expanding top of row at %f from %f to %f\n", row->intercept(), y_max, y_top);
1077 }
1078 swallowed_row = true;
1079 while (swallowed_row && !row_it.at_first()) {
1080 swallowed_row = false;
1081 // get one above
1082 test_row = row_it.data_relative(-1);
1083 if (test_row->min_y() < y_top) {
1084 if (test_row->max_y() < y_top) {
1085 if (textord_show_expanded_rows && testing_on) {
1086 tprintf("Eating row above at %f\n", test_row->intercept());
1087 }
1088 row_it.backward();
1089 blob_it.set_to_list(row->blob_list());
1090 #ifndef GRAPHICS_DISABLED
1091 if (textord_show_expanded_rows && testing_on) {
1092 plot_parallel_row(test_row, gradient, block_edge, ScrollView::WHITE, rotation);
1093 }
1094 #endif
1095 blob_it.add_list_after(test_row->blob_list());
1096 // swallow complete row
1097 delete row_it.extract();
1098 row_it.forward();
1099 swallowed_row = true;
1100 } else if (test_row->min_y() < y_max) {
1101 // shorter limit
1102 y_top = test_row->min_y();
1103 if (textord_show_expanded_rows && testing_on) {
1104 tprintf("Truncating limit to %f due to touching row at %f\n", y_top,
1105 test_row->intercept());
1106 }
1107 } else {
1108 y_top = y_max; // can't expand it
1109 if (textord_show_expanded_rows && testing_on) {
1110 tprintf("Not expanding limit beyond %f due to touching row at %f\n", y_top,
1111 test_row->intercept());
1112 }
1113 }
1114 }
1115 }
1116 y_max = y_top;
1117 }
1118 // new limits
1119 row->set_limits(y_min, y_max);
1120 row_it.backward();
1121 } while (!row_it.at_last());
1122 }
1123
1124 /**
1125 * adjust_row_limits
1126 *
1127 * Change the limits of rows to suit the default fractions.
1128 */
adjust_row_limits(TO_BLOCK * block)1129 void adjust_row_limits( // tidy limits
1130 TO_BLOCK *block // block to do
1131 ) {
1132 TO_ROW *row; // current row
1133 float size; // size of row
1134 float ymax; // top of row
1135 float ymin; // bottom of row
1136 TO_ROW_IT row_it = block->get_rows();
1137
1138 if (textord_show_expanded_rows) {
1139 tprintf("Adjusting row limits for block(%d,%d)\n", block->block->pdblk.bounding_box().left(),
1140 block->block->pdblk.bounding_box().top());
1141 }
1142 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1143 row = row_it.data();
1144 size = row->max_y() - row->min_y();
1145 if (textord_show_expanded_rows) {
1146 tprintf("Row at %f has min %f, max %f, size %f\n", row->intercept(), row->min_y(),
1147 row->max_y(), size);
1148 }
1149 size /= tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction +
1150 tesseract::CCStruct::kDescenderFraction;
1151 ymax = size * (tesseract::CCStruct::kXHeightFraction + tesseract::CCStruct::kAscenderFraction);
1152 ymin = -size * tesseract::CCStruct::kDescenderFraction;
1153 row->set_limits(row->intercept() + ymin, row->intercept() + ymax);
1154 row->merged = false;
1155 }
1156 }
1157
1158 /**
1159 * @name compute_row_stats
1160 *
1161 * Compute the linespacing and offset.
1162 */
compute_row_stats(TO_BLOCK * block,bool testing_on)1163 void compute_row_stats( // find lines
1164 TO_BLOCK *block, // block to do
1165 bool testing_on // correct orientation
1166 ) {
1167 int32_t row_index; // of median
1168 TO_ROW *row; // current row
1169 TO_ROW *prev_row; // previous row
1170 float iqr; // inter quartile range
1171 TO_ROW_IT row_it = block->get_rows();
1172 // number of rows
1173 int16_t rowcount = row_it.length();
1174 // for choose nth
1175 std::vector<TO_ROW *> rows(rowcount);
1176 rowcount = 0;
1177 prev_row = nullptr;
1178 row_it.move_to_last(); // start at bottom
1179 do {
1180 row = row_it.data();
1181 if (prev_row != nullptr) {
1182 rows[rowcount++] = prev_row;
1183 prev_row->spacing = row->intercept() - prev_row->intercept();
1184 if (prev_row->spacing < 0.1 && prev_row->spacing > -0.1) {
1185 // Avoid small spacing values which give a small disp_quant_factor_.
1186 // That can cause large memory allocations with out-of-memory.
1187 prev_row->spacing = 0;
1188 }
1189 if (testing_on) {
1190 tprintf("Row at %g yields spacing of %g\n", row->intercept(), prev_row->spacing);
1191 }
1192 }
1193 prev_row = row;
1194 row_it.backward();
1195 } while (!row_it.at_last());
1196 block->key_row = prev_row;
1197 block->baseline_offset = std::fmod(prev_row->parallel_c(), block->line_spacing);
1198 if (testing_on) {
1199 tprintf("Blob based spacing=(%g,%g), offset=%g", block->line_size, block->line_spacing,
1200 block->baseline_offset);
1201 }
1202 if (rowcount > 0) {
1203 rows.resize(rowcount);
1204 row_index = rowcount * 3 / 4;
1205 std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
1206 iqr = rows[row_index]->spacing;
1207 row_index = rowcount / 4;
1208 std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
1209 iqr -= rows[row_index]->spacing;
1210 row_index = rowcount / 2;
1211 std::nth_element(rows.begin(), rows.begin() + row_index, rows.end(), row_spacing_order);
1212 block->key_row = rows[row_index];
1213 if (testing_on) {
1214 tprintf(" row based=%g(%g)", rows[row_index]->spacing, iqr);
1215 }
1216 if (rowcount > 2 && iqr < rows[row_index]->spacing * textord_linespace_iqrlimit) {
1217 if (!textord_new_initial_xheight) {
1218 if (rows[row_index]->spacing < block->line_spacing &&
1219 rows[row_index]->spacing > block->line_size) {
1220 // within range
1221 block->line_size = rows[row_index]->spacing;
1222 // spacing=size
1223 } else if (rows[row_index]->spacing > block->line_spacing) {
1224 block->line_size = block->line_spacing;
1225 }
1226 // too big so use max
1227 } else {
1228 if (rows[row_index]->spacing < block->line_spacing) {
1229 block->line_size = rows[row_index]->spacing;
1230 } else {
1231 block->line_size = block->line_spacing;
1232 }
1233 // too big so use max
1234 }
1235 if (block->line_size < textord_min_xheight) {
1236 block->line_size = (float)textord_min_xheight;
1237 }
1238 block->line_spacing = rows[row_index]->spacing;
1239 block->max_blob_size = block->line_spacing * textord_excess_blobsize;
1240 }
1241 block->baseline_offset = std::fmod(rows[row_index]->intercept(), block->line_spacing);
1242 }
1243 if (testing_on) {
1244 tprintf("\nEstimate line size=%g, spacing=%g, offset=%g\n", block->line_size,
1245 block->line_spacing, block->baseline_offset);
1246 }
1247 }
1248
1249 /**
1250 * @name compute_block_xheight
1251 *
1252 * Compute the xheight of the individual rows, then correlate them
1253 * and interpret ascenderless lines, correcting xheights.
1254 *
1255 * First we compute our best guess of the x-height of each row independently
1256 * with compute_row_xheight(), which looks for a pair of commonly occurring
1257 * heights that could be x-height and ascender height. This function also
1258 * attempts to find descenders of lowercase letters (i.e. not the small
1259 * descenders that could appear in upper case letters as Q,J).
1260 *
1261 * After this computation each row falls into one of the following categories:
1262 * ROW_ASCENDERS_FOUND: we found xheight and ascender modes, so this must be
1263 * a regular row; we'll use its xheight to compute
1264 * xheight and ascrise estimates for the block
1265 * ROW_DESCENDERS_FOUND: no ascenders, so we do not have a high confidence in
1266 * the xheight of this row (don't use it for estimating
1267 * block xheight), but this row can't contain all caps
1268 * ROW_UNKNOWN: a row with no ascenders/descenders, could be all lowercase
1269 * (or mostly lowercase for fonts with very few ascenders),
1270 * all upper case or small caps
1271 * ROW_INVALID: no meaningful xheight could be found for this row
1272 *
1273 * We then run correct_row_xheight() and use the computed xheight and ascrise
1274 * averages to correct xheight values of the rows in ROW_DESCENDERS_FOUND,
1275 * ROW_UNKNOWN and ROW_INVALID categories.
1276 *
1277 */
compute_block_xheight(TO_BLOCK * block,float gradient)1278 void Textord::compute_block_xheight(TO_BLOCK *block, float gradient) {
1279 TO_ROW *row; // current row
1280 float asc_frac_xheight = CCStruct::kAscenderFraction / CCStruct::kXHeightFraction;
1281 float desc_frac_xheight = CCStruct::kDescenderFraction / CCStruct::kXHeightFraction;
1282 int32_t min_height, max_height; // limits on xheight
1283 TO_ROW_IT row_it = block->get_rows();
1284 if (row_it.empty()) {
1285 return; // no rows
1286 }
1287
1288 // Compute the best guess of xheight of each row individually.
1289 // Use xheight and ascrise values of the rows where ascenders were found.
1290 get_min_max_xheight(block->line_size, &min_height, &max_height);
1291 STATS row_asc_xheights(min_height, max_height + 1);
1292 STATS row_asc_ascrise(static_cast<int>(min_height * asc_frac_xheight),
1293 static_cast<int>(max_height * asc_frac_xheight) + 1);
1294 int min_desc_height = static_cast<int>(min_height * desc_frac_xheight);
1295 int max_desc_height = static_cast<int>(max_height * desc_frac_xheight);
1296 STATS row_asc_descdrop(min_desc_height, max_desc_height + 1);
1297 STATS row_desc_xheights(min_height, max_height + 1);
1298 STATS row_desc_descdrop(min_desc_height, max_desc_height + 1);
1299 STATS row_cap_xheights(min_height, max_height + 1);
1300 STATS row_cap_floating_xheights(min_height, max_height + 1);
1301 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1302 row = row_it.data();
1303 // Compute the xheight of this row if it has not been computed before.
1304 if (row->xheight <= 0) {
1305 compute_row_xheight(row, block->block->classify_rotation(), gradient, block->line_size);
1306 }
1307 ROW_CATEGORY row_category = get_row_category(row);
1308 if (row_category == ROW_ASCENDERS_FOUND) {
1309 row_asc_xheights.add(static_cast<int32_t>(row->xheight), row->xheight_evidence);
1310 row_asc_ascrise.add(static_cast<int32_t>(row->ascrise), row->xheight_evidence);
1311 row_asc_descdrop.add(static_cast<int32_t>(-row->descdrop), row->xheight_evidence);
1312 } else if (row_category == ROW_DESCENDERS_FOUND) {
1313 row_desc_xheights.add(static_cast<int32_t>(row->xheight), row->xheight_evidence);
1314 row_desc_descdrop.add(static_cast<int32_t>(-row->descdrop), row->xheight_evidence);
1315 } else if (row_category == ROW_UNKNOWN) {
1316 fill_heights(row, gradient, min_height, max_height, &row_cap_xheights,
1317 &row_cap_floating_xheights);
1318 }
1319 }
1320
1321 float xheight = 0.0;
1322 float ascrise = 0.0;
1323 float descdrop = 0.0;
1324 // Compute our best guess of xheight of this block.
1325 if (row_asc_xheights.get_total() > 0) {
1326 // Determine xheight from rows where ascenders were found.
1327 xheight = row_asc_xheights.median();
1328 ascrise = row_asc_ascrise.median();
1329 descdrop = -row_asc_descdrop.median();
1330 } else if (row_desc_xheights.get_total() > 0) {
1331 // Determine xheight from rows where descenders were found.
1332 xheight = row_desc_xheights.median();
1333 descdrop = -row_desc_descdrop.median();
1334 } else if (row_cap_xheights.get_total() > 0) {
1335 // All the rows in the block were (a/de)scenderless.
1336 // Try to search for two modes in row_cap_heights that could
1337 // be the xheight and the capheight (e.g. some of the rows
1338 // were lowercase, but did not have enough (a/de)scenders.
1339 // If such two modes can not be found, this block is most
1340 // likely all caps (or all small caps, in which case the code
1341 // still works as intended).
1342 compute_xheight_from_modes(
1343 &row_cap_xheights, &row_cap_floating_xheights,
1344 textord_single_height_mode && block->block->classify_rotation().y() == 0.0, min_height,
1345 max_height, &(xheight), &(ascrise));
1346 if (ascrise == 0) { // assume only caps in the whole block
1347 xheight = row_cap_xheights.median() * CCStruct::kXHeightCapRatio;
1348 }
1349 } else { // default block sizes
1350 xheight = block->line_size * CCStruct::kXHeightFraction;
1351 }
1352 // Correct xheight, ascrise and descdrop if necessary.
1353 bool corrected_xheight = false;
1354 if (xheight < textord_min_xheight) {
1355 xheight = static_cast<float>(textord_min_xheight);
1356 corrected_xheight = true;
1357 }
1358 if (corrected_xheight || ascrise <= 0) {
1359 ascrise = xheight * asc_frac_xheight;
1360 }
1361 if (corrected_xheight || descdrop >= 0) {
1362 descdrop = -(xheight * desc_frac_xheight);
1363 }
1364 block->xheight = xheight;
1365
1366 if (textord_debug_xheights) {
1367 tprintf("Block average xheight=%.4f, ascrise=%.4f, descdrop=%.4f\n", xheight, ascrise,
1368 descdrop);
1369 }
1370 // Correct xheight, ascrise, descdrop of rows based on block averages.
1371 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1372 correct_row_xheight(row_it.data(), xheight, ascrise, descdrop);
1373 }
1374 }
1375
1376 /**
1377 * @name compute_row_xheight
1378 *
1379 * Estimate the xheight of this row.
1380 * Compute the ascender rise and descender drop at the same time.
1381 * Set xheigh_evidence to the number of blobs with the chosen xheight
1382 * that appear in this row.
1383 */
compute_row_xheight(TO_ROW * row,const FCOORD & rotation,float gradient,int block_line_size)1384 void Textord::compute_row_xheight(TO_ROW *row, // row to do
1385 const FCOORD &rotation,
1386 float gradient, // global skew
1387 int block_line_size) {
1388 // Find blobs representing repeated characters in rows and mark them.
1389 // This information is used for computing row xheight and at a later
1390 // stage when words are formed by make_words.
1391 if (!row->rep_chars_marked()) {
1392 mark_repeated_chars(row);
1393 }
1394
1395 int min_height, max_height;
1396 get_min_max_xheight(block_line_size, &min_height, &max_height);
1397 STATS heights(min_height, max_height + 1);
1398 STATS floating_heights(min_height, max_height + 1);
1399 fill_heights(row, gradient, min_height, max_height, &heights, &floating_heights);
1400 row->ascrise = 0.0f;
1401 row->xheight = 0.0f;
1402 row->xheight_evidence = compute_xheight_from_modes(
1403 &heights, &floating_heights, textord_single_height_mode && rotation.y() == 0.0, min_height,
1404 max_height, &(row->xheight), &(row->ascrise));
1405 row->descdrop = 0.0f;
1406 if (row->xheight > 0) {
1407 row->descdrop =
1408 static_cast<float>(compute_row_descdrop(row, gradient, row->xheight_evidence, &heights));
1409 }
1410 }
1411
1412 /**
1413 * @name fill_heights
1414 *
1415 * Fill the given heights with heights of the blobs that are legal
1416 * candidates for estimating xheight.
1417 */
fill_heights(TO_ROW * row,float gradient,int min_height,int max_height,STATS * heights,STATS * floating_heights)1418 void fill_heights(TO_ROW *row, float gradient, int min_height, int max_height, STATS *heights,
1419 STATS *floating_heights) {
1420 float xcentre; // centre of blob
1421 float top; // top y coord of blob
1422 float height; // height of blob
1423 BLOBNBOX *blob; // current blob
1424 int repeated_set;
1425 BLOBNBOX_IT blob_it = row->blob_list();
1426 if (blob_it.empty()) {
1427 return; // no blobs in this row
1428 }
1429 bool has_rep_chars = row->rep_chars_marked() && row->num_repeated_sets() > 0;
1430 do {
1431 blob = blob_it.data();
1432 if (!blob->joined_to_prev()) {
1433 xcentre = (blob->bounding_box().left() + blob->bounding_box().right()) / 2.0f;
1434 top = blob->bounding_box().top();
1435 height = blob->bounding_box().height();
1436 if (textord_fix_xheight_bug) {
1437 top -= row->baseline.y(xcentre);
1438 } else {
1439 top -= gradient * xcentre + row->parallel_c();
1440 }
1441 if (top >= min_height && top <= max_height) {
1442 heights->add(static_cast<int32_t>(floor(top + 0.5)), 1);
1443 if (height / top < textord_min_blob_height_fraction) {
1444 floating_heights->add(static_cast<int32_t>(floor(top + 0.5)), 1);
1445 }
1446 }
1447 }
1448 // Skip repeated chars, since they are likely to skew the height stats.
1449 if (has_rep_chars && blob->repeated_set() != 0) {
1450 repeated_set = blob->repeated_set();
1451 blob_it.forward();
1452 while (!blob_it.at_first() && blob_it.data()->repeated_set() == repeated_set) {
1453 blob_it.forward();
1454 if (textord_debug_xheights) {
1455 tprintf("Skipping repeated char when computing xheight\n");
1456 }
1457 }
1458 } else {
1459 blob_it.forward();
1460 }
1461 } while (!blob_it.at_first());
1462 }
1463
1464 /**
1465 * @name compute_xheight_from_modes
1466 *
1467 * Given a STATS object heights, looks for two most frequently occurring
1468 * heights that look like xheight and xheight + ascrise. If found, sets
1469 * the values of *xheight and *ascrise accordingly, otherwise sets xheight
1470 * to any most frequently occurring height and sets *ascrise to 0.
1471 * Returns the number of times xheight occurred in heights.
1472 * For each mode that is considered for being an xheight the count of
1473 * floating blobs (stored in floating_heights) is subtracted from the
1474 * total count of the blobs of this height. This is done because blobs
1475 * that sit far above the baseline could represent valid ascenders, but
1476 * it is highly unlikely that such a character's height will be an xheight
1477 * (e.g. -, ', =, ^, `, ", ', etc)
1478 * If cap_only, then force finding of only the top mode.
1479 */
compute_xheight_from_modes(STATS * heights,STATS * floating_heights,bool cap_only,int min_height,int max_height,float * xheight,float * ascrise)1480 int compute_xheight_from_modes(STATS *heights, STATS *floating_heights, bool cap_only,
1481 int min_height, int max_height, float *xheight, float *ascrise) {
1482 int blob_index = heights->mode(); // find mode
1483 int blob_count = heights->pile_count(blob_index); // get count of mode
1484 if (textord_debug_xheights) {
1485 tprintf("min_height=%d, max_height=%d, mode=%d, count=%d, total=%d\n", min_height, max_height,
1486 blob_index, blob_count, heights->get_total());
1487 heights->print();
1488 floating_heights->print();
1489 }
1490 if (blob_count == 0) {
1491 return 0;
1492 }
1493 int modes[MAX_HEIGHT_MODES]; // biggest piles
1494 bool in_best_pile = false;
1495 int prev_size = -INT32_MAX;
1496 int best_count = 0;
1497 int mode_count = compute_height_modes(heights, min_height, max_height, modes, MAX_HEIGHT_MODES);
1498 if (cap_only && mode_count > 1) {
1499 mode_count = 1;
1500 }
1501 int x;
1502 if (textord_debug_xheights) {
1503 tprintf("found %d modes: ", mode_count);
1504 for (x = 0; x < mode_count; x++) {
1505 tprintf("%d ", modes[x]);
1506 }
1507 tprintf("\n");
1508 }
1509
1510 for (x = 0; x < mode_count - 1; x++) {
1511 if (modes[x] != prev_size + 1) {
1512 in_best_pile = false; // had empty height
1513 }
1514 int modes_x_count = heights->pile_count(modes[x]) - floating_heights->pile_count(modes[x]);
1515 if ((modes_x_count >= blob_count * textord_xheight_mode_fraction) &&
1516 (in_best_pile || modes_x_count > best_count)) {
1517 for (int asc = x + 1; asc < mode_count; asc++) {
1518 float ratio = static_cast<float>(modes[asc]) / static_cast<float>(modes[x]);
1519 if (textord_ascx_ratio_min < ratio && ratio < textord_ascx_ratio_max &&
1520 (heights->pile_count(modes[asc]) >= blob_count * textord_ascheight_mode_fraction)) {
1521 if (modes_x_count > best_count) {
1522 in_best_pile = true;
1523 best_count = modes_x_count;
1524 }
1525 if (textord_debug_xheights) {
1526 tprintf("X=%d, asc=%d, count=%d, ratio=%g\n", modes[x], modes[asc] - modes[x],
1527 modes_x_count, ratio);
1528 }
1529 prev_size = modes[x];
1530 *xheight = static_cast<float>(modes[x]);
1531 *ascrise = static_cast<float>(modes[asc] - modes[x]);
1532 }
1533 }
1534 }
1535 }
1536 if (*xheight == 0) { // single mode
1537 // Remove counts of the "floating" blobs (the one whose height is too
1538 // small in relation to it's top end of the bounding box) from heights
1539 // before computing the single-mode xheight.
1540 // Restore the counts in heights after the mode is found, since
1541 // floating blobs might be useful for determining potential ascenders
1542 // in compute_row_descdrop().
1543 if (floating_heights->get_total() > 0) {
1544 for (x = min_height; x < max_height; ++x) {
1545 heights->add(x, -(floating_heights->pile_count(x)));
1546 }
1547 blob_index = heights->mode(); // find the modified mode
1548 for (x = min_height; x < max_height; ++x) {
1549 heights->add(x, floating_heights->pile_count(x));
1550 }
1551 }
1552 *xheight = static_cast<float>(blob_index);
1553 *ascrise = 0.0f;
1554 best_count = heights->pile_count(blob_index);
1555 if (textord_debug_xheights) {
1556 tprintf("Single mode xheight set to %g\n", *xheight);
1557 }
1558 } else if (textord_debug_xheights) {
1559 tprintf("Multi-mode xheight set to %g, asc=%g\n", *xheight, *ascrise);
1560 }
1561 return best_count;
1562 }
1563
1564 /**
1565 * @name compute_row_descdrop
1566 *
1567 * Estimates the descdrop of this row. This function looks for
1568 * "significant" descenders of lowercase letters (those that could
1569 * not just be the small descenders of upper case letters like Q,J).
1570 * The function also takes into account how many potential ascenders
1571 * this row might contain. If the number of potential ascenders along
1572 * with descenders is close to the expected fraction of the total
1573 * number of blobs in the row, the function returns the descender
1574 * height, returns 0 otherwise.
1575 */
compute_row_descdrop(TO_ROW * row,float gradient,int xheight_blob_count,STATS * asc_heights)1576 int32_t compute_row_descdrop(TO_ROW *row, float gradient, int xheight_blob_count,
1577 STATS *asc_heights) {
1578 // Count how many potential ascenders are in this row.
1579 int i_min = asc_heights->min_bucket();
1580 if ((i_min / row->xheight) < textord_ascx_ratio_min) {
1581 i_min = static_cast<int>(floor(row->xheight * textord_ascx_ratio_min + 0.5));
1582 }
1583 int i_max = asc_heights->max_bucket();
1584 if ((i_max / row->xheight) > textord_ascx_ratio_max) {
1585 i_max = static_cast<int>(floor(row->xheight * textord_ascx_ratio_max));
1586 }
1587 int num_potential_asc = 0;
1588 for (int i = i_min; i <= i_max; ++i) {
1589 num_potential_asc += asc_heights->pile_count(i);
1590 }
1591 auto min_height = static_cast<int32_t>(floor(row->xheight * textord_descx_ratio_min + 0.5));
1592 auto max_height = static_cast<int32_t>(floor(row->xheight * textord_descx_ratio_max));
1593 float xcentre; // centre of blob
1594 float height; // height of blob
1595 BLOBNBOX_IT blob_it = row->blob_list();
1596 BLOBNBOX *blob; // current blob
1597 STATS heights(min_height, max_height + 1);
1598 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1599 blob = blob_it.data();
1600 if (!blob->joined_to_prev()) {
1601 xcentre = (blob->bounding_box().left() + blob->bounding_box().right()) / 2.0f;
1602 height = (gradient * xcentre + row->parallel_c() - blob->bounding_box().bottom());
1603 if (height >= min_height && height <= max_height) {
1604 heights.add(static_cast<int>(floor(height + 0.5)), 1);
1605 }
1606 }
1607 }
1608 int blob_index = heights.mode(); // find mode
1609 int blob_count = heights.pile_count(blob_index); // get count of mode
1610 float total_fraction = (textord_descheight_mode_fraction + textord_ascheight_mode_fraction);
1611 if (static_cast<float>(blob_count + num_potential_asc) < xheight_blob_count * total_fraction) {
1612 blob_count = 0;
1613 }
1614 int descdrop = blob_count > 0 ? -blob_index : 0;
1615 if (textord_debug_xheights) {
1616 tprintf("Descdrop: %d (potential ascenders %d, descenders %d)\n", descdrop, num_potential_asc,
1617 blob_count);
1618 heights.print();
1619 }
1620 return descdrop;
1621 }
1622
1623 /**
1624 * @name compute_height_modes
1625 *
1626 * Find the top maxmodes values in the input array and put their
1627 * indices in the output in the order in which they occurred.
1628 */
compute_height_modes(STATS * heights,int32_t min_height,int32_t max_height,int32_t * modes,int32_t maxmodes)1629 int32_t compute_height_modes(STATS *heights, // stats to search
1630 int32_t min_height, // bottom of range
1631 int32_t max_height, // top of range
1632 int32_t *modes, // output array
1633 int32_t maxmodes) { // size of modes
1634 int32_t pile_count; // no in source pile
1635 int32_t src_count; // no of source entries
1636 int32_t src_index; // current entry
1637 int32_t least_count; // height of smalllest
1638 int32_t least_index; // index of least
1639 int32_t dest_count; // index in modes
1640
1641 src_count = max_height + 1 - min_height;
1642 dest_count = 0;
1643 least_count = INT32_MAX;
1644 least_index = -1;
1645 for (src_index = 0; src_index < src_count; src_index++) {
1646 pile_count = heights->pile_count(min_height + src_index);
1647 if (pile_count > 0) {
1648 if (dest_count < maxmodes) {
1649 if (pile_count < least_count) {
1650 // find smallest in array
1651 least_count = pile_count;
1652 least_index = dest_count;
1653 }
1654 modes[dest_count++] = min_height + src_index;
1655 } else if (pile_count >= least_count) {
1656 while (least_index < maxmodes - 1) {
1657 modes[least_index] = modes[least_index + 1];
1658 // shuffle up
1659 least_index++;
1660 }
1661 // new one on end
1662 modes[maxmodes - 1] = min_height + src_index;
1663 if (pile_count == least_count) {
1664 // new smallest
1665 least_index = maxmodes - 1;
1666 } else {
1667 least_count = heights->pile_count(modes[0]);
1668 least_index = 0;
1669 for (dest_count = 1; dest_count < maxmodes; dest_count++) {
1670 pile_count = heights->pile_count(modes[dest_count]);
1671 if (pile_count < least_count) {
1672 // find smallest
1673 least_count = pile_count;
1674 least_index = dest_count;
1675 }
1676 }
1677 }
1678 }
1679 }
1680 }
1681 return dest_count;
1682 }
1683
1684 /**
1685 * @name correct_row_xheight
1686 *
1687 * Adjust the xheight etc of this row if not within reasonable limits
1688 * of the average for the block.
1689 */
correct_row_xheight(TO_ROW * row,float xheight,float ascrise,float descdrop)1690 void correct_row_xheight(TO_ROW *row, float xheight, float ascrise, float descdrop) {
1691 ROW_CATEGORY row_category = get_row_category(row);
1692 if (textord_debug_xheights) {
1693 tprintf(
1694 "correcting row xheight: row->xheight %.4f"
1695 ", row->acrise %.4f row->descdrop %.4f\n",
1696 row->xheight, row->ascrise, row->descdrop);
1697 }
1698 bool normal_xheight = within_error_margin(row->xheight, xheight, textord_xheight_error_margin);
1699 bool cap_xheight =
1700 within_error_margin(row->xheight, xheight + ascrise, textord_xheight_error_margin);
1701 // Use the average xheight/ascrise for the following cases:
1702 // -- the xheight of the row could not be determined at all
1703 // -- the row has descenders (e.g. "many groups", "ISBN 12345 p.3")
1704 // and its xheight is close to either cap height or average xheight
1705 // -- the row does not have ascenders or descenders, but its xheight
1706 // is close to the average block xheight (e.g. row with "www.mmm.com")
1707 if (row_category == ROW_ASCENDERS_FOUND) {
1708 if (row->descdrop >= 0) {
1709 row->descdrop = row->xheight * (descdrop / xheight);
1710 }
1711 } else if (row_category == ROW_INVALID ||
1712 (row_category == ROW_DESCENDERS_FOUND && (normal_xheight || cap_xheight)) ||
1713 (row_category == ROW_UNKNOWN && normal_xheight)) {
1714 if (textord_debug_xheights) {
1715 tprintf("using average xheight\n");
1716 }
1717 row->xheight = xheight;
1718 row->ascrise = ascrise;
1719 row->descdrop = descdrop;
1720 } else if (row_category == ROW_DESCENDERS_FOUND) {
1721 // Assume this is a row with mostly lowercase letters and it's xheight
1722 // is computed correctly (unfortunately there is no way to distinguish
1723 // this from the case when descenders are found, but the most common
1724 // height is capheight).
1725 if (textord_debug_xheights) {
1726 tprintf("lowercase, corrected ascrise\n");
1727 }
1728 row->ascrise = row->xheight * (ascrise / xheight);
1729 } else if (row_category == ROW_UNKNOWN) {
1730 // Otherwise assume this row is an all-caps or small-caps row
1731 // and adjust xheight and ascrise of the row.
1732
1733 row->all_caps = true;
1734 if (cap_xheight) { // regular all caps
1735 if (textord_debug_xheights) {
1736 tprintf("all caps\n");
1737 }
1738 row->xheight = xheight;
1739 row->ascrise = ascrise;
1740 row->descdrop = descdrop;
1741 } else { // small caps or caps with an odd xheight
1742 if (textord_debug_xheights) {
1743 if (row->xheight < xheight + ascrise && row->xheight > xheight) {
1744 tprintf("small caps\n");
1745 } else {
1746 tprintf("all caps with irregular xheight\n");
1747 }
1748 }
1749 row->ascrise = row->xheight * (ascrise / (xheight + ascrise));
1750 row->xheight -= row->ascrise;
1751 row->descdrop = row->xheight * (descdrop / xheight);
1752 }
1753 }
1754 if (textord_debug_xheights) {
1755 tprintf(
1756 "corrected row->xheight = %.4f, row->acrise = %.4f, row->descdrop"
1757 " = %.4f\n",
1758 row->xheight, row->ascrise, row->descdrop);
1759 }
1760 }
1761
CountOverlaps(const TBOX & box,int min_height,BLOBNBOX_LIST * blobs)1762 static int CountOverlaps(const TBOX &box, int min_height, BLOBNBOX_LIST *blobs) {
1763 int overlaps = 0;
1764 BLOBNBOX_IT blob_it(blobs);
1765 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1766 BLOBNBOX *blob = blob_it.data();
1767 const TBOX &blob_box = blob->bounding_box();
1768 if (blob_box.height() >= min_height && box.major_overlap(blob_box)) {
1769 ++overlaps;
1770 }
1771 }
1772 return overlaps;
1773 }
1774
1775 /**
1776 * @name separate_underlines
1777 *
1778 * Test wide objects for being potential underlines. If they are then
1779 * put them in a separate list in the block.
1780 */
separate_underlines(TO_BLOCK * block,float gradient,FCOORD rotation,bool testing_on)1781 void separate_underlines(TO_BLOCK *block, // block to do
1782 float gradient, // skew angle
1783 FCOORD rotation, // inverse landscape
1784 bool testing_on) { // correct orientation
1785 BLOBNBOX *blob; // current blob
1786 C_BLOB *rotated_blob; // rotated blob
1787 TO_ROW *row; // current row
1788 float length; // of g_vec
1789 TBOX blob_box;
1790 FCOORD blob_rotation; // inverse of rotation
1791 FCOORD g_vec; // skew rotation
1792 BLOBNBOX_IT blob_it; // iterator
1793 // iterator
1794 BLOBNBOX_IT under_it = &block->underlines;
1795 BLOBNBOX_IT large_it = &block->large_blobs;
1796 TO_ROW_IT row_it = block->get_rows();
1797 int min_blob_height = static_cast<int>(textord_min_blob_height_fraction * block->line_size + 0.5);
1798
1799 // length of vector
1800 length = std::sqrt(1 + gradient * gradient);
1801 g_vec = FCOORD(1 / length, -gradient / length);
1802 blob_rotation = FCOORD(rotation.x(), -rotation.y());
1803 blob_rotation.rotate(g_vec); // undoing everything
1804 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1805 row = row_it.data();
1806 // get blobs
1807 blob_it.set_to_list(row->blob_list());
1808 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1809 blob = blob_it.data();
1810 blob_box = blob->bounding_box();
1811 if (blob_box.width() > block->line_size * textord_underline_width) {
1812 ASSERT_HOST(blob->cblob() != nullptr);
1813 rotated_blob = crotate_cblob(blob->cblob(), blob_rotation);
1814 if (test_underline(testing_on && textord_show_final_rows, rotated_blob,
1815 static_cast<int16_t>(row->intercept()),
1816 static_cast<int16_t>(block->line_size *
1817 (tesseract::CCStruct::kXHeightFraction +
1818 tesseract::CCStruct::kAscenderFraction / 2.0f)))) {
1819 under_it.add_after_then_move(blob_it.extract());
1820 if (testing_on && textord_show_final_rows) {
1821 tprintf("Underlined blob at:");
1822 rotated_blob->bounding_box().print();
1823 tprintf("Was:");
1824 blob_box.print();
1825 }
1826 } else if (CountOverlaps(blob->bounding_box(), min_blob_height, row->blob_list()) >
1827 textord_max_blob_overlaps) {
1828 large_it.add_after_then_move(blob_it.extract());
1829 if (testing_on && textord_show_final_rows) {
1830 tprintf("Large blob overlaps %d blobs at:",
1831 CountOverlaps(blob_box, min_blob_height, row->blob_list()));
1832 blob_box.print();
1833 }
1834 }
1835 delete rotated_blob;
1836 }
1837 }
1838 }
1839 }
1840
1841 /**
1842 * @name pre_associate_blobs
1843 *
1844 * Associate overlapping blobs and fake chop wide blobs.
1845 */
pre_associate_blobs(ICOORD page_tr,TO_BLOCK * block,FCOORD rotation,bool testing_on)1846 void pre_associate_blobs( // make rough chars
1847 ICOORD page_tr, // top right
1848 TO_BLOCK *block, // block to do
1849 FCOORD rotation, // inverse landscape
1850 bool testing_on // correct orientation
1851 ) {
1852 #ifndef GRAPHICS_DISABLED
1853 ScrollView::Color colour; // of boxes
1854 #endif
1855 BLOBNBOX *blob; // current blob
1856 BLOBNBOX *nextblob; // next in list
1857 TBOX blob_box;
1858 FCOORD blob_rotation; // inverse of rotation
1859 BLOBNBOX_IT blob_it; // iterator
1860 BLOBNBOX_IT start_it; // iterator
1861 TO_ROW_IT row_it = block->get_rows();
1862
1863 #ifndef GRAPHICS_DISABLED
1864 colour = ScrollView::RED;
1865 #endif
1866
1867 blob_rotation = FCOORD(rotation.x(), -rotation.y());
1868 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1869 // get blobs
1870 blob_it.set_to_list(row_it.data()->blob_list());
1871 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1872 blob = blob_it.data();
1873 blob_box = blob->bounding_box();
1874 start_it = blob_it; // save start point
1875 // if (testing_on && textord_show_final_blobs)
1876 // {
1877 // tprintf("Blob at (%d,%d)->(%d,%d),
1878 // addr=%x, count=%d\n",
1879 // blob_box.left(),blob_box.bottom(),
1880 // blob_box.right(),blob_box.top(),
1881 // (void*)blob,blob_it.length());
1882 // }
1883 bool overlap;
1884 do {
1885 overlap = false;
1886 if (!blob_it.at_last()) {
1887 nextblob = blob_it.data_relative(1);
1888 overlap = blob_box.major_x_overlap(nextblob->bounding_box());
1889 if (overlap) {
1890 blob->merge(nextblob); // merge new blob
1891 blob_box = blob->bounding_box(); // get bigger box
1892 blob_it.forward();
1893 }
1894 }
1895 } while (overlap);
1896 blob->chop(&start_it, &blob_it, blob_rotation,
1897 block->line_size * tesseract::CCStruct::kXHeightFraction * textord_chop_width);
1898 // attempt chop
1899 }
1900 #ifndef GRAPHICS_DISABLED
1901 if (testing_on && textord_show_final_blobs) {
1902 if (to_win == nullptr) {
1903 create_to_win(page_tr);
1904 }
1905 to_win->Pen(colour);
1906 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1907 blob = blob_it.data();
1908 blob_box = blob->bounding_box();
1909 blob_box.rotate(rotation);
1910 if (!blob->joined_to_prev()) {
1911 to_win->Rectangle(blob_box.left(), blob_box.bottom(), blob_box.right(), blob_box.top());
1912 }
1913 }
1914 colour = static_cast<ScrollView::Color>(colour + 1);
1915 if (colour > ScrollView::MAGENTA) {
1916 colour = ScrollView::RED;
1917 }
1918 }
1919 #endif
1920 }
1921 }
1922
1923 /**
1924 * @name fit_parallel_rows
1925 *
1926 * Re-fit the rows in the block to the given gradient.
1927 */
fit_parallel_rows(TO_BLOCK * block,float gradient,FCOORD rotation,int32_t block_edge,bool testing_on)1928 void fit_parallel_rows( // find lines
1929 TO_BLOCK *block, // block to do
1930 float gradient, // gradient to fit
1931 FCOORD rotation, // for drawing
1932 int32_t block_edge, // edge of block
1933 bool testing_on // correct orientation
1934 ) {
1935 #ifndef GRAPHICS_DISABLED
1936 ScrollView::Color colour; // of row
1937 #endif
1938 TO_ROW_IT row_it = block->get_rows();
1939
1940 row_it.move_to_first();
1941 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1942 if (row_it.data()->blob_list()->empty()) {
1943 delete row_it.extract(); // nothing in it
1944 } else {
1945 fit_parallel_lms(gradient, row_it.data());
1946 }
1947 }
1948 #ifndef GRAPHICS_DISABLED
1949 if (testing_on) {
1950 colour = ScrollView::RED;
1951 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1952 plot_parallel_row(row_it.data(), gradient, block_edge, colour, rotation);
1953 colour = static_cast<ScrollView::Color>(colour + 1);
1954 if (colour > ScrollView::MAGENTA) {
1955 colour = ScrollView::RED;
1956 }
1957 }
1958 }
1959 #endif
1960 row_it.sort(row_y_order); // may have gone out of order
1961 }
1962
1963 /**
1964 * @name fit_parallel_lms
1965 *
1966 * Fit an LMS line to a row.
1967 * Make the fit parallel to the given gradient and set the
1968 * row accordingly.
1969 */
fit_parallel_lms(float gradient,TO_ROW * row)1970 void fit_parallel_lms(float gradient, TO_ROW *row) {
1971 float c; // fitted line
1972 int blobcount; // no of blobs
1973 tesseract::DetLineFit lms;
1974 BLOBNBOX_IT blob_it = row->blob_list();
1975
1976 blobcount = 0;
1977 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
1978 if (!blob_it.data()->joined_to_prev()) {
1979 const TBOX &box = blob_it.data()->bounding_box();
1980 lms.Add(ICOORD((box.left() + box.right()) / 2, box.bottom()));
1981 blobcount++;
1982 }
1983 }
1984 double error = lms.ConstrainedFit(gradient, &c);
1985 row->set_parallel_line(gradient, c, error);
1986 if (textord_straight_baselines && blobcount > textord_lms_line_trials) {
1987 error = lms.Fit(&gradient, &c);
1988 }
1989 // set the other too
1990 row->set_line(gradient, c, error);
1991 }
1992
1993 /**
1994 * @name make_spline_rows
1995 *
1996 * Re-fit the rows in the block to the given gradient.
1997 */
make_spline_rows(TO_BLOCK * block,float gradient,bool testing_on)1998 void Textord::make_spline_rows(TO_BLOCK *block, // block to do
1999 float gradient, // gradient to fit
2000 bool testing_on) {
2001 #ifndef GRAPHICS_DISABLED
2002 ScrollView::Color colour; // of row
2003 #endif
2004 TO_ROW_IT row_it = block->get_rows();
2005
2006 row_it.move_to_first();
2007 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
2008 if (row_it.data()->blob_list()->empty()) {
2009 delete row_it.extract(); // nothing in it
2010 } else {
2011 make_baseline_spline(row_it.data(), block);
2012 }
2013 }
2014 if (textord_old_baselines) {
2015 #ifndef GRAPHICS_DISABLED
2016 if (testing_on) {
2017 colour = ScrollView::RED;
2018 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
2019 row_it.data()->baseline.plot(to_win, colour);
2020 colour = static_cast<ScrollView::Color>(colour + 1);
2021 if (colour > ScrollView::MAGENTA) {
2022 colour = ScrollView::RED;
2023 }
2024 }
2025 }
2026 #endif
2027 make_old_baselines(block, testing_on, gradient);
2028 }
2029 #ifndef GRAPHICS_DISABLED
2030 if (testing_on) {
2031 colour = ScrollView::RED;
2032 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
2033 row_it.data()->baseline.plot(to_win, colour);
2034 colour = static_cast<ScrollView::Color>(colour + 1);
2035 if (colour > ScrollView::MAGENTA) {
2036 colour = ScrollView::RED;
2037 }
2038 }
2039 }
2040 #endif
2041 }
2042
2043 /**
2044 * @name make_baseline_spline
2045 *
2046 * Fit an LMS line to a row.
2047 * Make the fit parallel to the given gradient and set the
2048 * row accordingly.
2049 */
make_baseline_spline(TO_ROW * row,TO_BLOCK * block)2050 void make_baseline_spline(TO_ROW *row, // row to fit
2051 TO_BLOCK *block) {
2052 double *coeffs; // quadratic coeffs
2053 int32_t segments; // no of segments
2054
2055 // spline boundaries
2056 auto *xstarts = new int32_t[row->blob_list()->length() + 1];
2057 if (segment_baseline(row, block, segments, xstarts) && !textord_straight_baselines &&
2058 !textord_parallel_baselines) {
2059 coeffs = linear_spline_baseline(row, block, segments, xstarts);
2060 } else {
2061 xstarts[1] = xstarts[segments];
2062 segments = 1;
2063 coeffs = new double[3];
2064 coeffs[0] = 0;
2065 coeffs[1] = row->line_m();
2066 coeffs[2] = row->line_c();
2067 }
2068 row->baseline = QSPLINE(segments, xstarts, coeffs);
2069 delete[] coeffs;
2070 delete[] xstarts;
2071 }
2072
2073 /**
2074 * @name segment_baseline
2075 *
2076 * Divide the baseline up into segments which require a different
2077 * quadratic fitted to them.
2078 * Return true if enough blobs were far enough away to need a quadratic.
2079 */
segment_baseline(TO_ROW * row,TO_BLOCK * block,int32_t & segments,int32_t * xstarts)2080 bool segment_baseline( // split baseline
2081 TO_ROW *row, // row to fit
2082 TO_BLOCK *block, // block it came from
2083 int32_t &segments, // no fo segments
2084 int32_t *xstarts // coords of segments
2085 ) {
2086 bool needs_curve; // needs curved line
2087 int blobcount; // no of blobs
2088 int blobindex; // current blob
2089 int last_state; // above, on , below
2090 int state; // of current blob
2091 float yshift; // from baseline
2092 TBOX box; // blob box
2093 TBOX new_box; // new_it box
2094 float middle; // xcentre of blob
2095 // blobs
2096 BLOBNBOX_IT blob_it = row->blob_list();
2097 BLOBNBOX_IT new_it = blob_it; // front end
2098 SORTED_FLOATS yshifts; // shifts from baseline
2099
2100 needs_curve = false;
2101 box = box_next_pre_chopped(&blob_it);
2102 xstarts[0] = box.left();
2103 segments = 1;
2104 blobcount = row->blob_list()->length();
2105 if (textord_oldbl_debug) {
2106 tprintf("Segmenting baseline of %d blobs at (%d,%d)\n", blobcount, box.left(), box.bottom());
2107 }
2108 if (blobcount <= textord_spline_medianwin || blobcount < textord_spline_minblobs) {
2109 blob_it.move_to_last();
2110 box = blob_it.data()->bounding_box();
2111 xstarts[1] = box.right();
2112 return false;
2113 }
2114 last_state = 0;
2115 new_it.mark_cycle_pt();
2116 for (blobindex = 0; blobindex < textord_spline_medianwin; blobindex++) {
2117 new_box = box_next_pre_chopped(&new_it);
2118 middle = (new_box.left() + new_box.right()) / 2.0;
2119 yshift = new_box.bottom() - row->line_m() * middle - row->line_c();
2120 // record shift
2121 yshifts.add(yshift, blobindex);
2122 if (new_it.cycled_list()) {
2123 xstarts[1] = new_box.right();
2124 return false;
2125 }
2126 }
2127 for (blobcount = 0; blobcount < textord_spline_medianwin / 2; blobcount++) {
2128 box = box_next_pre_chopped(&blob_it);
2129 }
2130 do {
2131 new_box = box_next_pre_chopped(&new_it);
2132 // get middle one
2133 yshift = yshifts[textord_spline_medianwin / 2];
2134 if (yshift > textord_spline_shift_fraction * block->line_size) {
2135 state = 1;
2136 } else if (-yshift > textord_spline_shift_fraction * block->line_size) {
2137 state = -1;
2138 } else {
2139 state = 0;
2140 }
2141 if (state != 0) {
2142 needs_curve = true;
2143 }
2144 // tprintf("State=%d, prev=%d, shift=%g\n",
2145 // state,last_state,yshift);
2146 if (state != last_state && blobcount > textord_spline_minblobs) {
2147 xstarts[segments++] = box.left();
2148 blobcount = 0;
2149 }
2150 last_state = state;
2151 yshifts.remove(blobindex - textord_spline_medianwin);
2152 box = box_next_pre_chopped(&blob_it);
2153 middle = (new_box.left() + new_box.right()) / 2.0;
2154 yshift = new_box.bottom() - row->line_m() * middle - row->line_c();
2155 yshifts.add(yshift, blobindex);
2156 blobindex++;
2157 blobcount++;
2158 } while (!new_it.cycled_list());
2159 if (blobcount > textord_spline_minblobs || segments == 1) {
2160 xstarts[segments] = new_box.right();
2161 } else {
2162 xstarts[--segments] = new_box.right();
2163 }
2164 if (textord_oldbl_debug) {
2165 tprintf("Made %d segments on row at (%d,%d)\n", segments, box.right(), box.bottom());
2166 }
2167 return needs_curve;
2168 }
2169
2170 /**
2171 * @name linear_spline_baseline
2172 *
2173 * Divide the baseline up into segments which require a different
2174 * quadratic fitted to them.
2175 * @return true if enough blobs were far enough away to need a quadratic.
2176 */
linear_spline_baseline(TO_ROW * row,TO_BLOCK * block,int32_t & segments,int32_t xstarts[])2177 double *linear_spline_baseline( // split baseline
2178 TO_ROW *row, // row to fit
2179 TO_BLOCK *block, // block it came from
2180 int32_t &segments, // no fo segments
2181 int32_t xstarts[] // coords of segments
2182 ) {
2183 int blobcount; // no of blobs
2184 int blobindex; // current blob
2185 int index1, index2; // blob numbers
2186 int blobs_per_segment; // blobs in each
2187 TBOX box; // blob box
2188 TBOX new_box; // new_it box
2189 // blobs
2190 BLOBNBOX_IT blob_it = row->blob_list();
2191 BLOBNBOX_IT new_it = blob_it; // front end
2192 float b, c; // fitted curve
2193 tesseract::DetLineFit lms;
2194 int32_t segment; // current segment
2195
2196 box = box_next_pre_chopped(&blob_it);
2197 xstarts[0] = box.left();
2198 blobcount = 1;
2199 while (!blob_it.at_first()) {
2200 blobcount++;
2201 box = box_next_pre_chopped(&blob_it);
2202 }
2203 segments = blobcount / textord_spline_medianwin;
2204 if (segments < 1) {
2205 segments = 1;
2206 }
2207 blobs_per_segment = blobcount / segments;
2208 // quadratic coeffs
2209 auto *coeffs = new double[segments * 3];
2210 if (textord_oldbl_debug) {
2211 tprintf(
2212 "Linear splining baseline of %d blobs at (%d,%d), into %d segments of "
2213 "%d blobs\n",
2214 blobcount, box.left(), box.bottom(), segments, blobs_per_segment);
2215 }
2216 segment = 1;
2217 for (index2 = 0; index2 < blobs_per_segment / 2; index2++) {
2218 box_next_pre_chopped(&new_it);
2219 }
2220 index1 = 0;
2221 blobindex = index2;
2222 do {
2223 blobindex += blobs_per_segment;
2224 lms.Clear();
2225 while (index1 < blobindex || (segment == segments && index1 < blobcount)) {
2226 box = box_next_pre_chopped(&blob_it);
2227 int middle = (box.left() + box.right()) / 2;
2228 lms.Add(ICOORD(middle, box.bottom()));
2229 index1++;
2230 if (index1 == blobindex - blobs_per_segment / 2 || index1 == blobcount - 1) {
2231 xstarts[segment] = box.left();
2232 }
2233 }
2234 lms.Fit(&b, &c);
2235 coeffs[segment * 3 - 3] = 0;
2236 coeffs[segment * 3 - 2] = b;
2237 coeffs[segment * 3 - 1] = c;
2238 segment++;
2239 if (segment > segments) {
2240 break;
2241 }
2242
2243 blobindex += blobs_per_segment;
2244 lms.Clear();
2245 while (index2 < blobindex || (segment == segments && index2 < blobcount)) {
2246 new_box = box_next_pre_chopped(&new_it);
2247 int middle = (new_box.left() + new_box.right()) / 2;
2248 lms.Add(ICOORD(middle, new_box.bottom()));
2249 index2++;
2250 if (index2 == blobindex - blobs_per_segment / 2 || index2 == blobcount - 1) {
2251 xstarts[segment] = new_box.left();
2252 }
2253 }
2254 lms.Fit(&b, &c);
2255 coeffs[segment * 3 - 3] = 0;
2256 coeffs[segment * 3 - 2] = b;
2257 coeffs[segment * 3 - 1] = c;
2258 segment++;
2259 } while (segment <= segments);
2260 return coeffs;
2261 }
2262
2263 /**
2264 * @name assign_blobs_to_rows
2265 *
2266 * Make enough rows to allocate all the given blobs to one.
2267 * If a block skew is given, use that, else attempt to track it.
2268 */
assign_blobs_to_rows(TO_BLOCK * block,float * gradient,int pass,bool reject_misses,bool make_new_rows,bool drawing_skew)2269 void assign_blobs_to_rows( // find lines
2270 TO_BLOCK *block, // block to do
2271 float *gradient, // block skew
2272 int pass, // identification
2273 bool reject_misses, // chuck big ones out
2274 bool make_new_rows, // add rows for unmatched
2275 bool drawing_skew // draw smoothed skew
2276 ) {
2277 OVERLAP_STATE overlap_result; // what to do with it
2278 float ycoord; // current y
2279 float top, bottom; // of blob
2280 float g_length = 1.0f; // from gradient
2281 int16_t row_count; // no of rows
2282 int16_t left_x; // left edge
2283 int16_t last_x; // previous edge
2284 float block_skew; // y delta
2285 float smooth_factor; // for new coords
2286 float near_dist; // dist to nearest row
2287 ICOORD testpt; // testing only
2288 BLOBNBOX *blob; // current blob
2289 TO_ROW *row; // current row
2290 TO_ROW *dest_row = nullptr; // row to put blob in
2291 // iterators
2292 BLOBNBOX_IT blob_it = &block->blobs;
2293 TO_ROW_IT row_it = block->get_rows();
2294
2295 ycoord =
2296 (block->block->pdblk.bounding_box().bottom() + block->block->pdblk.bounding_box().top()) /
2297 2.0f;
2298 if (gradient != nullptr) {
2299 g_length = std::sqrt(1 + *gradient * *gradient);
2300 }
2301 #ifndef GRAPHICS_DISABLED
2302 if (drawing_skew) {
2303 to_win->SetCursor(block->block->pdblk.bounding_box().left(), ycoord);
2304 }
2305 #endif
2306 testpt = ICOORD(textord_test_x, textord_test_y);
2307 blob_it.sort(blob_x_order);
2308 smooth_factor = 1.0;
2309 block_skew = 0.0f;
2310 row_count = row_it.length(); // might have rows
2311 if (!blob_it.empty()) {
2312 left_x = blob_it.data()->bounding_box().left();
2313 } else {
2314 left_x = block->block->pdblk.bounding_box().left();
2315 }
2316 last_x = left_x;
2317 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
2318 blob = blob_it.data();
2319 if (gradient != nullptr) {
2320 block_skew = (1 - 1 / g_length) * blob->bounding_box().bottom() +
2321 *gradient / g_length * blob->bounding_box().left();
2322 } else if (blob->bounding_box().left() - last_x > block->line_size / 2 &&
2323 last_x - left_x > block->line_size * 2 && textord_interpolating_skew) {
2324 // tprintf("Interpolating skew from %g",block_skew);
2325 block_skew *= static_cast<float>(blob->bounding_box().left() - left_x) / (last_x - left_x);
2326 // tprintf("to %g\n",block_skew);
2327 }
2328 last_x = blob->bounding_box().left();
2329 top = blob->bounding_box().top() - block_skew;
2330 bottom = blob->bounding_box().bottom() - block_skew;
2331 #ifndef GRAPHICS_DISABLED
2332 if (drawing_skew) {
2333 to_win->DrawTo(blob->bounding_box().left(), ycoord + block_skew);
2334 }
2335 #endif
2336 if (!row_it.empty()) {
2337 for (row_it.move_to_first(); !row_it.at_last() && row_it.data()->min_y() > top;
2338 row_it.forward()) {
2339 ;
2340 }
2341 row = row_it.data();
2342 if (row->min_y() <= top && row->max_y() >= bottom) {
2343 // any overlap
2344 dest_row = row;
2345 overlap_result = most_overlapping_row(&row_it, dest_row, top, bottom, block->line_size,
2346 blob->bounding_box().contains(testpt));
2347 if (overlap_result == NEW_ROW && !reject_misses) {
2348 overlap_result = ASSIGN;
2349 }
2350 } else {
2351 overlap_result = NEW_ROW;
2352 if (!make_new_rows) {
2353 near_dist = row_it.data_relative(-1)->min_y() - top;
2354 // below bottom
2355 if (bottom < row->min_y()) {
2356 if (row->min_y() - bottom <= (block->line_spacing - block->line_size) *
2357 tesseract::CCStruct::kDescenderFraction) {
2358 // done it
2359 overlap_result = ASSIGN;
2360 dest_row = row;
2361 }
2362 } else if (near_dist > 0 && near_dist < bottom - row->max_y()) {
2363 row_it.backward();
2364 dest_row = row_it.data();
2365 if (dest_row->min_y() - bottom <= (block->line_spacing - block->line_size) *
2366 tesseract::CCStruct::kDescenderFraction) {
2367 // done it
2368 overlap_result = ASSIGN;
2369 }
2370 } else {
2371 if (top - row->max_y() <=
2372 (block->line_spacing - block->line_size) *
2373 (textord_overlap_x + tesseract::CCStruct::kAscenderFraction)) {
2374 // done it
2375 overlap_result = ASSIGN;
2376 dest_row = row;
2377 }
2378 }
2379 }
2380 }
2381 if (overlap_result == ASSIGN) {
2382 dest_row->add_blob(blob_it.extract(), top, bottom, block->line_size);
2383 }
2384 if (overlap_result == NEW_ROW) {
2385 if (make_new_rows && top - bottom < block->max_blob_size) {
2386 dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size);
2387 row_count++;
2388 if (bottom > row_it.data()->min_y()) {
2389 row_it.add_before_then_move(dest_row);
2390 // insert in right place
2391 } else {
2392 row_it.add_after_then_move(dest_row);
2393 }
2394 smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset);
2395 } else {
2396 overlap_result = REJECT;
2397 }
2398 }
2399 } else if (make_new_rows && top - bottom < block->max_blob_size) {
2400 overlap_result = NEW_ROW;
2401 dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size);
2402 row_count++;
2403 row_it.add_after_then_move(dest_row);
2404 smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset2);
2405 } else {
2406 overlap_result = REJECT;
2407 }
2408 if (blob->bounding_box().contains(testpt) && textord_debug_blob) {
2409 if (overlap_result != REJECT) {
2410 tprintf("Test blob assigned to row at (%g,%g) on pass %d\n", dest_row->min_y(),
2411 dest_row->max_y(), pass);
2412 } else {
2413 tprintf("Test blob assigned to no row on pass %d\n", pass);
2414 }
2415 }
2416 if (overlap_result != REJECT) {
2417 while (!row_it.at_first() && row_it.data()->min_y() > row_it.data_relative(-1)->min_y()) {
2418 row = row_it.extract();
2419 row_it.backward();
2420 row_it.add_before_then_move(row);
2421 }
2422 while (!row_it.at_last() && row_it.data()->min_y() < row_it.data_relative(1)->min_y()) {
2423 row = row_it.extract();
2424 row_it.forward();
2425 // Keep rows in order.
2426 row_it.add_after_then_move(row);
2427 }
2428 BLOBNBOX_IT added_blob_it(dest_row->blob_list());
2429 added_blob_it.move_to_last();
2430 TBOX prev_box = added_blob_it.data_relative(-1)->bounding_box();
2431 if (dest_row->blob_list()->singleton() || !prev_box.major_x_overlap(blob->bounding_box())) {
2432 block_skew = (1 - smooth_factor) * block_skew +
2433 smooth_factor * (blob->bounding_box().bottom() - dest_row->initial_min_y());
2434 }
2435 }
2436 }
2437 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
2438 if (row_it.data()->blob_list()->empty()) {
2439 delete row_it.extract(); // Discard empty rows.
2440 }
2441 }
2442 }
2443
2444 /**
2445 * @name most_overlapping_row
2446 *
2447 * Return the row which most overlaps the blob.
2448 */
most_overlapping_row(TO_ROW_IT * row_it,TO_ROW * & best_row,float top,float bottom,float rowsize,bool testing_blob)2449 OVERLAP_STATE most_overlapping_row( // find best row
2450 TO_ROW_IT *row_it, // iterator
2451 TO_ROW *&best_row, // output row
2452 float top, // top of blob
2453 float bottom, // bottom of blob
2454 float rowsize, // max row size
2455 bool testing_blob // test stuff
2456 ) {
2457 OVERLAP_STATE result; // result of tests
2458 float overlap; // of blob & row
2459 float bestover; // nearest row
2460 float merge_top, merge_bottom; // size of merged row
2461 ICOORD testpt; // testing only
2462 TO_ROW *row; // current row
2463 TO_ROW *test_row; // for multiple overlaps
2464 BLOBNBOX_IT blob_it; // for merging rows
2465
2466 result = ASSIGN;
2467 row = row_it->data();
2468 bestover = top - bottom;
2469 if (top > row->max_y()) {
2470 bestover -= top - row->max_y();
2471 }
2472 if (bottom < row->min_y()) {
2473 // compute overlap
2474 bestover -= row->min_y() - bottom;
2475 }
2476 if (testing_blob && textord_debug_blob) {
2477 tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f\n", bottom, top, row->min_y(),
2478 row->max_y(), rowsize, bestover);
2479 }
2480 test_row = row;
2481 do {
2482 if (!row_it->at_last()) {
2483 row_it->forward();
2484 test_row = row_it->data();
2485 if (test_row->min_y() <= top && test_row->max_y() >= bottom) {
2486 merge_top = test_row->max_y() > row->max_y() ? test_row->max_y() : row->max_y();
2487 merge_bottom = test_row->min_y() < row->min_y() ? test_row->min_y() : row->min_y();
2488 if (merge_top - merge_bottom <= rowsize) {
2489 if (testing_blob && textord_debug_blob) {
2490 tprintf("Merging rows at (%g,%g), (%g,%g)\n", row->min_y(), row->max_y(),
2491 test_row->min_y(), test_row->max_y());
2492 }
2493 test_row->set_limits(merge_bottom, merge_top);
2494 blob_it.set_to_list(test_row->blob_list());
2495 blob_it.add_list_after(row->blob_list());
2496 blob_it.sort(blob_x_order);
2497 row_it->backward();
2498 delete row_it->extract();
2499 row_it->forward();
2500 bestover = -1.0f; // force replacement
2501 }
2502 overlap = top - bottom;
2503 if (top > test_row->max_y()) {
2504 overlap -= top - test_row->max_y();
2505 }
2506 if (bottom < test_row->min_y()) {
2507 overlap -= test_row->min_y() - bottom;
2508 }
2509 if (bestover >= rowsize - 1 && overlap >= rowsize - 1) {
2510 result = REJECT;
2511 }
2512 if (overlap > bestover) {
2513 bestover = overlap; // find biggest overlap
2514 row = test_row;
2515 }
2516 if (testing_blob && textord_debug_blob) {
2517 tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f->%f\n", bottom, top,
2518 test_row->min_y(), test_row->max_y(), rowsize, overlap, bestover);
2519 }
2520 }
2521 }
2522 } while (!row_it->at_last() && test_row->min_y() <= top && test_row->max_y() >= bottom);
2523 while (row_it->data() != row) {
2524 row_it->backward(); // make it point to row
2525 }
2526 // doesn't overlap much
2527 if (top - bottom - bestover > rowsize * textord_overlap_x &&
2528 (!textord_fix_makerow_bug || bestover < rowsize * textord_overlap_x) && result == ASSIGN) {
2529 result = NEW_ROW; // doesn't overlap enough
2530 }
2531 best_row = row;
2532 return result;
2533 }
2534
2535 /**
2536 * @name blob_x_order
2537 *
2538 * Sort function to sort blobs in x from page left.
2539 */
blob_x_order(const void * item1,const void * item2)2540 int blob_x_order( // sort function
2541 const void *item1, // items to compare
2542 const void *item2) {
2543 // converted ptr
2544 const BLOBNBOX *blob1 = *reinterpret_cast<const BLOBNBOX *const *>(item1);
2545 // converted ptr
2546 const BLOBNBOX *blob2 = *reinterpret_cast<const BLOBNBOX *const *>(item2);
2547
2548 if (blob1->bounding_box().left() < blob2->bounding_box().left()) {
2549 return -1;
2550 } else if (blob1->bounding_box().left() > blob2->bounding_box().left()) {
2551 return 1;
2552 } else {
2553 return 0;
2554 }
2555 }
2556
2557 /**
2558 * @name mark_repeated_chars
2559 *
2560 * Mark blobs marked with BTFT_LEADER in repeated sets using the
2561 * repeated_set member of BLOBNBOX.
2562 */
mark_repeated_chars(TO_ROW * row)2563 void mark_repeated_chars(TO_ROW *row) {
2564 BLOBNBOX_IT box_it(row->blob_list()); // Iterator.
2565 int num_repeated_sets = 0;
2566 if (!box_it.empty()) {
2567 do {
2568 BLOBNBOX *bblob = box_it.data();
2569 int repeat_length = 1;
2570 if (bblob->flow() == BTFT_LEADER && !bblob->joined_to_prev() && bblob->cblob() != nullptr) {
2571 BLOBNBOX_IT test_it(box_it);
2572 for (test_it.forward(); !test_it.at_first();) {
2573 bblob = test_it.data();
2574 if (bblob->flow() != BTFT_LEADER) {
2575 break;
2576 }
2577 test_it.forward();
2578 bblob = test_it.data();
2579 if (bblob->joined_to_prev() || bblob->cblob() == nullptr) {
2580 repeat_length = 0;
2581 break;
2582 }
2583 ++repeat_length;
2584 }
2585 }
2586 if (repeat_length >= kMinLeaderCount) {
2587 num_repeated_sets++;
2588 for (; repeat_length > 0; box_it.forward(), --repeat_length) {
2589 bblob = box_it.data();
2590 bblob->set_repeated_set(num_repeated_sets);
2591 }
2592 } else {
2593 bblob->set_repeated_set(0);
2594 box_it.forward();
2595 }
2596 } while (!box_it.at_first()); // until all done
2597 }
2598 row->set_num_repeated_sets(num_repeated_sets);
2599 }
2600
2601 } // namespace tesseract
2602