27 "If true, word delimiter spaces are assumed to have "
28 "variable width, even though characters have fixed pitch.");
33 static const float kFPTolerance = 0.1;
37 static const float kFixedPitchThreshold = 0.35;
42 SimpleStats(): finalized_(false), values_() { }
50 void Add(
float value) {
51 values_.push_back(value);
56 values_.sort(float_compare);
60 float ile(
double frac) {
61 if (!finalized_) Finish();
62 if (values_.empty())
return 0.0;
63 if (frac >= 1.0)
return values_.back();
64 if (frac <= 0.0 || values_.size() == 1)
return values_[0];
65 int index =
static_cast<int>((values_.size() - 1) * frac);
66 float reminder = (values_.size() - 1) * frac - index;
68 return values_[index] * (1.0 - reminder) +
69 values_[index + 1] * reminder;
77 if (!finalized_) Finish();
78 if (values_.empty())
return 0.0;
79 return values_.back();
83 if (!finalized_) Finish();
84 if (values_.empty())
return 0.0;
89 return values_.size();
93 static int float_compare(
const void* a,
const void* b) {
94 const float* f_a =
reinterpret_cast<const float*
>(a);
95 const float* f_b =
reinterpret_cast<const float*
>(b);
96 return (*f_a > *f_b) ? 1 : ((*f_a < *f_b) ? -1 : 0);
106 class LocalCorrelation {
113 LocalCorrelation(): finalized_(false) { }
114 ~LocalCorrelation() { }
117 values_.sort(float_pair_compare);
125 void Add(
float x,
float y,
int v) {
126 struct float_pair value;
130 values_.push_back(value);
134 float EstimateYFor(
float x,
float r) {
136 int start = 0, end = values_.size();
139 while (start < values_.size() && values_[start].x < x * (1.0 - r)) start++;
140 while (end - 1 >= 0 && values_[end - 1].x > x * (1.0 + r)) end--;
146 end = values_.size();
152 for (
int i = start; i < end; i++) {
153 rc += values_[i].vote * x * values_[i].y / values_[i].x;
154 vote += values_[i].vote;
161 static int float_pair_compare(
const void* a,
const void* b) {
162 const float_pair* f_a =
reinterpret_cast<const float_pair*
>(a);
163 const float_pair* f_b =
reinterpret_cast<const float_pair*
>(b);
164 return (f_a->x > f_b->x) ? 1 : ((f_a->x < f_b->x) ? -1 : 0);
176 ALIGN_UNKNOWN, ALIGN_GOOD, ALIGN_BAD
179 FPChar(): box_(), real_body_(),
180 from_(
NULL), to_(
NULL), num_blobs_(0), max_gap_(0),
181 final_(false), alignment_(ALIGN_UNKNOWN),
182 merge_to_prev_(false), delete_flag_(false) {
195 void Merge(
const FPChar &next) {
196 int gap = real_body_.x_gap(next.real_body_);
197 if (gap > max_gap_) max_gap_ = gap;
200 real_body_ += next.real_body_;
202 num_blobs_ += next.num_blobs_;
206 const TBOX &box()
const {
return box_; }
207 void set_box(
const TBOX &box) {
210 const TBOX &real_body()
const {
return real_body_; }
212 bool is_final()
const {
return final_; }
213 void set_final(
bool flag) {
217 const Alignment& alignment()
const {
220 void set_alignment(Alignment alignment) {
221 alignment_ = alignment;
224 bool merge_to_prev()
const {
225 return merge_to_prev_;
227 void set_merge_to_prev(
bool flag) {
228 merge_to_prev_ = flag;
231 bool delete_flag()
const {
234 void set_delete_flag(
bool flag) {
238 int max_gap()
const {
242 int num_blobs()
const {
258 Alignment alignment_;
271 FPRow() : pitch_(0.0f), estimated_pitch_(0.0f),
272 all_pitches_(), all_gaps_(), good_pitches_(), good_gaps_(),
273 heights_(), characters_(), real_row_(
NULL) {
285 void EstimatePitch(
bool pass1);
301 void MergeFragments();
305 void FinalizeLargeChars();
308 void OutputEstimations();
310 void DebugOutputResult(
int row_index);
313 return good_pitches_.size();
317 return good_gaps_.size();
324 float estimated_pitch() {
325 return estimated_pitch_;
328 void set_estimated_pitch(
float v) {
329 estimated_pitch_ = v;
336 float height_pitch_ratio() {
337 if (good_pitches_.size() < 2)
return -1.0;
338 return height_ / good_pitches_.median();
346 return characters_.size();
349 return &characters_[i];
352 const TBOX &box(
int i) {
353 return characters_[i].box();
356 const TBOX &real_body(
int i) {
357 return characters_[i].real_body();
360 bool is_box_modified(
int i) {
361 return !(characters_[i].box() == characters_[i].real_body());
364 float center_x(
int i) {
365 return (characters_[i].box().left() + characters_[i].box().right()) / 2.0;
368 bool is_final(
int i) {
369 return characters_[i].is_final();
372 void finalize(
int i) {
373 characters_[i].set_final(
true);
376 bool is_good(
int i) {
377 return characters_[i].alignment() == FPChar::ALIGN_GOOD;
381 return characters_[i].alignment() == FPChar::ALIGN_BAD;
384 bool is_unknown(
int i) {
385 return characters_[i].alignment() == FPChar::ALIGN_UNKNOWN;
388 void mark_good(
int i) {
389 characters_[i].set_alignment(FPChar::ALIGN_GOOD);
392 void mark_bad(
int i) {
393 characters_[i].set_alignment(FPChar::ALIGN_BAD);
396 void clear_alignment(
int i) {
397 characters_[i].set_alignment(FPChar::ALIGN_UNKNOWN);
401 static float x_overlap_fraction(
const TBOX& box1,
const TBOX& box2) {
406 static bool mostly_overlap(
const TBOX& box1,
const TBOX& box2) {
407 return x_overlap_fraction(box1, box2) > 0.9;
410 static bool significant_overlap(
const TBOX& box1,
const TBOX& box2) {
412 int overlap = -box1.
x_gap(box2);
413 return overlap > 1 || x_overlap_fraction(box1, box2) > 0.1;
416 static float box_pitch(
const TBOX& ref,
const TBOX& box) {
421 static bool is_good_pitch(
float pitch,
const TBOX& box1,
const TBOX& box2) {
423 if (box1.
width() >= pitch * (1.0 + kFPTolerance) ||
424 box2.
width() >= pitch * (1.0 + kFPTolerance) ||
425 box1.
height() >= pitch * (1.0 + kFPTolerance) ||
426 box2.
height() >= pitch * (1.0 + kFPTolerance))
return false;
428 const float real_pitch = box_pitch(box1, box2);
429 if (fabs(real_pitch - pitch) < pitch * kFPTolerance)
return true;
434 if (real_pitch > pitch && real_pitch < pitch * 2.0 &&
435 real_pitch - box1.
x_gap(box2) < pitch) {
442 static bool is_interesting_blob(
const BLOBNBOX *blob) {
449 for (
int i = 0; i < characters_.size(); ++i) {
450 if (!characters_[i].delete_flag()) {
451 if (index != i) characters_[index] = characters_[i];
455 characters_.truncate(index);
459 float estimated_pitch_;
465 SimpleStats all_pitches_;
467 SimpleStats all_gaps_;
470 SimpleStats good_pitches_;
473 SimpleStats good_gaps_;
475 SimpleStats heights_;
481 void FPRow::Init(
TO_ROW *row) {
490 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
491 if (is_interesting_blob(blob_it.data())) {
493 fp_char.Init(blob_it.data());
495 if (!characters_.empty() &&
496 significant_overlap(fp_char.box(), characters_.back().box())) {
497 characters_.back().Merge(fp_char);
499 characters_.push_back(fp_char);
501 TBOX bound = blob_it.data()->bounding_box();
503 heights_.Add(bound.
height());
508 height_ = heights_.ile(0.875);
511 void FPRow::OutputEstimations() {
512 if (good_pitches_.size() == 0) {
518 pitch_ = good_pitches_.median();
519 real_row_->fixed_pitch = pitch_;
523 real_row_->kern_size = real_row_->pr_nonsp =
524 MIN(good_gaps_.ile(0.125),
MAX(pitch_ - height_, 0));
525 real_row_->body_size = pitch_ - real_row_->kern_size;
527 if (good_pitches_.size() < all_pitches_.size() * kFixedPitchThreshold) {
536 }
else if (good_pitches_.size() > all_pitches_.size() * 0.75) {
542 real_row_->space_size = real_row_->pr_space = pitch_;
545 real_row_->min_space = (pitch_ + good_gaps_.minimum()) * 0.5;
549 real_row_->max_nonspace =
MAX(pitch_ * 0.25 + good_gaps_.minimum(),
550 (double)good_gaps_.ile(0.875));
552 int space_threshold =
553 MIN((real_row_->max_nonspace + real_row_->min_space) / 2,
558 for (
int i = 0; i < num_chars(); ++i) {
559 if (characters_[i].max_gap() > real_row_->max_nonspace) {
560 real_row_->max_nonspace = characters_[i].max_gap();
563 real_row_->space_threshold =
564 MIN((real_row_->max_nonspace + real_row_->min_space) / 2,
566 real_row_->used_dm_model =
false;
569 ICOORDELT_IT cell_it = &real_row_->char_cells;
571 cell_it.add_after_then_move(cell);
573 int right = real_body(0).right();
574 for (
int i = 1; i < num_chars(); ++i) {
579 if ((is_final(i - 1) || is_final(i)) &&
580 real_body(i - 1).x_gap(real_body(i)) > space_threshold) {
582 cell_it.add_after_then_move(cell);
583 while (right + pitch_ < box(i).left()) {
586 cell_it.add_after_then_move(cell);
588 right = box(i).
left();
590 cell =
new ICOORDELT((right + real_body(i).left()) / 2, 0);
591 cell_it.add_after_then_move(cell);
592 right = real_body(i).right();
596 cell_it.add_after_then_move(cell);
603 void FPRow::EstimatePitch(
bool pass1) {
604 good_pitches_.Clear();
605 all_pitches_.Clear();
609 if (num_chars() == 0)
return;
612 bool prev_was_good = is_good(0);
615 heights_.Add(box(0).height());
616 for (
int i = 1; i < num_chars(); i++) {
618 inT32 pitch = cx1 - cx0;
619 inT32 gap =
MAX(0, real_body(i - 1).x_gap(real_body(i)));
621 heights_.Add(box(i).height());
624 if (pitch > height_ * 0.5) {
625 all_pitches_.Add(pitch);
634 if (pass1 || (prev_was_good &&
635 fabs(estimated_pitch_ - pitch) <
636 kFPTolerance * estimated_pitch_)) {
637 good_pitches_.Add(pitch);
638 if (!is_box_modified(i - 1) && !is_box_modified(i)) {
642 prev_was_good =
true;
644 prev_was_good =
false;
650 good_pitches_.Finish();
651 all_pitches_.Finish();
656 height_ = heights_.ile(0.875);
657 if (all_pitches_.size() == 0) {
660 }
else if (good_pitches_.size() < 2) {
663 pitch_ = all_pitches_.median();
665 gap_ = all_gaps_.ile(0.125);
667 pitch_ = good_pitches_.median();
669 gap_ = good_gaps_.ile(0.125);
673 void FPRow::DebugOutputResult(
int row_index) {
674 if (num_chars() > 0) {
675 tprintf(
"Row %d: pitch_decision=%d, fixed_pitch=%f, max_nonspace=%d, "
676 "space_size=%f, space_threshold=%d, xheight=%f\n",
677 row_index, (
int)(real_row_->pitch_decision),
678 real_row_->fixed_pitch, real_row_->max_nonspace,
679 real_row_->space_size, real_row_->space_threshold,
682 for (
int i = 0; i < num_chars(); i++) {
683 tprintf(
"Char %d: is_final=%d is_good=%d num_blobs=%d: ",
684 i, is_final(i), is_good(i),
character(i)->num_blobs());
690 void FPRow::Pass1Analyze() {
691 if (num_chars() < 2)
return;
693 if (estimated_pitch_ > 0.0f) {
694 for (
int i = 2; i < num_chars(); i++) {
695 if (is_good_pitch(estimated_pitch_, box(i - 2), box(i-1)) &&
696 is_good_pitch(estimated_pitch_, box(i - 1), box(i))) {
701 for (
int i = 2; i < num_chars(); i++) {
702 if (is_good_pitch(box_pitch(box(i-2), box(i-1)), box(i - 1), box(i))) {
708 character(num_chars() - 1)->set_alignment(
709 character(num_chars() - 2)->alignment());
712 bool FPRow::Pass2Analyze() {
713 bool changed =
false;
714 if (num_chars() <= 1 || estimated_pitch_ == 0.0f) {
717 for (
int i = 0; i < num_chars(); i++) {
718 if (is_final(i))
continue;
720 FPChar::Alignment alignment =
character(i)->alignment();
721 bool intersecting =
false;
722 bool not_intersecting =
false;
724 if (i < num_chars() - 1 && is_final(i + 1)) {
728 bool skipped_whitespaces =
false;
729 float c1 = center_x(i + 1) - 1.5 * estimated_pitch_;
730 while (c1 > box(i).right()) {
731 skipped_whitespaces =
true;
732 c1 -= estimated_pitch_;
734 TBOX ibody(c1, box(i).bottom(), c1 + estimated_pitch_, box(i).top());
740 while (j >= 0 && !is_final(j) && mostly_overlap(ibody, box(j)) &&
742 estimated_pitch_ * (1 + kFPTolerance)) {
747 if (j >= 0 && significant_overlap(ibody, box(j))) {
750 if (!is_final(j)) intersecting =
true;
752 not_intersecting =
true;
758 if (!skipped_whitespaces) mark_good(i);
762 if (box(i).width() <= estimated_pitch_ * 0.5) {
769 for (
int k = i; k > j + 1; k--) {
776 if (i > 0 && is_final(i - 1)) {
780 bool skipped_whitespaces =
false;
781 float c1 = center_x(i - 1) + 1.5 * estimated_pitch_;
782 while (c1 < box(i).left()) {
783 skipped_whitespaces =
true;
784 c1 += estimated_pitch_;
786 TBOX ibody(c1 - estimated_pitch_, box(i).bottom(), c1, box(i).top());
790 while (j < num_chars() && !is_final(j) && mostly_overlap(ibody, box(j)) &&
792 estimated_pitch_ * (1 + kFPTolerance)) {
797 if (j < num_chars() && significant_overlap(ibody, box(j))) {
798 if (!is_final(j)) intersecting =
true;
800 not_intersecting =
true;
803 if (!skipped_whitespaces) mark_good(i);
804 if (box(i).width() <= estimated_pitch_ * 0.5) {
811 for (
int k = i + 1; k < j; k++) {
821 if (intersecting && !not_intersecting) mark_bad(i);
822 if (
character(i)->alignment() != alignment ||
831 void FPRow::MergeFragments() {
834 for (
int j = 0; j < num_chars(); ++j) {
838 clear_alignment(last_char);
839 character(j-1)->set_merge_to_prev(
false);
847 void FPRow::FinalizeLargeChars() {
848 float row_pitch = estimated_pitch();
849 for (
int i = 0; i < num_chars(); i++) {
850 if (is_final(i))
continue;
853 if (i > 0 && is_final(i - 1) && i < num_chars() - 1 && is_final(i + 1)) {
858 float cx = center_x(i);
859 TBOX ibody(cx - 0.5 * row_pitch, 0, cx + 0.5 * row_pitch, 1);
864 if (x_overlap_fraction(ibody, box(i - 1)) > 0.1)
continue;
865 if (!is_final(i - 1)) {
866 TBOX merged = box(i);
867 merged += box(i - 1);
868 if (merged.
width() < row_pitch)
continue;
874 if (i < num_chars() - 1) {
875 if (x_overlap_fraction(ibody, box(i + 1)) > 0.1)
continue;
876 if (!is_final(i + 1)) {
877 TBOX merged = box(i);
878 merged += box(i + 1);
879 if (merged.
width() < row_pitch)
continue;
890 for (
int i = 0; i < num_chars(); i++) {
891 if (!is_final(i))
continue;
892 bool good_pitch =
false;
893 bool bad_pitch =
false;
894 if (i > 0 && is_final(i - 1)) {
895 if (is_good_pitch(row_pitch, box(i - 1), box(i))) {
901 if (i < num_chars() - 1 && is_final(i + 1)) {
902 if (is_good_pitch(row_pitch, box(i), box(i + 1))) {
908 if (good_pitch && !bad_pitch) mark_good(i);
909 else if (!good_pitch && bad_pitch) mark_bad(i);
915 FPAnalyzer(): page_tr_(), rows_() { }
918 void Init(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks);
920 void Pass1Analyze() {
921 for (
int i = 0; i < rows_.size(); i++) rows_[i].Pass1Analyze();
927 void EstimatePitch(
bool pass1);
929 bool maybe_fixed_pitch() {
931 rows_.size() <= num_bad_rows_ + num_tall_rows_ + 1)
return false;
935 void MergeFragments() {
936 for (
int i = 0; i < rows_.size(); i++) rows_[i].MergeFragments();
939 void FinalizeLargeChars() {
940 for (
int i = 0; i < rows_.size(); i++) rows_[i].FinalizeLargeChars();
943 bool Pass2Analyze() {
944 bool changed =
false;
945 for (
int i = 0; i < rows_.size(); i++) {
946 if (rows_[i].Pass2Analyze()) {
953 void OutputEstimations() {
954 for (
int i = 0; i < rows_.size(); i++) rows_[i].OutputEstimations();
958 void DebugOutputResult() {
959 tprintf(
"FPAnalyzer: final result\n");
960 for (
int i = 0; i < rows_.size(); i++) rows_[i].DebugOutputResult(i);
968 int max_iteration() {
971 return max_chars_per_row_ + 100;
980 int max_chars_per_row_;
983 void FPAnalyzer::Init(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks) {
986 TO_BLOCK_IT block_it;
987 block_it.set_to_list (port_blocks);
989 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
990 block_it.forward()) {
999 max_chars_per_row_ = 0;
1000 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
1001 block_it.forward()) {
1002 TO_ROW_IT row_it = block_it.data()->get_rows();
1003 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
1005 row.Init(row_it.data());
1006 rows_.push_back(row);
1007 int num_chars = rows_.back().num_chars();
1008 if (num_chars <= 1) num_empty_rows_++;
1009 if (num_chars > max_chars_per_row_) max_chars_per_row_ = num_chars;
1014 void FPAnalyzer::EstimatePitch(
bool pass1) {
1015 LocalCorrelation pitch_height_stats;
1019 pitch_height_stats.Clear();
1020 for (
int i = 0; i < rows_.size(); i++) {
1021 rows_[i].EstimatePitch(pass1);
1022 if (rows_[i].good_pitches()) {
1023 pitch_height_stats.Add(rows_[i].height() + rows_[i].gap(),
1024 rows_[i].pitch(), rows_[i].good_pitches());
1025 if (rows_[i].height_pitch_ratio() > 1.1) num_tall_rows_++;
1031 pitch_height_stats.Finish();
1032 for (
int i = 0; i < rows_.size(); i++) {
1033 if (rows_[i].good_pitches() >= 5) {
1036 rows_[i].set_estimated_pitch(rows_[i].pitch());
1037 }
else if (rows_[i].num_chars() > 1) {
1038 float estimated_pitch =
1039 pitch_height_stats.EstimateYFor(rows_[i].height() + rows_[i].gap(),
1045 if (estimated_pitch > rows_[i].pitch() ||
1046 rows_[i].pitch() > rows_[i].height() * 2.0) {
1047 rows_[i].set_estimated_pitch(estimated_pitch);
1049 rows_[i].set_estimated_pitch(rows_[i].pitch());
1058 TO_BLOCK_LIST *port_blocks) {
1059 FPAnalyzer analyzer;
1060 analyzer.Init(page_tr, port_blocks);
1061 if (analyzer.num_rows() == 0)
return;
1063 analyzer.Pass1Analyze();
1064 analyzer.EstimatePitch(
true);
1068 analyzer.Pass1Analyze();
1069 analyzer.EstimatePitch(
true);
1072 if (!analyzer.maybe_fixed_pitch()) {
1074 tprintf(
"Page doesn't seem to contain fixed pitch rows\n");
1081 analyzer.MergeFragments();
1082 analyzer.FinalizeLargeChars();
1083 analyzer.EstimatePitch(
false);
1085 }
while (analyzer.Pass2Analyze() && iteration < analyzer.max_iteration());
1088 tprintf(
"compute_fixed_pitch_cjk finished after %d iteration (limit=%d)\n",
1089 iteration, analyzer.max_iteration());
1092 analyzer.OutputEstimations();
void compute_fixed_pitch_cjk(ICOORD page_tr, TO_BLOCK_LIST *port_blocks)
bool joined_to_prev() const
bool textord_space_size_is_variable
#define BOOL_VAR(name, val, comment)
BLOBNBOX_LIST * blob_list()
EXTERN bool textord_debug_pitch_test
TBOX bounding_union(const TBOX &box) const
PITCH_TYPE pitch_decision
int x_gap(const TBOX &box) const
void find_repeated_chars(TO_BLOCK *block, BOOL8 testing_on)
const TBOX & bounding_box() const
BlobTextFlowType flow() const