Coverage Report

Created: 2026-07-25 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tesseract/src/textord/tordmain.cpp
Line
Count
Source
1
/**********************************************************************
2
 * File:        tordmain.cpp  (Formerly textordp.c)
3
 * Description: C++ top level textord code.
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
#define _USE_MATH_DEFINES // for M_PI
20
21
#ifdef HAVE_CONFIG_H
22
#  include "config_auto.h"
23
#endif
24
25
#include "tordmain.h"
26
27
#include "blobbox.h"     // for BLOBNBOX_IT, BLOBNBOX, TO_BLOCK, TO_B...
28
#include "ccstruct.h"    // for CCStruct, CCStruct::kXHeightFraction
29
#include "clst.h"        // for CLISTIZE
30
#include "coutln.h"      // for C_OUTLINE_IT, C_OUTLINE_LIST, C_OUTLINE
31
#include "drawtord.h"    // for plot_box_list, to_win, create_to_win
32
#include "edgblob.h"     // for extract_edges
33
#include "errcode.h"     // for ASSERT_HOST, ...
34
#include "image.h"       // for Image, Leptonica (pixDestroy, pixGetHeight, ...)
35
#include "makerow.h"     // for textord_test_x, textord_test_y, texto...
36
#include "ocrblock.h"    // for BLOCK_IT, BLOCK, BLOCK_LIST (ptr only)
37
#include "ocrrow.h"      // for ROW, ROW_IT, ROW_LIST, tweak_row_base...
38
#include "params.h"      // for DoubleParam, BoolParam, IntParam
39
#include "pdblock.h"     // for PDBLK
40
#include "points.h"      // for FCOORD, ICOORD
41
#include "polyblk.h"     // for POLY_BLOCK
42
#include "quadratc.h"    // for QUAD_COEFFS
43
#include "quspline.h"    // for QSPLINE, tweak_row_baseline
44
#include "rect.h"        // for TBOX
45
#include "scrollview.h"  // for ScrollView, ScrollView::WHITE
46
#include "statistc.h"    // for STATS
47
#include "stepblob.h"    // for C_BLOB_IT, C_BLOB, C_BLOB_LIST
48
#include "textord.h"     // for Textord, WordWithBox, WordGrid, WordS...
49
#include "tprintf.h"     // for tprintf
50
#include "werd.h"        // for WERD_IT, WERD, WERD_LIST, W_DONT_CHOP
51
52
#include <cfloat>  // for FLT_MAX
53
#include <cmath>   // for ceil, floor, M_PI
54
#include <cstdint> // for INT16_MAX, uint32_t, int32_t, int16_t
55
#include <memory>
56
57
namespace tesseract {
58
59
15.4k
#define MAX_NEAREST_DIST 600 // for block skew stats
60
61
/**********************************************************************
62
 * SetBlobStrokeWidth
63
 *
64
 * Set the horizontal and vertical stroke widths in the blob.
65
 **********************************************************************/
66
2.65M
void SetBlobStrokeWidth(Image pix, BLOBNBOX *blob) {
67
  // Cut the blob rectangle into a Pix.
68
2.65M
  int pix_height = pixGetHeight(pix);
69
2.65M
  const TBOX &box = blob->bounding_box();
70
2.65M
  int width = box.width();
71
2.65M
  int height = box.height();
72
2.65M
  Box *blob_pix_box = boxCreate(box.left(), pix_height - box.top(), width, height);
73
2.65M
  Image pix_blob = pixClipRectangle(pix, blob_pix_box, nullptr);
74
2.65M
  boxDestroy(&blob_pix_box);
75
2.65M
  Image dist_pix = pixDistanceFunction(pix_blob, 4, 8, L_BOUNDARY_BG);
76
2.65M
  pix_blob.destroy();
77
  // Compute the stroke widths.
78
2.65M
  uint32_t *data = pixGetData(dist_pix);
79
2.65M
  int wpl = pixGetWpl(dist_pix);
80
  // Horizontal width of stroke.
81
2.65M
  STATS h_stats(0, width);
82
19.4M
  for (int y = 0; y < height; ++y) {
83
16.7M
    uint32_t *pixels = data + y * wpl;
84
16.7M
    int prev_pixel = 0;
85
16.7M
    int pixel = Image::getDataByte(pixels, 0);
86
131M
    for (int x = 1; x < width; ++x) {
87
115M
      int next_pixel = Image::getDataByte(pixels, x);
88
      // We are looking for a pixel that is equal to its vertical neighbours,
89
      // yet greater than its left neighbour.
90
115M
      if (prev_pixel < pixel && (y == 0 || pixel == Image::getDataByte(pixels - wpl, x - 1)) &&
91
17.6M
          (y == height - 1 || pixel == Image::getDataByte(pixels + wpl, x - 1))) {
92
11.7M
        if (pixel > next_pixel) {
93
          // Single local max, so an odd width.
94
6.41M
          h_stats.add(pixel * 2 - 1, 1);
95
6.41M
        } else if (pixel == next_pixel && x + 1 < width && pixel > Image::getDataByte(pixels, x + 1)) {
96
          // Double local max, so an even width.
97
2.72M
          h_stats.add(pixel * 2, 1);
98
2.72M
        }
99
11.7M
      }
100
115M
      prev_pixel = pixel;
101
115M
      pixel = next_pixel;
102
115M
    }
103
16.7M
  }
104
  // Vertical width of stroke.
105
2.65M
  STATS v_stats(0, height);
106
14.2M
  for (int x = 0; x < width; ++x) {
107
11.5M
    int prev_pixel = 0;
108
11.5M
    int pixel = Image::getDataByte(data, x);
109
131M
    for (int y = 1; y < height; ++y) {
110
120M
      uint32_t *pixels = data + y * wpl;
111
120M
      int next_pixel = Image::getDataByte(pixels, x);
112
      // We are looking for a pixel that is equal to its horizontal neighbours,
113
      // yet greater than its upper neighbour.
114
120M
      if (prev_pixel < pixel && (x == 0 || pixel == Image::getDataByte(pixels - wpl, x - 1)) &&
115
16.9M
          (x == width - 1 || pixel == Image::getDataByte(pixels - wpl, x + 1))) {
116
11.4M
        if (pixel > next_pixel) {
117
          // Single local max, so an odd width.
118
5.42M
          v_stats.add(pixel * 2 - 1, 1);
119
6.05M
        } else if (pixel == next_pixel && y + 1 < height &&
120
3.78M
                   pixel > Image::getDataByte(pixels + wpl, x)) {
121
          // Double local max, so an even width.
122
2.20M
          v_stats.add(pixel * 2, 1);
123
2.20M
        }
124
11.4M
      }
125
120M
      prev_pixel = pixel;
126
120M
      pixel = next_pixel;
127
120M
    }
128
11.5M
  }
129
2.65M
  dist_pix.destroy();
130
  // Store the horizontal and vertical width in the blob, keeping both
131
  // widths if there is enough information, otherwise only the one with
132
  // the most samples.
133
  // If there are insufficient samples, store zero, rather than using
134
  // 2*area/perimeter, as the numbers that gives do not match the numbers
135
  // from the distance method.
136
2.65M
  if (h_stats.get_total() >= (width + height) / 4) {
137
648k
    blob->set_horz_stroke_width(h_stats.ile(0.5f));
138
648k
    if (v_stats.get_total() >= (width + height) / 4) {
139
168k
      blob->set_vert_stroke_width(v_stats.ile(0.5f));
140
479k
    } else {
141
479k
      blob->set_vert_stroke_width(0.0f);
142
479k
    }
143
2.01M
  } else {
144
2.01M
    if (v_stats.get_total() >= (width + height) / 4 || v_stats.get_total() > h_stats.get_total()) {
145
305k
      blob->set_horz_stroke_width(0.0f);
146
305k
      blob->set_vert_stroke_width(v_stats.ile(0.5f));
147
1.70M
    } else {
148
1.70M
      blob->set_horz_stroke_width(h_stats.get_total() > 2 ? h_stats.ile(0.5f) : 0.0f);
149
1.70M
      blob->set_vert_stroke_width(0.0f);
150
1.70M
    }
151
2.01M
  }
152
2.65M
}
153
154
/**********************************************************************
155
 * assign_blobs_to_blocks2
156
 *
157
 * Make a list of TO_BLOCKs for portrait and landscape orientation.
158
 **********************************************************************/
