28 static BOOL_VAR(textord_space_size_is_variable,
false,
29 "If true, word delimiter spaces are assumed to have " 30 "variable width, even though characters have fixed pitch.");
35 static const float kFPTolerance = 0.1;
39 static const float kFixedPitchThreshold = 0.35;
44 SimpleStats(): finalized_(false), values_() { }
52 void Add(
float value) {
53 values_.push_back(value);
58 values_.sort(float_compare);
62 float ile(
double frac) {
63 if (!finalized_) Finish();
64 if (values_.empty())
return 0.0;
65 if (frac >= 1.0)
return values_.back();
66 if (frac <= 0.0 || values_.size() == 1)
return values_[0];
67 int index =
static_cast<int>((values_.size() - 1) * frac);
68 float reminder = (values_.size() - 1) * frac - index;
70 return values_[index] * (1.0 - reminder) +
71 values_[index + 1] * reminder;
79 if (!finalized_) Finish();
80 if (values_.empty())
return 0.0;
85 return values_.size();
89 static int float_compare(
const void* a,
const void* b) {
90 const auto* f_a =
static_cast<const float*
>(a);
91 const auto* f_b =
static_cast<const float*
>(b);
92 return (*f_a > *f_b) ? 1 : ((*f_a < *f_b) ? -1 : 0);
102 class LocalCorrelation {
109 LocalCorrelation(): finalized_(false) { }
110 ~LocalCorrelation() { }
113 values_.sort(float_pair_compare);
121 void Add(
float x,
float y,
int v) {
122 struct float_pair value;
126 values_.push_back(value);
130 float EstimateYFor(
float x,
float r) {
132 int start = 0, end = values_.size();
135 while (start < values_.size() && values_[start].x < x * (1.0 - r)) start++;
136 while (end - 1 >= 0 && values_[end - 1].x > x * (1.0 + r)) end--;
142 end = values_.size();
148 for (
int i = start; i < end; i++) {
149 rc += values_[i].vote * x * values_[i].y / values_[i].x;
150 vote += values_[i].vote;
157 static int float_pair_compare(
const void* a,
const void* b) {
158 const auto* f_a =
static_cast<const float_pair*
>(a);
159 const auto* f_b =
static_cast<const float_pair*
>(b);
160 return (f_a->x > f_b->x) ? 1 : ((f_a->x < f_b->x) ? -1 : 0);
172 ALIGN_UNKNOWN, ALIGN_GOOD, ALIGN_BAD
175 FPChar(): box_(), real_body_(),
176 from_(nullptr), to_(nullptr), num_blobs_(0), max_gap_(0),
177 final_(false), alignment_(ALIGN_UNKNOWN),
178 merge_to_prev_(false), delete_flag_(false) {
191 void Merge(
const FPChar &next) {
192 int gap = real_body_.x_gap(next.real_body_);
193 if (gap > max_gap_) max_gap_ = gap;
196 real_body_ += next.real_body_;
198 num_blobs_ += next.num_blobs_;
202 const TBOX &box()
const {
return box_; }
203 void set_box(
const TBOX &box) {
206 const TBOX &real_body()
const {
return real_body_; }
208 bool is_final()
const {
return final_; }
209 void set_final(
bool flag) {
213 const Alignment& alignment()
const {
216 void set_alignment(Alignment alignment) {
217 alignment_ = alignment;
220 bool merge_to_prev()
const {
221 return merge_to_prev_;
223 void set_merge_to_prev(
bool flag) {
224 merge_to_prev_ = flag;
227 bool delete_flag()
const {
230 void set_delete_flag(
bool flag) {
234 int max_gap()
const {
238 int num_blobs()
const {
254 Alignment alignment_;
267 FPRow() : pitch_(0.0f), estimated_pitch_(0.0f),
268 all_pitches_(), all_gaps_(), good_pitches_(), good_gaps_(),
269 heights_(), characters_(), real_row_(nullptr) {
281 void EstimatePitch(
bool pass1);
297 void MergeFragments();
301 void FinalizeLargeChars();
304 void OutputEstimations();
306 void DebugOutputResult(
int row_index);
309 return good_pitches_.size();
316 float estimated_pitch() {
317 return estimated_pitch_;
320 void set_estimated_pitch(
float v) {
321 estimated_pitch_ = v;
328 float height_pitch_ratio() {
329 if (good_pitches_.size() < 2)
return -1.0;
330 return height_ / good_pitches_.median();
338 return characters_.size();
341 return &characters_[i];
344 const TBOX &box(
int i) {
345 return characters_[i].box();
348 const TBOX &real_body(
int i) {
349 return characters_[i].real_body();
352 bool is_box_modified(
int i) {
353 return !(characters_[i].box() == characters_[i].real_body());
356 float center_x(
int i) {
357 return (characters_[i].box().left() + characters_[i].box().right()) / 2.0;
360 bool is_final(
int i) {
361 return characters_[i].is_final();
364 void finalize(
int i) {
365 characters_[i].set_final(
true);
368 bool is_good(
int i) {
369 return characters_[i].alignment() == FPChar::ALIGN_GOOD;
372 void mark_good(
int i) {
373 characters_[i].set_alignment(FPChar::ALIGN_GOOD);
376 void mark_bad(
int i) {
377 characters_[i].set_alignment(FPChar::ALIGN_BAD);
380 void clear_alignment(
int i) {
381 characters_[i].set_alignment(FPChar::ALIGN_UNKNOWN);
385 static float x_overlap_fraction(
const TBOX& box1,
const TBOX& box2) {
386 if (std::min(box1.
width(), box2.
width()) == 0)
return 0.0;
387 return -box1.
x_gap(box2) /
static_cast<float>(std::min(box1.
width(), box2.
width()));
390 static bool mostly_overlap(
const TBOX& box1,
const TBOX& box2) {
391 return x_overlap_fraction(box1, box2) > 0.9;
394 static bool significant_overlap(
const TBOX& box1,
const TBOX& box2) {
395 if (std::min(box1.
width(), box2.
width()) == 0)
return false;
396 int overlap = -box1.
x_gap(box2);
397 return overlap > 1 || x_overlap_fraction(box1, box2) > 0.1;
400 static float box_pitch(
const TBOX& ref,
const TBOX& box) {
405 static bool is_good_pitch(
float pitch,
const TBOX& box1,
const TBOX& box2) {
407 if (box1.
width() >= pitch * (1.0 + kFPTolerance) ||
408 box2.
width() >= pitch * (1.0 + kFPTolerance) ||
409 box1.
height() >= pitch * (1.0 + kFPTolerance) ||
410 box2.
height() >= pitch * (1.0 + kFPTolerance))
return false;
412 const float real_pitch = box_pitch(box1, box2);
413 if (fabs(real_pitch - pitch) < pitch * kFPTolerance)
return true;
415 if (textord_space_size_is_variable) {
418 if (real_pitch > pitch && real_pitch < pitch * 2.0 &&
419 real_pitch - box1.
x_gap(box2) < pitch) {
426 static bool is_interesting_blob(
const BLOBNBOX *blob) {
433 for (
int i = 0; i < characters_.size(); ++i) {
434 if (!characters_[i].delete_flag()) {
435 if (index != i) characters_[index] = characters_[i];
439 characters_.truncate(index);
443 float estimated_pitch_;
449 SimpleStats all_pitches_;
451 SimpleStats all_gaps_;
454 SimpleStats good_pitches_;
457 SimpleStats good_gaps_;
459 SimpleStats heights_;
465 void FPRow::Init(
TO_ROW *row) {
474 for (blob_it.mark_cycle_pt(); !blob_it.cycled_list(); blob_it.forward()) {
475 if (is_interesting_blob(blob_it.data())) {
477 fp_char.Init(blob_it.data());
479 if (!characters_.empty() &&
480 significant_overlap(fp_char.box(), characters_.back().box())) {
481 characters_.back().Merge(fp_char);
483 characters_.push_back(fp_char);
485 TBOX bound = blob_it.data()->bounding_box();
487 heights_.Add(bound.
height());
492 height_ = heights_.ile(0.875);
495 void FPRow::OutputEstimations() {
496 if (good_pitches_.size() == 0) {
502 pitch_ = good_pitches_.median();
503 real_row_->fixed_pitch = pitch_;
507 real_row_->kern_size = real_row_->pr_nonsp =
508 std::min(good_gaps_.ile(0.125), std::max(pitch_ - height_, 0.0f));
509 real_row_->body_size = pitch_ - real_row_->kern_size;
511 if (good_pitches_.size() < all_pitches_.size() * kFixedPitchThreshold) {
520 }
else if (good_pitches_.size() > all_pitches_.size() * 0.75) {
526 real_row_->space_size = real_row_->pr_space = pitch_;
529 real_row_->min_space = (pitch_ + good_gaps_.minimum()) * 0.5;
533 real_row_->max_nonspace = std::max(pitch_ * 0.25 + good_gaps_.minimum(),
534 static_cast<double>(good_gaps_.ile(0.875)));
536 int space_threshold =
537 std::min((real_row_->max_nonspace + real_row_->min_space) / 2,
538 static_cast<int>(real_row_->xheight));
542 for (
size_t i = 0; i < num_chars(); ++i) {
543 if (characters_[i].max_gap() > real_row_->max_nonspace) {
544 real_row_->max_nonspace = characters_[i].max_gap();
547 real_row_->space_threshold =
548 std::min((real_row_->max_nonspace + real_row_->min_space) / 2,
549 static_cast<int>(real_row_->xheight));
550 real_row_->used_dm_model =
false;
553 ICOORDELT_IT cell_it = &real_row_->char_cells;
554 auto *cell =
new ICOORDELT(real_body(0).left(), 0);
555 cell_it.add_after_then_move(cell);
557 int right = real_body(0).right();
558 for (
size_t i = 1; i < num_chars(); ++i) {
563 if ((is_final(i - 1) || is_final(i)) &&
564 real_body(i - 1).x_gap(real_body(i)) > space_threshold) {
566 cell_it.add_after_then_move(cell);
567 while (right + pitch_ < box(i).left()) {
570 cell_it.add_after_then_move(cell);
572 right = box(i).
left();
574 cell =
new ICOORDELT((right + real_body(i).left()) / 2, 0);
575 cell_it.add_after_then_move(cell);
576 right = real_body(i).right();
580 cell_it.add_after_then_move(cell);
587 void FPRow::EstimatePitch(
bool pass1) {
588 good_pitches_.Clear();
589 all_pitches_.Clear();
593 if (num_chars() == 0)
return;
596 bool prev_was_good = is_good(0);
599 heights_.Add(box(0).height());
600 for (
size_t i = 1; i < num_chars(); i++) {
602 int32_t pitch = cx1 - cx0;
603 int32_t gap = std::max(0, real_body(i - 1).x_gap(real_body(i)));
605 heights_.Add(box(i).height());
608 if (pitch > height_ * 0.5) {
609 all_pitches_.Add(pitch);
618 if (pass1 || (prev_was_good &&
619 fabs(estimated_pitch_ - pitch) <
620 kFPTolerance * estimated_pitch_)) {
621 good_pitches_.Add(pitch);
622 if (!is_box_modified(i - 1) && !is_box_modified(i)) {
626 prev_was_good =
true;
628 prev_was_good =
false;
634 good_pitches_.Finish();
635 all_pitches_.Finish();
640 height_ = heights_.ile(0.875);
641 if (all_pitches_.size() == 0) {
644 }
else if (good_pitches_.size() < 2) {
647 pitch_ = all_pitches_.median();
649 gap_ = all_gaps_.ile(0.125);
651 pitch_ = good_pitches_.median();
653 gap_ = good_gaps_.ile(0.125);
657 void FPRow::DebugOutputResult(
int row_index) {
658 if (num_chars() > 0) {
659 tprintf(
"Row %d: pitch_decision=%d, fixed_pitch=%f, max_nonspace=%d, " 660 "space_size=%f, space_threshold=%d, xheight=%f\n",
661 row_index, static_cast<int>(real_row_->pitch_decision),
662 real_row_->fixed_pitch, real_row_->max_nonspace,
663 real_row_->space_size, real_row_->space_threshold,
666 for (
unsigned i = 0; i < num_chars(); i++) {
667 tprintf(
"Char %u: is_final=%d is_good=%d num_blobs=%d: ",
668 i, is_final(i), is_good(i),
character(i)->num_blobs());
674 void FPRow::Pass1Analyze() {
675 if (num_chars() < 2)
return;
677 if (estimated_pitch_ > 0.0f) {
678 for (
size_t i = 2; i < num_chars(); i++) {
679 if (is_good_pitch(estimated_pitch_, box(i - 2), box(i-1)) &&
680 is_good_pitch(estimated_pitch_, box(i - 1), box(i))) {
685 for (
size_t i = 2; i < num_chars(); i++) {
686 if (is_good_pitch(box_pitch(box(i-2), box(i-1)), box(i - 1), box(i))) {
692 character(num_chars() - 1)->set_alignment(
693 character(num_chars() - 2)->alignment());
696 bool FPRow::Pass2Analyze() {
697 bool changed =
false;
698 if (num_chars() <= 1 || estimated_pitch_ == 0.0f) {
701 for (
size_t i = 0; i < num_chars(); i++) {
702 if (is_final(i))
continue;
704 FPChar::Alignment alignment =
character(i)->alignment();
705 bool intersecting =
false;
706 bool not_intersecting =
false;
708 if (i < num_chars() - 1 && is_final(i + 1)) {
712 bool skipped_whitespaces =
false;
713 float c1 = center_x(i + 1) - 1.5 * estimated_pitch_;
714 while (c1 > box(i).right()) {
715 skipped_whitespaces =
true;
716 c1 -= estimated_pitch_;
718 TBOX ibody(c1, box(i).bottom(), c1 + estimated_pitch_, box(i).top());
724 while (j >= 0 && !is_final(j) && mostly_overlap(ibody, box(j)) &&
726 estimated_pitch_ * (1 + kFPTolerance)) {
731 if (j >= 0 && significant_overlap(ibody, box(j))) {
734 if (!is_final(j)) intersecting =
true;
736 not_intersecting =
true;
742 if (!skipped_whitespaces) mark_good(i);
746 if (box(i).width() <= estimated_pitch_ * 0.5) {
753 for (
int k = i; k > j + 1; k--) {
760 if (i > 0 && is_final(i - 1)) {
764 bool skipped_whitespaces =
false;
765 float c1 = center_x(i - 1) + 1.5 * estimated_pitch_;
766 while (c1 < box(i).left()) {
767 skipped_whitespaces =
true;
768 c1 += estimated_pitch_;
770 TBOX ibody(c1 - estimated_pitch_, box(i).bottom(), c1, box(i).top());
774 while (j < num_chars() && !is_final(j) && mostly_overlap(ibody, box(j)) &&
776 estimated_pitch_ * (1 + kFPTolerance)) {
781 if (j < num_chars() && significant_overlap(ibody, box(j))) {
782 if (!is_final(j)) intersecting =
true;
784 not_intersecting =
true;
787 if (!skipped_whitespaces) mark_good(i);
788 if (box(i).width() <= estimated_pitch_ * 0.5) {
795 for (
size_t k = i + 1; k < j; k++) {
805 if (intersecting && !not_intersecting) mark_bad(i);
806 if (
character(i)->alignment() != alignment ||
815 void FPRow::MergeFragments() {
818 for (
size_t j = 0; j < num_chars(); ++j) {
822 clear_alignment(last_char);
823 character(j-1)->set_merge_to_prev(
false);
831 void FPRow::FinalizeLargeChars() {
832 float row_pitch = estimated_pitch();
833 for (
size_t i = 0; i < num_chars(); i++) {
834 if (is_final(i))
continue;
837 if (i > 0 && is_final(i - 1) && i < num_chars() - 1 && is_final(i + 1)) {
842 float cx = center_x(i);
843 TBOX ibody(cx - 0.5 * row_pitch, 0, cx + 0.5 * row_pitch, 1);
848 if (x_overlap_fraction(ibody, box(i - 1)) > 0.1)
continue;
849 if (!is_final(i - 1)) {
850 TBOX merged = box(i);
851 merged += box(i - 1);
852 if (merged.
width() < row_pitch)
continue;
858 if (i < num_chars() - 1) {
859 if (x_overlap_fraction(ibody, box(i + 1)) > 0.1)
continue;
860 if (!is_final(i + 1)) {
861 TBOX merged = box(i);
862 merged += box(i + 1);
863 if (merged.
width() < row_pitch)
continue;
874 for (
size_t i = 0; i < num_chars(); i++) {
875 if (!is_final(i))
continue;
876 bool good_pitch =
false;
877 bool bad_pitch =
false;
878 if (i > 0 && is_final(i - 1)) {
879 if (is_good_pitch(row_pitch, box(i - 1), box(i))) {
885 if (i < num_chars() - 1 && is_final(i + 1)) {
886 if (is_good_pitch(row_pitch, box(i), box(i + 1))) {
892 if (good_pitch && !bad_pitch) mark_good(i);
893 else if (!good_pitch && bad_pitch) mark_bad(i);
899 FPAnalyzer(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks);
902 void Pass1Analyze() {
903 for (
auto & row : rows_) row.Pass1Analyze();
909 void EstimatePitch(
bool pass1);
911 bool maybe_fixed_pitch() {
913 rows_.size() <= num_bad_rows_ + num_tall_rows_ + 1)
return false;
917 void MergeFragments() {
918 for (
auto & row : rows_) row.MergeFragments();
921 void FinalizeLargeChars() {
922 for (
auto & row : rows_) row.FinalizeLargeChars();
925 bool Pass2Analyze() {
926 bool changed =
false;
927 for (
auto & row : rows_) {
928 if (row.Pass2Analyze()) {
935 void OutputEstimations() {
936 for (
auto & row : rows_) row.OutputEstimations();
940 void DebugOutputResult() {
941 tprintf(
"FPAnalyzer: final result\n");
942 for (
size_t i = 0; i < rows_.size(); i++) rows_[i].DebugOutputResult(i);
950 unsigned max_iteration() {
953 return max_chars_per_row_ + 100;
958 std::vector<FPRow> rows_;
959 unsigned num_tall_rows_;
960 unsigned num_bad_rows_;
962 unsigned num_empty_rows_;
963 unsigned max_chars_per_row_;
966 FPAnalyzer::FPAnalyzer(
ICOORD page_tr, TO_BLOCK_LIST *port_blocks)
971 max_chars_per_row_(0)
973 TO_BLOCK_IT block_it(port_blocks);
975 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
976 block_it.forward()) {
984 for (block_it.mark_cycle_pt(); !block_it.cycled_list();
985 block_it.forward()) {
986 TO_ROW_IT row_it = block_it.data()->get_rows();
987 for (row_it.mark_cycle_pt(); !row_it.cycled_list(); row_it.forward()) {
989 row.Init(row_it.data());
990 rows_.push_back(row);
991 size_t num_chars = rows_.back().num_chars();
992 if (num_chars <= 1) num_empty_rows_++;
993 if (num_chars > max_chars_per_row_) max_chars_per_row_ = num_chars;
998 void FPAnalyzer::EstimatePitch(
bool pass1) {
999 LocalCorrelation pitch_height_stats;
1003 pitch_height_stats.Clear();
1004 for (
auto & row : rows_) {
1005 row.EstimatePitch(pass1);
1006 if (row.good_pitches()) {
1007 pitch_height_stats.Add(row.height() + row.gap(),
1008 row.pitch(), row.good_pitches());
1009 if (row.height_pitch_ratio() > 1.1) num_tall_rows_++;
1015 pitch_height_stats.Finish();
1016 for (
auto & row : rows_) {
1017 if (row.good_pitches() >= 5) {
1020 row.set_estimated_pitch(row.pitch());
1021 }
else if (row.num_chars() > 1) {
1022 float estimated_pitch =
1023 pitch_height_stats.EstimateYFor(row.height() + row.gap(),
1029 if (estimated_pitch > row.pitch() ||
1030 row.pitch() > row.height() * 2.0) {
1031 row.set_estimated_pitch(estimated_pitch);
1033 row.set_estimated_pitch(row.pitch());
1042 TO_BLOCK_LIST *port_blocks) {
1043 FPAnalyzer analyzer(page_tr, port_blocks);
1044 if (analyzer.num_rows() == 0)
return;
1046 analyzer.Pass1Analyze();
1047 analyzer.EstimatePitch(
true);
1051 analyzer.Pass1Analyze();
1052 analyzer.EstimatePitch(
true);
1055 if (!analyzer.maybe_fixed_pitch()) {
1057 tprintf(
"Page doesn't seem to contain fixed pitch rows\n");
1062 unsigned iteration = 0;
1064 analyzer.MergeFragments();
1065 analyzer.FinalizeLargeChars();
1066 analyzer.EstimatePitch(
false);
1068 }
while (analyzer.Pass2Analyze() && iteration < analyzer.max_iteration());
1071 tprintf(
"compute_fixed_pitch_cjk finished after %u iteration (limit=%u)\n",
1072 iteration, analyzer.max_iteration());
1075 analyzer.OutputEstimations();
PITCH_TYPE pitch_decision
void compute_fixed_pitch_cjk(ICOORD page_tr, TO_BLOCK_LIST *port_blocks)
TBOX bounding_union(const TBOX &box) const
void find_repeated_chars(TO_BLOCK *block, bool testing_on)
const TBOX & bounding_box() const
BlobTextFlowType flow() const
int x_gap(const TBOX &box) const
bool textord_debug_pitch_test
DLLSYM void tprintf(const char *format,...)
BLOBNBOX_LIST * blob_list()
#define BOOL_VAR(name, val, comment)
bool joined_to_prev() const