Commit 780f5d1a for tesseract
commit 780f5d1a387143a1fd46de8bb2447c2cada8289b
Author: Stefan Weil <sw@weilnetz.de>
Date: Tue Nov 10 19:19:31 2026 +0100
wordrec: let a real result win against a fake result of an exhausted budget
In combined mode (OEM_TESSERACT_LSTM_COMBINED) a language is still tried
in pass 1 when its classification budget is used up, because only some
words (odd-sized words, and words which the LSTM recognizer could not
read) fall back to the legacy engine. For such a word classify_word_pass1()
sets up a fake result of spaces. The fake has certainty -1 and a rating of
10 per blob, so in SelectBestWords() it won against a real result of
another language which still had budget unless that result was a
dictionary word with a rating within classify_max_rating_ratio: a real
non-dictionary reading with certainty -3 lost against blanks.
Mark such fakes with a new WERD_RES flag, budget_skipped, and let a real
result always win against a flagged fake in SelectBestWords(). Two
flagged fakes, or two real results, are compared as before, so a fake is
still kept when no language can recognize the word. The flag is copied
with the other simple fields and cleared by ClearResults(), so it does
not survive a new recognition attempt of the word.
Assisted-by: Claude Fable 5.1 (Anthropic)
Signed-off-by: Stefan Weil <sw@weilnetz.de>
diff --git a/src/ccmain/control.cpp b/src/ccmain/control.cpp
index 6d5b84d5..c11f1650 100644
--- a/src/ccmain/control.cpp
+++ b/src/ccmain/control.cpp
@@ -801,9 +801,11 @@ static void WordGap(const PointerVector<WERD_RES> &words, unsigned index, int *r
}
// Factored helper computes the rating, certainty, badness and validity of
-// the permuter of the words in [first_index, end_index).
+// the permuter of the words in [first_index, end_index), and whether any of
+// them is a fake because the classification budget was used up.
static void EvaluateWordSpan(const PointerVector<WERD_RES> &words, unsigned first_index, unsigned end_index,
- float *rating, float *certainty, bool *bad, bool *valid_permuter) {
+ float *rating, float *certainty, bool *bad, bool *valid_permuter,
+ bool *budget_skipped) {
if (end_index <= first_index) {
*bad = true;
*valid_permuter = false;
@@ -819,13 +821,19 @@ static void EvaluateWordSpan(const PointerVector<WERD_RES> &words, unsigned firs
*valid_permuter = false;
}
}
+ if (words[index]->budget_skipped) {
+ *budget_skipped = true;
+ }
}
}
// Helper chooses the best combination of words, transferring good ones from
// new_words to best_words. To win, a new word must have (better rating and
// certainty) or (better permuter status and rating within rating ratio and
-// certainty within certainty margin) than current best.
+// certainty within certainty margin) than current best. A real result always
+// wins against a fake result of a language which has used up its
+// classification budget, so such a fake is only kept if no language could
+// recognize the word.
// All the new_words are consumed (moved to best_words or deleted.)
// The return value is the number of new_words used minus the number of
// best_words that remain in the output.
@@ -867,16 +875,20 @@ static int SelectBestWords(double rating_ratio, double certainty_margin, bool de
bool b_bad = false, n_bad = false;
// True if all words have a valid permuter.
bool b_valid_permuter = true, n_valid_permuter = true;
+ // True if any word is a fake because the classification budget was used up.
+ bool b_budget_skipped = false, n_budget_skipped = false;
const int end_b = b < best_words->size() ? b + 1 : b;
const int end_n = n < new_words->size() ? n + 1 : n;
EvaluateWordSpan(*best_words, start_b, end_b, &b_rating, &b_certainty, &b_bad,
- &b_valid_permuter);
+ &b_valid_permuter, &b_budget_skipped);
EvaluateWordSpan(*new_words, start_n, end_n, &n_rating, &n_certainty, &n_bad,
- &n_valid_permuter);
+ &n_valid_permuter, &n_budget_skipped);
bool new_better = false;
- if (!n_bad && (b_bad || (n_certainty > b_certainty && n_rating < b_rating) ||
- (!b_valid_permuter && n_valid_permuter && n_rating < b_rating * rating_ratio &&
- n_certainty > b_certainty - certainty_margin))) {
+ if (!n_bad && (b_bad || (b_budget_skipped && !n_budget_skipped) ||
+ (b_budget_skipped == n_budget_skipped &&
+ ((n_certainty > b_certainty && n_rating < b_rating) ||
+ (!b_valid_permuter && n_valid_permuter && n_rating < b_rating * rating_ratio &&
+ n_certainty > b_certainty - certainty_margin))))) {
// New is better.
for (int i = start_n; i < end_n; ++i) {
out_words.push_back((*new_words)[i]);
@@ -1425,6 +1437,7 @@ void Tesseract::classify_word_and_language(int pass_n, PAGE_RES_IT *pr_it, WordD
// result from an earlier pass, so fake it as for a timeout. Later
// passes expect that the word knows its language.
word_data->word->SetupFake(unicharset);
+ word_data->word->budget_skipped = true;
word_data->word->tesseract = this;
}
return;
@@ -1496,8 +1509,10 @@ void Tesseract::classify_word_pass1(const WordData &word_data, WERD_RES **in_wor
#ifndef DISABLED_LEGACY_ENGINE
if (ClassificationBudgetExhausted()) {
- // No budget left for the fallback, so use a fake.
+ // No budget left for the fallback, so use a fake. The flag lets a real
+ // result of another language win against it in SelectBestWords().
(*in_word)->SetupFake(lstm_recognizer_->GetUnicharset());
+ (*in_word)->budget_skipped = true;
return;
}
// Fall back to tesseract for failed words or odd words.
diff --git a/src/ccstruct/pageres.cpp b/src/ccstruct/pageres.cpp
index afe7552a..5f0c09d2 100644
--- a/src/ccstruct/pageres.cpp
+++ b/src/ccstruct/pageres.cpp
@@ -259,6 +259,7 @@ void WERD_RES::CopySimpleFields(const WERD_RES &source) {
unlv_crunch_mode = source.unlv_crunch_mode;
small_caps = source.small_caps;
odd_size = source.odd_size;
+ budget_skipped = source.budget_skipped;
fontinfo = source.fontinfo;
fontinfo2 = source.fontinfo2;
fontinfo_id_count = source.fontinfo_id_count;
@@ -1142,6 +1143,7 @@ void WERD_RES::Clear() {
void WERD_RES::ClearResults() {
done = false;
+ budget_skipped = false;
fontinfo = nullptr;
fontinfo2 = nullptr;
fontinfo_id_count = 0;
diff --git a/src/ccstruct/pageres.h b/src/ccstruct/pageres.h
index f37b4df0..0d19ef67 100644
--- a/src/ccstruct/pageres.h
+++ b/src/ccstruct/pageres.h
@@ -303,6 +303,9 @@ public:
bool done = false; // ready for output?
bool small_caps = false; // word appears to be small caps
bool odd_size = false; // word is bigger than line or leader dots.
+ // The result is a fake because the blob classification budget of the
+ // language was used up (see Wordrec::ClassificationBudgetExhausted()).
+ bool budget_skipped = false;
// The fontinfos are pointers to data owned by the classifier.
const FontInfo *fontinfo = nullptr;
const FontInfo *fontinfo2 = nullptr;