159
160
void assign_blobs_to_blocks2(Image pix,
161
                             BLOCK_LIST *blocks,           // blocks to process
162
15.4k
                             TO_BLOCK_LIST *port_blocks) { // output list
163
15.4k
  BLOCK_IT block_it = blocks;
164
15.4k
  C_BLOB_IT blob_it;       // iterator
165
15.4k
  BLOBNBOX_IT port_box_it; // iterator
166
                           // destination iterator
167
15.4k
  TO_BLOCK_IT port_block_it = port_blocks;
168
169
30.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
170
15.4k
    auto block = block_it.data();
171
15.4k
    auto port_block = new TO_BLOCK(block);
172
173
    // Convert the good outlines to block->blob_list
174
15.4k
    port_box_it.set_to_list(&port_block->blobs);
175
15.4k
    blob_it.set_to_list(block->blob_list());
176
2.67M
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
177
2.65M
      auto blob = blob_it.extract();
178
2.65M
      auto newblob = new BLOBNBOX(blob); // Convert blob to BLOBNBOX.
179
2.65M
      newblob->set_owns_cblob(true);
180
2.65M
      SetBlobStrokeWidth(pix, newblob);
181
2.65M
      port_box_it.add_after_then_move(newblob);
182
2.65M
    }
183
184
    // Put the rejected outlines in block->noise_blobs, which allows them to
185
    // be reconsidered and sorted back into rows and recover outlines mistakenly
186
    // rejected.
187
15.4k
    port_box_it.set_to_list(&port_block->noise_blobs);
188
15.4k
    blob_it.set_to_list(block->reject_blobs());
189
18.6k
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
190
3.29k
      auto blob = blob_it.extract();
191
3.29k
      auto newblob = new BLOBNBOX(blob); // Convert blob to BLOBNBOX.
192
3.29k
      newblob->set_owns_cblob(true);
193
3.29k
      SetBlobStrokeWidth(pix, newblob);
194
3.29k
      port_box_it.add_after_then_move(newblob);
195
3.29k
    }
196
197
15.4k
    port_block_it.add_after_then_move(port_block);
198
15.4k
  }
199
15.4k
}
200
201
/**********************************************************************
202
 * find_components
203
 *
204
 * Find the C_OUTLINEs of the connected components in each block, put them
205
 * in C_BLOBs, and filter them by size, putting the different size
206
 * grades on different lists in the matching TO_BLOCK in to_blocks.
207
 **********************************************************************/
208
209
15.4k
void Textord::find_components(Image pix, BLOCK_LIST *blocks, TO_BLOCK_LIST *to_blocks) {
210
15.4k
  int width = pixGetWidth(pix);
211
15.4k
  int height = pixGetHeight(pix);
212
15.4k
  if (width > INT16_MAX || height > INT16_MAX) {
213
0
    tprintf("Input image too large! (%d, %d)\n", width, height);
214
0
    return; // Can't handle it.
215
0
  }
216
217
15.4k
  BLOCK_IT block_it(blocks); // iterator
218
30.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
219
15.4k
    BLOCK *block = block_it.data();
220
15.4k
    if (block->pdblk.poly_block() == nullptr || block->pdblk.poly_block()->IsText()) {
221
15.4k
      extract_edges(pix, block);
222
15.4k
    }
223
15.4k
  }
224
225
15.4k
  assign_blobs_to_blocks2(pix, blocks, to_blocks);
226
15.4k
  ICOORD page_tr(width, height);
227
15.4k
  filter_blobs(page_tr, to_blocks, !textord_test_landscape);
228
15.4k
}
229
230
/**********************************************************************
231
 * filter_blobs
232
 *
233
 * Sort the blobs into sizes in all the blocks for later work.
234
 **********************************************************************/
