diff options
Diffstat (limited to 'tesseract/src/textord/topitch.cpp')
-rw-r--r-- | tesseract/src/textord/topitch.cpp | 1847 |
1 files changed, 1847 insertions, 0 deletions
diff --git a/tesseract/src/textord/topitch.cpp b/tesseract/src/textord/topitch.cpp new file mode 100644 index 00000000..655f75bd --- /dev/null +++ b/tesseract/src/textord/topitch.cpp @@ -0,0 +1,1847 @@ +/********************************************************************** + * File: topitch.cpp (Formerly to_pitch.c) + * Description: Code to determine fixed pitchness and the pitch if fixed. + * Author: Ray Smith + * + * (C) Copyright 1993, Hewlett-Packard Ltd. + ** Licensed under the Apache License, Version 2.0 (the "License"); + ** you may not use this file except in compliance with the License. + ** You may obtain a copy of the License at + ** http://www.apache.org/licenses/LICENSE-2.0 + ** Unless required by applicable law or agreed to in writing, software + ** distributed under the License is distributed on an "AS IS" BASIS, + ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ** See the License for the specific language governing permissions and + ** limitations under the License. + * + **********************************************************************/ + + // Include automatically generated configuration file if running autoconf. +#ifdef HAVE_CONFIG_H +#include "config_auto.h" +#endif + +#include "topitch.h" + +#include "blobbox.h" +#include "statistc.h" +#include "drawtord.h" +#include "makerow.h" +#include "pitsync1.h" +#include "pithsync.h" +#include "tovars.h" +#include "wordseg.h" + +#include "helpers.h" + +#include <memory> + +namespace tesseract { + +static BOOL_VAR (textord_all_prop, false, "All doc is proportial text"); +BOOL_VAR (textord_debug_pitch_test, false, +"Debug on fixed pitch test"); +static BOOL_VAR (textord_disable_pitch_test, false, +"Turn off dp fixed pitch algorithm"); +BOOL_VAR (textord_fast_pitch_test, false, +"Do even faster pitch algorithm"); +BOOL_VAR (textord_debug_pitch_metric, false, +"Write full metric stuff"); +BOOL_VAR (textord_show_row_cuts, false, "Draw row-level cuts"); +BOOL_VAR (textord_show_page_cuts, false, "Draw page-level cuts"); +BOOL_VAR (textord_pitch_cheat, false, +"Use correct answer for fixed/prop"); +BOOL_VAR (textord_blockndoc_fixed, false, +"Attempt whole doc/block fixed pitch"); +double_VAR (textord_projection_scale, 0.200, "Ding rate for mid-cuts"); +double_VAR (textord_balance_factor, 1.0, +"Ding rate for unbalanced char cells"); + +#define BLOCK_STATS_CLUSTERS 10 +#define MAX_ALLOWED_PITCH 100 //max pixel pitch. + +// qsort function to sort 2 floats. +static int sort_floats(const void *arg1, const void *arg2) { + float diff = *reinterpret_cast<const float*>(arg1) - + *reinterpret_cast<const float*>(arg2); + if (diff > 0) { + return 1; + } else if (diff < 0) { + return -1; + } else { + return 0; + } +} + +/********************************************************************** + * compute_fixed_pitch + * + * Decide whether each row is fixed pitch individually. + * Correlate definite and uncertain results to obtain an individual + * result for each row in the TO_ROW class. + **********************************************************************/ + +void compute_fixed_pitch(ICOORD page_tr, // top right + TO_BLOCK_LIST* port_blocks, // input list + float gradient, // page skew + FCOORD rotation, // for drawing + bool testing_on) { // correct orientation + TO_BLOCK_IT block_it; //iterator + TO_BLOCK *block; //current block; + TO_ROW *row; //current row + int block_index; //block number + int row_index; //row number + +#ifndef GRAPHICS_DISABLED + if (textord_show_initial_words && testing_on) { + if (to_win == nullptr) + create_to_win(page_tr); + } +#endif + + block_it.set_to_list (port_blocks); + block_index = 1; + for (block_it.mark_cycle_pt (); !block_it.cycled_list (); + block_it.forward ()) { + block = block_it.data (); + compute_block_pitch(block, rotation, block_index, testing_on); + block_index++; + } + + if (!try_doc_fixed (page_tr, port_blocks, gradient)) { + block_index = 1; + for (block_it.mark_cycle_pt (); !block_it.cycled_list (); + block_it.forward ()) { + block = block_it.data (); + if (!try_block_fixed (block, block_index)) + try_rows_fixed(block, block_index, testing_on); + block_index++; + } + } + + block_index = 1; + for (block_it.mark_cycle_pt(); !block_it.cycled_list(); + block_it.forward()) { + block = block_it.data (); + POLY_BLOCK* pb = block->block->pdblk.poly_block(); + if (pb != nullptr && !pb->IsText()) continue; // Non-text doesn't exist! + // row iterator + TO_ROW_IT row_it(block->get_rows()); + row_index = 1; + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + fix_row_pitch(row, block, port_blocks, row_index, block_index); + row_index++; + } + block_index++; + } +#ifndef GRAPHICS_DISABLED + if (textord_show_initial_words && testing_on) { + ScrollView::Update(); + } +#endif +} + + +/********************************************************************** + * fix_row_pitch + * + * Get a pitch_decision for this row by voting among similar rows in the + * block, then similar rows over all the page, or any other rows at all. + **********************************************************************/ + +void fix_row_pitch(TO_ROW *bad_row, // row to fix + TO_BLOCK *bad_block, // block of bad_row + TO_BLOCK_LIST *blocks, // blocks to scan + int32_t row_target, // number of row + int32_t block_target) { // number of block + int16_t mid_cuts; + int block_votes; //votes in block + int like_votes; //votes over page + int other_votes; //votes of unlike blocks + int block_index; //number of block + int row_index; //number of row + int maxwidth; //max pitch + TO_BLOCK_IT block_it = blocks; //block iterator + TO_BLOCK *block; //current block + TO_ROW *row; //current row + float sp_sd; //space deviation + STATS block_stats; //pitches in block + STATS like_stats; //pitches in page + + block_votes = like_votes = other_votes = 0; + maxwidth = static_cast<int32_t>(ceil (bad_row->xheight * textord_words_maxspace)); + if (bad_row->pitch_decision != PITCH_DEF_FIXED + && bad_row->pitch_decision != PITCH_DEF_PROP) { + block_stats.set_range (0, maxwidth); + like_stats.set_range (0, maxwidth); + block_index = 1; + for (block_it.mark_cycle_pt(); !block_it.cycled_list(); + block_it.forward()) { + block = block_it.data(); + POLY_BLOCK* pb = block->block->pdblk.poly_block(); + if (pb != nullptr && !pb->IsText()) continue; // Non text doesn't exist! + row_index = 1; + TO_ROW_IT row_it(block->get_rows()); + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); + row_it.forward ()) { + row = row_it.data (); + if ((bad_row->all_caps + && row->xheight + row->ascrise + < + (bad_row->xheight + bad_row->ascrise) * (1 + + textord_pitch_rowsimilarity) + && row->xheight + row->ascrise > + (bad_row->xheight + bad_row->ascrise) * (1 - + textord_pitch_rowsimilarity)) + || (!bad_row->all_caps + && row->xheight < + bad_row->xheight * (1 + textord_pitch_rowsimilarity) + && row->xheight > + bad_row->xheight * (1 - textord_pitch_rowsimilarity))) { + if (block_index == block_target) { + if (row->pitch_decision == PITCH_DEF_FIXED) { + block_votes += textord_words_veto_power; + block_stats.add (static_cast<int32_t>(row->fixed_pitch), + textord_words_veto_power); + } + else if (row->pitch_decision == PITCH_MAYBE_FIXED + || row->pitch_decision == PITCH_CORR_FIXED) { + block_votes++; + block_stats.add (static_cast<int32_t>(row->fixed_pitch), 1); + } + else if (row->pitch_decision == PITCH_DEF_PROP) + block_votes -= textord_words_veto_power; + else if (row->pitch_decision == PITCH_MAYBE_PROP + || row->pitch_decision == PITCH_CORR_PROP) + block_votes--; + } + else { + if (row->pitch_decision == PITCH_DEF_FIXED) { + like_votes += textord_words_veto_power; + like_stats.add (static_cast<int32_t>(row->fixed_pitch), + textord_words_veto_power); + } + else if (row->pitch_decision == PITCH_MAYBE_FIXED + || row->pitch_decision == PITCH_CORR_FIXED) { + like_votes++; + like_stats.add (static_cast<int32_t>(row->fixed_pitch), 1); + } + else if (row->pitch_decision == PITCH_DEF_PROP) + like_votes -= textord_words_veto_power; + else if (row->pitch_decision == PITCH_MAYBE_PROP + || row->pitch_decision == PITCH_CORR_PROP) + like_votes--; + } + } + else { + if (row->pitch_decision == PITCH_DEF_FIXED) + other_votes += textord_words_veto_power; + else if (row->pitch_decision == PITCH_MAYBE_FIXED + || row->pitch_decision == PITCH_CORR_FIXED) + other_votes++; + else if (row->pitch_decision == PITCH_DEF_PROP) + other_votes -= textord_words_veto_power; + else if (row->pitch_decision == PITCH_MAYBE_PROP + || row->pitch_decision == PITCH_CORR_PROP) + other_votes--; + } + row_index++; + } + block_index++; + } + if (block_votes > textord_words_veto_power) { + bad_row->fixed_pitch = block_stats.ile (0.5); + bad_row->pitch_decision = PITCH_CORR_FIXED; + } + else if (block_votes <= textord_words_veto_power && like_votes > 0) { + bad_row->fixed_pitch = like_stats.ile (0.5); + bad_row->pitch_decision = PITCH_CORR_FIXED; + } + else { + bad_row->pitch_decision = PITCH_CORR_PROP; + if (block_votes == 0 && like_votes == 0 && other_votes > 0 + && (textord_debug_pitch_test || textord_debug_pitch_metric)) + tprintf + ("Warning:row %d of block %d set prop with no like rows against trend\n", + row_target, block_target); + } + } + if (textord_debug_pitch_metric) { + tprintf(":b_votes=%d:l_votes=%d:o_votes=%d", + block_votes, like_votes, other_votes); + tprintf("x=%g:asc=%g\n", bad_row->xheight, bad_row->ascrise); + } + if (bad_row->pitch_decision == PITCH_CORR_FIXED) { + if (bad_row->fixed_pitch < textord_min_xheight) { + if (block_votes > 0) + bad_row->fixed_pitch = block_stats.ile (0.5); + else if (block_votes == 0 && like_votes > 0) + bad_row->fixed_pitch = like_stats.ile (0.5); + else { + tprintf + ("Warning:guessing pitch as xheight on row %d, block %d\n", + row_target, block_target); + bad_row->fixed_pitch = bad_row->xheight; + } + } + if (bad_row->fixed_pitch < textord_min_xheight) + bad_row->fixed_pitch = (float) textord_min_xheight; + bad_row->kern_size = bad_row->fixed_pitch / 4; + bad_row->min_space = static_cast<int32_t>(bad_row->fixed_pitch * 0.6); + bad_row->max_nonspace = static_cast<int32_t>(bad_row->fixed_pitch * 0.4); + bad_row->space_threshold = + (bad_row->min_space + bad_row->max_nonspace) / 2; + bad_row->space_size = bad_row->fixed_pitch; + if (bad_row->char_cells.empty() && !bad_row->blob_list()->empty()) { + tune_row_pitch (bad_row, &bad_row->projection, + bad_row->projection_left, bad_row->projection_right, + (bad_row->fixed_pitch + + bad_row->max_nonspace * 3) / 4, bad_row->fixed_pitch, + sp_sd, mid_cuts, &bad_row->char_cells, false); + } + } + else if (bad_row->pitch_decision == PITCH_CORR_PROP + || bad_row->pitch_decision == PITCH_DEF_PROP) { + bad_row->fixed_pitch = 0.0f; + bad_row->char_cells.clear (); + } +} + + +/********************************************************************** + * compute_block_pitch + * + * Decide whether each block is fixed pitch individually. + **********************************************************************/ + +void compute_block_pitch(TO_BLOCK* block, // input list + FCOORD rotation, // for drawing + int32_t block_index, // block number + bool testing_on) { // correct orientation + TBOX block_box; //bounding box + + block_box = block->block->pdblk.bounding_box (); + if (testing_on && textord_debug_pitch_test) { + tprintf ("Block %d at (%d,%d)->(%d,%d)\n", + block_index, + block_box.left (), block_box.bottom (), + block_box.right (), block_box.top ()); + } + block->min_space = static_cast<int32_t>(floor (block->xheight + * textord_words_default_minspace)); + block->max_nonspace = static_cast<int32_t>(ceil (block->xheight + * textord_words_default_nonspace)); + block->fixed_pitch = 0.0f; + block->space_size = static_cast<float>(block->min_space); + block->kern_size = static_cast<float>(block->max_nonspace); + block->pr_nonsp = block->xheight * words_default_prop_nonspace; + block->pr_space = block->pr_nonsp * textord_spacesize_ratioprop; + if (!block->get_rows ()->empty ()) { + ASSERT_HOST (block->xheight > 0); + find_repeated_chars(block, textord_show_initial_words && testing_on); +#ifndef GRAPHICS_DISABLED + if (textord_show_initial_words && testing_on) + //overlap_picture_ops(true); + ScrollView::Update(); +#endif + compute_rows_pitch(block, + block_index, + textord_debug_pitch_test && testing_on); + } +} + + +/********************************************************************** + * compute_rows_pitch + * + * Decide whether each row is fixed pitch individually. + **********************************************************************/ + +bool compute_rows_pitch( //find line stats + TO_BLOCK* block, //block to do + int32_t block_index, //block number + bool testing_on //correct orientation +) { + int32_t maxwidth; //of spaces + TO_ROW *row; //current row + int32_t row_index; //row number. + float lower, upper; //cluster thresholds + TO_ROW_IT row_it = block->get_rows (); + + row_index = 1; + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + ASSERT_HOST (row->xheight > 0); + row->compute_vertical_projection (); + maxwidth = static_cast<int32_t>(ceil (row->xheight * textord_words_maxspace)); + if (row_pitch_stats (row, maxwidth, testing_on) + && find_row_pitch (row, maxwidth, + textord_dotmatrix_gap + 1, block, block_index, + row_index, testing_on)) { + if (row->fixed_pitch == 0) { + lower = row->pr_nonsp; + upper = row->pr_space; + row->space_size = upper; + row->kern_size = lower; + } + } + else { + row->fixed_pitch = 0.0f; //insufficient data + row->pitch_decision = PITCH_DUNNO; + } + row_index++; + } + return false; +} + + +/********************************************************************** + * try_doc_fixed + * + * Attempt to call the entire document fixed pitch. + **********************************************************************/ + +bool try_doc_fixed( //determine pitch + ICOORD page_tr, //top right + TO_BLOCK_LIST* port_blocks, //input list + float gradient //page skew +) { + int16_t master_x; //uniform shifts + int16_t pitch; //median pitch. + int x; //profile coord + int prop_blocks; //correct counts + int fixed_blocks; + int total_row_count; //total in page + //iterator + TO_BLOCK_IT block_it = port_blocks; + TO_BLOCK *block; //current block; + TO_ROW *row; //current row + int16_t projection_left; //edges + int16_t projection_right; + int16_t row_left; //edges of row + int16_t row_right; + float master_y; //uniform shifts + float shift_factor; //page skew correction + float final_pitch; //output pitch + float row_y; //baseline + STATS projection; //entire page + STATS pitches (0, MAX_ALLOWED_PITCH); + //for median + float sp_sd; //space sd + int16_t mid_cuts; //no of cheap cuts + float pitch_sd; //sync rating + + if (block_it.empty () + // || block_it.data()==block_it.data_relative(1) + || !textord_blockndoc_fixed) + return false; + shift_factor = gradient / (gradient * gradient + 1); + // row iterator + TO_ROW_IT row_it(block_it.data ()->get_rows()); + master_x = row_it.data ()->projection_left; + master_y = row_it.data ()->baseline.y (master_x); + projection_left = INT16_MAX; + projection_right = -INT16_MAX; + prop_blocks = 0; + fixed_blocks = 0; + total_row_count = 0; + + for (block_it.mark_cycle_pt (); !block_it.cycled_list (); + block_it.forward ()) { + block = block_it.data (); + row_it.set_to_list (block->get_rows ()); + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + total_row_count++; + if (row->fixed_pitch > 0) + pitches.add (static_cast<int32_t>(row->fixed_pitch), 1); + //find median + row_y = row->baseline.y (master_x); + row_left = + static_cast<int16_t>(row->projection_left - + shift_factor * (master_y - row_y)); + row_right = + static_cast<int16_t>(row->projection_right - + shift_factor * (master_y - row_y)); + if (row_left < projection_left) + projection_left = row_left; + if (row_right > projection_right) + projection_right = row_right; + } + } + if (pitches.get_total () == 0) + return false; + projection.set_range (projection_left, projection_right); + + for (block_it.mark_cycle_pt (); !block_it.cycled_list (); + block_it.forward ()) { + block = block_it.data (); + row_it.set_to_list (block->get_rows ()); + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + row_y = row->baseline.y (master_x); + row_left = + static_cast<int16_t>(row->projection_left - + shift_factor * (master_y - row_y)); + for (x = row->projection_left; x < row->projection_right; + x++, row_left++) { + projection.add (row_left, row->projection.pile_count (x)); + } + } + } + + row_it.set_to_list (block_it.data ()->get_rows ()); + row = row_it.data (); +#ifndef GRAPHICS_DISABLED + if (textord_show_page_cuts && to_win != nullptr) + projection.plot (to_win, projection_left, + row->intercept (), 1.0f, -1.0f, ScrollView::CORAL); +#endif + final_pitch = pitches.ile (0.5); + pitch = static_cast<int16_t>(final_pitch); + pitch_sd = + tune_row_pitch (row, &projection, projection_left, projection_right, + pitch * 0.75, final_pitch, sp_sd, mid_cuts, + &row->char_cells, false); + + if (textord_debug_pitch_metric) + tprintf + ("try_doc:props=%d:fixed=%d:pitch=%d:final_pitch=%g:pitch_sd=%g:sp_sd=%g:sd/trc=%g:sd/p=%g:sd/trc/p=%g\n", + prop_blocks, fixed_blocks, pitch, final_pitch, pitch_sd, sp_sd, + pitch_sd / total_row_count, pitch_sd / pitch, + pitch_sd / total_row_count / pitch); + +#ifndef GRAPHICS_DISABLED + if (textord_show_page_cuts && to_win != nullptr) { + float row_shift; //shift for row + ICOORDELT_LIST *master_cells; //cells for page + master_cells = &row->char_cells; + for (block_it.mark_cycle_pt (); !block_it.cycled_list (); + block_it.forward ()) { + block = block_it.data (); + row_it.set_to_list (block->get_rows ()); + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); + row_it.forward ()) { + row = row_it.data (); + row_y = row->baseline.y (master_x); + row_shift = shift_factor * (master_y - row_y); + plot_row_cells(to_win, ScrollView::GOLDENROD, row, row_shift, master_cells); + } + } + } +#endif + row->char_cells.clear (); + return false; +} + + +/********************************************************************** + * try_block_fixed + * + * Try to call the entire block fixed. + **********************************************************************/ + +bool try_block_fixed( //find line stats + TO_BLOCK* block, //block to do + int32_t block_index //block number +) { + return false; +} + + +/********************************************************************** + * try_rows_fixed + * + * Decide whether each row is fixed pitch individually. + **********************************************************************/ + +bool try_rows_fixed( //find line stats + TO_BLOCK* block, //block to do + int32_t block_index, //block number + bool testing_on //correct orientation +) { + TO_ROW *row; //current row + int32_t row_index; //row number. + int32_t def_fixed = 0; //counters + int32_t def_prop = 0; + int32_t maybe_fixed = 0; + int32_t maybe_prop = 0; + int32_t dunno = 0; + int32_t corr_fixed = 0; + int32_t corr_prop = 0; + float lower, upper; //cluster thresholds + TO_ROW_IT row_it = block->get_rows (); + + row_index = 1; + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + ASSERT_HOST (row->xheight > 0); + if (row->fixed_pitch > 0 && + fixed_pitch_row(row, block->block, block_index)) { + if (row->fixed_pitch == 0) { + lower = row->pr_nonsp; + upper = row->pr_space; + row->space_size = upper; + row->kern_size = lower; + } + } + row_index++; + } + count_block_votes(block, + def_fixed, + def_prop, + maybe_fixed, + maybe_prop, + corr_fixed, + corr_prop, + dunno); + if (testing_on + && (textord_debug_pitch_test + || textord_blocksall_prop || textord_blocksall_fixed)) { + tprintf ("Initially:"); + print_block_counts(block, block_index); + } + if (def_fixed > def_prop * textord_words_veto_power) + block->pitch_decision = PITCH_DEF_FIXED; + else if (def_prop > def_fixed * textord_words_veto_power) + block->pitch_decision = PITCH_DEF_PROP; + else if (def_fixed > 0 || def_prop > 0) + block->pitch_decision = PITCH_DUNNO; + else if (maybe_fixed > maybe_prop * textord_words_veto_power) + block->pitch_decision = PITCH_MAYBE_FIXED; + else if (maybe_prop > maybe_fixed * textord_words_veto_power) + block->pitch_decision = PITCH_MAYBE_PROP; + else + block->pitch_decision = PITCH_DUNNO; + return false; +} + + +/********************************************************************** + * print_block_counts + * + * Count up how many rows have what decision and print the results. + **********************************************************************/ + +void print_block_counts( //find line stats + TO_BLOCK *block, //block to do + int32_t block_index //block number + ) { + int32_t def_fixed = 0; //counters + int32_t def_prop = 0; + int32_t maybe_fixed = 0; + int32_t maybe_prop = 0; + int32_t dunno = 0; + int32_t corr_fixed = 0; + int32_t corr_prop = 0; + + count_block_votes(block, + def_fixed, + def_prop, + maybe_fixed, + maybe_prop, + corr_fixed, + corr_prop, + dunno); + tprintf ("Block %d has (%d,%d,%d)", + block_index, def_fixed, maybe_fixed, corr_fixed); + if (textord_blocksall_prop && (def_fixed || maybe_fixed || corr_fixed)) + tprintf (" (Wrongly)"); + tprintf (" fixed, (%d,%d,%d)", def_prop, maybe_prop, corr_prop); + if (textord_blocksall_fixed && (def_prop || maybe_prop || corr_prop)) + tprintf (" (Wrongly)"); + tprintf (" prop, %d dunno\n", dunno); +} + + +/********************************************************************** + * count_block_votes + * + * Count the number of rows in the block with each kind of pitch_decision. + **********************************************************************/ + +void count_block_votes( //find line stats + TO_BLOCK *block, //block to do + int32_t &def_fixed, //add to counts + int32_t &def_prop, + int32_t &maybe_fixed, + int32_t &maybe_prop, + int32_t &corr_fixed, + int32_t &corr_prop, + int32_t &dunno) { + TO_ROW *row; //current row + TO_ROW_IT row_it = block->get_rows (); + + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + switch (row->pitch_decision) { + case PITCH_DUNNO: + dunno++; + break; + case PITCH_DEF_PROP: + def_prop++; + break; + case PITCH_MAYBE_PROP: + maybe_prop++; + break; + case PITCH_DEF_FIXED: + def_fixed++; + break; + case PITCH_MAYBE_FIXED: + maybe_fixed++; + break; + case PITCH_CORR_PROP: + corr_prop++; + break; + case PITCH_CORR_FIXED: + corr_fixed++; + break; + } + } +} + + +/********************************************************************** + * row_pitch_stats + * + * Decide whether each row is fixed pitch individually. + **********************************************************************/ + +bool row_pitch_stats( //find line stats + TO_ROW* row, //current row + int32_t maxwidth, //of spaces + bool testing_on //correct orientation +) { + BLOBNBOX *blob; //current blob + int gap_index; //current gap + int32_t prev_x; //end of prev blob + int32_t cluster_count; //no of clusters + int32_t prev_count; //of clusters + int32_t smooth_factor; //for smoothing stats + TBOX blob_box; //bounding box + float lower, upper; //cluster thresholds + //gap sizes + float gaps[BLOCK_STATS_CLUSTERS]; + //blobs + BLOBNBOX_IT blob_it = row->blob_list (); + STATS gap_stats (0, maxwidth); + STATS cluster_stats[BLOCK_STATS_CLUSTERS + 1]; + //clusters + + smooth_factor = + static_cast<int32_t>(row->xheight * textord_wordstats_smooth_factor + 1.5); + if (!blob_it.empty ()) { + prev_x = blob_it.data ()->bounding_box ().right (); + blob_it.forward (); + while (!blob_it.at_first ()) { + blob = blob_it.data (); + if (!blob->joined_to_prev ()) { + blob_box = blob->bounding_box (); + if (blob_box.left () - prev_x < maxwidth) + gap_stats.add (blob_box.left () - prev_x, 1); + prev_x = blob_box.right (); + } + blob_it.forward (); + } + } + if (gap_stats.get_total () == 0) { + return false; + } + cluster_count = 0; + lower = row->xheight * words_initial_lower; + upper = row->xheight * words_initial_upper; + gap_stats.smooth (smooth_factor); + do { + prev_count = cluster_count; + cluster_count = gap_stats.cluster (lower, upper, + textord_spacesize_ratioprop, + BLOCK_STATS_CLUSTERS, cluster_stats); + } + while (cluster_count > prev_count && cluster_count < BLOCK_STATS_CLUSTERS); + if (cluster_count < 1) { + return false; + } + for (gap_index = 0; gap_index < cluster_count; gap_index++) + gaps[gap_index] = cluster_stats[gap_index + 1].ile (0.5); + //get medians + if (testing_on) { + tprintf ("cluster_count=%d:", cluster_count); + for (gap_index = 0; gap_index < cluster_count; gap_index++) + tprintf (" %g(%d)", gaps[gap_index], + cluster_stats[gap_index + 1].get_total ()); + tprintf ("\n"); + } + qsort (gaps, cluster_count, sizeof (float), sort_floats); + + //Try to find proportional non-space and space for row. + lower = row->xheight * words_default_prop_nonspace; + upper = row->xheight * textord_words_min_minspace; + for (gap_index = 0; gap_index < cluster_count + && gaps[gap_index] < lower; gap_index++); + if (gap_index == 0) { + if (testing_on) + tprintf ("No clusters below nonspace threshold!!\n"); + if (cluster_count > 1) { + row->pr_nonsp = gaps[0]; + row->pr_space = gaps[1]; + } + else { + row->pr_nonsp = lower; + row->pr_space = gaps[0]; + } + } + else { + row->pr_nonsp = gaps[gap_index - 1]; + while (gap_index < cluster_count && gaps[gap_index] < upper) + gap_index++; + if (gap_index == cluster_count) { + if (testing_on) + tprintf ("No clusters above nonspace threshold!!\n"); + row->pr_space = lower * textord_spacesize_ratioprop; + } + else + row->pr_space = gaps[gap_index]; + } + + //Now try to find the fixed pitch space and non-space. + upper = row->xheight * words_default_fixed_space; + for (gap_index = 0; gap_index < cluster_count + && gaps[gap_index] < upper; gap_index++); + if (gap_index == 0) { + if (testing_on) + tprintf ("No clusters below space threshold!!\n"); + row->fp_nonsp = upper; + row->fp_space = gaps[0]; + } + else { + row->fp_nonsp = gaps[gap_index - 1]; + if (gap_index == cluster_count) { + if (testing_on) + tprintf ("No clusters above space threshold!!\n"); + row->fp_space = row->xheight; + } + else + row->fp_space = gaps[gap_index]; + } + if (testing_on) { + tprintf + ("Initial estimates:pr_nonsp=%g, pr_space=%g, fp_nonsp=%g, fp_space=%g\n", + row->pr_nonsp, row->pr_space, row->fp_nonsp, row->fp_space); + } + return true; //computed some stats +} + + +/********************************************************************** + * find_row_pitch + * + * Check to see if this row could be fixed pitch using the given spacings. + * Blobs with gaps smaller than the lower threshold are assumed to be one. + * The larger threshold is the word gap threshold. + **********************************************************************/ + +bool find_row_pitch( //find lines + TO_ROW* row, //row to do + int32_t maxwidth, //max permitted space + int32_t dm_gap, //ignorable gaps + TO_BLOCK* block, //block of row + int32_t block_index, //block_number + int32_t row_index, //number of row + bool testing_on //correct orientation +) { + bool used_dm_model; //looks like dot matrix + float min_space; //estimate threshold + float non_space; //gap size + float gap_iqr; //interquartile range + float pitch_iqr; + float dm_gap_iqr; //interquartile range + float dm_pitch_iqr; + float dm_pitch; //pitch with dm on + float pitch; //revised estimate + float initial_pitch; //guess at pitch + STATS gap_stats (0, maxwidth); + //centre-centre + STATS pitch_stats (0, maxwidth); + + row->fixed_pitch = 0.0f; + initial_pitch = row->fp_space; + if (initial_pitch > row->xheight * (1 + words_default_fixed_limit)) + initial_pitch = row->xheight;//keep pitch decent + non_space = row->fp_nonsp; + if (non_space > initial_pitch) + non_space = initial_pitch; + min_space = (initial_pitch + non_space) / 2; + + if (!count_pitch_stats (row, &gap_stats, &pitch_stats, + initial_pitch, min_space, true, false, dm_gap)) { + dm_gap_iqr = 0.0001f; + dm_pitch_iqr = maxwidth * 2.0f; + dm_pitch = initial_pitch; + } + else { + dm_gap_iqr = gap_stats.ile (0.75) - gap_stats.ile (0.25); + dm_pitch_iqr = pitch_stats.ile (0.75) - pitch_stats.ile (0.25); + dm_pitch = pitch_stats.ile (0.5); + } + gap_stats.clear (); + pitch_stats.clear (); + if (!count_pitch_stats (row, &gap_stats, &pitch_stats, + initial_pitch, min_space, true, false, 0)) { + gap_iqr = 0.0001f; + pitch_iqr = maxwidth * 3.0f; + } + else { + gap_iqr = gap_stats.ile (0.75) - gap_stats.ile (0.25); + pitch_iqr = pitch_stats.ile (0.75) - pitch_stats.ile (0.25); + if (testing_on) + tprintf + ("First fp iteration:initial_pitch=%g, gap_iqr=%g, pitch_iqr=%g, pitch=%g\n", + initial_pitch, gap_iqr, pitch_iqr, pitch_stats.ile (0.5)); + initial_pitch = pitch_stats.ile (0.5); + if (min_space > initial_pitch + && count_pitch_stats (row, &gap_stats, &pitch_stats, + initial_pitch, initial_pitch, true, false, 0)) { + min_space = initial_pitch; + gap_iqr = gap_stats.ile (0.75) - gap_stats.ile (0.25); + pitch_iqr = pitch_stats.ile (0.75) - pitch_stats.ile (0.25); + if (testing_on) + tprintf + ("Revised fp iteration:initial_pitch=%g, gap_iqr=%g, pitch_iqr=%g, pitch=%g\n", + initial_pitch, gap_iqr, pitch_iqr, pitch_stats.ile (0.5)); + initial_pitch = pitch_stats.ile (0.5); + } + } + if (textord_debug_pitch_metric) + tprintf("Blk=%d:Row=%d:%c:p_iqr=%g:g_iqr=%g:dm_p_iqr=%g:dm_g_iqr=%g:%c:", + block_index, row_index, 'X', + pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr, + pitch_iqr > maxwidth && dm_pitch_iqr > maxwidth ? 'D' : + (pitch_iqr * dm_gap_iqr <= dm_pitch_iqr * gap_iqr ? 'S' : 'M')); + if (pitch_iqr > maxwidth && dm_pitch_iqr > maxwidth) { + row->pitch_decision = PITCH_DUNNO; + if (textord_debug_pitch_metric) + tprintf ("\n"); + return false; //insufficient data + } + if (pitch_iqr * dm_gap_iqr <= dm_pitch_iqr * gap_iqr) { + if (testing_on) + tprintf + ("Choosing non dm version:pitch_iqr=%g, gap_iqr=%g, dm_pitch_iqr=%g, dm_gap_iqr=%g\n", + pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr); + gap_iqr = gap_stats.ile (0.75) - gap_stats.ile (0.25); + pitch_iqr = pitch_stats.ile (0.75) - pitch_stats.ile (0.25); + pitch = pitch_stats.ile (0.5); + used_dm_model = false; + } + else { + if (testing_on) + tprintf + ("Choosing dm version:pitch_iqr=%g, gap_iqr=%g, dm_pitch_iqr=%g, dm_gap_iqr=%g\n", + pitch_iqr, gap_iqr, dm_pitch_iqr, dm_gap_iqr); + gap_iqr = dm_gap_iqr; + pitch_iqr = dm_pitch_iqr; + pitch = dm_pitch; + used_dm_model = true; + } + if (textord_debug_pitch_metric) { + tprintf ("rev_p_iqr=%g:rev_g_iqr=%g:pitch=%g:", + pitch_iqr, gap_iqr, pitch); + tprintf ("p_iqr/g=%g:p_iqr/x=%g:iqr_res=%c:", + pitch_iqr / gap_iqr, pitch_iqr / block->xheight, + pitch_iqr < gap_iqr * textord_fpiqr_ratio + && pitch_iqr < block->xheight * textord_max_pitch_iqr + && pitch < block->xheight * textord_words_default_maxspace + ? 'F' : 'P'); + } + if (pitch_iqr < gap_iqr * textord_fpiqr_ratio + && pitch_iqr < block->xheight * textord_max_pitch_iqr + && pitch < block->xheight * textord_words_default_maxspace) + row->pitch_decision = PITCH_MAYBE_FIXED; + else + row->pitch_decision = PITCH_MAYBE_PROP; + row->fixed_pitch = pitch; + row->kern_size = gap_stats.ile (0.5); + row->min_space = static_cast<int32_t>(row->fixed_pitch + non_space) / 2; + if (row->min_space > row->fixed_pitch) + row->min_space = static_cast<int32_t>(row->fixed_pitch); + row->max_nonspace = row->min_space; + row->space_size = row->fixed_pitch; + row->space_threshold = (row->max_nonspace + row->min_space) / 2; + row->used_dm_model = used_dm_model; + return true; +} + + +/********************************************************************** + * fixed_pitch_row + * + * Check to see if this row could be fixed pitch using the given spacings. + * Blobs with gaps smaller than the lower threshold are assumed to be one. + * The larger threshold is the word gap threshold. + **********************************************************************/ + +bool fixed_pitch_row(TO_ROW* row, // row to do + BLOCK* block, + int32_t block_index // block_number +) { + const char *res_string; // pitch result + int16_t mid_cuts; // no of cheap cuts + float non_space; // gap size + float pitch_sd; // error on pitch + float sp_sd = 0.0f; // space sd + + non_space = row->fp_nonsp; + if (non_space > row->fixed_pitch) + non_space = row->fixed_pitch; + POLY_BLOCK* pb = block != nullptr ? block->pdblk.poly_block() : nullptr; + if (textord_all_prop || (pb != nullptr && !pb->IsText())) { + // Set the decision to definitely proportional. + pitch_sd = textord_words_def_prop * row->fixed_pitch; + row->pitch_decision = PITCH_DEF_PROP; + } else { + pitch_sd = tune_row_pitch (row, &row->projection, row->projection_left, + row->projection_right, + (row->fixed_pitch + non_space * 3) / 4, + row->fixed_pitch, sp_sd, mid_cuts, + &row->char_cells, + block_index == textord_debug_block); + if (pitch_sd < textord_words_pitchsd_threshold * row->fixed_pitch + && ((pitsync_linear_version & 3) < 3 + || ((pitsync_linear_version & 3) >= 3 && (row->used_dm_model + || sp_sd > 20 + || (pitch_sd == 0 && sp_sd > 10))))) { + if (pitch_sd < textord_words_def_fixed * row->fixed_pitch + && !row->all_caps + && ((pitsync_linear_version & 3) < 3 || sp_sd > 20)) + row->pitch_decision = PITCH_DEF_FIXED; + else + row->pitch_decision = PITCH_MAYBE_FIXED; + } + else if ((pitsync_linear_version & 3) < 3 + || sp_sd > 20 + || mid_cuts > 0 + || pitch_sd >= textord_words_pitchsd_threshold * row->fixed_pitch) { + if (pitch_sd < textord_words_def_prop * row->fixed_pitch) + row->pitch_decision = PITCH_MAYBE_PROP; + else + row->pitch_decision = PITCH_DEF_PROP; + } + else + row->pitch_decision = PITCH_DUNNO; + } + + if (textord_debug_pitch_metric) { + res_string = "??"; + switch (row->pitch_decision) { + case PITCH_DEF_PROP: + res_string = "DP"; + break; + case PITCH_MAYBE_PROP: + res_string = "MP"; + break; + case PITCH_DEF_FIXED: + res_string = "DF"; + break; + case PITCH_MAYBE_FIXED: + res_string = "MF"; + break; + default: + res_string = "??"; + } + tprintf (":sd/p=%g:occ=%g:init_res=%s\n", + pitch_sd / row->fixed_pitch, sp_sd, res_string); + } + return true; +} + + +/********************************************************************** + * count_pitch_stats + * + * Count up the gap and pitch stats on the block to see if it is fixed pitch. + * Blobs with gaps smaller than the lower threshold are assumed to be one. + * The larger threshold is the word gap threshold. + * The return value indicates whether there were any decent values to use. + **********************************************************************/ + +bool count_pitch_stats( //find lines + TO_ROW* row, //row to do + STATS* gap_stats, //blob gaps + STATS* pitch_stats, //centre-centre stats + float initial_pitch, //guess at pitch + float min_space, //estimate space size + bool ignore_outsize, //discard big objects + bool split_outsize, //split big objects + int32_t dm_gap //ignorable gaps +) { + bool prev_valid; //not word broken + BLOBNBOX *blob; //current blob + //blobs + BLOBNBOX_IT blob_it = row->blob_list (); + int32_t prev_right; //end of prev blob + int32_t prev_centre; //centre of previous blob + int32_t x_centre; //centre of this blob + int32_t blob_width; //width of blob + int32_t width_units; //no of widths in blob + float width; //blob width + TBOX blob_box; //bounding box + TBOX joined_box; //of super blob + + gap_stats->clear (); + pitch_stats->clear (); + if (blob_it.empty ()) + return false; + prev_valid = false; + prev_centre = 0; + prev_right = 0; // stop compiler warning + joined_box = blob_it.data ()->bounding_box (); + do { + blob_it.forward (); + blob = blob_it.data (); + if (!blob->joined_to_prev ()) { + blob_box = blob->bounding_box (); + if ((blob_box.left () - joined_box.right () < dm_gap + && !blob_it.at_first ()) + || blob->cblob() == nullptr) + joined_box += blob_box; //merge blobs + else { + blob_width = joined_box.width (); + if (split_outsize) { + width_units = + static_cast<int32_t>(floor (static_cast<float>(blob_width) / initial_pitch + 0.5)); + if (width_units < 1) + width_units = 1; + width_units--; + } + else if (ignore_outsize) { + width = static_cast<float>(blob_width) / initial_pitch; + width_units = width < 1 + words_default_fixed_limit + && width > 1 - words_default_fixed_limit ? 0 : -1; + } + else + width_units = 0; //everything in + x_centre = static_cast<int32_t>(joined_box.left () + + (blob_width - + width_units * initial_pitch) / 2); + if (prev_valid && width_units >= 0) { + // if (width_units>0) + // { + // tprintf("wu=%d, width=%d, xc=%d, adding %d\n", + // width_units,blob_width,x_centre,x_centre-prev_centre); + // } + gap_stats->add (joined_box.left () - prev_right, 1); + pitch_stats->add (x_centre - prev_centre, 1); + } + prev_centre = static_cast<int32_t>(x_centre + width_units * initial_pitch); + prev_right = joined_box.right (); + prev_valid = blob_box.left () - joined_box.right () < min_space; + prev_valid = prev_valid && width_units >= 0; + joined_box = blob_box; + } + } + } + while (!blob_it.at_first ()); + return gap_stats->get_total () >= 3; +} + + +/********************************************************************** + * tune_row_pitch + * + * Use a dp algorithm to fit the character cells and return the sd of + * the cell size over the row. + **********************************************************************/ + +float tune_row_pitch( //find fp cells + TO_ROW* row, //row to do + STATS* projection, //vertical projection + int16_t projection_left, //edge of projection + int16_t projection_right, //edge of projection + float space_size, //size of blank + float& initial_pitch, //guess at pitch + float& best_sp_sd, //space sd + int16_t& best_mid_cuts, //no of cheap cuts + ICOORDELT_LIST* best_cells, //row cells + bool testing_on //inidividual words +) { + int pitch_delta; //offset pitch + int16_t mid_cuts; //cheap cuts + float pitch_sd; //current sd + float best_sd; //best result + float best_pitch; //pitch for best result + float initial_sd; //starting error + float sp_sd; //space sd + ICOORDELT_LIST test_cells; //row cells + ICOORDELT_IT best_it; //start of best list + + if (textord_fast_pitch_test) + return tune_row_pitch2 (row, projection, projection_left, + projection_right, space_size, initial_pitch, + best_sp_sd, + //space sd + best_mid_cuts, best_cells, testing_on); + if (textord_disable_pitch_test) { + best_sp_sd = initial_pitch; + return initial_pitch; + } + initial_sd = + compute_pitch_sd(row, + projection, + projection_left, + projection_right, + space_size, + initial_pitch, + best_sp_sd, + best_mid_cuts, + best_cells, + testing_on); + best_sd = initial_sd; + best_pitch = initial_pitch; + if (testing_on) + tprintf ("tune_row_pitch:start pitch=%g, sd=%g\n", best_pitch, best_sd); + for (pitch_delta = 1; pitch_delta <= textord_pitch_range; pitch_delta++) { + pitch_sd = + compute_pitch_sd (row, projection, projection_left, projection_right, + space_size, initial_pitch + pitch_delta, sp_sd, + mid_cuts, &test_cells, testing_on); + if (testing_on) + tprintf ("testing pitch at %g, sd=%g\n", initial_pitch + pitch_delta, + pitch_sd); + if (pitch_sd < best_sd) { + best_sd = pitch_sd; + best_mid_cuts = mid_cuts; + best_sp_sd = sp_sd; + best_pitch = initial_pitch + pitch_delta; + best_cells->clear (); + best_it.set_to_list (best_cells); + best_it.add_list_after (&test_cells); + } + else + test_cells.clear (); + if (pitch_sd > initial_sd) + break; //getting worse + } + for (pitch_delta = 1; pitch_delta <= textord_pitch_range; pitch_delta++) { + pitch_sd = + compute_pitch_sd (row, projection, projection_left, projection_right, + space_size, initial_pitch - pitch_delta, sp_sd, + mid_cuts, &test_cells, testing_on); + if (testing_on) + tprintf ("testing pitch at %g, sd=%g\n", initial_pitch - pitch_delta, + pitch_sd); + if (pitch_sd < best_sd) { + best_sd = pitch_sd; + best_mid_cuts = mid_cuts; + best_sp_sd = sp_sd; + best_pitch = initial_pitch - pitch_delta; + best_cells->clear (); + best_it.set_to_list (best_cells); + best_it.add_list_after (&test_cells); + } + else + test_cells.clear (); + if (pitch_sd > initial_sd) + break; + } + initial_pitch = best_pitch; + + if (textord_debug_pitch_metric) + print_pitch_sd(row, + projection, + projection_left, + projection_right, + space_size, + best_pitch); + + return best_sd; +} + + +/********************************************************************** + * tune_row_pitch + * + * Use a dp algorithm to fit the character cells and return the sd of + * the cell size over the row. + **********************************************************************/ + +float tune_row_pitch2( //find fp cells + TO_ROW* row, //row to do + STATS* projection, //vertical projection + int16_t projection_left, //edge of projection + int16_t projection_right, //edge of projection + float space_size, //size of blank + float& initial_pitch, //guess at pitch + float& best_sp_sd, //space sd + int16_t& best_mid_cuts, //no of cheap cuts + ICOORDELT_LIST* best_cells, //row cells + bool testing_on //inidividual words +) { + int pitch_delta; //offset pitch + int16_t pixel; //pixel coord + int16_t best_pixel; //pixel coord + int16_t best_delta; //best pitch + int16_t best_pitch; //best pitch + int16_t start; //of good range + int16_t end; //of good range + int32_t best_count; //lowest sum + float best_sd; //best result + + best_sp_sd = initial_pitch; + + best_pitch = static_cast<int>(initial_pitch); + if (textord_disable_pitch_test || best_pitch <= textord_pitch_range) { + return initial_pitch; + } + std::unique_ptr<STATS[]> sum_proj(new STATS[textord_pitch_range * 2 + 1]); //summed projection + + for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; + pitch_delta++) + sum_proj[textord_pitch_range + pitch_delta].set_range (0, + best_pitch + + pitch_delta + 1); + for (pixel = projection_left; pixel <= projection_right; pixel++) { + for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; + pitch_delta++) { + sum_proj[textord_pitch_range + pitch_delta].add( + (pixel - projection_left) % (best_pitch + pitch_delta), + projection->pile_count(pixel)); + } + } + best_count = sum_proj[textord_pitch_range].pile_count (0); + best_delta = 0; + best_pixel = 0; + for (pitch_delta = -textord_pitch_range; pitch_delta <= textord_pitch_range; + pitch_delta++) { + for (pixel = 0; pixel < best_pitch + pitch_delta; pixel++) { + if (sum_proj[textord_pitch_range + pitch_delta].pile_count (pixel) + < best_count) { + best_count = + sum_proj[textord_pitch_range + + pitch_delta].pile_count (pixel); + best_delta = pitch_delta; + best_pixel = pixel; + } + } + } + if (testing_on) + tprintf ("tune_row_pitch:start pitch=%g, best_delta=%d, count=%d\n", + initial_pitch, best_delta, best_count); + best_pitch += best_delta; + initial_pitch = best_pitch; + best_count++; + best_count += best_count; + for (start = best_pixel - 2; start > best_pixel - best_pitch + && sum_proj[textord_pitch_range + + best_delta].pile_count (start % best_pitch) <= best_count; + start--); + for (end = best_pixel + 2; + end < best_pixel + best_pitch + && sum_proj[textord_pitch_range + + best_delta].pile_count (end % best_pitch) <= best_count; + end++); + + best_sd = + compute_pitch_sd(row, + projection, + projection_left, + projection_right, + space_size, + initial_pitch, + best_sp_sd, + best_mid_cuts, + best_cells, + testing_on, + start, + end); + if (testing_on) + tprintf ("tune_row_pitch:output pitch=%g, sd=%g\n", initial_pitch, + best_sd); + + if (textord_debug_pitch_metric) + print_pitch_sd(row, + projection, + projection_left, + projection_right, + space_size, + initial_pitch); + + return best_sd; +} + + +/********************************************************************** + * compute_pitch_sd + * + * Use a dp algorithm to fit the character cells and return the sd of + * the cell size over the row. + **********************************************************************/ + +float compute_pitch_sd( //find fp cells + TO_ROW* row, //row to do + STATS* projection, //vertical projection + int16_t projection_left, //edge + int16_t projection_right, //edge + float space_size, //size of blank + float initial_pitch, //guess at pitch + float& sp_sd, //space sd + int16_t& mid_cuts, //no of free cuts + ICOORDELT_LIST* row_cells, //list of chop pts + bool testing_on, //inidividual words + int16_t start, //start of good range + int16_t end //end of good range +) { + int16_t occupation; //no of cells in word. + //blobs + BLOBNBOX_IT blob_it = row->blob_list (); + BLOBNBOX_IT start_it; //start of word + BLOBNBOX_IT plot_it; //for plotting + int16_t blob_count; //no of blobs + TBOX blob_box; //bounding box + TBOX prev_box; //of super blob + int32_t prev_right; //of word sync + int scale_factor; //on scores for big words + int32_t sp_count; //spaces + FPSEGPT_LIST seg_list; //char cells + FPSEGPT_IT seg_it; //iterator + int16_t segpos; //position of segment + int16_t cellpos; //previous cell boundary + //iterator + ICOORDELT_IT cell_it = row_cells; + ICOORDELT *cell; //new cell + double sqsum; //sum of squares + double spsum; //of spaces + double sp_var; //space error + double word_sync; //result for word + int32_t total_count; //total blobs + + if ((pitsync_linear_version & 3) > 1) { + word_sync = compute_pitch_sd2 (row, projection, projection_left, + projection_right, initial_pitch, + occupation, mid_cuts, row_cells, + testing_on, start, end); + sp_sd = occupation; + return word_sync; + } + mid_cuts = 0; + cellpos = 0; + total_count = 0; + sqsum = 0; + sp_count = 0; + spsum = 0; + prev_right = -1; + if (blob_it.empty ()) + return space_size * 10; +#ifndef GRAPHICS_DISABLED + if (testing_on && to_win != nullptr) { + blob_box = blob_it.data ()->bounding_box (); + projection->plot (to_win, projection_left, + row->intercept (), 1.0f, -1.0f, ScrollView::CORAL); + } +#endif + start_it = blob_it; + blob_count = 0; + blob_box = box_next (&blob_it);//first blob + blob_it.mark_cycle_pt (); + do { + for (; blob_count > 0; blob_count--) + box_next(&start_it); + do { + prev_box = blob_box; + blob_count++; + blob_box = box_next (&blob_it); + } + while (!blob_it.cycled_list () + && blob_box.left () - prev_box.right () < space_size); + plot_it = start_it; + if (pitsync_linear_version & 3) + word_sync = + check_pitch_sync2 (&start_it, blob_count, static_cast<int16_t>(initial_pitch), 2, + projection, projection_left, projection_right, + row->xheight * textord_projection_scale, + occupation, &seg_list, start, end); + else + word_sync = + check_pitch_sync (&start_it, blob_count, static_cast<int16_t>(initial_pitch), 2, + projection, &seg_list); + if (testing_on) { + tprintf ("Word ending at (%d,%d), len=%d, sync rating=%g, ", + prev_box.right (), prev_box.top (), + seg_list.length () - 1, word_sync); + seg_it.set_to_list (&seg_list); + for (seg_it.mark_cycle_pt (); !seg_it.cycled_list (); + seg_it.forward ()) { + if (seg_it.data ()->faked) + tprintf ("(F)"); + tprintf ("%d, ", seg_it.data ()->position ()); + // tprintf("C=%g, s=%g, sq=%g\n", + // seg_it.data()->cost_function(), + // seg_it.data()->sum(), + // seg_it.data()->squares()); + } + tprintf ("\n"); + } +#ifndef GRAPHICS_DISABLED + if (textord_show_fixed_cuts && blob_count > 0 && to_win != nullptr) + plot_fp_cells2(to_win, ScrollView::GOLDENROD, row, &seg_list); +#endif + seg_it.set_to_list (&seg_list); + if (prev_right >= 0) { + sp_var = seg_it.data ()->position () - prev_right; + sp_var -= floor (sp_var / initial_pitch + 0.5) * initial_pitch; + sp_var *= sp_var; + spsum += sp_var; + sp_count++; + } + for (seg_it.mark_cycle_pt (); !seg_it.cycled_list (); seg_it.forward ()) { + segpos = seg_it.data ()->position (); + if (cell_it.empty () || segpos > cellpos + initial_pitch / 2) { + //big gap + while (!cell_it.empty () && segpos > cellpos + initial_pitch * 3 / 2) { + cell = new ICOORDELT (cellpos + static_cast<int16_t>(initial_pitch), 0); + cell_it.add_after_then_move (cell); + cellpos += static_cast<int16_t>(initial_pitch); + } + //make new one + cell = new ICOORDELT (segpos, 0); + cell_it.add_after_then_move (cell); + cellpos = segpos; + } + else if (segpos > cellpos - initial_pitch / 2) { + cell = cell_it.data (); + //average positions + cell->set_x ((cellpos + segpos) / 2); + cellpos = cell->x (); + } + } + seg_it.move_to_last (); + prev_right = seg_it.data ()->position (); + if (textord_pitch_scalebigwords) { + scale_factor = (seg_list.length () - 2) / 2; + if (scale_factor < 1) + scale_factor = 1; + } + else + scale_factor = 1; + sqsum += word_sync * scale_factor; + total_count += (seg_list.length () - 1) * scale_factor; + seg_list.clear (); + } + while (!blob_it.cycled_list ()); + sp_sd = sp_count > 0 ? sqrt (spsum / sp_count) : 0; + return total_count > 0 ? sqrt (sqsum / total_count) : space_size * 10; +} + + +/********************************************************************** + * compute_pitch_sd2 + * + * Use a dp algorithm to fit the character cells and return the sd of + * the cell size over the row. + **********************************************************************/ + +float compute_pitch_sd2( //find fp cells + TO_ROW* row, //row to do + STATS* projection, //vertical projection + int16_t projection_left, //edge + int16_t projection_right, //edge + float initial_pitch, //guess at pitch + int16_t& occupation, //no of occupied cells + int16_t& mid_cuts, //no of free cuts + ICOORDELT_LIST* row_cells, //list of chop pts + bool testing_on, //inidividual words + int16_t start, //start of good range + int16_t end //end of good range +) { + //blobs + BLOBNBOX_IT blob_it = row->blob_list (); + BLOBNBOX_IT plot_it; + int16_t blob_count; //no of blobs + TBOX blob_box; //bounding box + FPSEGPT_LIST seg_list; //char cells + FPSEGPT_IT seg_it; //iterator + int16_t segpos; //position of segment + //iterator + ICOORDELT_IT cell_it = row_cells; + ICOORDELT *cell; //new cell + double word_sync; //result for word + + mid_cuts = 0; + if (blob_it.empty ()) { + occupation = 0; + return initial_pitch * 10; + } +#ifndef GRAPHICS_DISABLED + if (testing_on && to_win != nullptr) { + projection->plot (to_win, projection_left, + row->intercept (), 1.0f, -1.0f, ScrollView::CORAL); + } +#endif + blob_count = 0; + blob_it.mark_cycle_pt (); + do { + //first blob + blob_box = box_next (&blob_it); + blob_count++; + } + while (!blob_it.cycled_list ()); + plot_it = blob_it; + word_sync = check_pitch_sync2 (&blob_it, blob_count, static_cast<int16_t>(initial_pitch), + 2, projection, projection_left, + projection_right, + row->xheight * textord_projection_scale, + occupation, &seg_list, start, end); + if (testing_on) { + tprintf ("Row ending at (%d,%d), len=%d, sync rating=%g, ", + blob_box.right (), blob_box.top (), + seg_list.length () - 1, word_sync); + seg_it.set_to_list (&seg_list); + for (seg_it.mark_cycle_pt (); !seg_it.cycled_list (); seg_it.forward ()) { + if (seg_it.data ()->faked) + tprintf ("(F)"); + tprintf ("%d, ", seg_it.data ()->position ()); + // tprintf("C=%g, s=%g, sq=%g\n", + // seg_it.data()->cost_function(), + // seg_it.data()->sum(), + // seg_it.data()->squares()); + } + tprintf ("\n"); + } +#ifndef GRAPHICS_DISABLED + if (textord_show_fixed_cuts && blob_count > 0 && to_win != nullptr) + plot_fp_cells2(to_win, ScrollView::GOLDENROD, row, &seg_list); +#endif + seg_it.set_to_list (&seg_list); + for (seg_it.mark_cycle_pt (); !seg_it.cycled_list (); seg_it.forward ()) { + segpos = seg_it.data ()->position (); + //make new one + cell = new ICOORDELT (segpos, 0); + cell_it.add_after_then_move (cell); + if (seg_it.at_last ()) + mid_cuts = seg_it.data ()->cheap_cuts (); + } + seg_list.clear (); + return occupation > 0 ? sqrt (word_sync / occupation) : initial_pitch * 10; +} + + +/********************************************************************** + * print_pitch_sd + * + * Use a dp algorithm to fit the character cells and return the sd of + * the cell size over the row. + **********************************************************************/ + +void print_pitch_sd( //find fp cells + TO_ROW *row, //row to do + STATS *projection, //vertical projection + int16_t projection_left, //edges //size of blank + int16_t projection_right, + float space_size, + float initial_pitch //guess at pitch + ) { + const char *res2; //pitch result + int16_t occupation; //used cells + float sp_sd; //space sd + //blobs + BLOBNBOX_IT blob_it = row->blob_list (); + BLOBNBOX_IT start_it; //start of word + BLOBNBOX_IT row_start; //start of row + int16_t blob_count; //no of blobs + int16_t total_blob_count; //total blobs in line + TBOX blob_box; //bounding box + TBOX prev_box; //of super blob + int32_t prev_right; //of word sync + int scale_factor; //on scores for big words + int32_t sp_count; //spaces + FPSEGPT_LIST seg_list; //char cells + FPSEGPT_IT seg_it; //iterator + double sqsum; //sum of squares + double spsum; //of spaces + double sp_var; //space error + double word_sync; //result for word + double total_count; //total cuts + + if (blob_it.empty ()) + return; + row_start = blob_it; + total_blob_count = 0; + + total_count = 0; + sqsum = 0; + sp_count = 0; + spsum = 0; + prev_right = -1; + blob_it = row_start; + start_it = blob_it; + blob_count = 0; + blob_box = box_next (&blob_it);//first blob + blob_it.mark_cycle_pt (); + do { + for (; blob_count > 0; blob_count--) + box_next(&start_it); + do { + prev_box = blob_box; + blob_count++; + blob_box = box_next (&blob_it); + } + while (!blob_it.cycled_list () + && blob_box.left () - prev_box.right () < space_size); + word_sync = + check_pitch_sync2 (&start_it, blob_count, static_cast<int16_t>(initial_pitch), 2, + projection, projection_left, projection_right, + row->xheight * textord_projection_scale, + occupation, &seg_list, 0, 0); + total_blob_count += blob_count; + seg_it.set_to_list (&seg_list); + if (prev_right >= 0) { + sp_var = seg_it.data ()->position () - prev_right; + sp_var -= floor (sp_var / initial_pitch + 0.5) * initial_pitch; + sp_var *= sp_var; + spsum += sp_var; + sp_count++; + } + seg_it.move_to_last (); + prev_right = seg_it.data ()->position (); + if (textord_pitch_scalebigwords) { + scale_factor = (seg_list.length () - 2) / 2; + if (scale_factor < 1) + scale_factor = 1; + } + else + scale_factor = 1; + sqsum += word_sync * scale_factor; + total_count += (seg_list.length () - 1) * scale_factor; + seg_list.clear (); + } + while (!blob_it.cycled_list ()); + sp_sd = sp_count > 0 ? sqrt (spsum / sp_count) : 0; + word_sync = total_count > 0 ? sqrt (sqsum / total_count) : space_size * 10; + tprintf ("new_sd=%g:sd/p=%g:new_sp_sd=%g:res=%c:", + word_sync, word_sync / initial_pitch, sp_sd, + word_sync < textord_words_pitchsd_threshold * initial_pitch + ? 'F' : 'P'); + + start_it = row_start; + blob_it = row_start; + word_sync = + check_pitch_sync2 (&blob_it, total_blob_count, static_cast<int16_t>(initial_pitch), 2, + projection, projection_left, projection_right, + row->xheight * textord_projection_scale, occupation, + &seg_list, 0, 0); + if (occupation > 1) + word_sync /= occupation; + word_sync = sqrt (word_sync); + +#ifndef GRAPHICS_DISABLED + if (textord_show_row_cuts && to_win != nullptr) + plot_fp_cells2(to_win, ScrollView::CORAL, row, &seg_list); +#endif + seg_list.clear (); + if (word_sync < textord_words_pitchsd_threshold * initial_pitch) { + if (word_sync < textord_words_def_fixed * initial_pitch + && !row->all_caps) + res2 = "DF"; + else + res2 = "MF"; + } + else + res2 = word_sync < textord_words_def_prop * initial_pitch ? "MP" : "DP"; + tprintf + ("row_sd=%g:sd/p=%g:res=%c:N=%d:res2=%s,init pitch=%g, row_pitch=%g, all_caps=%d\n", + word_sync, word_sync / initial_pitch, + word_sync < textord_words_pitchsd_threshold * initial_pitch ? 'F' : 'P', + occupation, res2, initial_pitch, row->fixed_pitch, row->all_caps); +} + +/********************************************************************** + * find_repeated_chars + * + * Extract marked leader blobs and put them + * into words in advance of fixed pitch checking and word generation. + **********************************************************************/ +void find_repeated_chars(TO_BLOCK* block, // Block to search. + bool testing_on) { // Debug mode. + POLY_BLOCK* pb = block->block->pdblk.poly_block(); + if (pb != nullptr && !pb->IsText()) + return; // Don't find repeated chars in non-text blocks. + + TO_ROW *row; + BLOBNBOX_IT box_it; + BLOBNBOX_IT search_it; // forward search + WERD *word; // new word + TBOX word_box; // for plotting + int blobcount, repeated_set; + + TO_ROW_IT row_it = block->get_rows(); + if (row_it.empty()) return; // empty block + for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) { + row = row_it.data(); + box_it.set_to_list(row->blob_list()); + if (box_it.empty()) continue; // no blobs in this row + if (!row->rep_chars_marked()) { + mark_repeated_chars(row); + } + if (row->num_repeated_sets() == 0) continue; // nothing to do for this row + // new words + WERD_IT word_it(&row->rep_words); + do { + if (box_it.data()->repeated_set() != 0 && + !box_it.data()->joined_to_prev()) { + blobcount = 1; + repeated_set = box_it.data()->repeated_set(); + search_it = box_it; + search_it.forward(); + while (!search_it.at_first() && + search_it.data()->repeated_set() == repeated_set) { + blobcount++; + search_it.forward(); + } + // After the call to make_real_word() all the blobs from this + // repeated set will be removed from the blob list. box_it will be + // set to point to the blob after the end of the extracted sequence. + word = make_real_word(&box_it, blobcount, box_it.at_first(), 1); + if (!box_it.empty() && box_it.data()->joined_to_prev()) { + tprintf("Bad box joined to prev at"); + box_it.data()->bounding_box().print(); + tprintf("After repeated word:"); + word->bounding_box().print(); + } + ASSERT_HOST(box_it.empty() || !box_it.data()->joined_to_prev()); + word->set_flag(W_REP_CHAR, true); + word->set_flag(W_DONT_CHOP, true); + word_it.add_after_then_move(word); + } else { + box_it.forward(); + } + } while (!box_it.at_first()); + } +} + + +/********************************************************************** + * plot_fp_word + * + * Plot a block of words as if fixed pitch. + **********************************************************************/ + +#ifndef GRAPHICS_DISABLED +void plot_fp_word( //draw block of words + TO_BLOCK *block, //block to draw + float pitch, //pitch to draw with + float nonspace //for space threshold + ) { + TO_ROW *row; //current row + TO_ROW_IT row_it = block->get_rows (); + + for (row_it.mark_cycle_pt (); !row_it.cycled_list (); row_it.forward ()) { + row = row_it.data (); + row->min_space = static_cast<int32_t>((pitch + nonspace) / 2); + row->max_nonspace = row->min_space; + row->space_threshold = row->min_space; + plot_word_decisions (to_win, static_cast<int16_t>(pitch), row); + } +} +#endif + +} // namespace tesseract |