Mercurial > hgrepos > Python2 > PyMuPDF
comparison mupdf-source/thirdparty/tesseract/src/textord/makerow.cpp @ 2:b50eed0cc0ef upstream
ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4.
The directory name has changed: no version number in the expanded directory now.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:43:07 +0200 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| 1:1d09e1dec1d9 | 2:b50eed0cc0ef |
|---|---|
| 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 */ | |
| 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 */ | |
| 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. | |
| 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. | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 | |
| 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); | |
| 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->remove_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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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); | |
| 1292 STATS row_asc_ascrise(static_cast<int>(min_height * asc_frac_xheight), | |
| 1293 static_cast<int>(max_height * asc_frac_xheight)); | |
| 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); | |
| 1297 STATS row_desc_xheights(min_height, max_height); | |
| 1298 STATS row_desc_descdrop(min_desc_height, max_desc_height); | |
| 1299 STATS row_cap_xheights(min_height, max_height); | |
| 1300 STATS row_cap_floating_xheights(min_height, max_height); | |
| 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 cannot 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 */ | |
| 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); | |
| 1398 STATS floating_heights(min_height, max_height); | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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); | |
| 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 */ | |
| 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 */ | |
| 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 | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 */ | |
| 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 if (testing_on && to_win == nullptr) { | |
| 2004 create_to_win(page_tr_); | |
| 2005 } | |
| 2006 #endif | |
| 2007 TO_ROW_IT row_it = block->get_rows(); | |
| 2008 | |
| 2009 row_it.move_to_first(); | |
| 2010 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { | |
| 2011 if (row_it.data()->blob_list()->empty()) { | |
| 2012 delete row_it.extract(); // nothing in it | |
| 2013 } else { | |
| 2014 make_baseline_spline(row_it.data(), block); | |
| 2015 } | |
| 2016 } | |
| 2017 if (textord_old_baselines) { | |
| 2018 #ifndef GRAPHICS_DISABLED | |
| 2019 if (testing_on) { | |
| 2020 colour = ScrollView::RED; | |
| 2021 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { | |
| 2022 row_it.data()->baseline.plot(to_win, colour); | |
| 2023 colour = static_cast<ScrollView::Color>(colour + 1); | |
| 2024 if (colour > ScrollView::MAGENTA) { | |
| 2025 colour = ScrollView::RED; | |
| 2026 } | |
| 2027 } | |
| 2028 } | |
| 2029 #endif | |
| 2030 make_old_baselines(block, testing_on, gradient); | |
| 2031 } | |
| 2032 #ifndef GRAPHICS_DISABLED | |
| 2033 if (testing_on) { | |
| 2034 colour = ScrollView::RED; | |
| 2035 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { | |
| 2036 row_it.data()->baseline.plot(to_win, colour); | |
| 2037 colour = static_cast<ScrollView::Color>(colour + 1); | |
| 2038 if (colour > ScrollView::MAGENTA) { | |
| 2039 colour = ScrollView::RED; | |
| 2040 } | |
| 2041 } | |
| 2042 } | |
| 2043 #endif | |
| 2044 } | |
| 2045 | |
| 2046 /** | |
| 2047 * @name make_baseline_spline | |
| 2048 * | |
| 2049 * Fit an LMS line to a row. | |
| 2050 * Make the fit parallel to the given gradient and set the | |
| 2051 * row accordingly. | |
| 2052 */ | |
| 2053 void make_baseline_spline(TO_ROW *row, // row to fit | |
| 2054 TO_BLOCK *block) { | |
| 2055 double *coeffs; // quadratic coeffs | |
| 2056 int32_t segments; // no of segments | |
| 2057 | |
| 2058 // spline boundaries | |
| 2059 auto *xstarts = new int32_t[row->blob_list()->length() + 1]; | |
| 2060 if (segment_baseline(row, block, segments, xstarts) && !textord_straight_baselines && | |
| 2061 !textord_parallel_baselines) { | |
| 2062 coeffs = linear_spline_baseline(row, block, segments, xstarts); | |
| 2063 } else { | |
| 2064 xstarts[1] = xstarts[segments]; | |
| 2065 segments = 1; | |
| 2066 coeffs = new double[3]; | |
| 2067 coeffs[0] = 0; | |
| 2068 coeffs[1] = row->line_m(); | |
| 2069 coeffs[2] = row->line_c(); | |
| 2070 } | |
| 2071 row->baseline = QSPLINE(segments, xstarts, coeffs); | |
| 2072 delete[] coeffs; | |
| 2073 delete[] xstarts; | |
| 2074 } | |
| 2075 | |
| 2076 /** | |
| 2077 * @name segment_baseline | |
| 2078 * | |
| 2079 * Divide the baseline up into segments which require a different | |
| 2080 * quadratic fitted to them. | |
| 2081 * Return true if enough blobs were far enough away to need a quadratic. | |
| 2082 */ | |
| 2083 bool segment_baseline( // split baseline | |
| 2084 TO_ROW *row, // row to fit | |
| 2085 TO_BLOCK *block, // block it came from | |
| 2086 int32_t &segments, // no fo segments | |
| 2087 int32_t *xstarts // coords of segments | |
| 2088 ) { | |
| 2089 bool needs_curve; // needs curved line | |
| 2090 int blobcount; // no of blobs | |
| 2091 int blobindex; // current blob | |
| 2092 int last_state; // above, on , below | |
| 2093 int state; // of current blob | |
| 2094 float yshift; // from baseline | |
| 2095 TBOX box; // blob box | |
| 2096 TBOX new_box; // new_it box | |
| 2097 float middle; // xcentre of blob | |
| 2098 // blobs | |
| 2099 BLOBNBOX_IT blob_it = row->blob_list(); | |
| 2100 BLOBNBOX_IT new_it = blob_it; // front end | |
| 2101 SORTED_FLOATS yshifts; // shifts from baseline | |
| 2102 | |
| 2103 needs_curve = false; | |
| 2104 box = box_next_pre_chopped(&blob_it); | |
| 2105 xstarts[0] = box.left(); | |
| 2106 segments = 1; | |
| 2107 blobcount = row->blob_list()->length(); | |
| 2108 if (textord_oldbl_debug) { | |
| 2109 tprintf("Segmenting baseline of %d blobs at (%d,%d)\n", blobcount, box.left(), box.bottom()); | |
| 2110 } | |
| 2111 if (blobcount <= textord_spline_medianwin || blobcount < textord_spline_minblobs) { | |
| 2112 blob_it.move_to_last(); | |
| 2113 box = blob_it.data()->bounding_box(); | |
| 2114 xstarts[1] = box.right(); | |
| 2115 return false; | |
| 2116 } | |
| 2117 last_state = 0; | |
| 2118 new_it.mark_cycle_pt(); | |
| 2119 for (blobindex = 0; blobindex < textord_spline_medianwin; blobindex++) { | |
| 2120 new_box = box_next_pre_chopped(&new_it); | |
| 2121 middle = (new_box.left() + new_box.right()) / 2.0; | |
| 2122 yshift = new_box.bottom() - row->line_m() * middle - row->line_c(); | |
| 2123 // record shift | |
| 2124 yshifts.add(yshift, blobindex); | |
| 2125 if (new_it.cycled_list()) { | |
| 2126 xstarts[1] = new_box.right(); | |
| 2127 return false; | |
| 2128 } | |
| 2129 } | |
| 2130 for (blobcount = 0; blobcount < textord_spline_medianwin / 2; blobcount++) { | |
| 2131 box = box_next_pre_chopped(&blob_it); | |
| 2132 } | |
| 2133 do { | |
| 2134 new_box = box_next_pre_chopped(&new_it); | |
| 2135 // get middle one | |
| 2136 yshift = yshifts[textord_spline_medianwin / 2]; | |
| 2137 if (yshift > textord_spline_shift_fraction * block->line_size) { | |
| 2138 state = 1; | |
| 2139 } else if (-yshift > textord_spline_shift_fraction * block->line_size) { | |
| 2140 state = -1; | |
| 2141 } else { | |
| 2142 state = 0; | |
| 2143 } | |
| 2144 if (state != 0) { | |
| 2145 needs_curve = true; | |
| 2146 } | |
| 2147 // tprintf("State=%d, prev=%d, shift=%g\n", | |
| 2148 // state,last_state,yshift); | |
| 2149 if (state != last_state && blobcount > textord_spline_minblobs) { | |
| 2150 xstarts[segments++] = box.left(); | |
| 2151 blobcount = 0; | |
| 2152 } | |
| 2153 last_state = state; | |
| 2154 yshifts.remove(blobindex - textord_spline_medianwin); | |
| 2155 box = box_next_pre_chopped(&blob_it); | |
| 2156 middle = (new_box.left() + new_box.right()) / 2.0; | |
| 2157 yshift = new_box.bottom() - row->line_m() * middle - row->line_c(); | |
| 2158 yshifts.add(yshift, blobindex); | |
| 2159 blobindex++; | |
| 2160 blobcount++; | |
| 2161 } while (!new_it.cycled_list()); | |
| 2162 if (blobcount > textord_spline_minblobs || segments == 1) { | |
| 2163 xstarts[segments] = new_box.right(); | |
| 2164 } else { | |
| 2165 xstarts[--segments] = new_box.right(); | |
| 2166 } | |
| 2167 if (textord_oldbl_debug) { | |
| 2168 tprintf("Made %d segments on row at (%d,%d)\n", segments, box.right(), box.bottom()); | |
| 2169 } | |
| 2170 return needs_curve; | |
| 2171 } | |
| 2172 | |
| 2173 /** | |
| 2174 * @name linear_spline_baseline | |
| 2175 * | |
| 2176 * Divide the baseline up into segments which require a different | |
| 2177 * quadratic fitted to them. | |
| 2178 * @return true if enough blobs were far enough away to need a quadratic. | |
| 2179 */ | |
| 2180 double *linear_spline_baseline( // split baseline | |
| 2181 TO_ROW *row, // row to fit | |
| 2182 TO_BLOCK *block, // block it came from | |
| 2183 int32_t &segments, // no fo segments | |
| 2184 int32_t xstarts[] // coords of segments | |
| 2185 ) { | |
| 2186 int blobcount; // no of blobs | |
| 2187 int blobindex; // current blob | |
| 2188 int index1, index2; // blob numbers | |
| 2189 int blobs_per_segment; // blobs in each | |
| 2190 TBOX box; // blob box | |
| 2191 TBOX new_box; // new_it box | |
| 2192 // blobs | |
| 2193 BLOBNBOX_IT blob_it = row->blob_list(); | |
| 2194 BLOBNBOX_IT new_it = blob_it; // front end | |
| 2195 float b, c; // fitted curve | |
| 2196 tesseract::DetLineFit lms; | |
| 2197 int32_t segment; // current segment | |
| 2198 | |
| 2199 box = box_next_pre_chopped(&blob_it); | |
| 2200 xstarts[0] = box.left(); | |
| 2201 blobcount = 1; | |
| 2202 while (!blob_it.at_first()) { | |
| 2203 blobcount++; | |
| 2204 box = box_next_pre_chopped(&blob_it); | |
| 2205 } | |
| 2206 segments = blobcount / textord_spline_medianwin; | |
| 2207 if (segments < 1) { | |
| 2208 segments = 1; | |
| 2209 } | |
| 2210 blobs_per_segment = blobcount / segments; | |
| 2211 // quadratic coeffs | |
| 2212 auto *coeffs = new double[segments * 3]; | |
| 2213 if (textord_oldbl_debug) { | |
| 2214 tprintf( | |
| 2215 "Linear splining baseline of %d blobs at (%d,%d), into %d segments of " | |
| 2216 "%d blobs\n", | |
| 2217 blobcount, box.left(), box.bottom(), segments, blobs_per_segment); | |
| 2218 } | |
| 2219 segment = 1; | |
| 2220 for (index2 = 0; index2 < blobs_per_segment / 2; index2++) { | |
| 2221 box_next_pre_chopped(&new_it); | |
| 2222 } | |
| 2223 index1 = 0; | |
| 2224 blobindex = index2; | |
| 2225 do { | |
| 2226 blobindex += blobs_per_segment; | |
| 2227 lms.Clear(); | |
| 2228 while (index1 < blobindex || (segment == segments && index1 < blobcount)) { | |
| 2229 box = box_next_pre_chopped(&blob_it); | |
| 2230 int middle = (box.left() + box.right()) / 2; | |
| 2231 lms.Add(ICOORD(middle, box.bottom())); | |
| 2232 index1++; | |
| 2233 if (index1 == blobindex - blobs_per_segment / 2 || index1 == blobcount - 1) { | |
| 2234 xstarts[segment] = box.left(); | |
| 2235 } | |
| 2236 } | |
| 2237 lms.Fit(&b, &c); | |
| 2238 coeffs[segment * 3 - 3] = 0; | |
| 2239 coeffs[segment * 3 - 2] = b; | |
| 2240 coeffs[segment * 3 - 1] = c; | |
| 2241 segment++; | |
| 2242 if (segment > segments) { | |
| 2243 break; | |
| 2244 } | |
| 2245 | |
| 2246 blobindex += blobs_per_segment; | |
| 2247 lms.Clear(); | |
| 2248 while (index2 < blobindex || (segment == segments && index2 < blobcount)) { | |
| 2249 new_box = box_next_pre_chopped(&new_it); | |
| 2250 int middle = (new_box.left() + new_box.right()) / 2; | |
| 2251 lms.Add(ICOORD(middle, new_box.bottom())); | |
| 2252 index2++; | |
| 2253 if (index2 == blobindex - blobs_per_segment / 2 || index2 == blobcount - 1) { | |
| 2254 xstarts[segment] = new_box.left(); | |
| 2255 } | |
| 2256 } | |
| 2257 lms.Fit(&b, &c); | |
| 2258 coeffs[segment * 3 - 3] = 0; | |
| 2259 coeffs[segment * 3 - 2] = b; | |
| 2260 coeffs[segment * 3 - 1] = c; | |
| 2261 segment++; | |
| 2262 } while (segment <= segments); | |
| 2263 return coeffs; | |
| 2264 } | |
| 2265 | |
| 2266 /** | |
| 2267 * @name assign_blobs_to_rows | |
| 2268 * | |
| 2269 * Make enough rows to allocate all the given blobs to one. | |
| 2270 * If a block skew is given, use that, else attempt to track it. | |
| 2271 */ | |
| 2272 void assign_blobs_to_rows( // find lines | |
| 2273 TO_BLOCK *block, // block to do | |
| 2274 float *gradient, // block skew | |
| 2275 int pass, // identification | |
| 2276 bool reject_misses, // chuck big ones out | |
| 2277 bool make_new_rows, // add rows for unmatched | |
| 2278 bool drawing_skew // draw smoothed skew | |
| 2279 ) { | |
| 2280 OVERLAP_STATE overlap_result; // what to do with it | |
| 2281 float ycoord; // current y | |
| 2282 float top, bottom; // of blob | |
| 2283 float g_length = 1.0f; // from gradient | |
| 2284 int16_t row_count; // no of rows | |
| 2285 int16_t left_x; // left edge | |
| 2286 int16_t last_x; // previous edge | |
| 2287 float block_skew; // y delta | |
| 2288 float smooth_factor; // for new coords | |
| 2289 float near_dist; // dist to nearest row | |
| 2290 ICOORD testpt; // testing only | |
| 2291 BLOBNBOX *blob; // current blob | |
| 2292 TO_ROW *row; // current row | |
| 2293 TO_ROW *dest_row = nullptr; // row to put blob in | |
| 2294 // iterators | |
| 2295 BLOBNBOX_IT blob_it = &block->blobs; | |
| 2296 TO_ROW_IT row_it = block->get_rows(); | |
| 2297 | |
| 2298 ycoord = | |
| 2299 (block->block->pdblk.bounding_box().bottom() + block->block->pdblk.bounding_box().top()) / | |
| 2300 2.0f; | |
| 2301 if (gradient != nullptr) { | |
| 2302 g_length = std::sqrt(1 + *gradient * *gradient); | |
| 2303 } | |
| 2304 #ifndef GRAPHICS_DISABLED | |
| 2305 if (drawing_skew) { | |
| 2306 to_win->SetCursor(block->block->pdblk.bounding_box().left(), ycoord); | |
| 2307 } | |
| 2308 #endif | |
| 2309 testpt = ICOORD(textord_test_x, textord_test_y); | |
| 2310 blob_it.sort(blob_x_order); | |
| 2311 smooth_factor = 1.0; | |
| 2312 block_skew = 0.0f; | |
| 2313 row_count = row_it.length(); // might have rows | |
| 2314 if (!blob_it.empty()) { | |
| 2315 left_x = blob_it.data()->bounding_box().left(); | |
| 2316 } else { | |
| 2317 left_x = block->block->pdblk.bounding_box().left(); | |
| 2318 } | |
| 2319 last_x = left_x; | |
| 2320 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) { | |
| 2321 blob = blob_it.data(); | |
| 2322 if (gradient != nullptr) { | |
| 2323 block_skew = (1 - 1 / g_length) * blob->bounding_box().bottom() + | |
| 2324 *gradient / g_length * blob->bounding_box().left(); | |
| 2325 } else if (blob->bounding_box().left() - last_x > block->line_size / 2 && | |
| 2326 last_x - left_x > block->line_size * 2 && textord_interpolating_skew) { | |
| 2327 // tprintf("Interpolating skew from %g",block_skew); | |
| 2328 block_skew *= static_cast<float>(blob->bounding_box().left() - left_x) / (last_x - left_x); | |
| 2329 // tprintf("to %g\n",block_skew); | |
| 2330 } | |
| 2331 last_x = blob->bounding_box().left(); | |
| 2332 top = blob->bounding_box().top() - block_skew; | |
| 2333 bottom = blob->bounding_box().bottom() - block_skew; | |
| 2334 #ifndef GRAPHICS_DISABLED | |
| 2335 if (drawing_skew) { | |
| 2336 to_win->DrawTo(blob->bounding_box().left(), ycoord + block_skew); | |
| 2337 } | |
| 2338 #endif | |
| 2339 if (!row_it.empty()) { | |
| 2340 for (row_it.move_to_first(); !row_it.at_last() && row_it.data()->min_y() > top; | |
| 2341 row_it.forward()) { | |
| 2342 } | |
| 2343 row = row_it.data(); | |
| 2344 if (row->min_y() <= top && row->max_y() >= bottom) { | |
| 2345 // any overlap | |
| 2346 dest_row = row; | |
| 2347 overlap_result = most_overlapping_row(&row_it, dest_row, top, bottom, block->line_size, | |
| 2348 blob->bounding_box().contains(testpt)); | |
| 2349 if (overlap_result == NEW_ROW && !reject_misses) { | |
| 2350 overlap_result = ASSIGN; | |
| 2351 } | |
| 2352 } else { | |
| 2353 overlap_result = NEW_ROW; | |
| 2354 if (!make_new_rows) { | |
| 2355 near_dist = row_it.data_relative(-1)->min_y() - top; | |
| 2356 // below bottom | |
| 2357 if (bottom < row->min_y()) { | |
| 2358 if (row->min_y() - bottom <= (block->line_spacing - block->line_size) * | |
| 2359 tesseract::CCStruct::kDescenderFraction) { | |
| 2360 // done it | |
| 2361 overlap_result = ASSIGN; | |
| 2362 dest_row = row; | |
| 2363 } | |
| 2364 } else if (near_dist > 0 && near_dist < bottom - row->max_y()) { | |
| 2365 row_it.backward(); | |
| 2366 dest_row = row_it.data(); | |
| 2367 if (dest_row->min_y() - bottom <= (block->line_spacing - block->line_size) * | |
| 2368 tesseract::CCStruct::kDescenderFraction) { | |
| 2369 // done it | |
| 2370 overlap_result = ASSIGN; | |
| 2371 } | |
| 2372 } else { | |
| 2373 if (top - row->max_y() <= | |
| 2374 (block->line_spacing - block->line_size) * | |
| 2375 (textord_overlap_x + tesseract::CCStruct::kAscenderFraction)) { | |
| 2376 // done it | |
| 2377 overlap_result = ASSIGN; | |
| 2378 dest_row = row; | |
| 2379 } | |
| 2380 } | |
| 2381 } | |
| 2382 } | |
| 2383 if (overlap_result == ASSIGN) { | |
| 2384 dest_row->add_blob(blob_it.extract(), top, bottom, block->line_size); | |
| 2385 } | |
| 2386 if (overlap_result == NEW_ROW) { | |
| 2387 if (make_new_rows && top - bottom < block->max_blob_size) { | |
| 2388 dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size); | |
| 2389 row_count++; | |
| 2390 if (bottom > row_it.data()->min_y()) { | |
| 2391 row_it.add_before_then_move(dest_row); | |
| 2392 // insert in right place | |
| 2393 } else { | |
| 2394 row_it.add_after_then_move(dest_row); | |
| 2395 } | |
| 2396 smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset); | |
| 2397 } else { | |
| 2398 overlap_result = REJECT; | |
| 2399 } | |
| 2400 } | |
| 2401 } else if (make_new_rows && top - bottom < block->max_blob_size) { | |
| 2402 overlap_result = NEW_ROW; | |
| 2403 dest_row = new TO_ROW(blob_it.extract(), top, bottom, block->line_size); | |
| 2404 row_count++; | |
| 2405 row_it.add_after_then_move(dest_row); | |
| 2406 smooth_factor = 1.0 / (row_count * textord_skew_lag + textord_skewsmooth_offset2); | |
| 2407 } else { | |
| 2408 overlap_result = REJECT; | |
| 2409 } | |
| 2410 if (blob->bounding_box().contains(testpt) && textord_debug_blob) { | |
| 2411 if (overlap_result != REJECT) { | |
| 2412 tprintf("Test blob assigned to row at (%g,%g) on pass %d\n", dest_row->min_y(), | |
| 2413 dest_row->max_y(), pass); | |
| 2414 } else { | |
| 2415 tprintf("Test blob assigned to no row on pass %d\n", pass); | |
| 2416 } | |
| 2417 } | |
| 2418 if (overlap_result != REJECT) { | |
| 2419 while (!row_it.at_first() && row_it.data()->min_y() > row_it.data_relative(-1)->min_y()) { | |
| 2420 row = row_it.extract(); | |
| 2421 row_it.backward(); | |
| 2422 row_it.add_before_then_move(row); | |
| 2423 } | |
| 2424 while (!row_it.at_last() && row_it.data()->min_y() < row_it.data_relative(1)->min_y()) { | |
| 2425 row = row_it.extract(); | |
| 2426 row_it.forward(); | |
| 2427 // Keep rows in order. | |
| 2428 row_it.add_after_then_move(row); | |
| 2429 } | |
| 2430 BLOBNBOX_IT added_blob_it(dest_row->blob_list()); | |
| 2431 added_blob_it.move_to_last(); | |
| 2432 TBOX prev_box = added_blob_it.data_relative(-1)->bounding_box(); | |
| 2433 if (dest_row->blob_list()->singleton() || !prev_box.major_x_overlap(blob->bounding_box())) { | |
| 2434 block_skew = (1 - smooth_factor) * block_skew + | |
| 2435 smooth_factor * (blob->bounding_box().bottom() - dest_row->initial_min_y()); | |
| 2436 } | |
| 2437 } | |
| 2438 } | |
| 2439 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { | |
| 2440 if (row_it.data()->blob_list()->empty()) { | |
| 2441 delete row_it.extract(); // Discard empty rows. | |
| 2442 } | |
| 2443 } | |
| 2444 } | |
| 2445 | |
| 2446 /** | |
| 2447 * @name most_overlapping_row | |
| 2448 * | |
| 2449 * Return the row which most overlaps the blob. | |
| 2450 */ | |
| 2451 OVERLAP_STATE most_overlapping_row( // find best row | |
| 2452 TO_ROW_IT *row_it, // iterator | |
| 2453 TO_ROW *&best_row, // output row | |
| 2454 float top, // top of blob | |
| 2455 float bottom, // bottom of blob | |
| 2456 float rowsize, // max row size | |
| 2457 bool testing_blob // test stuff | |
| 2458 ) { | |
| 2459 OVERLAP_STATE result; // result of tests | |
| 2460 float overlap; // of blob & row | |
| 2461 float bestover; // nearest row | |
| 2462 float merge_top, merge_bottom; // size of merged row | |
| 2463 ICOORD testpt; // testing only | |
| 2464 TO_ROW *row; // current row | |
| 2465 TO_ROW *test_row; // for multiple overlaps | |
| 2466 BLOBNBOX_IT blob_it; // for merging rows | |
| 2467 | |
| 2468 result = ASSIGN; | |
| 2469 row = row_it->data(); | |
| 2470 bestover = top - bottom; | |
| 2471 if (top > row->max_y()) { | |
| 2472 bestover -= top - row->max_y(); | |
| 2473 } | |
| 2474 if (bottom < row->min_y()) { | |
| 2475 // compute overlap | |
| 2476 bestover -= row->min_y() - bottom; | |
| 2477 } | |
| 2478 if (testing_blob && textord_debug_blob) { | |
| 2479 tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f\n", bottom, top, row->min_y(), | |
| 2480 row->max_y(), rowsize, bestover); | |
| 2481 } | |
| 2482 test_row = row; | |
| 2483 do { | |
| 2484 if (!row_it->at_last()) { | |
| 2485 row_it->forward(); | |
| 2486 test_row = row_it->data(); | |
| 2487 if (test_row->min_y() <= top && test_row->max_y() >= bottom) { | |
| 2488 merge_top = std::max(test_row->max_y(),row->max_y()); | |
| 2489 merge_bottom = std::min(test_row->min_y(),row->min_y()); | |
| 2490 if (merge_top - merge_bottom <= rowsize) { | |
| 2491 if (testing_blob && textord_debug_blob) { | |
| 2492 tprintf("Merging rows at (%g,%g), (%g,%g)\n", row->min_y(), row->max_y(), | |
| 2493 test_row->min_y(), test_row->max_y()); | |
| 2494 } | |
| 2495 test_row->set_limits(merge_bottom, merge_top); | |
| 2496 blob_it.set_to_list(test_row->blob_list()); | |
| 2497 blob_it.add_list_after(row->blob_list()); | |
| 2498 blob_it.sort(blob_x_order); | |
| 2499 row_it->backward(); | |
| 2500 delete row_it->extract(); | |
| 2501 row_it->forward(); | |
| 2502 bestover = -1.0f; // force replacement | |
| 2503 } | |
| 2504 overlap = top - bottom; | |
| 2505 if (top > test_row->max_y()) { | |
| 2506 overlap -= top - test_row->max_y(); | |
| 2507 } | |
| 2508 if (bottom < test_row->min_y()) { | |
| 2509 overlap -= test_row->min_y() - bottom; | |
| 2510 } | |
| 2511 if (bestover >= rowsize - 1 && overlap >= rowsize - 1) { | |
| 2512 result = REJECT; | |
| 2513 } | |
| 2514 if (overlap > bestover) { | |
| 2515 bestover = overlap; // find biggest overlap | |
| 2516 row = test_row; | |
| 2517 } | |
| 2518 if (testing_blob && textord_debug_blob) { | |
| 2519 tprintf("Test blob y=(%g,%g), row=(%f,%f), size=%g, overlap=%f->%f\n", bottom, top, | |
| 2520 test_row->min_y(), test_row->max_y(), rowsize, overlap, bestover); | |
| 2521 } | |
| 2522 } | |
| 2523 } | |
| 2524 } while (!row_it->at_last() && test_row->min_y() <= top && test_row->max_y() >= bottom); | |
| 2525 while (row_it->data() != row) { | |
| 2526 row_it->backward(); // make it point to row | |
| 2527 } | |
| 2528 // doesn't overlap much | |
| 2529 if (top - bottom - bestover > rowsize * textord_overlap_x && | |
| 2530 (!textord_fix_makerow_bug || bestover < rowsize * textord_overlap_x) && result == ASSIGN) { | |
| 2531 result = NEW_ROW; // doesn't overlap enough | |
| 2532 } | |
| 2533 best_row = row; | |
| 2534 return result; | |
| 2535 } | |
| 2536 | |
| 2537 /** | |
| 2538 * @name blob_x_order | |
| 2539 * | |
| 2540 * Sort function to sort blobs in x from page left. | |
| 2541 */ | |
| 2542 int blob_x_order( // sort function | |
| 2543 const void *item1, // items to compare | |
| 2544 const void *item2) { | |
| 2545 // converted ptr | |
| 2546 const BLOBNBOX *blob1 = *reinterpret_cast<const BLOBNBOX *const *>(item1); | |
| 2547 // converted ptr | |
| 2548 const BLOBNBOX *blob2 = *reinterpret_cast<const BLOBNBOX *const *>(item2); | |
| 2549 | |
| 2550 if (blob1->bounding_box().left() < blob2->bounding_box().left()) { | |
| 2551 return -1; | |
| 2552 } else if (blob1->bounding_box().left() > blob2->bounding_box().left()) { | |
| 2553 return 1; | |
| 2554 } else { | |
| 2555 return 0; | |
| 2556 } | |
| 2557 } | |
| 2558 | |
| 2559 /** | |
| 2560 * @name mark_repeated_chars | |
| 2561 * | |
| 2562 * Mark blobs marked with BTFT_LEADER in repeated sets using the | |
| 2563 * repeated_set member of BLOBNBOX. | |
| 2564 */ | |
| 2565 void mark_repeated_chars(TO_ROW *row) { | |
| 2566 BLOBNBOX_IT box_it(row->blob_list()); // Iterator. | |
| 2567 int num_repeated_sets = 0; | |
| 2568 if (!box_it.empty()) { | |
| 2569 do { | |
| 2570 BLOBNBOX *bblob = box_it.data(); | |
| 2571 int repeat_length = 1; | |
| 2572 if (bblob->flow() == BTFT_LEADER && !bblob->joined_to_prev() && bblob->cblob() != nullptr) { | |
| 2573 BLOBNBOX_IT test_it(box_it); | |
| 2574 for (test_it.forward(); !test_it.at_first();) { | |
| 2575 bblob = test_it.data(); | |
| 2576 if (bblob->flow() != BTFT_LEADER) { | |
| 2577 break; | |
| 2578 } | |
| 2579 test_it.forward(); | |
| 2580 bblob = test_it.data(); | |
| 2581 if (bblob->joined_to_prev() || bblob->cblob() == nullptr) { | |
| 2582 repeat_length = 0; | |
| 2583 break; | |
| 2584 } | |
| 2585 ++repeat_length; | |
| 2586 } | |
| 2587 } | |
| 2588 if (repeat_length >= kMinLeaderCount) { | |
| 2589 num_repeated_sets++; | |
| 2590 for (; repeat_length > 0; box_it.forward(), --repeat_length) { | |
| 2591 bblob = box_it.data(); | |
| 2592 bblob->set_repeated_set(num_repeated_sets); | |
| 2593 } | |
| 2594 } else { | |
| 2595 bblob->set_repeated_set(0); | |
| 2596 box_it.forward(); | |
| 2597 } | |
| 2598 } while (!box_it.at_first()); // until all done | |
| 2599 } | |
| 2600 row->set_num_repeated_sets(num_repeated_sets); | |
| 2601 } | |
| 2602 | |
| 2603 } // namespace tesseract |
