Coverage Report

Created: 2026-09-14 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tesseract/src/textord/topitch.cpp
Line
Count
Source
1
/**********************************************************************
2
 * File:        topitch.cpp  (Formerly to_pitch.c)
3
 * Description: Code to determine fixed pitchness and the pitch if fixed.
4
 * Author:      Ray Smith
5
 *
6
 * (C) Copyright 1993, 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 "topitch.h"
25
26
#include "blobbox.h"
27
#include "drawtord.h"
28
#include "makerow.h"
29
#include "pithsync.h"
30
#include "pitsync1.h"
31
#include "statistc.h"
32
#include "tovars.h"
33
#include "wordseg.h"
34
35
#include "helpers.h"
36
37
#include <memory>
38
39
namespace tesseract {
40
41
static BOOL_VAR(textord_all_prop, false, "All doc is proportial text");
42
BOOL_VAR(textord_debug_pitch_test, false, "Debug on fixed pitch test");
43
static BOOL_VAR(textord_disable_pitch_test, false, "Turn off dp fixed pitch algorithm");
44
BOOL_VAR(textord_fast_pitch_test, false, "Do even faster pitch algorithm");
45
BOOL_VAR(textord_debug_pitch_metric, false, "Write full metric stuff");
46
BOOL_VAR(textord_show_row_cuts, false, "Draw row-level cuts");
47
BOOL_VAR(textord_show_page_cuts, false, "Draw page-level cuts");
48
BOOL_VAR(textord_blockndoc_fixed, false, "Attempt whole doc/block fixed pitch");
49
double_VAR(textord_projection_scale, 0.200, "Ding rate for mid-cuts");
50
double_VAR(textord_balance_factor, 1.0, "Ding rate for unbalanced char cells");
51
52
367k
#define BLOCK_STATS_CLUSTERS 10
53
14.9k
#define MAX_ALLOWED_PITCH 100 // max pixel pitch.
54
55
// qsort function to sort 2 floats.
56
46.5k
static int sort_floats(const void *arg1, const void *arg2) {
57
46.5k
  float diff = *reinterpret_cast<const float *>(arg1) - *reinterpret_cast<const float *>(arg2);
58
46.5k
  if (diff > 0) {
59
6.30k
    return 1;
60
40.2k
  } else if (diff < 0) {
61
40.2k
    return -1;
62
40.2k
  } else {
63
0
    return 0;
64
0
  }
65
46.5k
}
66
67
/**********************************************************************
68
 * compute_fixed_pitch
69
 *
70
 * Decide whether each row is fixed pitch individually.
71
 * Correlate definite and uncertain results to obtain an individual
72
 * result for each row in the TO_ROW class.
73
 **********************************************************************/
74
75
void compute_fixed_pitch(ICOORD page_tr,             // top right
76
                         TO_BLOCK_LIST *port_blocks, // input list
77
                         float gradient,             // page skew
78
14.9k
                         bool testing_on) {          // correct orientation
79
14.9k
  TO_BLOCK_IT block_it;                              // iterator
80
14.9k
  TO_BLOCK *block;                                   // current block;
81
14.9k
  TO_ROW *row;                                       // current row
82
14.9k
  int block_index;                                   // block number
83
14.9k
  int row_index;                                     // row number
84
85
#ifndef GRAPHICS_DISABLED
86
  if (textord_show_initial_words && testing_on) {
87
    if (to_win == nullptr) {
88
      create_to_win(page_tr);
89
    }
90
  }
91
#endif
92
93
14.9k
  block_it.set_to_list(port_blocks);
94
14.9k
  block_index = 1;
95
29.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
96
14.9k
    block = block_it.data();
97
14.9k
    compute_block_pitch(block, block_index, testing_on);
98
14.9k
    block_index++;
99
14.9k
  }
100
101
14.9k
  if (!try_doc_fixed(port_blocks, gradient)) {
102
14.9k
    block_index = 1;
103
29.8k
    for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
104
14.9k
      block = block_it.data();
105
14.9k
      try_rows_fixed(block, block_index, testing_on);
106
14.9k
      block_index++;
107
14.9k
    }
108
14.9k
  }
109
110
14.9k
  block_index = 1;
111
29.8k
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
112
14.9k
    block = block_it.data();
113
14.9k
    POLY_BLOCK *pb = block->block->pdblk.poly_block();
114
14.9k
    if (pb != nullptr && !pb->IsText()) {
115
0
      continue; // Non-text doesn't exist!
116
0
    }
117
    // row iterator
118
14.9k
    TO_ROW_IT row_it(block->get_rows());
119
14.9k
    row_index = 1;
120
184k
    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
121
169k
      row = row_it.data();
122
169k
      fix_row_pitch(row, port_blocks, row_index, block_index);
123
169k
      row_index++;
124
169k
    }
125
14.9k
    block_index++;
126
14.9k
  }
127
#ifndef GRAPHICS_DISABLED
128
  if (textord_show_initial_words && testing_on) {
129
    ScrollView::Update();
130
  }
131
#endif
132
14.9k
}
133
134
/**********************************************************************
135
 * fix_row_pitch
136
 *
137
 * Get a pitch_decision for this row by voting among similar rows in the
138
 * block, then similar rows over all the page, or any other rows at all.
139
 **********************************************************************/
140
141
void fix_row_pitch(TO_ROW *bad_row,        // row to fix
142
                   TO_BLOCK_LIST *blocks,  // blocks to scan
143
                   int32_t row_target,     // number of row
144
169k
                   int32_t block_target) { // number of block
145
169k
  int16_t mid_cuts;
146
169k
  int block_votes;               // votes in block
147
169k
  int like_votes;                // votes over page
148
169k
  int other_votes;               // votes of unlike blocks
149
169k
  int block_index;               // number of block
150
169k
  int maxwidth;                  // max pitch
151
169k
  TO_BLOCK_IT block_it = blocks; // block iterator
152
169k
  TO_BLOCK *block;               // current block
153
169k
  TO_ROW *row;                   // current row
154
169k
  float sp_sd;                   // space deviation
155
169k
  STATS block_stats;             // pitches in block
156
169k
  STATS like_stats;              // pitches in page
157
158
169k
  block_votes = like_votes = other_votes = 0;
159
169k
  maxwidth = static_cast<int32_t>(ceil(bad_row->xheight * textord_words_maxspace));
160
169k
  if (bad_row->pitch_decision != PITCH_DEF_FIXED && bad_row->pitch_decision != PITCH_DEF_PROP) {
161
160k
    block_stats.set_range(0, maxwidth - 1);
162
160k
    like_stats.set_range(0, maxwidth - 1);
163
160k
    block_index = 1;
164
320k
    for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
165
160k
      block = block_it.data();
166
160k
      POLY_BLOCK *pb = block->block->pdblk.poly_block();
167
160k
      if (pb != nullptr && !pb->IsText()) {
168
0
        continue; // Non text doesn't exist!
169
0
      }
170
160k
      TO_ROW_IT row_it(block->get_rows());
171
3.57M
      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
172
3.41M
        row = row_it.data();
173
3.41M
        if ((bad_row->all_caps &&
174
391k
             row->xheight + row->ascrise <
175
391k
                 (bad_row->xheight + bad_row->ascrise) * (1 + textord_pitch_rowsimilarity) &&
176
199k
             row->xheight + row->ascrise >
177
199k
                 (bad_row->xheight + bad_row->ascrise) * (1 - textord_pitch_rowsimilarity)) ||
178
3.26M
            (!bad_row->all_caps &&
179
3.02M
             row->xheight < bad_row->xheight * (1 + textord_pitch_rowsimilarity) &&
180
2.89M
             row->xheight > bad_row->xheight * (1 - textord_pitch_rowsimilarity))) {
181
2.74M
          if (block_index == block_target) {
182
2.74M
            if (row->pitch_decision == PITCH_DEF_FIXED) {
183
19.0k
              block_votes += textord_words_veto_power;
184
19.0k
              block_stats.add(static_cast<int32_t>(row->fixed_pitch), textord_words_veto_power);
185
2.72M
            } else if (row->pitch_decision == PITCH_MAYBE_FIXED ||
186
2.71M
                       row->pitch_decision == PITCH_CORR_FIXED) {
187
42.3k
              block_votes++;
188
42.3k
              block_stats.add(static_cast<int32_t>(row->fixed_pitch), 1);
189
2.68M
            } else if (row->pitch_decision == PITCH_DEF_PROP) {
190
46.6k
              block_votes -= textord_words_veto_power;
191
2.63M
            } else if (row->pitch_decision == PITCH_MAYBE_PROP ||
192
2.60M
                       row->pitch_decision == PITCH_CORR_PROP) {
193
1.25M
              block_votes--;
194
1.25M
            }
195
2.74M
          } else {
196
0
            if (row->pitch_decision == PITCH_DEF_FIXED) {
197
0
              like_votes += textord_words_veto_power;
198
0
              like_stats.add(static_cast<int32_t>(row->fixed_pitch), textord_words_veto_power);
199
0
            } else if (row->pitch_decision == PITCH_MAYBE_FIXED ||
200
0
                       row->pitch_decision == PITCH_CORR_FIXED) {
201
0
              like_votes++;
202
0
              like_stats.add(static_cast<int32_t>(row->fixed_pitch), 1);
203
0
            } else if (row->pitch_decision == PITCH_DEF_PROP) {
204
0
              like_votes -= textord_words_veto_power;
205
0
            } else if (row->pitch_decision == PITCH_MAYBE_PROP ||
206
0
                       row->pitch_decision == PITCH_CORR_PROP) {
207
0
              like_votes--;
208
0
            }
209
0
          }
210
2.74M
        } else {
211
671k
          if (row->pitch_decision == PITCH_DEF_FIXED) {
212
2.48k
            other_votes += textord_words_veto_power;
213
668k
          } else if (row->pitch_decision == PITCH_MAYBE_FIXED ||
214
666k
                     row->pitch_decision == PITCH_CORR_FIXED) {
215
5.08k
            other_votes++;
216
663k
          } else if (row->pitch_decision == PITCH_DEF_PROP) {
217
30.2k
            other_votes -= textord_words_veto_power;
218
633k
          } else if (row->pitch_decision == PITCH_MAYBE_PROP ||
219
626k
                     row->pitch_decision == PITCH_CORR_PROP) {
220
322k
            other_votes--;
221
322k
          }
222
671k
        }
223
3.41M
      }
224
160k
      block_index++;
225
160k
    }