235
236
void Textord::filter_blobs(ICOORD page_tr,        // top right
237
                           TO_BLOCK_LIST *blocks, // output list
238
15.4k
                           bool testing_on) {     // for plotting
239
15.4k
  TO_BLOCK_IT block_it = blocks;                  // destination iterator
240
15.4k
  TO_BLOCK *block;                                // created block
241
242
#ifndef GRAPHICS_DISABLED
243
  if (to_win != nullptr) {
244
    to_win->Clear();
245
  }
246
#endif // !GRAPHICS_DISABLED
247
248
30.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
249
15.4k
    block = block_it.data();
250
15.4k
    block->line_size = filter_noise_blobs(&block->blobs, &block->noise_blobs, &block->small_blobs,
251
15.4k
                                          &block->large_blobs);
252
15.4k
    if (block->line_size == 0) {
253
425
      block->line_size = 1;
254
425
    }
255
15.4k
    block->line_spacing =
256
15.4k
        block->line_size *
257
15.4k
        (tesseract::CCStruct::kDescenderFraction + tesseract::CCStruct::kXHeightFraction +
258
15.4k
         2 * tesseract::CCStruct::kAscenderFraction) /
259
15.4k
        tesseract::CCStruct::kXHeightFraction;
260
15.4k
    block->line_size *= textord_min_linesize;
261
15.4k
    block->max_blob_size = block->line_size * textord_excess_blobsize;
262
263
#ifndef GRAPHICS_DISABLED
264
    if (textord_show_blobs && testing_on) {
265
      if (to_win == nullptr) {
266
        create_to_win(page_tr);
267
      }
268
      block->plot_graded_blobs(to_win);
269
    }
270
    if (textord_show_boxes && testing_on) {
271
      if (to_win == nullptr) {
272
        create_to_win(page_tr);
273
      }
274
      plot_box_list(to_win, &block->noise_blobs, ScrollView::WHITE);
275
      plot_box_list(to_win, &block->small_blobs, ScrollView::WHITE);
276
      plot_box_list(to_win, &block->large_blobs, ScrollView::WHITE);
277
      plot_box_list(to_win, &block->blobs, ScrollView::WHITE);
278
    }
279
#endif // !GRAPHICS_DISABLED
280
15.4k
  }
281
15.4k
}
282
283
/**********************************************************************
284
 * filter_noise_blobs
285
 *
286
 * Move small blobs to a separate list.
287
 **********************************************************************/
288
289
float Textord::filter_noise_blobs(BLOBNBOX_LIST *src_list,     // original list
290
                                  BLOBNBOX_LIST *noise_list,   // noise list
291
                                  BLOBNBOX_LIST *small_list,   // small blobs
292
15.4k
                                  BLOBNBOX_LIST *large_list) { // large blobs
293
15.4k
  int16_t height;                                              // height of blob
294
15.4k
  int16_t width;                                               // of blob
295
15.4k
  BLOBNBOX *blob;                                              // current blob
296
15.4k
  float initial_x;                                             // first guess
297
15.4k
  BLOBNBOX_IT src_it = src_list;                               // iterators
298
15.4k
  BLOBNBOX_IT noise_it = noise_list;
299
15.4k
  BLOBNBOX_IT small_it = small_list;
300
15.4k
  BLOBNBOX_IT large_it = large_list;
301
15.4k
  STATS size_stats(0, MAX_NEAREST_DIST - 1);
302
  // blob heights
303
15.4k
  float min_y; // size limits
304
15.4k
  float max_y;
305
15.4k
  float max_x;
306
15.4k
  float max_height; // of good blobs
307
308
2.67M
  for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) {
309
2.65M
    blob = src_it.data();
310
2.65M
    if (blob->bounding_box().height() < textord_max_noise_size) {
311
1.57M
      noise_it.add_after_then_move(src_it.extract());
312
1.57M
    } else if (blob->enclosed_area() >= blob->bounding_box().height() *
313
1.08M
                                            blob->bounding_box().width() *
314
1.08M
                                            textord_noise_area_ratio) {
315
520k
      small_it.add_after_then_move(src_it.extract());
316
520k
    }
317
2.65M
  }
318
576k
  for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) {
319
561k
    size_stats.add(src_it.data()->bounding_box().height(), 1);
320
561k
  }
321
15.4k
  initial_x = size_stats.ile(textord_initialx_ile);
322
15.4k
  max_y = ceil(initial_x *
323
15.4k
               (tesseract::CCStruct::kDescenderFraction + tesseract::CCStruct::kXHeightFraction +
324
15.4k
                2 * tesseract::CCStruct::kAscenderFraction) /
325
15.4k
               tesseract::CCStruct::kXHeightFraction);
326
15.4k
  min_y = std::floor(initial_x / 2);
327
15.4k
  max_x = ceil(initial_x * textord_width_limit);
328
15.4k
  small_it.move_to_first();
329
536k
  for (small_it.mark_cycle_pt(); !small_it.cycled_list(); small_it.forward()) {
330
520k
    height = small_it.data()->bounding_box().height();
331
520k
    if (height > max_y) {
332
3.16k
      large_it.add_after_then_move(small_it.extract());
333
517k
    } else if (height >= min_y) {
334
496k
      src_it.add_after_then_move(small_it.extract());
335
496k
    }
336
520k
  }
337
15.4k
  size_stats.clear();
338
1.07M
  for (src_it.mark_cycle_pt(); !src_it.cycled_list(); src_it.forward()) {
339
1.05M
    height = src_it.data()->bounding_box().height();
340
1.05M
    width = src_it.data()->bounding_box().width();
341
1.05M
    if (height < min_y) {
342
21.3k
      small_it.add_after_then_move(src_it.extract());
343
1.03M
    } else if (height > max_y || width > max_x) {
344
14.3k
      large_it.add_after_then_move(src_it.extract());
345
1.02M
    } else {
346
1.02M
      size_stats.add(height, 1);
347
1.02M
    }
348
1.05M
  }
349
15.4k
  max_height = size_stats.ile(textord_initialasc_ile);
350
  //      tprintf("max_y=%g, min_y=%g, initial_x=%g, max_height=%g,",
