30 "If true, word delimiter spaces are assumed to have " 31 "variable width, even though characters have fixed pitch.");
36 static const float kFPTolerance = 0.1;
40 static const float kFixedPitchThreshold = 0.35;
45 SimpleStats(): finalized_(false), values_() { }
53 void Add(
float value) {
54 values_.push_back(value);
59 values_.sort(float_compare);
63 float ile(
double frac) {
64 if (!finalized_) Finish();
65 if (values_.empty())
return 0.0;
66 if (frac >= 1.0)
return values_.back();
67 if (frac <= 0.0 || values_.size() == 1)
return values_[0];
68 int index =
static_cast<int>((values_.size() - 1) * frac);
69 float reminder = (values_.size() - 1) * frac - index;
71 return values_[index] * (1.0 - reminder) +
72 values_[index + 1] * reminder;
80 if (!finalized_) Finish();
81 if (values_.empty())
return 0.0;
82 return values_.back();
86 if (!finalized_) Finish();
87 if (values_.empty())
return 0.0;
92 return values_.size();
96 static int float_compare(
const void* a,
const void* b) {
97 const float* f_a =
static_cast<const float*
>(a);
98 const float* f_b =
static_cast<const float*
>(b);
99 return (*f_a > *f_b) ? 1 : ((*f_a < *f_b) ? -1 : 0);
109 class LocalCorrelation {
116 LocalCorrelation(): finalized_(false) { }
117 ~LocalCorrelation() { }
120 values_.sort(float_pair_compare);
128 void Add(
float x,
float y,
int v) {
129 struct float_pair value;
133 values_.push_back(value);
137 float EstimateYFor(
float x,
float r) {
139 int start = 0, end = values_.size();
142 while (start < values_.size() && values_[start].x < x * (1.0 - r)) start++;
143 while (end - 1 >= 0 && values_[end - 1].x > x * (1.0 + r)) end--;
149 end = values_.size();
155 for (
int i = start; i < end; i++) {
156 rc += values_[i].vote * x * values_[i].y / values_[i].x;
157 vote += values_[i].vote;
164 static int float_pair_compare(
const void* a,
const void* b) {
165 const float_pair* f_a =
static_cast<const float_pair*
>(a);
166 const float_pair* f_b =
static_cast<const float_pair*
>(b);
167 return (f_a->x > f_b->x) ? 1 : ((f_a->x < f_b->x) ? -1 : 0);
179 ALIGN_UNKNOWN, ALIGN_GOOD, ALIGN_BAD
182 FPChar(): box_(), real_body_(),
183 from_(nullptr), to_(nullptr), num_blobs_(0), max_gap_(0),
184 final_(false), alignment_(ALIGN_UNKNOWN),
185 merge_to_prev_(false), delete_flag_(false) {
198 void Merge(
const FPChar &next) {
199 int gap = real_body_.x_gap(next.real_body_);
200 if (gap > max_gap_) max_gap_ = gap;
203 real_body_ += next.real_body_;
205 num_blobs_ += next.num_blobs_;
209 const TBOX &box()
const {
return box_; }
210 void set_box(
const TBOX &box) {
213 const TBOX &real_body()
const {
return real_body_; }
215 bool is_final()
const {
return final_; }
216 void set_final(
bool flag) {
220 const Alignment& alignment()
const {
223 void set_alignment(Alignment alignment) {
224 alignment_ = alignment;
227 bool merge_to_prev()
const {
228 return merge_to_prev_;
230 void set_merge_to_prev(
bool flag) {
231 merge_to_prev_ = flag;
234 bool delete_flag()
const {
237 void set_delete_flag(
bool flag) {
241 int max_gap()
const {
245 int num_blobs()
const {
261 Alignment alignment_;
274 FPRow() : pitch_(0.0f), estimated_pitch_(0.0f),
275 all_pitches_(), all_gaps_(), good_pitches_(), good_gaps_(),
276 heights_(), characters_(), real_row_(nullptr) {
288 void EstimatePitch(
bool pass1);
304 void MergeFragments();
308 void FinalizeLargeChars();
311 void OutputEstimations();
313 void DebugOutputResult(
int row_index);
316 return good_pitches_.size();
320 return good_gaps_.size();
327 float estimated_pitch() {
328 return estimated_pitch_;
331 void set_estimated_pitch(
float v) {
332 estimated_pitch_ = v;
339 float height_pitch_ratio() {
340 if (good_pitches_.size() < 2)
return -1.0;
341 return height_ / good_pitches_.median();
349 return characters_.size();
352 return &characters_[i];
355 const TBOX &box(
int i) {
356 return characters_[i].box();
359 const TBOX &real_body(
int i) {
360 return characters_[i].real_body();
363 bool is_box_modified(
int i) {
364 return !(characters_[i].box() == characters_[i].real_body());
367 float center_x(
int i) {
368 return (characters_[i].box().left() + characters_[i].box().right()) / 2.0;
371 bool is_final(
int i) {
372 return characters_[i].is_final();
375 void finalize(
int i) {
376 characters_[i].set_final(
true);
379 bool is_good(
int i) {
380 return characters_[i].alignment() == FPChar::ALIGN_GOOD;
384 return characters_[i].alignment() == FPChar::ALIGN_BAD;
387 bool is_unknown(
int i) {
388 return characters_[i].alignment() == FPChar::ALIGN_UNKNOWN;
391 void mark_good(
int i) {
392 characters_[i].set_alignment(FPChar::ALIGN_GOOD);
395 void mark_bad(
int i) {
396 characters_[i].set_alignment(FPChar::ALIGN_BAD);
399 void clear_alignment(
int i) {
400 characters_[i].set_alignment(FPChar::ALIGN_UNKNOWN);
404 static float x_overlap_fraction(
const TBOX& box1,
const TBOX& box2) {
405 if (std::min(box1.
width(), box2.
width()) == 0)
return 0.0;
406 return -box1.
x_gap(box2) / (float)std::min(box1.
width(), box2.
width());
409 static bool mostly_overlap(
const TBOX& box1,
const TBOX& box2) {
410 return x_overlap_fraction(box1, box2) > 0.9;
413 static bool significant_overlap(
const TBOX& box1,
const TBOX& box2) {
414 if (std::min(box1.
width(), box2.
width()) == 0)
return false;
415 int overlap = -box1.
x_gap(box2);
416 return overlap > 1 || x_overlap_fraction(box1, box2) > 0.1;
419 static float box_pitch(
const TBOX& ref,
const TBOX& box) {
424 static bool is_good_pitch(
float pitch,
const TBOX& box1,
const TBOX& box2) {
426 if (box1.
width() >= pitch * (1.0 + kFPTolerance) ||
427 box2.
width() >= pitch * (1.0 + kFPTolerance) ||
428 box1.
height() >= pitch * (1.0 + kFPTolerance) ||
429 box2.
height() >= pitch * (1.0 + kFPTolerance))
return false;
431 const float real_pitch = box_pitch(box1, box2);
432 if (fabs(real_pitch - pitch) < pitch * kFPTolerance)
return true;
437 if (real_pitch > pitch && real_pitch < pitch * 2.0 &&
438 real_pitch - box1.
x_gap(box2) < pitch) {
445 static bool is_interesting_blob(
const BLOBNBOX *blob) {
452 for (
int i = 0; i < characters_.size(); ++i) {
453 if (!characters_[i].delete_flag()) {
454 if (index != i) characters_[index] = characters_[i];
458 characters_.truncate(index);
462 float estimated_pitch_;
468 SimpleStats all_pitches_;
470 SimpleStats all_gaps_;
473 SimpleStats good_pitches_;
476 SimpleStats good_gaps_;
478 SimpleStats heights_;
484 void FPRow::Init(
TO_ROW *row) {
493 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
494 if (is_interesting_blob(blob_it.data())) {
496 fp_char.Init(blob_it.data());
498 if (!characters_.empty() &&
499 significant_overlap(fp_char.box(), characters_.back().box())) {
500 characters_.back().Merge(fp_char);
502 characters_.push_back(fp_char);
504 TBOX bound = blob_it.data()->bounding_box();
506 heights_.Add(bound.
height());
511 height_ = heights_.ile(0.875);
514 void FPRow::OutputEstimations() {
515 if (good_pitches_.size() == 0) {
521 pitch_ = good_pitches_.median();
522 real_row_->fixed_pitch = pitch_;
526 real_row_->kern_size = real_row_->pr_nonsp =
527 std::min(good_gaps_.ile(0.125), std::max(pitch_ - height_, 0.0f));
528 real_row_->body_size = pitch_ - real_row_->kern_size;
530 if (good_pitches_.size() < all_pitches_.size() * kFixedPitchThreshold) {
539 }
else if (good_pitches_.size() > all_pitches_.size() * 0.75) {
545 real_row_->space_size = real_row_->pr_space = pitch_;
548 real_row_->min_space = (pitch_ + good_gaps_.minimum()) * 0.5;
552 real_row_->max_nonspace = std::max(pitch_ * 0.25 + good_gaps_.minimum(),
553 (double)good_gaps_.ile(0.875));
555 int space_threshold =
556 std::min((real_row_->max_nonspace + real_row_->min_space) / 2,
557 static_cast<int>(real_row_->xheight));
561 for (
size_t i = 0; i < num_chars(); ++i) {
562 if (characters_[i].max_gap() > real_row_->max_nonspace) {
563 real_row_->max_nonspace = characters_[i].max_gap();
566 real_row_->space_threshold =
567 std::min((real_row_->max_nonspace + real_row_->min_space) / 2,
568 static_cast<int>(real_row_->xheight));
569 real_row_->used_dm_model =
false;
572 ICOORDELT_IT cell_it = &real_row_->char_cells;
574 cell_it.add_after_then_move(cell);
576 int right = real_body(0).right();
577 for (
size_t i = 1; i < num_chars(); ++i) {
582 if ((is_final(i - 1) || is_final(i)) &&
583 real_body(i - 1).x_gap(real_body(i)) > space_threshold) {
585 cell_it.add_after_then_move(cell);
586 while (right + pitch_ < box(i).left()) {
589 cell_it.add_after_then_move(cell);
591 right = box(i).
left();
593 cell =
new ICOORDELT((right + real_body(i).left()) / 2, 0);
594 cell_it.add_after_then_move(cell);
595 right = real_body(i).right();
599 cell_it.add_after_then_move(cell);
606 void FPRow::EstimatePitch(
bool pass1) {
607 good_pitches_.Clear();
608 all_pitches_.Clear();
612 if (num_chars() == 0)
return;
615 bool prev_was_good = is_good(0);
618 heights_.Add(box(0).height());
619 for (
size_t i = 1; i < num_chars(); i++) {
621 int32_t pitch = cx1 - cx0;
622 int32_t gap = std::max(0, real_body(i - 1).x_gap(real_body(i)));
624 heights_.Add(box(i).height());
627 if (pitch > height_ * 0.5) {
628 all_pitches_.Add(pitch);
637 if (pass1 || (prev_was_good &&
638 fabs(estimated_pitch_ - pitch) <
639 kFPTolerance * estimated_pitch_)) {
640 good_pitches_.Add(pitch);
641 if (!is_box_modified(i - 1) && !is_box_modified(i)) {
645 prev_was_good =
true;
647 prev_was_good =
false;
653 good_pitches_.Finish();
654 all_pitches_.Finish();
659 height_ = heights_.ile(0.875);
660 if (all_pitches_.size() == 0) {
663 }
else if (good_pitches_.size() < 2) {
666 pitch_ = all_pitches_.median();
668 gap_ = all_gaps_.ile(0.125);
670 pitch_ = good_pitches_.median();
672 gap_ = good_gaps_.ile(0.125);
676 void FPRow::DebugOutputResult(
int row_index) {
677 if (num_chars() > 0) {
678 tprintf(
"Row %d: pitch_decision=%d, fixed_pitch=%f, max_nonspace=%d, " 679 "space_size=%f, space_threshold=%d, xheight=%f\n",
680 row_index, (
int)(real_row_->pitch_decision),
681 real_row_->fixed_pitch, real_row_->max_nonspace,
682 real_row_->space_size, real_row_->space_threshold,
685 for (
unsigned i = 0; i < num_chars(); i++) {
686 tprintf(
"Char %u: is_final=%d is_good=%d num_blobs=%d: ",
687 i, is_final(i), is_good(i),
character(i)->num_blobs());
693 void FPRow::Pass1Analyze() {
694 if (num_chars() < 2)
return;
696 if (estimated_pitch_ > 0.0f) {
697 for (
size_t i = 2; i < num_chars(); i++) {
698 if (is_good_pitch(estimated_pitch_, box(i - 2), box(i-1)) &&
699 is_good_pitch(estimated_pitch_, box(i - 1), box(i))) {
704 for (
size_t i = 2; i < num_chars(); i++) {
705 if (is_good_pitch(box_pitch(box(i-2), box(i-1)), box(i - 1), box(i))) {
711 character(num_chars() - 1)->set_alignment(
712 character(num_chars() - 2)->alignment());
715 bool FPRow::Pass2Analyze() {
716 bool changed =
false;
717 if (num_chars() <= 1 || estimated_pitch_ == 0.0f) {
720 for (
size_t i = 0; i < num_chars(); i++) {
721 if (is_final(i))
continue;
723 FPChar::Alignment alignment =
character(i)->alignment();
724 bool intersecting =
false;
725 bool not_intersecting =
false;
727 if (i < num_chars() - 1 && is_final(i + 1)) {
731 bool skipped_whitespaces =
false;
732 float c1 = center_x(i + 1) - 1.5 * estimated_pitch_;
733 while (c1 > box(i).right()) {
734 skipped_whitespaces =
true;
735 c1 -= estimated_pitch_;
737 TBOX ibody(c1, box(i).bottom(), c1 + estimated_pitch_, box(i).top());
743 while (j >= 0 && !is_final(j) && mostly_overlap(ibody, box(j)) &&
745 estimated_pitch_ * (1 + kFPTolerance)) {
750 if (j >= 0 && significant_overlap(ibody, box(j))) {
753 if (!is_final(j)) intersecting =
true;
755 not_intersecting =
true;
761 if (!skipped_whitespaces) mark_good(i);
765 if (box(i).width() <= estimated_pitch_ * 0.5) {
772 for (
int k = i; k > j + 1; k--) {
779 if (i > 0 && is_final(i - 1)) {
783 bool skipped_whitespaces =
false;
784 float c1 = center_x(i - 1) + 1.5 * estimated_pitch_;
785 while (c1 < box(i).left()) {
786 skipped_whitespaces =
true;
787 c1 += estimated_pitch_;
789 TBOX ibody(c1 - estimated_pitch_, box(i).bottom(), c1, box(i).top());
793 while (j < num_chars() && !is_final(j) && mostly_overlap(ibody, box(j)) &&
795 estimated_pitch_ * (1 + kFPTolerance)) {
800 if (j < num_chars() && significant_overlap(ibody, box(j))) {
801 if (!is_final(j)) intersecting =
true;
803 not_intersecting =
true;
806 if (!skipped_whitespaces) mark_good(i);
807 if (box(i).width() <= estimated_pitch_ * 0.5) {
814 for (
size_t k = i + 1; k < j; k++) {
824 if (intersecting && !not_intersecting) mark_bad(i);
825 if (
character(i)->alignment() != alignment ||
834 void FPRow::MergeFragments() {
837 for (
size_t j = 0; j < num_chars(); ++j) {
841 clear_alignment(last_char);
842 character(j-1)->set_merge_to_prev(
false);
850 void FPRow::FinalizeLargeChars() {
851 float row_pitch = estimated_pitch();
852 for (
size_t i = 0; i < num_chars(); i++) {
853 if (is_final(i))
continue;
856 if (i > 0 && is_final(i - 1) && i < num_chars() - 1 && is_final(i + 1)) {
861 float cx = center_x(i);
862 TBOX ibody(cx - 0.5 * row_pitch, 0, cx + 0.5 * row_pitch, 1);
867 if (x_overlap_fraction(ibody, box(i - 1)) > 0.1)
continue;
868 if (!is_final(i - 1)) {
869 TBOX merged = box(i);
870 merged += box(i - 1);
871 if (merged.
width() < row_pitch)
continue;
877 if (i < num_chars() - 1) {
878 if (x_overlap_fraction(ibody, box(i + 1)) > 0.1)
continue;
879 if (!is_final(i + 1)) {
880 TBOX merged = box(i);
881 merged += box(i + 1);
882 if (merged.
width() < row_pitch)
continue;
893 for (
size_t i = 0; i < num_chars(); i++) {
894 if (!is_final(i))
continue;
895 bool good_pitch =
false;
896 bool bad_pitch =
false;
897 if (i > 0 && is_final(i - 1)) {
898 if (is_good_pitch(row_pitch, box(i - 1), box(i))) {
904 if (i < num_chars() - 1 && is_final(i + 1)) {
905 if (is_good_pitch(row_pitch, box(i), box(i + 1))) {
911 if (good_pitch && !bad_pitch) mark_good(i);
912 else if (!good_pitch && bad_pitch) mark_bad(i);
918 FPAnalyzer(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks);
921 void Pass1Analyze() {
922 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].Pass1Analyze();
928 void EstimatePitch(
bool pass1);
930 bool maybe_fixed_pitch() {
932 rows_.size() <= num_bad_rows_ + num_tall_rows_ + 1)
return false;
936 void MergeFragments() {
937 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].MergeFragments();
940 void FinalizeLargeChars() {
941 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].FinalizeLargeChars();
944 bool Pass2Analyze() {
945 bool changed =
false;
946 for (
size_t i = 0; i < rows_.size(); i++) {
947 if (rows_[i].Pass2Analyze()) {
954 void OutputEstimations() {
955 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].OutputEstimations();
959 void DebugOutputResult() {
960 tprintf(
"FPAnalyzer: final result\n");
961 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].DebugOutputResult(i);
969 unsigned max_iteration() {
972 return max_chars_per_row_ + 100;
977 std::vector<FPRow> rows_;
978 unsigned num_tall_rows_;
979 unsigned num_bad_rows_;
981 unsigned num_empty_rows_;
982 unsigned max_chars_per_row_;
985 FPAnalyzer::FPAnalyzer(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks)
990 max_chars_per_row_(0)
992 TO_BLOCK_IT block_it(port_blocks);
994 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
995 block_it.forward()) {
1003 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
1004 block_it.forward()) {
1005 TO_ROW_IT row_it = block_it.data()->get_rows();
1006 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1008 row.Init(row_it.data());
1009 rows_.push_back(row);
1010 size_t num_chars = rows_.back().num_chars();
1011 if (num_chars <= 1) num_empty_rows_++;
1012 if (num_chars > max_chars_per_row_) max_chars_per_row_ = num_chars;
1017 void FPAnalyzer::EstimatePitch(
bool pass1) {
1018 LocalCorrelation pitch_height_stats;
1022 pitch_height_stats.Clear();
1023 for (
size_t i = 0; i < rows_.size(); i++) {
1024 rows_[i].EstimatePitch(pass1);
1025 if (rows_[i].good_pitches()) {
1026 pitch_height_stats.Add(rows_[i].height() + rows_[i].gap(),
1027 rows_[i].pitch(), rows_[i].good_pitches());
1028 if (rows_[i].height_pitch_ratio() > 1.1) num_tall_rows_++;
1034 pitch_height_stats.Finish();
1035 for (
size_t i = 0; i < rows_.size(); i++) {
1036 if (rows_[i].good_pitches() >= 5) {
1039 rows_[i].set_estimated_pitch(rows_[i].pitch());
1040 }
else if (rows_[i].num_chars() > 1) {
1041 float estimated_pitch =
1042 pitch_height_stats.EstimateYFor(rows_[i].height() + rows_[i].gap(),
1048 if (estimated_pitch > rows_[i].pitch() ||
1049 rows_[i].pitch() > rows_[i].height() * 2.0) {
1050 rows_[i].set_estimated_pitch(estimated_pitch);
1052 rows_[i].set_estimated_pitch(rows_[i].pitch());
1061 TO_BLOCK_LIST *port_blocks) {
1062 FPAnalyzer analyzer(page_tr, port_blocks);
1063 if (analyzer.num_rows() == 0)
return;
1065 analyzer.Pass1Analyze();
1066 analyzer.EstimatePitch(
true);
1070 analyzer.Pass1Analyze();
1071 analyzer.EstimatePitch(
true);
1074 if (!analyzer.maybe_fixed_pitch()) {
1076 tprintf(
"Page doesn't seem to contain fixed pitch rows\n");
1081 unsigned iteration = 0;
1083 analyzer.MergeFragments();
1084 analyzer.FinalizeLargeChars();
1085 analyzer.EstimatePitch(
false);
1087 }
while (analyzer.Pass2Analyze() && iteration < analyzer.max_iteration());
1090 tprintf(
"compute_fixed_pitch_cjk finished after %u iteration (limit=%u)\n",
1091 iteration, analyzer.max_iteration());
1094 analyzer.OutputEstimations();
#define BOOL_VAR(name, val, comment)
BlobTextFlowType flow() const
int x_gap(const TBOX &box) const
void find_repeated_chars(TO_BLOCK *block, bool testing_on)
bool joined_to_prev() const
DLLSYM void tprintf(const char *format,...)
void compute_fixed_pitch_cjk(ICOORD page_tr, TO_BLOCK_LIST *port_blocks)
const TBOX & bounding_box() const
bool textord_space_size_is_variable
TBOX bounding_union(const TBOX &box) const
BLOBNBOX_LIST * blob_list()
EXTERN bool textord_debug_pitch_test
PITCH_TYPE pitch_decision