226
160k
    if (block_votes > textord_words_veto_power) {
227
3.74k
      bad_row->fixed_pitch = block_stats.ile(0.5);
228
3.74k
      bad_row->pitch_decision = PITCH_CORR_FIXED;
229
156k
    } else if (block_votes <= textord_words_veto_power && like_votes > 0) {
230
0
      bad_row->fixed_pitch = like_stats.ile(0.5);
231
0
      bad_row->pitch_decision = PITCH_CORR_FIXED;
232
156k
    } else {
233
156k
      bad_row->pitch_decision = PITCH_CORR_PROP;
234
156k
      if (block_votes == 0 && like_votes == 0 && other_votes > 0 &&
235
304
          (textord_debug_pitch_test || textord_debug_pitch_metric)) {
236
0
        tprintf(
237
0
            "Warning:row %d of block %d set prop with no like rows against "
238
0
            "trend\n",
239
0
            row_target, block_target);
240
0
      }
241
156k
    }
242
160k
  }
243
169k
  if (textord_debug_pitch_metric) {
244
0
    tprintf(":b_votes=%d:l_votes=%d:o_votes=%d", block_votes, like_votes, other_votes);
245
0
    tprintf("x=%g:asc=%g\n", bad_row->xheight, bad_row->ascrise);
246
0
  }
247
169k
  if (bad_row->pitch_decision == PITCH_CORR_FIXED) {
248
3.74k
    if (bad_row->fixed_pitch < textord_min_xheight) {
249
2.07k
      if (block_votes > 0) {
250
2.07k
        bad_row->fixed_pitch = block_stats.ile(0.5);
251
2.07k
      } else if (block_votes == 0 && like_votes > 0) {
252
0
        bad_row->fixed_pitch = like_stats.ile(0.5);
253
0
      } else {
254
0
        tprintf("Warning:guessing pitch as xheight on row %d, block %d\n", row_target,
255
0
                block_target);
256
0
        bad_row->fixed_pitch = bad_row->xheight;
257
0
      }
258
2.07k
    }
259
3.74k
    if (bad_row->fixed_pitch < textord_min_xheight) {
260
2.07k
      bad_row->fixed_pitch = static_cast<float>(textord_min_xheight);
261
2.07k
    }
262
3.74k
    bad_row->kern_size = bad_row->fixed_pitch / 4;
263
3.74k
    bad_row->min_space = static_cast<int32_t>(bad_row->fixed_pitch * 0.6);
264
3.74k
    bad_row->max_nonspace = static_cast<int32_t>(bad_row->fixed_pitch * 0.4);
265
3.74k
    bad_row->space_threshold = (bad_row->min_space + bad_row->max_nonspace) / 2;
266
3.74k
    bad_row->space_size = bad_row->fixed_pitch;
267
3.74k
    if (bad_row->char_cells.empty() && !bad_row->blob_list()->empty()) {
268
2.62k
      tune_row_pitch(bad_row, &bad_row->projection, bad_row->projection_left,
269
2.62k
                     bad_row->projection_right,
270
2.62k
                     (bad_row->fixed_pitch + bad_row->max_nonspace * 3) / 4, bad_row->fixed_pitch,
271
2.62k
                     sp_sd, mid_cuts, &bad_row->char_cells, false);
272
2.62k
    }
273
166k
  } else if (bad_row->pitch_decision == PITCH_CORR_PROP ||
274
164k
             bad_row->pitch_decision == PITCH_DEF_PROP) {
275
164k
    bad_row->fixed_pitch = 0.0f;
276
164k
    bad_row->char_cells.clear();
277
164k
  }
278
169k
}
279
280
/**********************************************************************
281
 * compute_block_pitch
282
 *
283
 * Decide whether each block is fixed pitch individually.
284
 **********************************************************************/
285
286
void compute_block_pitch(TO_BLOCK *block,     // input list
287
                         int32_t block_index, // block number
288
14.9k
                         bool testing_on) {   // correct orientation
289
14.9k
  TBOX block_box;                             // bounding box
290
291
14.9k
  block_box = block->block->pdblk.bounding_box();
292
14.9k
  if (testing_on && textord_debug_pitch_test) {
293
0
    tprintf("Block %d at (%d,%d)->(%d,%d)\n", block_index, block_box.left(), block_box.bottom(),
294
0
            block_box.right(), block_box.top());
295
0
  }
296
14.9k
  block->min_space = static_cast<int32_t>(floor(block->xheight * textord_words_default_minspace));
297
14.9k
  block->max_nonspace = static_cast<int32_t>(ceil(block->xheight * textord_words_default_nonspace));
298
14.9k
  block->fixed_pitch = 0.0f;
299
14.9k
  block->space_size = static_cast<float>(block->min_space);
300
14.9k
  block->kern_size = static_cast<float>(block->max_nonspace);
301
14.9k
  block->pr_nonsp = block->xheight * words_default_prop_nonspace;
302
14.9k
  block->pr_space = block->pr_nonsp * textord_spacesize_ratioprop;
303
14.9k
  if (!block->get_rows()->empty()) {
304
14.5k
    ASSERT_HOST(block->xheight > 0);
305
14.5k
    find_repeated_chars(block);
306
#ifndef GRAPHICS_DISABLED
307
    if (textord_show_initial_words && testing_on) {
308
      // overlap_picture_ops(true);
309
      ScrollView::Update();
310
    }
311
#endif
312
14.5k
    compute_rows_pitch(block, block_index, textord_debug_pitch_test && testing_on);
313
14.5k
  }
314
14.9k
}
315
316
/**********************************************************************
317
 * compute_rows_pitch
318
 *
319
 * Decide whether each row is fixed pitch individually.
320
 **********************************************************************/
321
322
bool compute_rows_pitch( // find line stats
323
    TO_BLOCK *block,     // block to do
324
    int32_t block_index, // block number
325
    bool testing_on      // correct orientation
326
14.5k
) {
327
14.5k
  int32_t maxwidth;   // of spaces
328
14.5k
  TO_ROW *row;        // current row
329
14.5k
  int32_t row_index;  // row number.
330
14.5k
  float lower, upper; // cluster thresholds
331
14.5k
  TO_ROW_IT row_it = block->get_rows();
332
333
14.5k
  row_index = 1;
334
184k
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
335
169k
    row = row_it.data();
336
169k
    ASSERT_HOST(row->xheight > 0);
337
169k
    row->compute_vertical_projection();
338
169k
    maxwidth = static_cast<int32_t>(ceil(row->xheight * textord_words_maxspace));
339
169k
    if (row_pitch_stats(row, maxwidth, testing_on) &&
340
122k
        find_row_pitch(row, maxwidth, textord_dotmatrix_gap + 1, block, block_index, row_index,
341
122k
                       testing_on)) {
342
14.3k
      if (row->fixed_pitch == 0) {
343
0
        lower = row->pr_nonsp;
344
0
        upper = row->pr_space;
345
0
        row->space_size = upper;
346
0
        row->kern_size = lower;
347
0
      }
348
155k
    } else {
349
155k
      row->fixed_pitch = 0.0f; // insufficient data
350
155k
      row->pitch_decision = PITCH_DUNNO;
351
155k
    }
352
169k
    row_index++;
353
169k
  }
354
14.5k
  return false;
355
14.5k
}
356
357
/**********************************************************************
358
 * try_doc_fixed
359
 *
360
 * Attempt to call the entire document fixed pitch.
361
 **********************************************************************/
362
363
bool try_doc_fixed(             // determine pitch
364
    TO_BLOCK_LIST *port_blocks, // input list
365
    float gradient              // page skew
366
14.9k
) {
367
14.9k
  int16_t master_x; // uniform shifts
368
14.9k
  int16_t pitch;    // median pitch.
369
14.9k
  int x;            // profile coord
370
14.9k
  int prop_blocks;  // correct counts
371
14.9k
  int fixed_blocks;
372
14.9k
  int total_row_count; // total in page
373
                       // iterator
374
14.9k
  TO_BLOCK_IT block_it = port_blocks;
375
14.9k
  TO_BLOCK *block;         // current block;
376
14.9k
  TO_ROW *row;             // current row
377
14.9k
  int16_t projection_left; // edges
378
14.9k
  int16_t projection_right;
379
14.9k
  int16_t row_left; // edges of row
380
14.9k
  int16_t row_right;
381
14.9k
  float master_y;     // uniform shifts
382
14.9k
  float shift_factor; // page skew correction
383
14.9k
  float final_pitch;  // output pitch
384
14.9k
  float row_y;        // baseline
385
14.9k
  STATS projection;   // entire page
386
14.9k
  STATS pitches(0, MAX_ALLOWED_PITCH - 1);
387
  // for median
388
14.9k
  float sp_sd;      // space sd
389
14.9k
  int16_t mid_cuts; // no of cheap cuts
390
14.9k
  float pitch_sd;   // sync rating
391
392
14.9k
  if (!textord_blockndoc_fixed ||
393
14.9k
      block_it.empty() || block_it.data()->get_rows()->empty()) {
394
14.9k
    return false;
395
14.9k
  }
396
0
  shift_factor = gradient / (gradient * gradient + 1);
397
  // row iterator
398
0
  TO_ROW_IT row_it(block_it.data()->get_rows());
399
0
  master_x = row_it.data()->projection_left;
400
0
  master_y = row_it.data()->baseline.y(master_x);
401
0
  projection_left = INT16_MAX;
402
0
  projection_right = -INT16_MAX;
403
0
  prop_blocks = 0;
404
0
  fixed_blocks = 0;
405
0
  total_row_count = 0;
406
407
0
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
408
0
    block = block_it.data();
409
0
    row_it.set_to_list(block->get_rows());
410
0
    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
411
0
      row = row_it.data();
412
0
      total_row_count++;
413
0
      if (row->fixed_pitch > 0) {
414
0
        pitches.add(static_cast<int32_t>(row->fixed_pitch), 1);
415
0
      }
416
      // find median
417
0
      row_y = row->baseline.y(master_x);
418
0
      row_left = static_cast<int16_t>(row->projection_left - shift_factor * (master_y - row_y));
419
0
      row_right = static_cast<int16_t>(row->projection_right - shift_factor * (master_y - row_y));
420
0
      if (row_left < projection_left) {
421
0
        projection_left = row_left;
422
0
      }
423
0
      if (row_right > projection_right) {
424
0
        projection_right = row_right;
425
0
      }
426
0
    }
427
0
  }
428
0
  if (pitches.get_total() == 0) {
429
0
    return false;
430
0
  }
431
0
  projection.set_range(projection_left, projection_right - 1);
432
433
0
  for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
434
0
    block = block_it.data();
435
0
    row_it.set_to_list(block->get_rows());
436
0
    for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
437
0
      row = row_it.data();
438
0
      row_y = row->baseline.y(master_x);
439
0
      row_left = static_cast<int16_t>(row->projection_left - shift_factor * (master_y - row_y));
440
0
      for (x = row->projection_left; x < row->projection_right; x++, row_left++) {
441
0
        projection.add(row_left, row->projection.pile_count(x));
442
0
      }
443
0
    }
444
0
  }
445
446
0
  row_it.set_to_list(block_it.data()->get_rows());