351
  //              max_y,min_y,initial_x,max_height);
352
15.4k
  max_height *= tesseract::CCStruct::kXHeightCapRatio;
353
15.4k
  if (max_height > initial_x) {
354
2.17k
    initial_x = max_height;
355
2.17k
  }
356
  //      tprintf(" ret=%g\n",initial_x);
357
15.4k
  return initial_x;
358
15.4k
}
359
360
// Fixes the block so it obeys all the rules:
361
// Must have at least one ROW.
362
// Must have at least one WERD.
363
// WERDs contain a fake blob.
364
0
void Textord::cleanup_nontext_block(BLOCK *block) {
365
  // Non-text blocks must contain at least one row.
366
0
  ROW_IT row_it(block->row_list());
367
0
  if (row_it.empty()) {
368
0
    const TBOX &box = block->pdblk.bounding_box();
369
0
    float height = box.height();
370
0
    int32_t xstarts[2] = {box.left(), box.right()};
371
0
    double coeffs[3] = {0.0, 0.0, static_cast<double>(box.bottom())};
372
0
    ROW *row = new ROW(1, xstarts, coeffs, height / 2.0f, height / 4.0f, height / 4.0f, 0, 1);
373
0
    row_it.add_after_then_move(row);
374
0
  }
375
  // Each row must contain at least one word.
376
0
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
377
0
    ROW *row = row_it.data();
378
0
    WERD_IT w_it(row->word_list());
379
0
    if (w_it.empty()) {
380
      // Make a fake blob to put in the word.
381
0
      TBOX box = block->row_list()->singleton() ? block->pdblk.bounding_box() : row->bounding_box();
382
0
      C_BLOB *blob = C_BLOB::FakeBlob(box);
383
0
      C_BLOB_LIST blobs;
384
0
      C_BLOB_IT blob_it(&blobs);
385
0
      blob_it.add_after_then_move(blob);
386
0
      WERD *word = new WERD(&blobs, 0, nullptr);
387
0
      w_it.add_after_then_move(word);
388
0
    }
389
    // Each word must contain a fake blob.
390
0
    for (w_it.mark_cycle_pt(); !w_it.cycled_list(); w_it.forward()) {
391
0
      WERD *word = w_it.data();
392
      // Just assert that this is true, as it would be useful to find
393
      // out why it isn't.
394
0
      ASSERT_HOST(!word->cblob_list()->empty());
395
0
    }
396
0
    row->recalc_bounding_box();
397
0
  }
398
0
}
399
400
/**********************************************************************
401
 * cleanup_blocks
402
 *
403
 * Delete empty blocks, rows from the page.
404
 **********************************************************************/
405
406
15.4k
void Textord::cleanup_blocks(bool clean_noise, BLOCK_LIST *blocks) {
407
15.4k
  BLOCK_IT block_it = blocks; // iterator
408
15.4k
  ROW_IT row_it;              // row iterator
409
410
15.4k
  int num_rows = 0;
411
15.4k
  int num_rows_all = 0;
412
15.4k
  int num_blocks = 0;
413
15.4k
  int num_blocks_all = 0;
414
30.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
415
15.4k
    BLOCK *block = block_it.data();
416
15.4k
    if (block->pdblk.poly_block() != nullptr && !block->pdblk.poly_block()->IsText()) {
417
0
      cleanup_nontext_block(block);
418
0
      continue;
419
0
    }
420
15.4k
    num_rows = 0;
421
15.4k
    num_rows_all = 0;
422
15.4k
    if (clean_noise) {
423
15.4k
      row_it.set_to_list(block->row_list());
424
188k
      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
425
173k
        ROW *row = row_it.data();
426
173k
        ++num_rows_all;
427
173k
        clean_small_noise_from_words(row);
428
173k
        if ((textord_noise_rejrows && !row->word_list()->empty() && clean_noise_from_row(row)) ||
429
141k
            row->word_list()->empty()) {
430
36.2k
          delete row_it.extract(); // lose empty row.
431
136k
        } else {
432
136k
          if (textord_noise_rejwords) {
433
136k
            clean_noise_from_words(row_it.data());
434
136k
          }
435
136k
          if (textord_blshift_maxshift >= 0) {
436
136k
            tweak_row_baseline(row, textord_blshift_maxshift, textord_blshift_xfraction);
437
136k
          }
438
136k
          ++num_rows;
439
136k
        }
440
173k
      }
441
15.4k
    }
442
15.4k
    if (block->row_list()->empty()) {
443
1.60k
      delete block_it.extract(); // Lose empty text blocks.
444
13.8k
    } else {
445
13.8k
      ++num_blocks;
446
13.8k
    }
447
15.4k
    ++num_blocks_all;
448
15.4k
    if (textord_noise_debug) {
449
0
      tprintf("cleanup_blocks: # rows = %d / %d\n", num_rows, num_rows_all);
450
0
    }
451
15.4k
  }
452
15.4k
  if (textord_noise_debug) {
453
0
    tprintf("cleanup_blocks: # blocks = %d / %d\n", num_blocks, num_blocks_all);
454
0
  }
455
15.4k
}
456
457
/**********************************************************************
458
 * clean_noise_from_row
459
 *
460
 * Move blobs of words from rows of garbage into the reject blobs list.
461
 **********************************************************************/
