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