447
0
  row = row_it.data();
448
#ifndef GRAPHICS_DISABLED
449
  if (textord_show_page_cuts && to_win != nullptr) {
450
    projection.plot(to_win, projection_left, row->intercept(), 1.0f, -1.0f, ScrollView::CORAL);
451
  }
452
#endif
453
0
  final_pitch = pitches.ile(0.5);
454
0
  pitch = static_cast<int16_t>(final_pitch);
455
0
  pitch_sd = tune_row_pitch(row, &projection, projection_left, projection_right, pitch * 0.75,
456
0
                            final_pitch, sp_sd, mid_cuts, &row->char_cells, false);
457
458
0
  if (textord_debug_pitch_metric) {
459
0
    tprintf(
460
0
        "try_doc:props=%d:fixed=%d:pitch=%d:final_pitch=%g:pitch_sd=%g:sp_sd=%"
461
0
        "g:sd/trc=%g:sd/p=%g:sd/trc/p=%g\n",
462
0
        prop_blocks, fixed_blocks, pitch, final_pitch, pitch_sd, sp_sd, pitch_sd / total_row_count,
463
0
        pitch_sd / pitch, pitch_sd / total_row_count / pitch);
464
0
  }
465
466
#ifndef GRAPHICS_DISABLED
467
  if (textord_show_page_cuts && to_win != nullptr) {
468
    float row_shift;              // shift for row
469
    ICOORDELT_LIST *master_cells; // cells for page
470
    master_cells = &row->char_cells;
471
    for (block_it.mark_cycle_pt(); !block_it.cycled_list(); block_it.forward()) {
472
      block = block_it.data();
473
      row_it.set_to_list(block->get_rows());
474
      for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
475
        row = row_it.data();
476
        row_y = row->baseline.y(master_x);
477
        row_shift = shift_factor * (master_y - row_y);
478
        plot_row_cells(to_win, ScrollView::GOLDENROD, row, row_shift, master_cells);
479
      }
480
    }
481
  }
482
#endif
483
0
  row->char_cells.clear();
484
0
  return false;
485
0
}
486
487
/**********************************************************************
488
 * try_rows_fixed
489
 *
490
 * Decide whether each row is fixed pitch individually.
491
 **********************************************************************/
492
493
bool try_rows_fixed(     // find line stats
494
    TO_BLOCK *block,     // block to do
495
    int32_t block_index, // block number
496
    bool testing_on      // correct orientation
497
14.9k
) {
498
14.9k
  TO_ROW *row;           // current row
499
14.9k
  int32_t def_fixed = 0; // counters
500
14.9k
  int32_t def_prop = 0;
501
14.9k
  int32_t maybe_fixed = 0;
502
14.9k
  int32_t maybe_prop = 0;
503
14.9k
  int32_t dunno = 0;
504
14.9k
  int32_t corr_fixed = 0;
505
14.9k
  int32_t corr_prop = 0;
506
14.9k
  float lower, upper; // cluster thresholds
507
14.9k
  TO_ROW_IT row_it = block->get_rows();
508
509
184k
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
510
169k
    row = row_it.data();
511
169k
    ASSERT_HOST(row->xheight > 0);
512
169k
    if (row->fixed_pitch > 0 && fixed_pitch_row(row, block->block, block_index)) {
513
14.3k
      if (row->fixed_pitch == 0) {
514
0
        lower = row->pr_nonsp;
515
0
        upper = row->pr_space;
516
0
        row->space_size = upper;
517
0
        row->kern_size = lower;
518
0
      }
519
14.3k
    }
520
169k
  }
521
14.9k
  count_block_votes(block, def_fixed, def_prop, maybe_fixed, maybe_prop, corr_fixed, corr_prop,
522
14.9k
                    dunno);
523
14.9k
  if (testing_on &&
524
14.9k
      (textord_debug_pitch_test || textord_blocksall_prop || textord_blocksall_fixed)) {
525
0
    tprintf("Initially:");
526
0
    print_block_counts(block, block_index);
527
0
  }
528
14.9k
  if (def_fixed > def_prop * textord_words_veto_power) {
529
257
    block->pitch_decision = PITCH_DEF_FIXED;
530
14.6k
  } else if (def_prop > def_fixed * textord_words_veto_power) {
531
1.39k
    block->pitch_decision = PITCH_DEF_PROP;
532
13.2k
  } else if (def_fixed > 0 || def_prop > 0) {
533
203
    block->pitch_decision = PITCH_DUNNO;
534
13.0k
  } else if (maybe_fixed > maybe_prop * textord_words_veto_power) {
535
61
    block->pitch_decision = PITCH_MAYBE_FIXED;
536
13.0k
  } else if (maybe_prop > maybe_fixed * textord_words_veto_power) {
537
160
    block->pitch_decision = PITCH_MAYBE_PROP;
538
12.8k
  } else {
539
12.8k
    block->pitch_decision = PITCH_DUNNO;
540
12.8k
  }
541
14.9k
  return false;
542
14.9k
}
543
544
/**********************************************************************
545
 * print_block_counts
546
 *
547
 * Count up how many rows have what decision and print the results.
548
 **********************************************************************/
549
550
void print_block_counts( // find line stats
551
    TO_BLOCK *block,     // block to do
552
    int32_t block_index  // block number
553
0
) {
554
0
  int32_t def_fixed = 0; // counters
555
0
  int32_t def_prop = 0;
556
0
  int32_t maybe_fixed = 0;
557
0
  int32_t maybe_prop = 0;
558
0
  int32_t dunno = 0;
559
0
  int32_t corr_fixed = 0;
560
0
  int32_t corr_prop = 0;
561
562
0
  count_block_votes(block, def_fixed, def_prop, maybe_fixed, maybe_prop, corr_fixed, corr_prop,
563
0
                    dunno);
564
0
  tprintf("Block %d has (%d,%d,%d)", block_index, def_fixed, maybe_fixed, corr_fixed);
565
0
  if (textord_blocksall_prop && (def_fixed || maybe_fixed || corr_fixed)) {
566
0
    tprintf(" (Wrongly)");
567
0
  }
568
0
  tprintf(" fixed, (%d,%d,%d)", def_prop, maybe_prop, corr_prop);
569
0
  if (textord_blocksall_fixed && (def_prop || maybe_prop || corr_prop)) {
570
0
    tprintf(" (Wrongly)");
571
0
  }
572
0
  tprintf(" prop, %d dunno\n", dunno);
573
0
}
574
575
/**********************************************************************
576
 * count_block_votes
577
 *
578
 * Count the number of rows in the block with each kind of pitch_decision.
579
 **********************************************************************/
580
581
void count_block_votes( // find line stats
582
    TO_BLOCK *block,    // block to do
583
    int32_t &def_fixed, // add to counts
584
    int32_t &def_prop, int32_t &maybe_fixed, int32_t &maybe_prop, int32_t &corr_fixed,
585
14.9k
    int32_t &corr_prop, int32_t &dunno) {
586
14.9k
  TO_ROW *row; // current row
587
14.9k
  TO_ROW_IT row_it = block->get_rows();
588
589
184k
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
590
169k
    row = row_it.data();
591
169k
    switch (row->pitch_decision) {
592
155k
      case PITCH_DUNNO:
593
155k
        dunno++;
594
155k
        break;
595
7.73k
      case PITCH_DEF_PROP:
596
7.73k
        def_prop++;
597
7.73k
        break;
598
3.84k
      case PITCH_MAYBE_PROP:
599
3.84k
        maybe_prop++;
600
3.84k
        break;
601
1.69k
      case PITCH_DEF_FIXED:
602
1.69k
        def_fixed++;
603
1.69k
        break;
604
1.07k
      case PITCH_MAYBE_FIXED:
605
1.07k
        maybe_fixed++;
606
1.07k
        break;
607
0
      case PITCH_CORR_PROP:
608
0
        corr_prop++;
609
0
        break;
610
0
      case PITCH_CORR_FIXED:
611
0
        corr_fixed++;
612
0
        break;
613
169k
    }
614
169k
  }
615
14.9k
}
616
617
/**********************************************************************
618
 * row_pitch_stats
619
 *
620
 * Decide whether each row is fixed pitch individually.
621
 **********************************************************************/