462
463
bool Textord::clean_noise_from_row( // remove empties
464
    ROW *row                        // row to clean
465
167k
) {
466
167k
  bool testing_on;
467
167k
  TBOX blob_box;            // bounding box
468
167k
  C_BLOB *blob;             // current blob
469
167k
  C_OUTLINE *outline;       // current outline
470
167k
  WERD *word;               // current word
471
167k
  int32_t blob_size;        // biggest size
472
167k
  int32_t trans_count = 0;  // no of transitions
473
167k
  int32_t trans_threshold;  // noise tolerance
474
167k
  int32_t dot_count;        // small objects
475
167k
  int32_t norm_count;       // normal objects
476
167k
  int32_t super_norm_count; // real char-like
477
                            // words of row
478
167k
  WERD_IT word_it = row->word_list();
479
167k
  C_BLOB_IT blob_it;   // blob iterator
480
167k
  C_OUTLINE_IT out_it; // outline iterator
481
482
167k
  testing_on = textord_test_y > row->base_line(textord_test_x) && textord_show_blobs &&
483
0
               textord_test_y < row->base_line(textord_test_x) + row->x_height();
484
167k
  dot_count = 0;
485
167k
  norm_count = 0;
486
167k
  super_norm_count = 0;
487
438k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
488
270k
    word = word_it.data(); // current word
489
                           // blobs in word
490
270k
    blob_it.set_to_list(word->cblob_list());
491
1.31M
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
492
1.04M
      blob = blob_it.data();
493
1.04M
      if (!word->flag(W_DONT_CHOP)) {
494
        // get outlines
495
1.01M
        out_it.set_to_list(blob->out_list());
496
2.88M
        for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) {
497
1.86M
          outline = out_it.data();
498
1.86M
          blob_box = outline->bounding_box();
499
1.86M
          blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height();
500
1.86M
          if (blob_size < textord_noise_sizelimit * row->x_height()) {
501
1.14M
            dot_count++; // count small outlines
502
1.14M
          }
503
1.86M
          if (!outline->child()->empty() &&
504
62.0k
              blob_box.height() < (1 + textord_noise_syfract) * row->x_height() &&
505
53.3k
              blob_box.height() > (1 - textord_noise_syfract) * row->x_height() &&
506
15.3k
              blob_box.width() < (1 + textord_noise_sxfract) * row->x_height() &&
507
12.5k
              blob_box.width() > (1 - textord_noise_sxfract) * row->x_height()) {
508
4.06k
            super_norm_count++; // count small outlines
509
4.06k
          }
510
1.86M
        }
511
1.01M
      } else {
512
35.3k
        super_norm_count++;
513
35.3k
      }
514
1.04M
      blob_box = blob->bounding_box();
515
1.04M
      blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height();
516
1.04M
      if (blob_size >= textord_noise_sizelimit * row->x_height() &&
517
746k
          blob_size < row->x_height() * 2) {
518
705k
        trans_threshold = blob_size / textord_noise_sizefraction;
519
705k
        trans_count = blob->count_transitions(trans_threshold);
520
705k
        if (trans_count < textord_noise_translimit) {
521
635k
          norm_count++;
522
635k
        }
523
705k
      } else if (blob_box.height() > row->x_height() * 2 &&
524
19.8k
                 (!word_it.at_first() || !blob_it.at_first())) {
525
13.3k
        dot_count += 2;
526
13.3k
      }
527
1.04M
      if (testing_on) {
528
0
        tprintf("Blob at (%d,%d) -> (%d,%d), ols=%d, tc=%d, bldiff=%g\n", blob_box.left(),
529
0
                blob_box.bottom(), blob_box.right(), blob_box.top(), blob->out_list()->length(),
530
0
                trans_count, blob_box.bottom() - row->base_line(blob_box.left()));
531
0
      }
532
1.04M
    }
533
270k
  }
534
  // TODO: check whether `&& super_norm_count < textord_noise_sncount`should always be added here.
535
167k
  bool rejected = dot_count > norm_count * textord_noise_normratio &&
536
42.7k
                  dot_count > 2;
537
167k
  if (textord_noise_debug) {
538
0
    tprintf("Row ending at (%d,%g):", blob_box.right(), row->base_line(blob_box.right()));
539
0
    tprintf(" R=%g, dc=%d, nc=%d, %s\n",
540
0
            norm_count > 0 ? static_cast<float>(dot_count) / norm_count : 9999, dot_count,
541
0
            norm_count,
542
0
            rejected? "REJECTED": "ACCEPTED");
543
0
  }
544
167k
  return super_norm_count < textord_noise_sncount && rejected;
545
167k
}
546
547
/**********************************************************************
548
 * clean_noise_from_words
549
 *
550
 * Move blobs of words from rows of garbage into the reject blobs list.
551
 **********************************************************************/
552
553
void Textord::clean_noise_from_words( // remove empties
554
    ROW *row                          // row to clean
555
136k
) {
556
136k
  TBOX blob_box;           // bounding box
557
136k
  C_BLOB *blob;            // current blob
558
136k
  C_OUTLINE *outline;      // current outline
559
136k
  WERD *word;              // current word
560
136k
  int32_t blob_size;       // biggest size
561
136k
  int32_t trans_count;     // no of transitions
562
136k
  int32_t trans_threshold; // noise tolerance
563
136k
  int32_t dot_count;       // small objects
564
136k
  int32_t norm_count;      // normal objects
565
136k
  int32_t dud_words;       // number discarded
566
136k
  int32_t ok_words;        // number remaining
567
136k
  int32_t word_index;      // current word
568
                           // words of row
569
136k
  WERD_IT word_it = row->word_list();
570
136k
  C_BLOB_IT blob_it;   // blob iterator
571
136k
  C_OUTLINE_IT out_it; // outline iterator
572
573
136k
  ok_words = word_it.length();
574
136k
  if (ok_words == 0 || textord_no_rejects) {
575
0
    return;
576
0
  }
577
  // was it chucked
578
136k
  std::vector<int8_t> word_dud(ok_words);
579
136k
  dud_words = 0;
580
136k
  ok_words = 0;
581
136k
  word_index = 0;
582
333k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
583
196k
    word = word_it.data(); // current word
584
196k
    dot_count = 0;
585
196k
    norm_count = 0;
586
    // blobs in word
587
196k
    blob_it.set_to_list(word->cblob_list());
588
857k
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
589
661k
      blob = blob_it.data();
590
661k
      if (!word->flag(W_DONT_CHOP)) {
591
        // get outlines
592
626k
        out_it.set_to_list(blob->out_list());
593
1.56M
        for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) {
594
935k
          outline = out_it.data();
595
935k
          blob_box = outline->bounding_box();
596
935k
          blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height();
597
935k
          if (blob_size < textord_noise_sizelimit * row->x_height()) {
598
364k
            dot_count++; // count small outlines
599
364k
          }
600
935k
          if (!outline->child()->empty() &&
601
38.7k
              blob_box.height() < (1 + textord_noise_syfract) * row->x_height() &&
602
33.9k
              blob_box.height() > (1 - textord_noise_syfract) * row->x_height() &&
603
10.9k
              blob_box.width() < (1 + textord_noise_sxfract) * row->x_height() &&
604
9.50k
              blob_box.width() > (1 - textord_noise_sxfract) * row->x_height()) {
605
4.06k
            norm_count++; // count small outlines
606
4.06k
          }
607
935k
        }
608
626k
      } else {
609
35.3k
        norm_count++;
610
35.3k
      }