622
623
bool row_pitch_stats( // find line stats
624
    TO_ROW *row,      // current row
625
    int32_t maxwidth, // of spaces
626
    bool testing_on   // correct orientation
627
169k
) {
628
169k
  BLOBNBOX *blob;        // current blob
629
169k
  int gap_index;         // current gap
630
169k
  int32_t prev_x;        // end of prev blob
631
169k
  int32_t cluster_count; // no of clusters
632
169k
  int32_t prev_count;    // of clusters
633
169k
  int32_t smooth_factor; // for smoothing stats
634
169k
  TBOX blob_box;         // bounding box
635
169k
  float lower, upper;    // cluster thresholds
636
                         // gap sizes
637
169k
  float gaps[BLOCK_STATS_CLUSTERS];
638
  // blobs
639
169k
  BLOBNBOX_IT blob_it = row->blob_list();
640
169k
  STATS gap_stats(0, maxwidth - 1);
641
169k
  STATS cluster_stats[BLOCK_STATS_CLUSTERS + 1];
642
  // clusters
643
644
169k
  smooth_factor = static_cast<int32_t>(row->xheight * textord_wordstats_smooth_factor + 1.5);
645
169k
  if (!blob_it.empty()) {
646
169k
    prev_x = blob_it.data()->bounding_box().right();
647
169k
    blob_it.forward();
648
2.60M
    while (!blob_it.at_first()) {
649
2.43M
      blob = blob_it.data();
650
2.43M
      if (!blob->joined_to_prev()) {
651
1.26M
        blob_box = blob->bounding_box();
652
1.26M
        if (blob_box.left() - prev_x < maxwidth) {
653
1.26M
          gap_stats.add(blob_box.left() - prev_x, 1);
654
1.26M
        }
655
1.26M
        prev_x = blob_box.right();
656
1.26M
      }
657
2.43M
      blob_it.forward();
658
2.43M
    }
659
169k
  }
660
169k
  if (gap_stats.get_total() == 0) {
661
47.5k
    return false;
662
47.5k
  }
663
122k
  cluster_count = 0;
664
122k
  lower = row->xheight * words_initial_lower;
665
122k
  upper = row->xheight * words_initial_upper;
666
122k
  gap_stats.smooth(smooth_factor);
667
244k
  do {
668
244k
    prev_count = cluster_count;
669
244k
    cluster_count = gap_stats.cluster(lower, upper, textord_spacesize_ratioprop,
670
244k
                                      BLOCK_STATS_CLUSTERS, cluster_stats);
671
244k
  } while (cluster_count > prev_count && cluster_count < BLOCK_STATS_CLUSTERS);
672
122k
  if (cluster_count < 1) {
673
0
    return false;
674
0
  }
675
288k
  for (gap_index = 0; gap_index < cluster_count; gap_index++) {
676
166k
    gaps[gap_index] = cluster_stats[gap_index + 1].ile(0.5);
677
166k
  }
678
  // get medians
679
122k
  if (testing_on) {
680
0
    tprintf("cluster_count=%d:", cluster_count);
681
0
    for (gap_index = 0; gap_index < cluster_count; gap_index++) {
682
0
      tprintf(" %g(%d)", gaps[gap_index], cluster_stats[gap_index + 1].get_total());
683
0
    }
684
0
    tprintf("\n");
685
0
  }
686
122k
  qsort(gaps, cluster_count, sizeof(float), sort_floats);
687
688
  // Try to find proportional non-space and space for row.
689
122k
  lower = row->xheight * words_default_prop_nonspace;
690
122k
  upper = row->xheight * textord_words_min_minspace;
691
227k
  for (gap_index = 0; gap_index < cluster_count && gaps[gap_index] < lower; gap_index++) {
692
104k
    ;
693
104k
  }
694
122k
  if (gap_index == 0) {
695
17.8k
    if (testing_on) {
696
0
      tprintf("No clusters below nonspace threshold!!\n");
697
0
    }
698
17.8k
    if (cluster_count > 1) {
699
4.74k
      row->pr_nonsp = gaps[0];
700
4.74k
      row->pr_space = gaps[1];
701
13.0k
    } else {
702
13.0k
      row->pr_nonsp = lower;
703
13.0k
      row->pr_space = gaps[0];
704
13.0k
    }
705
104k
  } else {
706
104k
    row->pr_nonsp = gaps[gap_index - 1];
707
105k
    while (gap_index < cluster_count && gaps[gap_index] < upper) {
708
1.07k
      gap_index++;
709
1.07k
    }
710
104k
    if (gap_index == cluster_count) {
711
74.6k
      if (testing_on) {
712
0
        tprintf("No clusters above nonspace threshold!!\n");
713
0
      }
714
74.6k
      row->pr_space = lower * textord_spacesize_ratioprop;
715
74.6k
    } else {
716
29.8k
      row->pr_space = gaps[gap_index];
717
29.8k
    }
718
104k
  }
719
720
  // Now try to find the fixed pitch space and non-space.
721
122k
  upper = row->xheight * words_default_fixed_space;
722
267k
  for (gap_index = 0; gap_index < cluster_count && gaps[gap_index] < upper; gap_index++) {
723
145k
    ;
724
145k
  }
725
122k
  if (gap_index == 0) {
726
3.26k
    if (testing_on) {
727
0
      tprintf("No clusters below space threshold!!\n");
728
0
    }
729
3.26k
    row->fp_nonsp = upper;
730
3.26k
    row->fp_space = gaps[0];
731
119k
  } else {
732
119k
    row->fp_nonsp = gaps[gap_index - 1];
733
119k
    if (gap_index == cluster_count) {
734
103k
      if (testing_on) {
735
0
        tprintf("No clusters above space threshold!!\n");
736
0
      }
737
103k
      row->fp_space = row->xheight;
738
103k
    } else {
739
15.5k
      row->fp_space = gaps[gap_index];
740
15.5k
    }
741
119k
  }
742
122k
  if (testing_on) {
743
0
    tprintf(
744
0
        "Initial estimates:pr_nonsp=%g, pr_space=%g, fp_nonsp=%g, "
745
0
        "fp_space=%g\n",
746
0
        row->pr_nonsp, row->pr_space, row->fp_nonsp, row->fp_space);
747
0
  }
748
122k
  return true; // computed some stats
749
122k
}
750
751
/**********************************************************************
752
 * find_row_pitch
753
 *
754
 * Check to see if this row could be fixed pitch using the given spacings.
755
 * Blobs with gaps smaller than the lower threshold are assumed to be one.
756
 * The larger threshold is the word gap threshold.
757
 **********************************************************************/
758
759
bool find_row_pitch(     // find lines
760
    TO_ROW *row,         // row to do
761
    int32_t maxwidth,    // max permitted space
762
    int32_t dm_gap,      // ignorable gaps
763
    TO_BLOCK *block,     // block of row
764
    int32_t block_index, // block_number
765
    int32_t row_index,   // number of row
766
    bool testing_on      // correct orientation
767
122k
) {
768
122k
  bool used_dm_model; // looks like dot matrix
769
122k
  float min_space;    // estimate threshold
770
122k
  float non_space;    // gap size
771
122k
  float gap_iqr;      // interquartile range
772
122k
  float pitch_iqr;
773
122k
  float dm_gap_iqr; // interquartile range
774
122k
  float dm_pitch_iqr;
775
122k
  float dm_pitch;      // pitch with dm on
776
122k
  float pitch;         // revised estimate
777
122k
  float initial_pitch; // guess at pitch
778
122k
  STATS gap_stats(0, maxwidth - 1);
779
  // centre-centre
780
122k
  STATS pitch_stats(0, maxwidth - 1);
781
782
122k
  row->fixed_pitch = 0.0f;
783
122k
  initial_pitch = row->fp_space;
784
122k
  if (initial_pitch > row->xheight * (1 + words_default_fixed_limit)) {
785
5.12k
    initial_pitch = row->xheight; // keep pitch decent
786
5.12k
  }
787
122k
  non_space = row->fp_nonsp;
788
122k
  if (non_space > initial_pitch) {
789
0
    non_space = initial_pitch;
790
0
  }
791
122k
  min_space = (initial_pitch + non_space) / 2;
792
793
122k
  if (!count_pitch_stats(row, &gap_stats, &pitch_stats, initial_pitch, min_space, true, false,
794
122k
                         dm_gap)) {
795
119k
    dm_gap_iqr = 0.0001f;
796
119k
    dm_pitch_iqr = maxwidth * 2.0f;
797
119k
    dm_pitch = initial_pitch;
798
119k
  } else {
799
3.11k
    dm_gap_iqr = gap_stats.ile(0.75) - gap_stats.ile(0.25);
800
3.11k
    dm_pitch_iqr = pitch_stats.ile(0.75) - pitch_stats.ile(0.25);
801
3.11k
    dm_pitch = pitch_stats.ile(0.5);
802
3.11k
  }
803
122k
  gap_stats.clear();
804
122k
  pitch_stats.clear();
805
122k
  if (!count_pitch_stats(row, &gap_stats, &pitch_stats, initial_pitch, min_space, true, false, 0)) {
806
109k
    gap_iqr = 0.0001f;
807
109k
    pitch_iqr = maxwidth * 3.0f;
808
109k
  } else {
809
12.5k
    gap_iqr = gap_stats.ile(0.75) - gap_stats.ile(0.25);
810
12.5k
    pitch_iqr = pitch_stats.ile(0.75) - pitch_stats.ile(0.25);
811
12.5k
    if (testing_on) {
812
0
      tprintf(
813
0
          "First fp iteration:initial_pitch=%g, gap_iqr=%g, pitch_iqr=%g, "
814
0
          "pitch=%g\n",
815
0
          initial_pitch, gap_iqr, pitch_iqr, pitch_stats.ile(0.5));
816
0
    }
817
12.5k
    initial_pitch = pitch_stats.ile(0.5);
818
12.5k
    if (min_space > initial_pitch && count_pitch_stats(row, &gap_stats, &pitch_stats, initial_pitch,
819
3.35k
                                                       initial_pitch, true, false, 0)) {
820
3.26k
      min_space = initial_pitch;
821
3.26k
      gap_iqr = gap_stats.ile(0.75) - gap_stats.ile(0.25);
822
3.26k
      pitch_iqr = pitch_stats.ile(0.75) - pitch_stats.ile(0.25);
823
3.26k
      if (testing_on) {
824
0
        tprintf(
825
0
            "Revised fp iteration:initial_pitch=%g, gap_iqr=%g, pitch_iqr=%g, "
826
0
            "pitch=%g\n",
827
0
            initial_pitch, gap_iqr, pitch_iqr, pitch_stats.ile(0.5));
828
0
      }
829
3.26k
      initial_pitch = pitch_stats.ile(0.5);
830
3.26k
    }
831
12.5k
  }
832
122k
  if (textord_debug_pitch_metric) {
833
0
    tprintf("Blk=%d:Row=%d:%c:p_iqr=%g:g_iqr=%g:dm_p_iqr=%g:dm_g_iqr=%g:%c:", block_index,
834
0
            row_index, 'X', pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr,
835
0
            pitch_iqr > maxwidth && dm_pitch_iqr > maxwidth
836
0
                ? 'D'
837
0
                : (pitch_iqr * dm_gap_iqr <= dm_pitch_iqr * gap_iqr ? 'S' : 'M'));
838
0
  }
839
122k
  if (pitch_iqr > maxwidth && dm_pitch_iqr > maxwidth) {
840
108k
    row->pitch_decision = PITCH_DUNNO;
841
108k
    if (textord_debug_pitch_metric) {
842
0
      tprintf("\n");
843
0
    }
844
108k
    return false; // insufficient data
845
108k
  }
846
14.3k
  if (pitch_iqr * dm_gap_iqr <= dm_pitch_iqr * gap_iqr) {
847
12.4k
    if (testing_on) {
848
0
      tprintf(
849
0
          "Choosing non dm version:pitch_iqr=%g, gap_iqr=%g, dm_pitch_iqr=%g, "
850
0
          "dm_gap_iqr=%g\n",
851
0
          pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr);
852
0
    }
853
12.4k
    gap_iqr = gap_stats.ile(0.75) - gap_stats.ile(0.25);
854
12.4k
    pitch_iqr = pitch_stats.ile(0.75) - pitch_stats.ile(0.25);
855
12.4k
    pitch = pitch_stats.ile(0.5);
856
12.4k
    used_dm_model = false;
857
12.4k
  } else {
858
1.87k
    if (testing_on) {
859
0
      tprintf(
860
0
          "Choosing dm version:pitch_iqr=%g, gap_iqr=%g, dm_pitch_iqr=%g, "
861
0
          "dm_gap_iqr=%g\n",
862
0
          pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr);
863
0
    }
864
1.87k
    gap_iqr = dm_gap_iqr;
865
1.87k
    pitch_iqr = dm_pitch_iqr;
866
1.87k
    pitch = dm_pitch;
867
1.87k
    used_dm_model = true;
868
1.87k
  }
869
14.3k
  if (textord_debug_pitch_metric) {
870
0
    tprintf("rev_p_iqr=%g:rev_g_iqr=%g:pitch=%g:", pitch_iqr, gap_iqr, pitch);
871
0
    tprintf("p_iqr/g=%g:p_iqr/x=%g:iqr_res=%c:", pitch_iqr / gap_iqr, pitch_iqr / block->xheight,
872
0
            pitch_iqr < gap_iqr * textord_fpiqr_ratio &&
873
0
                    pitch_iqr < block->xheight * textord_max_pitch_iqr &&
874
0
                    pitch < block->xheight * textord_words_default_maxspace
875
0
                ? 'F'
876
0
                : 'P');
877
0
  }
878
14.3k
  if (pitch_iqr < gap_iqr * textord_fpiqr_ratio &&
879
8.57k
      pitch_iqr < block->xheight * textord_max_pitch_iqr &&
880
7.21k
      pitch < block->xheight * textord_words_default_maxspace) {
881
7.21k
    row->pitch_decision = PITCH_MAYBE_FIXED;
882
7.21k
  } else {
883
7.13k
    row->pitch_decision = PITCH_MAYBE_PROP;
884
7.13k
  }
885
14.3k
  row->fixed_pitch = pitch;
886
14.3k
  row->kern_size = gap_stats.ile(0.5);
887
14.3k
  row->min_space = static_cast<int32_t>(row->fixed_pitch + non_space) / 2;
888
14.3k
  if (row->min_space > row->fixed_pitch) {
889
174
    row->min_space = static_cast<int32_t>(row->fixed_pitch);
890
174
  }
891
14.3k
  row->max_nonspace = row->min_space;
892
14.3k
  row->space_size = row->fixed_pitch;
893
14.3k
  row->space_threshold = (row->max_nonspace + row->min_space) / 2;
894
14.3k
  row->used_dm_model = used_dm_model;
895
14.3k
  return true;
896
122k
}
897
898
/**********************************************************************
899
 * fixed_pitch_row
900
 *
901
 * Check to see if this row could be fixed pitch using the given spacings.
902
 * Blobs with gaps smaller than the lower threshold are assumed to be one.
903
 * The larger threshold is the word gap threshold.
904
 **********************************************************************/
905
906
bool fixed_pitch_row(TO_ROW *row, // row to do
907
                     BLOCK *block,
908
                     int32_t block_index // block_number
909
14.3k
) {
910
14.3k
  const char *res_string; // pitch result
911
14.3k
  int16_t mid_cuts;       // no of cheap cuts
912
14.3k
  float non_space;        // gap size
913
14.3k
  float pitch_sd;         // error on pitch
914
14.3k
  float sp_sd = 0.0f;     // space sd
915
916
14.3k
  non_space = row->fp_nonsp;
917
14.3k
  if (non_space > row->fixed_pitch) {
918
301
    non_space = row->fixed_pitch;
919
301
  }
920
14.3k
  POLY_BLOCK *pb = block != nullptr ? block->pdblk.poly_block() : nullptr;
921
14.3k
  if (textord_all_prop || (pb != nullptr && !pb->IsText())) {
922
    // Set the decision to definitely proportional.
923
0
    pitch_sd = textord_words_def_prop * row->fixed_pitch;
924
0
    row->pitch_decision = PITCH_DEF_PROP;
925
14.3k
  } else {
926
14.3k
    pitch_sd = tune_row_pitch(row, &row->projection, row->projection_left, row->projection_right,
927
14.3k
                              (row->fixed_pitch + non_space * 3) / 4, row->fixed_pitch, sp_sd,
928
14.3k
                              mid_cuts, &row->char_cells, block_index == textord_debug_block);
929
14.3k
    if (pitch_sd < textord_words_pitchsd_threshold * row->fixed_pitch &&
930
2.76k
        ((pitsync_linear_version & 3) < 3 ||
931
0
         ((pitsync_linear_version & 3) >= 3 &&
932
2.76k
          (row->used_dm_model || sp_sd > 20 || (pitch_sd == 0 && sp_sd > 10))))) {
933
2.76k
      if (pitch_sd < textord_words_def_fixed * row->fixed_pitch && !row->all_caps &&
934
1.69k
          ((pitsync_linear_version & 3) < 3 || sp_sd > 20)) {
935
1.69k
        row->pitch_decision = PITCH_DEF_FIXED;
936
1.69k
      } else {
937
1.07k
        row->pitch_decision = PITCH_MAYBE_FIXED;
938
1.07k
      }
939
11.5k
    } else if ((pitsync_linear_version & 3) < 3 || sp_sd > 20 || mid_cuts > 0 ||
940
11.5k
               pitch_sd >= textord_words_pitchsd_threshold * row->fixed_pitch) {
941
11.5k
      if (pitch_sd < textord_words_def_prop * row->fixed_pitch) {
942
3.84k
        row->pitch_decision = PITCH_MAYBE_PROP;
943
7.73k
      } else {
944
7.73k
        row->pitch_decision = PITCH_DEF_PROP;
945
7.73k
      }
946
11.5k
    } else {
947
0
      row->pitch_decision = PITCH_DUNNO;
948
0
    }
949
14.3k
  }
950
951
14.3k
  if (textord_debug_pitch_metric) {
952
0
    res_string = "??";
953
0
    switch (row->pitch_decision) {
954
0
      case PITCH_DEF_PROP:
955
0
        res_string = "DP";
956
0
        break;
957
0
      case PITCH_MAYBE_PROP:
958
0
        res_string = "MP";
959
0
        break;
960
0
      case PITCH_DEF_FIXED:
961
0
        res_string = "DF";
962
0
        break;
963
0
      case PITCH_MAYBE_FIXED:
964
0
        res_string = "MF";
965
0
        break;
966
0
      default:
967
0
        res_string = "??";
968
0
    }
969
0
    tprintf(":sd/p=%g:occ=%g:init_res=%s\n", pitch_sd / row->fixed_pitch, sp_sd, res_string);
970
0
  }
971
14.3k
  return true;
972
14.3k
}
973
974
/**********************************************************************
975
 * count_pitch_stats
976
 *
977
 * Count up the gap and pitch stats on the block to see if it is fixed pitch.
978
 * Blobs with gaps smaller than the lower threshold are assumed to be one.
979
 * The larger threshold is the word gap threshold.
980
 * The return value indicates whether there were any decent values to use.
981
 **********************************************************************/
982
983
bool count_pitch_stats(  // find lines
984
    TO_ROW *row,         // row to do
985
    STATS *gap_stats,    // blob gaps
986
    STATS *pitch_stats,  // centre-centre stats
987
    float initial_pitch, // guess at pitch
988
    float min_space,     // estimate space size
989
    bool ignore_outsize, // discard big objects
990
    bool split_outsize,  // split big objects
991
    int32_t dm_gap       // ignorable gaps
992
248k
) {
993
248k
  bool prev_valid; // not word broken
994
248k
  BLOBNBOX *blob;  // current blob
995
                   // blobs
996
248k
  BLOBNBOX_IT blob_it = row->blob_list();
997
248k
  int32_t prev_right;  // end of prev blob
998
248k
  int32_t prev_centre; // centre of previous blob
999
248k
  int32_t x_centre;    // centre of this blob
1000
248k
  int32_t blob_width;  // width of blob
1001
248k
  int32_t width_units; // no of widths in blob
1002
248k
  float width;         // blob width
1003
248k
  TBOX blob_box;       // bounding box
1004
248k
  TBOX joined_box;     // of super blob
1005
1006
248k
  gap_stats->clear();
1007
248k
  pitch_stats->clear();
1008
248k
  if (blob_it.empty()) {
1009
0
    return false;
1010
0
  }
1011
248k
  prev_valid = false;
1012
248k
  prev_centre = 0;
1013
248k
  prev_right = 0; // stop compiler warning
1014
248k
  joined_box = blob_it.data()->bounding_box();
1015
5.06M
  do {
1016
5.06M
    blob_it.forward();
1017
5.06M
    blob = blob_it.data();
1018
5.06M
    if (!blob->joined_to_prev()) {
1019
2.81M
      blob_box = blob->bounding_box();
1020
2.81M
      if ((blob_box.left() - joined_box.right() < dm_gap && !blob_it.at_first()) ||
1021
1.47M
          blob->cblob() == nullptr) {
1022
1.37M
        joined_box += blob_box; // merge blobs
1023
1.44M
      } else {
1024
1.44M
        blob_width = joined_box.width();
1025
1.44M
        if (split_outsize) {
1026
0
          width_units =
1027
0
              static_cast<int32_t>(floor(static_cast<float>(blob_width) / initial_pitch + 0.5));
1028
0
          if (width_units < 1) {
1029
0
            width_units = 1;
1030
0
          }
1031
0
          width_units--;
1032
1.44M
        } else if (ignore_outsize) {
1033
1.44M
          width = static_cast<float>(blob_width) / initial_pitch;
1034
1.44M
          width_units =
1035
1.44M
              width < 1 + words_default_fixed_limit && width > 1 - words_default_fixed_limit ? 0
1036
1.44M
                                                                                             : -1;
1037
1.44M
        } else {
1038
0
          width_units = 0; // everything in
1039
0
        }
1040
1.44M
        x_centre = static_cast<int32_t>(joined_box.left() +
1041
1.44M
                                        (blob_width - width_units * initial_pitch) / 2);
1042
1.44M
        if (prev_valid && width_units >= 0) {
1043
          //                                              if (width_units>0)
1044
          //                                              {
1045
          //                                                      tprintf("wu=%d,
1046
          //                                                      width=%d,
1047
          //                                                      xc=%d, adding
1048
          //                                                      %d\n",
1049
          //                                                              width_units,blob_width,x_centre,x_centre-prev_centre);
1050
          //                                              }
1051
135k
          gap_stats->add(joined_box.left() - prev_right, 1);
1052
135k
          pitch_stats->add(x_centre - prev_centre, 1);
1053
135k
        }
1054
1.44M
        prev_centre = static_cast<int32_t>(x_centre + width_units * initial_pitch);
1055
1.44M
        prev_right = joined_box.right();
1056
1.44M
        prev_valid = blob_box.left() - joined_box.right() < min_space;
1057
1.44M
        prev_valid = prev_valid && width_units >= 0;
1058
1.44M
        joined_box = blob_box;
1059
1.44M
      }
1060
2.81M
    }
1061
5.06M
  } while (!blob_it.at_first());
1062
248k
  return gap_stats->get_total() >= 3;
1063
248k
}
1064
1065
/**********************************************************************
1066
 * tune_row_pitch
1067
 *
1068
 * Use a dp algorithm to fit the character cells and return the sd of
1069
 * the cell size over the row.
1070
 **********************************************************************/