611
661k
      blob_box = blob->bounding_box();
612
661k
      blob_size = blob_box.width() > blob_box.height() ? blob_box.width() : blob_box.height();
613
661k
      if (blob_size >= textord_noise_sizelimit * row->x_height() &&
614
558k
          blob_size < row->x_height() * 2) {
615
535k
        trans_threshold = blob_size / textord_noise_sizefraction;
616
535k
        trans_count = blob->count_transitions(trans_threshold);
617
535k
        if (trans_count < textord_noise_translimit) {
618
502k
          norm_count++;
619
502k
        }
620
535k
      } else if (blob_box.height() > row->x_height() * 2 &&
621
10.9k
                 (!word_it.at_first() || !blob_it.at_first())) {
622
6.07k
        dot_count += 2;
623
6.07k
      }
624
661k
    }
625
196k
    if (dot_count > 2 && !word->flag(W_REP_CHAR)) {
626
27.8k
      if (dot_count > norm_count * textord_noise_normratio * 2) {
627
3.74k
        word_dud[word_index] = 2;
628
24.1k
      } else if (dot_count > norm_count * textord_noise_normratio) {
629
3.23k
        word_dud[word_index] = 1;
630
20.9k
      } else {
631
20.9k
        word_dud[word_index] = 0;
632
20.9k
      }
633
168k
    } else {
634
168k
      word_dud[word_index] = 0;
635
168k
    }
636
196k
    if (word_dud[word_index] == 2) {
637
3.74k
      dud_words++;
638
192k
    } else {
639
192k
      ok_words++;
640
192k
    }
641
196k
    word_index++;
642
196k
  }
643
644
136k
  word_index = 0;
645
333k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
646
196k
    if (word_dud[word_index] == 2 || (word_dud[word_index] == 1 && dud_words > ok_words)) {
647
3.95k
      word = word_it.data(); // Current word.
648
      // Previously we threw away the entire word.
649
      // Now just aggressively throw all small blobs into the reject list, where
650
      // the classifier can decide whether they are actually needed.
651
3.95k
      word->CleanNoise(textord_noise_sizelimit * row->x_height());
652
3.95k
    }
653
196k
    word_index++;
654
196k
  }
655
136k
}
656
657
// Remove outlines that are a tiny fraction in either width or height
658
// of the word height.
659
173k
void Textord::clean_small_noise_from_words(ROW *row) {
660
173k
  WERD_IT word_it(row->word_list());
661
449k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
662
276k
    WERD *word = word_it.data();
663
276k
    int min_size = static_cast<int>(textord_noise_hfract * word->bounding_box().height() + 0.5);
664
276k
    C_BLOB_IT blob_it(word->cblob_list());
665
1.48M
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
666
1.20M
      C_BLOB *blob = blob_it.data();
667
1.20M
      C_OUTLINE_IT out_it(blob->out_list());
668
3.50M
      for (out_it.mark_cycle_pt(); !out_it.cycled_list(); out_it.forward()) {
669
2.29M
        C_OUTLINE *outline = out_it.data();
670
2.29M
        outline->RemoveSmallRecursive(min_size, &out_it);
671
2.29M
      }
672
1.20M
      if (blob->out_list()->empty()) {
673
155k
        delete blob_it.extract();
674
155k
      }
675
1.20M
    }
676
276k
    if (word->cblob_list()->empty()) {
677
6.56k
      if (!word_it.at_last()) {
678
        // The next word is no longer a fuzzy non space if it was before,
679
        // since the word before is about to be deleted.
680
870
        WERD *next_word = word_it.data_relative(1);
681
870
        if (next_word->flag(W_FUZZY_NON)) {
682
254
          next_word->set_flag(W_FUZZY_NON, false);
683
254
        }
684
870
      }
685
6.56k
      delete word_it.extract();
686
6.56k
    }
687
276k
  }
688
173k
}
689
690
// Local struct to hold a group of blocks.
691
struct BlockGroup {
692
0
  BlockGroup() : rotation(1.0f, 0.0f), angle(0.0f), min_xheight(1.0f) {}
693
  explicit BlockGroup(BLOCK *block)
694
13.8k
      : bounding_box(block->pdblk.bounding_box())
695
13.8k
      , rotation(block->re_rotation())
696
13.8k
      , angle(block->re_rotation().angle())
697
13.8k
      , min_xheight(block->x_height()) {
698
13.8k
    blocks.push_back(block);
699
13.8k
  }
700
  // Union of block bounding boxes.
701
  TBOX bounding_box;
702
  // Common rotation of the blocks.
703
  FCOORD rotation;
704
  // Angle of rotation.
705
  float angle;
706
  // Min xheight of the blocks.
707
  float min_xheight;
708
  // Collection of borrowed pointers to the blocks in the group.
709
  std::vector<BLOCK *> blocks;
710
};
711
712
// Groups blocks by rotation, then, for each group, makes a WordGrid and calls
713
// TransferDiacriticsToWords to copy the diacritic blobs to the most
714
// appropriate words in the group of blocks. Source blobs are not touched.
715
15.4k
void Textord::TransferDiacriticsToBlockGroups(BLOBNBOX_LIST *diacritic_blobs, BLOCK_LIST *blocks) {
716
  // Angle difference larger than this is too much to consider equal.
717
  // They should only be in multiples of M_PI/2 anyway.
718
15.4k
  const double kMaxAngleDiff = 0.01; // About 0.6 degrees.
719
15.4k
  std::vector<std::unique_ptr<BlockGroup>> groups;
720
15.4k
  BLOCK_IT bk_it(blocks);
721
29.2k
  for (bk_it.mark_cycle_pt(); !bk_it.cycled_list(); bk_it.forward()) {
722
13.8k
    BLOCK *block = bk_it.data();
723
13.8k
    if (block->pdblk.poly_block() != nullptr && !block->pdblk.poly_block()->IsText()) {
724
0
      continue;
725
0
    }
726
    // Linear search of the groups to find a matching rotation.
727
13.8k
    float block_angle = block->re_rotation().angle();
728
13.8k
    int best_g = 0;
729
13.8k
    float best_angle_diff = FLT_MAX;
730
13.8k
    for (const auto &group : groups) {
731
0
      double angle_diff = std::fabs(block_angle - group->angle);
732
0
      if (angle_diff > M_PI) {
733
0
        angle_diff = fabs(angle_diff - 2.0 * M_PI);
734
0
      }
735
0
      if (angle_diff < best_angle_diff) {
736
0
        best_angle_diff = angle_diff;
737
0
        best_g = &group - &groups[0];
738
0
      }
739
0
    }
740
13.8k
    if (best_angle_diff > kMaxAngleDiff) {
741
13.8k
      groups.push_back(std::make_unique<BlockGroup>(block));
742
13.8k
    } else {
743
0
      groups[best_g]->blocks.push_back(block);
744
0
      groups[best_g]->bounding_box += block->pdblk.bounding_box();
745
0
      float x_height = block->x_height();
746
0
      if (x_height < groups[best_g]->min_xheight) {
747
0
        groups[best_g]->min_xheight = x_height;
748
0
      }
749
0
    }
750
13.8k
  }
751
  // Now process each group of blocks.
752
15.4k
  std::vector<std::unique_ptr<WordWithBox>> word_ptrs;
753
15.4k
  for (const auto &group : groups) {
754
13.8k
    if (group->bounding_box.null_box()) {
755
0
      continue;
756
0
    }
757
13.8k
    WordGrid word_grid(group->min_xheight, group->bounding_box.botleft(),
758
13.8k
                       group->bounding_box.topright());
759
13.8k
    for (auto b : group->blocks) {
760
13.8k
      ROW_IT row_it(b->row_list());
761
150k
      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
762
136k
        ROW *row = row_it.data();
763
        // Put the words of the row into the grid.
764
136k
        WERD_IT w_it(row->word_list());
765
333k
        for (w_it.mark_cycle_pt(); !w_it.cycled_list(); w_it.forward()) {
766
196k
          WERD *word = w_it.data();
767
196k
          auto box_word = std::make_unique<WordWithBox>(word);
768
196k
          word_grid.InsertBBox(true, true, box_word.get());
769
          // Save the pointer where it will be auto-deleted.
770
196k
          word_ptrs.emplace_back(std::move(box_word));
771
196k
        }
772
136k
      }
773
13.8k
    }
774
13.8k
    FCOORD rotation = group->rotation;
775
    // Make it a forward rotation that will transform blob coords to block.
776
13.8k
    rotation.set_y(-rotation.y());
777
13.8k
    TransferDiacriticsToWords(diacritic_blobs, rotation, &word_grid);
778
13.8k
  }
779
15.4k
}
780
781
// Places a copy of blobs that are near a word (after applying rotation to the
782
// blob) in the most appropriate word, unless there is doubt, in which case a
783
// blob can end up in two words. Source blobs are not touched.
784
void Textord::TransferDiacriticsToWords(BLOBNBOX_LIST *diacritic_blobs, const FCOORD &rotation,
785
13.8k
                                        WordGrid *word_grid) {
786
13.8k
  WordSearch ws(word_grid);
787
13.8k
  BLOBNBOX_IT b_it(diacritic_blobs);
788
  // Apply rotation to each blob before finding the nearest words. The rotation
789
  // allows us to only consider above/below placement and not left/right on
790
  // vertical text, because all text is horizontal here.
791
13.8k
  for (b_it.mark_cycle_pt(); !b_it.cycled_list(); b_it.forward()) {
792
0
    BLOBNBOX *blobnbox = b_it.data();
793
0
    TBOX blob_box = blobnbox->bounding_box();
794
0
    blob_box.rotate(rotation);
795
0
    ws.StartRectSearch(blob_box);
796
    // Above/below refer to word position relative to diacritic. Since some
797
    // scripts eg Kannada/Telugu habitually put diacritics below words, and
798
    // others eg Thai/Vietnamese/Latin put most diacritics above words, try
799
    // for both if there isn't much in it.
800
0
    WordWithBox *best_above_word = nullptr;
801
0
    WordWithBox *best_below_word = nullptr;
802
0
    int best_above_distance = 0;
803
0
    int best_below_distance = 0;
804
0
    for (WordWithBox *word = ws.NextRectSearch(); word != nullptr; word = ws.NextRectSearch()) {
805
0
      if (word->word()->flag(W_REP_CHAR)) {
806
0
        continue;
807
0
      }
808
0
      TBOX word_box = word->true_bounding_box();
809
0
      int x_distance = blob_box.x_gap(word_box);
810
0
      int y_distance = blob_box.y_gap(word_box);
811
0
      if (x_distance > 0) {
812
        // Arbitrarily divide x-distance by 2 if there is a major y overlap,
813
        // and the word is to the left of the diacritic. If the
814
        // diacritic is a dropped broken character between two words, this will
815
        // help send all the pieces to a single word, instead of splitting them
816
        // over the 2 words.
817
0
        if (word_box.major_y_overlap(blob_box) && blob_box.left() > word_box.right()) {
818
0
          x_distance /= 2;
819
0
        }
820
0
        y_distance += x_distance;
821
0
      }
822
0
      if (word_box.y_middle() > blob_box.y_middle() &&
823
0
          (best_above_word == nullptr || y_distance < best_above_distance)) {
824
0
        best_above_word = word;
825
0
        best_above_distance = y_distance;
826
0
      }
827
0
      if (word_box.y_middle() <= blob_box.y_middle() &&
828
0
          (best_below_word == nullptr || y_distance < best_below_distance)) {
829
0
        best_below_word = word;
830
0
        best_below_distance = y_distance;
831
0
      }
832
0
    }
833
0
    bool above_good = best_above_word != nullptr &&
834
0
                      (best_below_word == nullptr ||
835
0
                       best_above_distance < best_below_distance + blob_box.height());
836
0
    bool below_good = best_below_word != nullptr && best_below_word != best_above_word &&
837
0
                      (best_above_word == nullptr ||
838
0
                       best_below_distance < best_above_distance + blob_box.height());
839
0
    if (below_good) {
840
0
      C_BLOB *copied_blob = C_BLOB::deep_copy(blobnbox->cblob());
841
0
      copied_blob->rotate(rotation);
842
      // Put the blob into the word's reject blobs list.
843
0
      C_BLOB_IT blob_it(best_below_word->RejBlobs());
844
0
      blob_it.add_to_end(copied_blob);
845
0
    }
846
0
    if (above_good) {
847
0
      C_BLOB *copied_blob = C_BLOB::deep_copy(blobnbox->cblob());
848
0
      copied_blob->rotate(rotation);
849
      // Put the blob into the word's reject blobs list.
850
0
      C_BLOB_IT blob_it(best_above_word->RejBlobs());
851
0
      blob_it.add_to_end(copied_blob);
852
0
    }
853
0
  }
854
13.8k
}
855
856
/**********************************************************************
857
 * tweak_row_baseline
858
 *
859
 * Shift baseline to fit the blobs more accurately where they are
860
 * close enough.
861
 **********************************************************************/