1071
1072
float tune_row_pitch(           // find fp cells
1073
    TO_ROW *row,                // row to do
1074
    STATS *projection,          // vertical projection
1075
    int16_t projection_left,    // edge of projection
1076
    int16_t projection_right,   // edge of projection
1077
    float space_size,           // size of blank
1078
    float &initial_pitch,       // guess at pitch
1079
    float &best_sp_sd,          // space sd
1080
    int16_t &best_mid_cuts,     // no of cheap cuts
1081
    ICOORDELT_LIST *best_cells, // row cells
1082
    bool testing_on             // individual words
1083
16.9k
) {
1084
16.9k
  int pitch_delta;           // offset pitch
1085
16.9k
  int16_t mid_cuts;          // cheap cuts
1086
16.9k
  float pitch_sd;            // current sd
1087
16.9k
  float best_sd;             // best result
1088
16.9k
  float best_pitch;          // pitch for best result
1089
16.9k
  float initial_sd;          // starting error
1090
16.9k
  float sp_sd;               // space sd
1091
16.9k
  ICOORDELT_LIST test_cells; // row cells
1092
16.9k
  ICOORDELT_IT best_it;      // start of best list
1093
1094
16.9k
  if (textord_fast_pitch_test) {
1095
0
    return tune_row_pitch2(row, projection, projection_left, projection_right, space_size,
1096
0
                           initial_pitch, best_sp_sd,
1097
                           // space sd
1098
0
                           best_mid_cuts, best_cells, testing_on);
1099
0
  }
1100
16.9k
  if (textord_disable_pitch_test) {
1101
0
    best_sp_sd = initial_pitch;
1102
0
    return initial_pitch;
1103
0
  }
1104
16.9k
  initial_sd = compute_pitch_sd(row, projection, projection_left, projection_right, space_size,
1105
16.9k
                                initial_pitch, best_sp_sd, best_mid_cuts, best_cells, testing_on);
1106
16.9k
  best_sd = initial_sd;
1107
16.9k
  best_pitch = initial_pitch;
1108
16.9k
  if (testing_on) {
1109
0
    tprintf("tune_row_pitch:start pitch=%g, sd=%g\n", best_pitch, best_sd);
1110
0
  }
1111
27.5k
  for (pitch_delta = 1; pitch_delta <= textord_pitch_range; pitch_delta++) {
1112
23.4k
    pitch_sd =
1113
23.4k
        compute_pitch_sd(row, projection, projection_left, projection_right, space_size,
1114
23.4k
                         initial_pitch + pitch_delta, sp_sd, mid_cuts, &test_cells, testing_on);
1115
23.4k
    if (testing_on) {
1116
0
      tprintf("testing pitch at %g, sd=%g\n", initial_pitch + pitch_delta, pitch_sd);
1117
0
    }
1118
23.4k
    if (pitch_sd < best_sd) {
1119
8.11k
      best_sd = pitch_sd;
1120
8.11k
      best_mid_cuts = mid_cuts;
1121
8.11k
      best_sp_sd = sp_sd;
1122
8.11k
      best_pitch = initial_pitch + pitch_delta;
1123
8.11k
      best_cells->clear();
1124
8.11k
      best_it.set_to_list(best_cells);
1125
8.11k
      best_it.add_list_after(&test_cells);
1126
15.3k
    } else {
1127
15.3k
      test_cells.clear();
1128
15.3k
    }
1129
23.4k
    if (pitch_sd > initial_sd) {
1130
12.8k
      break; // getting worse
1131
12.8k
    }
1132
23.4k
  }
1133
28.3k
  for (pitch_delta = 1; pitch_delta <= textord_pitch_range; pitch_delta++) {
1134
23.7k
    pitch_sd =
1135
23.7k
        compute_pitch_sd(row, projection, projection_left, projection_right, space_size,
1136
23.7k
                         initial_pitch - pitch_delta, sp_sd, mid_cuts, &test_cells, testing_on);
1137
23.7k
    if (testing_on) {
1138
0
      tprintf("testing pitch at %g, sd=%g\n", initial_pitch - pitch_delta, pitch_sd);
1139
0
    }
1140
23.7k
    if (pitch_sd < best_sd) {
1141
5.55k
      best_sd = pitch_sd;
1142
5.55k
      best_mid_cuts = mid_cuts;
1143
5.55k
      best_sp_sd = sp_sd;
1144
5.55k
      best_pitch = initial_pitch - pitch_delta;
1145
5.55k
      best_cells->clear();
1146
5.55k
      best_it.set_to_list(best_cells);
1147
5.55k
      best_it.add_list_after(&test_cells);
1148
18.2k
    } else {
1149
18.2k
      test_cells.clear();
1150
18.2k
    }
1151
23.7k
    if (pitch_sd > initial_sd) {
1152
12.4k
      break;
1153
12.4k
    }
1154
23.7k
  }
1155
16.9k
  initial_pitch = best_pitch;
1156
1157
16.9k
  if (textord_debug_pitch_metric) {
1158
0
    print_pitch_sd(row, projection, projection_left, projection_right, space_size, best_pitch);
1159
0
  }
1160
1161
16.9k
  return best_sd;
1162
16.9k
}
1163
1164
/**********************************************************************
1165
 * tune_row_pitch
1166
 *
1167
 * Use a dp algorithm to fit the character cells and return the sd of
1168
 * the cell size over the row.
1169
 **********************************************************************/
1170
1171
float tune_row_pitch2(          // find fp cells
1172
    TO_ROW *row,                // row to do
1173
    STATS *projection,          // vertical projection
1174
    int16_t projection_left,    // edge of projection
1175
    int16_t projection_right,   // edge of projection
1176
    float space_size,           // size of blank
1177
    float &initial_pitch,       // guess at pitch
1178
    float &best_sp_sd,          // space sd
1179
    int16_t &best_mid_cuts,     // no of cheap cuts
1180
    ICOORDELT_LIST *best_cells, // row cells
1181
    bool testing_on             // individual words
1182
0
) {
1183
0
  int pitch_delta;    // offset pitch
1184
0
  int16_t pixel;      // pixel coord
1185
0
  int16_t best_pixel; // pixel coord
1186
0
  int16_t best_delta; // best pitch
1187
0
  int16_t best_pitch; // best pitch
1188
0
  int16_t start;      // of good range
1189
0
  int16_t end;        // of good range
1190
0
  int32_t best_count; // lowest sum
1191
0
  float best_sd;      // best result
1192
1193
0
  best_sp_sd = initial_pitch;
1194
1195
0
  best_pitch = static_cast<int>(initial_pitch);
1196
0
  if (textord_disable_pitch_test || best_pitch <= textord_pitch_range) {
1197
0
    return initial_pitch;
1198
0
  }
1199
0
  std::unique_ptr<STATS[]> sum_proj(new STATS[textord_pitch_range * 2 + 1]); // summed projection
1200
1201
0
  for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; pitch_delta++) {
1202
0
    sum_proj[textord_pitch_range + pitch_delta].set_range(0, best_pitch + pitch_delta);
1203
0
  }
1204
0
  for (pixel = projection_left; pixel <= projection_right; pixel++) {
1205
0
    for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; pitch_delta++) {
1206
0
      sum_proj[textord_pitch_range + pitch_delta].add(
1207
0
          (pixel - projection_left) % (best_pitch + pitch_delta), projection->pile_count(pixel));
1208
0
    }
1209
0
  }
1210
0
  best_count = sum_proj[textord_pitch_range].pile_count(0);
1211
0
  best_delta = 0;
1212
0
  best_pixel = 0;
1213
0
  for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; pitch_delta++) {
1214
0
    for (pixel = 0; pixel < best_pitch + pitch_delta; pixel++) {
1215
0
      if (sum_proj[textord_pitch_range + pitch_delta].pile_count(pixel) < best_count) {
1216
0
        best_count = sum_proj[textord_pitch_range + pitch_delta].pile_count(pixel);
1217
0
        best_delta = pitch_delta;
1218
0
        best_pixel = pixel;
1219
0
      }
1220
0
    }
1221
0
  }
1222
0
  if (testing_on) {
1223
0
    tprintf("tune_row_pitch:start pitch=%g, best_delta=%d, count=%d\n", initial_pitch, best_delta,
1224
0
            best_count);
1225
0
  }
1226
0
  best_pitch += best_delta;
1227
0
  initial_pitch = best_pitch;
1228
0
  best_count++;
1229
0
  best_count += best_count;
1230
0
  for (start = best_pixel - 2;
1231
0
       start > best_pixel - best_pitch &&
1232
0
       sum_proj[textord_pitch_range + best_delta].pile_count(start % best_pitch) <= best_count;
1233
0
       start--) {
1234
0
    ;
1235
0
  }
1236
0
  for (end = best_pixel + 2;
1237
0
       end < best_pixel + best_pitch &&
1238
0
       sum_proj[textord_pitch_range + best_delta].pile_count(end % best_pitch) <= best_count;
1239
0
       end++) {
1240
0
    ;
1241
0
  }
1242
1243
0
  best_sd = compute_pitch_sd(row, projection, projection_left, projection_right, space_size,
1244
0
                             initial_pitch, best_sp_sd, best_mid_cuts, best_cells, testing_on,
1245
0
                             start, end);
1246
0
  if (testing_on) {
1247
0
    tprintf("tune_row_pitch:output pitch=%g, sd=%g\n", initial_pitch, best_sd);
1248
0
  }
1249
1250
0
  if (textord_debug_pitch_metric) {
1251
0
    print_pitch_sd(row, projection, projection_left, projection_right, space_size, initial_pitch);
1252
0
  }
1253
1254
0
  return best_sd;
1255
0
}
1256
1257
/**********************************************************************
1258
 * compute_pitch_sd
1259
 *
1260
 * Use a dp algorithm to fit the character cells and return the sd of
1261
 * the cell size over the row.
1262
 **********************************************************************/