862
863
136k
void tweak_row_baseline(ROW *row, double blshift_maxshift, double blshift_xfraction) {
864
136k
  TBOX blob_box;      // bounding box
865
136k
  C_BLOB *blob;       // current blob
866
136k
  WERD *word;         // current word
867
136k
  int32_t blob_count; // no of blobs
868
136k
  int32_t src_index;  // source segment
869
136k
  int32_t dest_index; // destination segment
870
136k
  float ydiff;        // baseline error
871
136k
  float x_centre;     // centre of blob
872
                      // words of row
873
136k
  WERD_IT word_it = row->word_list();
874
136k
  C_BLOB_IT blob_it; // blob iterator
875
876
136k
  blob_count = 0;
877
333k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
878
196k
    word = word_it.data(); // current word
879
                           // get total blobs
880
196k
    blob_count += word->cblob_list()->length();
881
196k
  }
882
136k
  if (blob_count == 0) {
883
0
    return;
884
0
  }
885
  // spline segments
886
136k
  std::vector<int32_t> xstarts(blob_count + row->baseline.segments + 1);
887
  // spline coeffs
888
136k
  std::vector<double> coeffs((blob_count + row->baseline.segments) * 3);
889
890
136k
  src_index = 0;
891
136k
  dest_index = 0;
892
136k
  xstarts[0] = row->baseline.xcoords[0];
893
333k
  for (word_it.mark_cycle_pt(); !word_it.cycled_list(); word_it.forward()) {
894
196k
    word = word_it.data(); // current word
895
                           // blobs in word
896
196k
    blob_it.set_to_list(word->cblob_list());
897
843k
    for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
898
647k
      blob = blob_it.data();
899
647k
      blob_box = blob->bounding_box();
900
647k
      x_centre = (blob_box.left() + blob_box.right()) / 2.0;
901
647k
      ydiff = blob_box.bottom() - row->base_line(x_centre);
902
647k
      if (ydiff < 0) {
903
223k
        ydiff = -ydiff / row->x_height();
904
424k
      } else {
905
424k
        ydiff = ydiff / row->x_height();
906
424k
      }
907
647k
      if (ydiff < blshift_maxshift && blob_box.height() / row->x_height() > blshift_xfraction) {
908
0
        if (xstarts[dest_index] >= x_centre) {
909
0
          xstarts[dest_index] = blob_box.left();
910
0
        }
911
0
        coeffs[dest_index * 3] = 0;
912
0
        coeffs[dest_index * 3 + 1] = 0;
913
0
        coeffs[dest_index * 3 + 2] = blob_box.bottom();
914
        // shift it
915
0
        dest_index++;
916
0
        xstarts[dest_index] = blob_box.right() + 1;
917
647k
      } else {
918
647k
        if (xstarts[dest_index] <= x_centre) {
919
381k
          while (row->baseline.xcoords[src_index + 1] <= x_centre &&
920
153k
                 src_index < row->baseline.segments - 1) {
921
153k
            if (row->baseline.xcoords[src_index + 1] > xstarts[dest_index]) {
922
61.9k
              coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a;
923
61.9k
              coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b;
924
61.9k
              coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c;
925
61.9k
              dest_index++;
926
61.9k
              xstarts[dest_index] = row->baseline.xcoords[src_index + 1];
927
61.9k
            }
928
153k
            src_index++;
929
153k
          }
930
228k
          coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a;
931
228k
          coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b;
932
228k
          coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c;
933
228k
          dest_index++;
934
228k
          xstarts[dest_index] = row->baseline.xcoords[src_index + 1];
935
228k
        }
936
647k
      }
937
647k
    }
938
196k
  }
939
273k
  while (src_index < row->baseline.segments &&
940
248k
         row->baseline.xcoords[src_index + 1] <= xstarts[dest_index]) {
941
136k
    src_index++;
942
136k
  }
943
251k
  while (src_index < row->baseline.segments) {
944
114k
    coeffs[dest_index * 3] = row->baseline.quadratics[src_index].a;
945
114k
    coeffs[dest_index * 3 + 1] = row->baseline.quadratics[src_index].b;
946
114k
    coeffs[dest_index * 3 + 2] = row->baseline.quadratics[src_index].c;
947
114k
    dest_index++;
948
114k
    src_index++;
949
114k
    xstarts[dest_index] = row->baseline.xcoords[src_index];
950
114k
  }
951
  // turn to spline
952
136k
  row->baseline = QSPLINE(dest_index, &xstarts[0], &coeffs[0]);
953
136k
}
954
955
} // namespace tesseract