1263
1264
float compute_pitch_sd(        // find fp cells
1265
    TO_ROW *row,               // row to do
1266
    STATS *projection,         // vertical projection
1267
    int16_t projection_left,   // edge
1268
    int16_t projection_right,  // edge
1269
    float space_size,          // size of blank
1270
    float initial_pitch,       // guess at pitch
1271
    float &sp_sd,              // space sd
1272
    int16_t &mid_cuts,         // no of free cuts
1273
    ICOORDELT_LIST *row_cells, // list of chop pts
1274
    bool testing_on,           // individual words
1275
    int16_t start,             // start of good range
1276
    int16_t end                // end of good range
1277
64.1k
) {
1278
64.1k
  int16_t occupation; // no of cells in word.
1279
                      // blobs
1280
64.1k
  BLOBNBOX_IT blob_it = row->blob_list();
1281
64.1k
  BLOBNBOX_IT start_it;  // start of word
1282
64.1k
  BLOBNBOX_IT plot_it;   // for plotting
1283
64.1k
  int16_t blob_count;    // no of blobs
1284
64.1k
  TBOX blob_box;         // bounding box
1285
64.1k
  TBOX prev_box;         // of super blob
1286
64.1k
  int32_t prev_right;    // of word sync
1287
64.1k
  int scale_factor;      // on scores for big words
1288
64.1k
  int32_t sp_count;      // spaces
1289
64.1k
  FPSEGPT_LIST seg_list; // char cells
1290
64.1k
  FPSEGPT_IT seg_it;     // iterator
1291
64.1k
  int16_t segpos;        // position of segment
1292
64.1k
  int16_t cellpos;       // previous cell boundary
1293
                         // iterator
1294
64.1k
  ICOORDELT_IT cell_it = row_cells;
1295
64.1k
  ICOORDELT *cell;     // new cell
1296
64.1k
  double sqsum;        // sum of squares
1297
64.1k
  double spsum;        // of spaces
1298
64.1k
  double sp_var;       // space error
1299
64.1k
  double word_sync;    // result for word
1300
64.1k
  int32_t total_count; // total blobs
1301
1302
64.1k
  if ((pitsync_linear_version & 3) > 1) {
1303
64.1k
    word_sync = compute_pitch_sd2(row, projection, projection_left, projection_right, initial_pitch,
1304
64.1k
                                  occupation, mid_cuts, row_cells, testing_on, start, end);
1305
64.1k
    sp_sd = occupation;
1306
64.1k
    return word_sync;
1307
64.1k
  }
1308
0
  mid_cuts = 0;
1309
0
  cellpos = 0;
1310
0
  total_count = 0;
1311
0
  sqsum = 0;
1312
0
  sp_count = 0;
1313
0
  spsum = 0;
1314
0
  prev_right = -1;
1315
0
  if (blob_it.empty()) {
1316
0
    return space_size * 10;
1317
0
  }
1318
#ifndef GRAPHICS_DISABLED
1319
  if (testing_on && to_win != nullptr) {
1320
    blob_box = blob_it.data()->bounding_box();
1321
    projection->plot(to_win, projection_left, row->intercept(), 1.0f, -1.0f, ScrollView::CORAL);
1322
  }
1323
#endif
1324
0
  start_it = blob_it;
1325
0
  blob_count = 0;
1326
0
  blob_box = box_next(&blob_it); // first blob
1327
0
  blob_it.mark_cycle_pt();
1328
0
  do {
1329
0
    for (; blob_count > 0; blob_count--) {
1330
0
      box_next(&start_it);
1331
0
    }
1332
0
    do {
1333
0
      prev_box = blob_box;
1334
0
      blob_count++;
1335
0
      blob_box = box_next(&blob_it);
1336
0
    } while (!blob_it.cycled_list() && blob_box.left() - prev_box.right() < space_size);
1337
0
    plot_it = start_it;
1338
0
    if (pitsync_linear_version & 3) {
1339
0
      word_sync = check_pitch_sync2(&start_it, blob_count, static_cast<int16_t>(initial_pitch), 2,
1340
0
                                    projection, projection_left, projection_right,
1341
0
                                    row->xheight * textord_projection_scale, occupation, &seg_list,
1342
0
                                    start, end);
1343
0
    } else {
1344
0
      word_sync = check_pitch_sync(&start_it, blob_count, static_cast<int16_t>(initial_pitch), 2,
1345
0
                                   projection, &seg_list);
1346
0
    }
1347
0
    if (testing_on) {
1348
0
      tprintf("Word ending at (%d,%d), len=%d, sync rating=%g, ", prev_box.right(), prev_box.top(),
1349
0
              seg_list.length() - 1, word_sync);
1350
0
      seg_it.set_to_list(&seg_list);
1351
0
      for (seg_it.mark_cycle_pt(); !seg_it.cycled_list(); seg_it.forward()) {
1352
0
        if (seg_it.data()->faked) {
1353
0
          tprintf("(F)");
1354
0
        }
1355
0
        tprintf("%d, ", seg_it.data()->position());
1356
        //                              tprintf("C=%g, s=%g, sq=%g\n",
1357
        //                                      seg_it.data()->cost_function(),
1358
        //                                      seg_it.data()->sum(),
1359
        //                                      seg_it.data()->squares());
1360
0
      }
1361
0
      tprintf("\n");
1362
0
    }
1363
#ifndef GRAPHICS_DISABLED
1364
    if (textord_show_fixed_cuts && blob_count > 0 && to_win != nullptr) {
1365
      plot_fp_cells2(to_win, ScrollView::GOLDENROD, row, &seg_list);
1366
    }
1367
#endif
1368
0
    seg_it.set_to_list(&seg_list);
1369
0
    if (prev_right >= 0) {
1370
0
      sp_var = seg_it.data()->position() - prev_right;
1371
0
      sp_var -= floor(sp_var / initial_pitch + 0.5) * initial_pitch;
1372
0
      sp_var *= sp_var;
1373
0
      spsum += sp_var;
1374
0
      sp_count++;
1375
0
    }
1376
0
    for (seg_it.mark_cycle_pt(); !seg_it.cycled_list(); seg_it.forward()) {
1377
0
      segpos = seg_it.data()->position();
1378
0
      if (cell_it.empty() || segpos > cellpos + initial_pitch / 2) {
1379
        // big gap
1380
0
        while (!cell_it.empty() && segpos > cellpos + initial_pitch * 3 / 2) {
1381
0
          cell = new ICOORDELT(cellpos + static_cast<int16_t>(initial_pitch), 0);
1382
0
          cell_it.add_after_then_move(cell);
1383
0
          cellpos += static_cast<int16_t>(initial_pitch);
1384
0
        }
1385
        // make new one
1386
0
        cell = new ICOORDELT(segpos, 0);
1387
0
        cell_it.add_after_then_move(cell);
1388
0
        cellpos = segpos;
1389
0
      } else if (segpos > cellpos - initial_pitch / 2) {
1390
0
        cell = cell_it.data();
1391
        // average positions
1392
0
        cell->set_x((cellpos + segpos) / 2);
1393
0
        cellpos = cell->x();
1394
0
      }
1395
0
    }
1396
0
    seg_it.move_to_last();
1397
0
    prev_right = seg_it.data()->position();
1398
0
    if (textord_pitch_scalebigwords) {
1399
0
      scale_factor = (seg_list.length() - 2) / 2;
1400
0
      if (scale_factor < 1) {
1401
0
        scale_factor = 1;
1402
0
      }
1403
0
    } else {
1404
0
      scale_factor = 1;
1405
0
    }
1406
0
    sqsum += word_sync * scale_factor;
1407
0
    total_count += (seg_list.length() - 1) * scale_factor;
1408
0
    seg_list.clear();
1409
0
  } while (!blob_it.cycled_list());
1410
0
  sp_sd = sp_count > 0 ? sqrt(spsum / sp_count) : 0;
1411
0
  return total_count > 0 ? sqrt(sqsum / total_count) : space_size * 10;
1412
0
}
1413
1414
/**********************************************************************
1415
 * compute_pitch_sd2
1416
 *
1417
 * Use a dp algorithm to fit the character cells and return the sd of
1418
 * the cell size over the row.
1419
 **********************************************************************/
1420
1421
float compute_pitch_sd2(       // find fp cells
1422
    TO_ROW *row,               // row to do
1423
    STATS *projection,         // vertical projection
1424
    int16_t projection_left,   // edge
1425
    int16_t projection_right,  // edge
1426
    float initial_pitch,       // guess at pitch
1427
    int16_t &occupation,       // no of occupied cells
1428
    int16_t &mid_cuts,         // no of free cuts
1429
    ICOORDELT_LIST *row_cells, // list of chop pts
1430
    bool testing_on,           // individual words
1431
    int16_t start,             // start of good range
1432
    int16_t end                // end of good range
1433
64.1k
) {
1434
  // blobs
1435
64.1k
  BLOBNBOX_IT blob_it = row->blob_list();
1436
64.1k
  BLOBNBOX_IT plot_it;
1437
64.1k
  int16_t blob_count;    // no of blobs
1438
64.1k
  TBOX blob_box;         // bounding box
1439
64.1k
  FPSEGPT_LIST seg_list; // char cells
1440
64.1k
  FPSEGPT_IT seg_it;     // iterator
1441
64.1k
  int16_t segpos;        // position of segment
1442
                         // iterator
1443
64.1k
  ICOORDELT_IT cell_it = row_cells;
1444
64.1k
  ICOORDELT *cell;  // new cell
1445
64.1k
  double word_sync; // result for word
1446
1447
64.1k
  mid_cuts = 0;
1448
64.1k
  if (blob_it.empty()) {
1449
0
    occupation = 0;
1450
0
    return initial_pitch * 10;
1451
0
  }
1452
#ifndef GRAPHICS_DISABLED
1453
  if (testing_on && to_win != nullptr) {
1454
    projection->plot(to_win, projection_left, row->intercept(), 1.0f, -1.0f, ScrollView::CORAL);
1455
  }
1456
#endif
1457
64.1k
  blob_count = 0;
1458
64.1k
  blob_it.mark_cycle_pt();
1459
929k
  do {
1460
    // first blob
1461
929k
    blob_box = box_next(&blob_it);
1462
929k
    blob_count++;
1463
929k
  } while (!blob_it.cycled_list());
1464
64.1k
  plot_it = blob_it;
1465
64.1k
  word_sync = check_pitch_sync2(
1466
64.1k
      &blob_it, blob_count, static_cast<int16_t>(initial_pitch), 2, projection, projection_left,
1467
64.1k
      projection_right, row->xheight * textord_projection_scale, occupation, &seg_list, start, end);
1468
64.1k
  if (testing_on) {
1469
0
    tprintf("Row ending at (%d,%d), len=%d, sync rating=%g, ", blob_box.right(), blob_box.top(),
1470
0
            seg_list.length() - 1, word_sync);
1471
0
    seg_it.set_to_list(&seg_list);
1472
0
    for (seg_it.mark_cycle_pt(); !seg_it.cycled_list(); seg_it.forward()) {
1473
0
      if (seg_it.data()->faked) {
1474
0
        tprintf("(F)");
1475
0
      }
1476
0
      tprintf("%d, ", seg_it.data()->position());
1477
      //                              tprintf("C=%g, s=%g, sq=%g\n",
1478
      //                                      seg_it.data()->cost_function(),
1479
      //                                      seg_it.data()->sum(),
1480
      //                                      seg_it.data()->squares());
1481
0
    }
1482
0
    tprintf("\n");
1483
0
  }
1484
#ifndef GRAPHICS_DISABLED
1485
  if (textord_show_fixed_cuts && blob_count > 0 && to_win != nullptr) {
1486
    plot_fp_cells2(to_win, ScrollView::GOLDENROD, row, &seg_list);
1487
  }
1488
#endif
1489
64.1k
  seg_it.set_to_list(&seg_list);
1490
883k
  for (seg_it.mark_cycle_pt(); !seg_it.cycled_list(); seg_it.forward()) {
1491
819k
    segpos = seg_it.data()->position();
1492
    // make new one
1493
819k
    cell = new ICOORDELT(segpos, 0);
1494
819k
    cell_it.add_after_then_move(cell);
1495
819k
    if (seg_it.at_last()) {
1496
64.1k
      mid_cuts = seg_it.data()->cheap_cuts();
1497
64.1k
    }
1498
819k
  }
1499
64.1k
  seg_list.clear();
1500
64.1k
  return occupation > 0 ? sqrt(word_sync / occupation) : initial_pitch * 10;
1501
64.1k
}
1502
1503
/**********************************************************************
1504
 * print_pitch_sd
1505
 *
1506
 * Use a dp algorithm to fit the character cells and return the sd of
1507
 * the cell size over the row.
1508
 **********************************************************************/
1509
1510
void print_pitch_sd(         // find fp cells
1511
    TO_ROW *row,             // row to do
1512
    STATS *projection,       // vertical projection
1513
    int16_t projection_left, // edges //size of blank
1514
    int16_t projection_right, float space_size,
1515
    float initial_pitch // guess at pitch
1516
0
) {
1517
0
  const char *res2;   // pitch result
1518
0
  int16_t occupation; // used cells
1519
0
  float sp_sd;        // space sd
1520
                      // blobs
1521
0
  BLOBNBOX_IT blob_it = row->blob_list();
1522
0
  BLOBNBOX_IT start_it;     // start of word
1523
0
  BLOBNBOX_IT row_start;    // start of row
1524
0
  int16_t blob_count;       // no of blobs
1525
0
  int16_t total_blob_count; // total blobs in line
1526
0
  TBOX blob_box;            // bounding box
1527
0
  TBOX prev_box;            // of super blob
1528
0
  int32_t prev_right;       // of word sync
1529
0
  int scale_factor;         // on scores for big words
1530
0
  int32_t sp_count;         // spaces
1531
0
  FPSEGPT_LIST seg_list;    // char cells
1532
0
  FPSEGPT_IT seg_it;        // iterator
1533
0
  double sqsum;             // sum of squares
1534
0
  double spsum;             // of spaces
1535
0
  double sp_var;            // space error
1536
0
  double word_sync;         // result for word
1537
0
  double total_count;       // total cuts
1538
1539
0
  if (blob_it.empty()) {
1540
0
    return;
1541
0
  }
1542
0
  row_start = blob_it;
1543
0
  total_blob_count = 0;
1544
1545
0
  total_count = 0;
1546
0
  sqsum = 0;
1547
0
  sp_count = 0;
1548
0
  spsum = 0;
1549
0
  prev_right = -1;
1550
0
  blob_it = row_start;
1551
0
  start_it = blob_it;
1552
0
  blob_count = 0;
1553
0
  blob_box = box_next(&blob_it); // first blob
1554
0
  blob_it.mark_cycle_pt();
1555
0
  do {
1556
0
    for (; blob_count > 0; blob_count--) {
1557
0
      box_next(&start_it);
1558
0
    }
1559
0
    do {
1560
0
      prev_box = blob_box;
1561
0
      blob_count++;
1562
0
      blob_box = box_next(&blob_it);
1563
0
    } while (!blob_it.cycled_list() && blob_box.left() - prev_box.right() < space_size);
1564
0
    word_sync = check_pitch_sync2(
1565
0
        &start_it, blob_count, static_cast<int16_t>(initial_pitch), 2, projection, projection_left,
1566
0
        projection_right, row->xheight * textord_projection_scale, occupation, &seg_list, 0, 0);
1567
0
    total_blob_count += blob_count;
1568
0
    seg_it.set_to_list(&seg_list);
1569
0
    if (prev_right >= 0) {
1570
0
      sp_var = seg_it.data()->position() - prev_right;
1571
0
      sp_var -= floor(sp_var / initial_pitch + 0.5) * initial_pitch;
1572
0
      sp_var *= sp_var;
1573
0
      spsum += sp_var;
1574
0
      sp_count++;
1575
0
    }
1576
0
    seg_it.move_to_last();
1577
0
    prev_right = seg_it.data()->position();
1578
0
    if (textord_pitch_scalebigwords) {
1579
0
      scale_factor = (seg_list.length() - 2) / 2;
1580
0
      if (scale_factor < 1) {
1581
0
        scale_factor = 1;
1582
0
      }
1583
0
    } else {
1584
0
      scale_factor = 1;
1585
0
    }
1586
0
    sqsum += word_sync * scale_factor;
1587
0
    total_count += (seg_list.length() - 1) * scale_factor;
1588
0
    seg_list.clear();
1589
0
  } while (!blob_it.cycled_list());
1590
0
  sp_sd = sp_count > 0 ? sqrt(spsum / sp_count) : 0;
1591
0
  word_sync = total_count > 0 ? sqrt(sqsum / total_count) : space_size * 10;
1592
0
  tprintf("new_sd=%g:sd/p=%g:new_sp_sd=%g:res=%c:", word_sync, word_sync / initial_pitch, sp_sd,
1593
0
          word_sync < textord_words_pitchsd_threshold * initial_pitch ? 'F' : 'P');
1594
1595
0
  start_it = row_start;
1596
0
  blob_it = row_start;
1597
0
  word_sync =
1598
0
      check_pitch_sync2(&blob_it, total_blob_count, static_cast<int16_t>(initial_pitch), 2,
1599
0
                        projection, projection_left, projection_right,
1600
0
                        row->xheight * textord_projection_scale, occupation, &seg_list, 0, 0);
1601
0
  if (occupation > 1) {
1602
0
    word_sync /= occupation;
1603
0
  }
1604
0
  word_sync = sqrt(word_sync);
1605
1606
#ifndef GRAPHICS_DISABLED
1607
  if (textord_show_row_cuts && to_win != nullptr) {
1608
    plot_fp_cells2(to_win, ScrollView::CORAL, row, &seg_list);
1609
  }
1610
#endif
1611
0
  seg_list.clear();
1612
0
  if (word_sync < textord_words_pitchsd_threshold * initial_pitch) {
1613
0
    if (word_sync < textord_words_def_fixed * initial_pitch && !row->all_caps) {
1614
0
      res2 = "DF";
1615
0
    } else {
1616
0
      res2 = "MF";
1617
0
    }
1618
0
  } else {
1619
0
    res2 = word_sync < textord_words_def_prop * initial_pitch ? "MP" : "DP";
1620
0
  }
1621
0
  tprintf(
1622
0
      "row_sd=%g:sd/p=%g:res=%c:N=%d:res2=%s,init pitch=%g, row_pitch=%g, "
1623
0
      "all_caps=%d\n",
1624
0
      word_sync, word_sync / initial_pitch,
1625
0
      word_sync < textord_words_pitchsd_threshold * initial_pitch ? 'F' : 'P', occupation, res2,
1626
0
      initial_pitch, row->fixed_pitch, row->all_caps);
1627
0
}
1628
1629
/**********************************************************************
1630
 * find_repeated_chars
1631
 *
1632
 * Extract marked leader blobs and put them
1633
 * into words in advance of fixed pitch checking and word generation.
1634
 **********************************************************************/
1635
14.5k
void find_repeated_chars(TO_BLOCK *block) { // Block to search.
1636
14.5k
  POLY_BLOCK *pb = block->block->pdblk.poly_block();
1637
14.5k
  if (pb != nullptr && !pb->IsText()) {
1638
0
    return; // Don't find repeated chars in non-text blocks.
1639
0
  }
1640
1641
14.5k
  TO_ROW *row;
1642
14.5k
  BLOBNBOX_IT box_it;
1643
14.5k
  BLOBNBOX_IT search_it; // forward search
1644
14.5k
  WERD *word;            // new word
1645
14.5k
  TBOX word_box;         // for plotting
1646
14.5k
  int blobcount, repeated_set;
1647
1648
14.5k
  TO_ROW_IT row_it = block->get_rows();
1649
14.5k
  if (row_it.empty()) {
1650
0
    return; // empty block
1651
0
  }
1652
184k
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1653
169k
    row = row_it.data();
1654
169k
    box_it.set_to_list(row->blob_list());
1655
169k
    if (box_it.empty()) {
1656
0
      continue; // no blobs in this row
1657
0
    }
1658
169k
    if (!row->rep_chars_marked()) {
1659
0
      mark_repeated_chars(row);
1660
0
    }
1661
169k
    if (row->num_repeated_sets() == 0) {
1662
169k
      continue; // nothing to do for this row
1663
169k
    }
1664
    // new words
1665
0
    WERD_IT word_it(&row->rep_words);
1666
0
    do {
1667
0
      if (box_it.data()->repeated_set() != 0 && !box_it.data()->joined_to_prev()) {
1668
0
        blobcount = 1;
1669
0
        repeated_set = box_it.data()->repeated_set();
1670
0
        search_it = box_it;
1671
0
        search_it.forward();
1672
0
        while (!search_it.at_first() && search_it.data()->repeated_set() == repeated_set) {
1673
0
          blobcount++;
1674
0
          search_it.forward();
1675
0
        }
1676
        // After the call to make_real_word() all the blobs from this
1677
        // repeated set will be removed from the blob list. box_it will be
1678
        // set to point to the blob after the end of the extracted sequence.
1679
0
        word = make_real_word(&box_it, blobcount, box_it.at_first(), 1);
1680
0
        if (!box_it.empty() && box_it.data()->joined_to_prev()) {
1681
0
          tprintf("Bad box joined to prev at");
1682
0
          box_it.data()->bounding_box().print();
1683
0
          tprintf("After repeated word:");
1684
0
          word->bounding_box().print();
1685
0
        }
1686
0
        ASSERT_HOST(box_it.empty() || !box_it.data()->joined_to_prev());
1687
0
        word->set_flag(W_REP_CHAR, true);
1688
0
        word->set_flag(W_DONT_CHOP, true);
1689
0
        word_it.add_after_then_move(word);
1690
0
      } else {
1691
0
        box_it.forward();
1692
0
      }
1693
0
    } while (!box_it.at_first());
1694
0
  }
1695
14.5k
}
1696
1697
/**********************************************************************
1698
 * plot_fp_word
1699
 *
1700
 * Plot a block of words as if fixed pitch.
1701
 **********************************************************************/
1702
1703
#ifndef GRAPHICS_DISABLED
1704
void plot_fp_word(   // draw block of words
1705
    TO_BLOCK *block, // block to draw
1706
    float pitch,     // pitch to draw with
1707
    float nonspace   // for space threshold
1708
) {
1709
  TO_ROW *row; // current row
1710
  TO_ROW_IT row_it = block->get_rows();
1711
1712
  for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1713
    row = row_it.data();
1714
    row->min_space = static_cast<int32_t>((pitch + nonspace) / 2);
1715
    row->max_nonspace = row->min_space;
1716
    row->space_threshold = row->min_space;
1717
    plot_word_decisions(to_win, static_cast<int16_t>(pitch), row);
1718
  }
1719
}
1720
#endif
1721
1722
} // namespace tesseract