Coverage Report

Created: 2026-07-25 06:27

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tesseract/src/ccstruct/blamer.cpp
Line
Count
Source
1
///////////////////////////////////////////////////////////////////////
2
// File:        blamer.cpp
3
// Description: Module allowing precise error causes to be allocated.
4
// Author:      Rike Antonova
5
// Refactored:  Ray Smith
6
//
7
// (C) Copyright 2013, Google Inc.
8
// Licensed under the Apache License, Version 2.0 (the "License");
9
// you may not use this file except in compliance with the License.
10
// You may obtain a copy of the License at
11
// http://www.apache.org/licenses/LICENSE-2.0
12
// Unless required by applicable law or agreed to in writing, software
13
// distributed under the License is distributed on an "AS IS" BASIS,
14
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
// See the License for the specific language governing permissions and
16
// limitations under the License.
17
//
18
///////////////////////////////////////////////////////////////////////
19
20
#include "blamer.h"
21
22
#include "blobs.h"   // for TPOINT, TWERD, TBLOB
23
#include "errcode.h" // for ASSERT_HOST
24
#if !defined(DISABLED_LEGACY_ENGINE)
25
#  include "lm_pain_points.h" // for LMPainPoints
26
#endif
27
#include "matrix.h"     // for MATRIX
28
#include "normalis.h"   // for DENORM
29
#include "pageres.h"    // for WERD_RES
30
#include "unicharset.h" // for UNICHARSET
31
32
#include <cmath>   // for abs
33
#include <cstdlib> // for abs
34
35
namespace tesseract {
36
37
// Names for each value of IncorrectResultReason enum. Keep in sync.
38
const char kBlameCorrect[] = "corr";
39
const char kBlameClassifier[] = "cl";
40
const char kBlameChopper[] = "chop";
41
const char kBlameClassLMTradeoff[] = "cl/LM";
42
const char kBlamePageLayout[] = "pglt";
43
const char kBlameSegsearchHeur[] = "ss_heur";
44
const char kBlameSegsearchPP[] = "ss_pp";
45
const char kBlameClassOldLMTradeoff[] = "cl/old_LM";
46
const char kBlameAdaption[] = "adapt";
47
const char kBlameNoTruthSplit[] = "no_tr_spl";
48
const char kBlameNoTruth[] = "no_tr";
49
const char kBlameUnknown[] = "unkn";
50
51
const char *const kIncorrectResultReasonNames[] = {
52
    kBlameCorrect,    kBlameClassifier,    kBlameChopper,     kBlameClassLMTradeoff,
53
    kBlamePageLayout, kBlameSegsearchHeur, kBlameSegsearchPP, kBlameClassOldLMTradeoff,
54
    kBlameAdaption,   kBlameNoTruthSplit,  kBlameNoTruth,     kBlameUnknown};
55
56
0
const char *BlamerBundle::IncorrectReasonName(IncorrectResultReason irr) {
57
0
  return kIncorrectResultReasonNames[irr];
58
0
}
59
60
0
const char *BlamerBundle::IncorrectReason() const {
61
0
  return kIncorrectResultReasonNames[incorrect_result_reason_];
62
0
}
63
64
// Functions to setup the blamer.
65
// Whole word string, whole word bounding box.
66
void BlamerBundle::SetWordTruth(const UNICHARSET &unicharset, const char *truth_str,
67
0
                                const TBOX &word_box) {
68
0
  truth_word_.InsertBox(0, word_box);
69
0
  truth_has_char_boxes_ = false;
70
  // Encode the string as UNICHAR_IDs.
71
0
  std::vector<UNICHAR_ID> encoding;
72
0
  std::vector<char> lengths;
73
0
  unicharset.encode_string(truth_str, false, &encoding, &lengths, nullptr);
74
0
  int total_length = 0;
75
0
  for (size_t i = 0; i < encoding.size(); total_length += lengths[i++]) {
76
0
    std::string uch(truth_str + total_length);
77
0
    uch.resize(lengths[i] - total_length);
78
0
    UNICHAR_ID id = encoding[i];
79
0
    if (id != INVALID_UNICHAR_ID) {
80
0
      uch = unicharset.get_normed_unichar(id);
81
0
    }
82
0
    truth_text_.push_back(uch);
83
0
  }
84
0
}
85
86
// Single "character" string, "character" bounding box.
87
// May be called multiple times to indicate the characters in a word.
88
void BlamerBundle::SetSymbolTruth(const UNICHARSET &unicharset, const char *char_str,
89
0
                                  const TBOX &char_box) {
90
0
  std::string symbol_str(char_str);
91
0
  UNICHAR_ID id = unicharset.unichar_to_id(char_str);
92
0
  if (id != INVALID_UNICHAR_ID) {
93
0
    std::string normed_uch(unicharset.get_normed_unichar(id));
94
0
    if (normed_uch.length() > 0) {
95
0
      symbol_str = std::move(normed_uch);
96
0
    }
97
0
  }
98
0
  int length = truth_word_.length();
99
0
  truth_text_.push_back(symbol_str);
100
0
  truth_word_.InsertBox(length, char_box);
101
0
  if (length == 0) {
102
0
    truth_has_char_boxes_ = true;
103
0
  } else if (truth_word_.BlobBox(length - 1) == char_box) {
104
0
    truth_has_char_boxes_ = false;
105
0
  }
106
0
}
107
108
// Marks that there is something wrong with the truth text, like it contains
109
// reject characters.
110
0
void BlamerBundle::SetRejectedTruth() {
111
0
  incorrect_result_reason_ = IRR_NO_TRUTH;
112
0
  truth_has_char_boxes_ = false;
113
0
}
114
115
// Returns true if the provided word_choice is correct.
116
0
bool BlamerBundle::ChoiceIsCorrect(const WERD_CHOICE *word_choice) const {
117
0
  if (word_choice == nullptr) {
118
0
    return false;
119
0
  }
120
0
  const UNICHARSET *uni_set = word_choice->unicharset();
121
0
  std::string normed_choice_str;
122
0
  for (unsigned i = 0; i < word_choice->length(); ++i) {
123
0
    normed_choice_str += uni_set->get_normed_unichar(word_choice->unichar_id(i));
124
0
  }
125
0
  std::string truth_str = TruthString();
126
0
  return truth_str == normed_choice_str;
127
0
}
128
129
0
void BlamerBundle::FillDebugString(const std::string &msg, const WERD_CHOICE *choice, std::string &debug) {
130
0
  debug += "Truth ";
131
0
  for (auto &text : this->truth_text_) {
132
0
    debug += text;
133
0
  }
134
0
  if (!this->truth_has_char_boxes_) {
135
0
    debug += " (no char boxes)";
136
0
  }
137
0
  if (choice != nullptr) {
138
0
    debug += " Choice ";
139
0
    std::string choice_str;
140
0
    choice->string_and_lengths(&choice_str, nullptr);
141
0
    debug += choice_str;
142
0
  }
143
0
  if (msg.length() > 0) {
144
0
    debug += "\n";
145
0
    debug += msg;
146
0
  }
147
0
  debug += "\n";
148
0
}
149
150
// Sets up the norm_truth_word from truth_word using the given DENORM.
151
0
void BlamerBundle::SetupNormTruthWord(const DENORM &denorm) {
152
  // TODO(rays) Is this the last use of denorm in WERD_RES and can it go?
153
0
  norm_box_tolerance_ = kBlamerBoxTolerance * denorm.x_scale();
154
0
  TPOINT topleft;
155
0
  TPOINT botright;
156
0
  TPOINT norm_topleft;
157
0
  TPOINT norm_botright;
158
0
  for (unsigned b = 0; b < truth_word_.length(); ++b) {
159
0
    const TBOX &box = truth_word_.BlobBox(b);
160
0
    topleft.x = box.left();
161
0
    topleft.y = box.top();
162
0
    botright.x = box.right();
163
0
    botright.y = box.bottom();
164
0
    denorm.NormTransform(nullptr, topleft, &norm_topleft);
165
0
    denorm.NormTransform(nullptr, botright, &norm_botright);
166
0
    TBOX norm_box(norm_topleft.x, norm_botright.y, norm_botright.x, norm_topleft.y);
167
0
    norm_truth_word_.InsertBox(b, norm_box);
168
0
  }
169
0
}
170
171
// Splits *this into two pieces in bundle1 and bundle2 (preallocated, empty
172
// bundles) where the right edge/ of the left-hand word is word1_right,
173
// and the left edge of the right-hand word is word2_left.
174
void BlamerBundle::SplitBundle(int word1_right, int word2_left, bool debug, BlamerBundle *bundle1,
175
0
                               BlamerBundle *bundle2) const {
176
0
  std::string debug_str;
177
  // Find truth boxes that correspond to the split in the blobs.
178
0
  unsigned begin2_truth_index = 0;
179
0
  if (incorrect_result_reason_ != IRR_NO_TRUTH && truth_has_char_boxes_) {
180
0
    debug_str = "Looking for truth split at";
181
0
    debug_str += " end1_x " + std::to_string(word1_right);
182
0
    debug_str += " begin2_x " + std::to_string(word2_left);
183
0
    debug_str += "\nnorm_truth_word boxes:\n";
184
0
    if (norm_truth_word_.length() > 1) {
185
0
      norm_truth_word_.BlobBox(0).print_to_str(debug_str);
186
0
      for (unsigned b = 1; b < norm_truth_word_.length(); ++b) {
187
0
        norm_truth_word_.BlobBox(b).print_to_str(debug_str);
188
0
        if ((abs(word1_right - norm_truth_word_.BlobBox(b - 1).right()) < norm_box_tolerance_) &&
189
0
            (abs(word2_left - norm_truth_word_.BlobBox(b).left()) < norm_box_tolerance_)) {
190
0
          begin2_truth_index = b;
191
0
          debug_str += "Split found";
192
0
          break;
193
0
        }
194
0
      }
195
0
      debug_str += '\n';
196
0
    }
197
0
  }
198
  // Populate truth information in word and word2 with the first and second
199
  // part of the original truth.
200
0
  if (begin2_truth_index > 0) {
201
0
    bundle1->truth_has_char_boxes_ = true;
202
0
    bundle1->norm_box_tolerance_ = norm_box_tolerance_;
203
0
    bundle2->truth_has_char_boxes_ = true;
204
0
    bundle2->norm_box_tolerance_ = norm_box_tolerance_;
205
0
    BlamerBundle *curr_bb = bundle1;
206
0
    for (unsigned b = 0; b < norm_truth_word_.length(); ++b) {
207
0
      if (b == begin2_truth_index) {
208
0
        curr_bb = bundle2;
209
0
      }
210
0
      curr_bb->norm_truth_word_.InsertBox(b, norm_truth_word_.BlobBox(b));
211
0
      curr_bb->truth_word_.InsertBox(b, truth_word_.BlobBox(b));
212
0
      curr_bb->truth_text_.push_back(truth_text_[b]);
213
0
    }
214
0
  } else if (incorrect_result_reason_ == IRR_NO_TRUTH) {
215
0
    bundle1->incorrect_result_reason_ = IRR_NO_TRUTH;
216
0
    bundle2->incorrect_result_reason_ = IRR_NO_TRUTH;
217
0
  } else {
218
0
    debug_str += "Truth split not found";
219
0
    debug_str += truth_has_char_boxes_ ? "\n" : " (no truth char boxes)\n";
220
0
    bundle1->SetBlame(IRR_NO_TRUTH_SPLIT, debug_str, nullptr, debug);
221
0
    bundle2->SetBlame(IRR_NO_TRUTH_SPLIT, debug_str, nullptr, debug);
222
0
  }
223
0
}
224
225
// "Joins" the blames from bundle1 and bundle2 into *this.
226
void BlamerBundle::JoinBlames(const BlamerBundle &bundle1, const BlamerBundle &bundle2,
227
0
                              bool debug) {
228
0
  std::string debug_str;
229
0
  IncorrectResultReason irr = incorrect_result_reason_;
230
0
  if (irr != IRR_NO_TRUTH_SPLIT) {
231
0
    debug_str = "";
232
0
  }
233
0
  if (bundle1.incorrect_result_reason_ != IRR_CORRECT &&
234
0
      bundle1.incorrect_result_reason_ != IRR_NO_TRUTH &&
235
0
      bundle1.incorrect_result_reason_ != IRR_NO_TRUTH_SPLIT) {
236
0
    debug_str += "Blame from part 1: ";
237
0
    debug_str += bundle1.debug_;
238
0
    irr = bundle1.incorrect_result_reason_;
239
0
  }
240
0
  if (bundle2.incorrect_result_reason_ != IRR_CORRECT &&
241
0
      bundle2.incorrect_result_reason_ != IRR_NO_TRUTH &&
242
0
      bundle2.incorrect_result_reason_ != IRR_NO_TRUTH_SPLIT) {
243
0
    debug_str += "Blame from part 2: ";
244
0
    debug_str += bundle2.debug_;
245
0
    if (irr == IRR_CORRECT) {
246
0
      irr = bundle2.incorrect_result_reason_;
247
0
    } else if (irr != bundle2.incorrect_result_reason_) {
248
0
      irr = IRR_UNKNOWN;
249
0
    }
250
0
  }
251
0
  incorrect_result_reason_ = irr;
252
0
  if (irr != IRR_CORRECT && irr != IRR_NO_TRUTH) {
253
0
    SetBlame(irr, debug_str, nullptr, debug);
254
0
  }
255
0
}
256
257
// If a blob with the same bounding box as one of the truth character
258
// bounding boxes is not classified as the corresponding truth character
259
// blames character classifier for incorrect answer.
260
void BlamerBundle::BlameClassifier(const UNICHARSET &unicharset, const TBOX &blob_box,
261
0
                                   const BLOB_CHOICE_LIST &choices, bool debug) {
262
0
  if (!truth_has_char_boxes_ || incorrect_result_reason_ != IRR_CORRECT) {
263
0
    return; // Nothing to do here.
264
0
  }
265
266
0
  for (unsigned b = 0; b < norm_truth_word_.length(); ++b) {
267
0
    const TBOX &truth_box = norm_truth_word_.BlobBox(b);
268
    // Note that we are more strict on the bounding box boundaries here
269
    // than in other places (chopper, segmentation search), since we do
270
    // not have the ability to check the previous and next bounding box.
271
0
    if (blob_box.x_almost_equal(truth_box, norm_box_tolerance_ / 2)) {
272
0
      bool found = false;
273
0
      bool incorrect_adapted = false;
274
0
      UNICHAR_ID incorrect_adapted_id = INVALID_UNICHAR_ID;
275
0
      const char *truth_str = truth_text_[b].c_str();
276
      // We promise not to modify the list or its contents, using a
277
      // const BLOB_CHOICE* below.
278
0
      BLOB_CHOICE_IT choices_it(const_cast<BLOB_CHOICE_LIST *>(&choices));
279
0
      for (choices_it.mark_cycle_pt(); !choices_it.cycled_list(); choices_it.forward()) {
280
0
        const BLOB_CHOICE *choice = choices_it.data();
281
0
        if (strcmp(truth_str, unicharset.get_normed_unichar(choice->unichar_id())) == 0) {
282
0
          found = true;
283
0
          break;
284
0
        } else if (choice->IsAdapted()) {
285
0
          incorrect_adapted = true;
286
0
          incorrect_adapted_id = choice->unichar_id();
287
0
        }
288
0
      } // end choices_it for loop
289
0
      if (!found) {
290
0
        std::string debug_str = "unichar ";
291
0
        debug_str += truth_str;
292
0
        debug_str += " not found in classification list";
293
0
        SetBlame(IRR_CLASSIFIER, debug_str, nullptr, debug);
294
0
      } else if (incorrect_adapted) {
295
0
        std::string debug_str = "better rating for adapted ";
296
0
        debug_str += unicharset.id_to_unichar(incorrect_adapted_id);
297
0
        debug_str += " than for correct ";
298
0
        debug_str += truth_str;
299
0
        SetBlame(IRR_ADAPTION, debug_str, nullptr, debug);
300
0
      }
301
0
      break;
302
0
    }
303
0
  } // end iterating over blamer_bundle->norm_truth_word
304
0
}
305
306
// Checks whether chops were made at all the character bounding box
307
// boundaries in word->truth_word. If not - blames the chopper for an
308
// incorrect answer.
309
0
void BlamerBundle::SetChopperBlame(const WERD_RES *word, bool debug) {
310
0
  if (NoTruth() || !truth_has_char_boxes_ || word->chopped_word->blobs.empty()) {
311
0
    return;
312
0
  }
313
0
  bool missing_chop = false;
314
0
  int num_blobs = word->chopped_word->blobs.size();
315
0
  unsigned box_index = 0;
316
0
  int blob_index = 0;
317
0
  int16_t truth_x = -1;
318
0
  while (box_index < truth_word_.length() && blob_index < num_blobs) {
319
0
    truth_x = norm_truth_word_.BlobBox(box_index).right();
320
0
    TBLOB *curr_blob = word->chopped_word->blobs[blob_index];
321
0
    if (curr_blob->bounding_box().right() < truth_x - norm_box_tolerance_) {
322
0
      ++blob_index;
323
0
      continue; // encountered an extra chop, keep looking
324
0
    } else if (curr_blob->bounding_box().right() > truth_x + norm_box_tolerance_) {
325
0
      missing_chop = true;
326
0
      break;
327
0
    } else {
328
0
      ++blob_index;
329
0
    }
330
0
  }
331
0
  if (missing_chop || box_index < norm_truth_word_.length()) {
332
0
    std::string debug_str;
333
0
    if (missing_chop) {
334
0
      debug_str += "Detected missing chop (tolerance=" + std::to_string(norm_box_tolerance_);
335
0
      debug_str += ") at Bounding Box=";
336
0
      TBLOB *curr_blob = word->chopped_word->blobs[blob_index];
337
0
      curr_blob->bounding_box().print_to_str(debug_str);
338
0
      debug_str += "\nNo chop for truth at x=" + std::to_string(truth_x);
339
0
    } else {
340
0
      debug_str += "Missing chops for last " + std::to_string(norm_truth_word_.length() - box_index);
341
0
      debug_str += " truth box(es)";
342
0
    }
343
0
    debug_str += "\nMaximally chopped word boxes:\n";
344
0
    for (blob_index = 0; blob_index < num_blobs; ++blob_index) {
345
0
      TBLOB *curr_blob = word->chopped_word->blobs[blob_index];
346
0
      curr_blob->bounding_box().print_to_str(debug_str);
347
0
      debug_str += '\n';
348
0
    }
349
0
    debug_str += "Truth  bounding  boxes:\n";
350
0
    for (box_index = 0; box_index < norm_truth_word_.length(); ++box_index) {
351
0
      norm_truth_word_.BlobBox(box_index).print_to_str(debug_str);
352
0
      debug_str += '\n';
353
0
    }
354
0
    SetBlame(IRR_CHOPPER, debug_str, word->best_choice, debug);
355
0
  }
356
0
}
357
358
// Blames the classifier or the language model if, after running only the
359
// chopper, best_choice is incorrect and no blame has been yet set.
360
// Blames the classifier if best_choice is classifier's top choice and is a
361
// dictionary word (i.e. language model could not have helped).
362
// Otherwise, blames the language model (formerly permuter word adjustment).
363
void BlamerBundle::BlameClassifierOrLangModel(const WERD_RES *word, const UNICHARSET &unicharset,
364
0
                                              bool valid_permuter, bool debug) {
365
0
  if (valid_permuter) {
366
    // Find out whether best choice is a top choice.
367
0
    best_choice_is_dict_and_top_choice_ = true;
368
0
    for (unsigned i = 0; i < word->best_choice->length(); ++i) {
369
0
      BLOB_CHOICE_IT blob_choice_it(word->GetBlobChoices(i));
370
0
      ASSERT_HOST(!blob_choice_it.empty());
371
0
      BLOB_CHOICE *first_choice = nullptr;
372
0
      for (blob_choice_it.mark_cycle_pt(); !blob_choice_it.cycled_list();
373
0
           blob_choice_it.forward()) { // find first non-fragment choice
374
0
        if (!(unicharset.get_fragment(blob_choice_it.data()->unichar_id()))) {
375
0
          first_choice = blob_choice_it.data();
376
0
          break;
377
0
        }
378
0
      }
379
0
      ASSERT_HOST(first_choice != nullptr);
380
0
      if (first_choice->unichar_id() != word->best_choice->unichar_id(i)) {
381
0
        best_choice_is_dict_and_top_choice_ = false;
382
0
        break;
383
0
      }
384
0
    }
385
0
  }
386
0
  std::string debug_str;
387
0
  if (best_choice_is_dict_and_top_choice_) {
388
0
    debug_str = "Best choice is: incorrect, top choice, dictionary word";
389
0
    debug_str += " with permuter ";
390
0
    debug_str += word->best_choice->permuter_name();
391
0
  } else {
392
0
    debug_str = "Classifier/Old LM tradeoff is to blame";
393
0
  }
394
0
  SetBlame(best_choice_is_dict_and_top_choice_ ? IRR_CLASSIFIER : IRR_CLASS_OLD_LM_TRADEOFF,
395
0
           debug_str, word->best_choice, debug);
396
0
}
397
398
// Sets up the correct_segmentation_* to mark the correct bounding boxes.
399
0
void BlamerBundle::SetupCorrectSegmentation(const TWERD *word, bool debug) {
400
0
#ifndef DISABLED_LEGACY_ENGINE
401
0
  params_training_bundle_.StartHypothesisList();
402
0
#endif //  ndef DISABLED_LEGACY_ENGINE
403
0
  if (incorrect_result_reason_ != IRR_CORRECT || !truth_has_char_boxes_) {
404
0
    return; // Nothing to do here.
405
0
  }
406
407
0
  std::string debug_str = "Blamer computing correct_segmentation_cols\n";
408
0
  int curr_box_col = 0;
409
0
  int next_box_col = 0;
410
0
  int num_blobs = word->NumBlobs();
411
0
  if (num_blobs == 0) {
412
0
    return; // No blobs to play with.
413
0
  }
414
0
  int blob_index = 0;
415
0
  int16_t next_box_x = word->blobs[blob_index]->bounding_box().right();
416
0
  for (unsigned truth_idx = 0; blob_index < num_blobs && truth_idx < norm_truth_word_.length();
417
0
       ++blob_index) {
418
0
    ++next_box_col;
419
0
    int16_t curr_box_x = next_box_x;
420
0
    if (blob_index + 1 < num_blobs) {
421
0
      next_box_x = word->blobs[blob_index + 1]->bounding_box().right();
422
0
    }
423
0
    int16_t truth_x = norm_truth_word_.BlobBox(truth_idx).right();
424
0
    debug_str += "Box x coord vs. truth: " + std::to_string(curr_box_x);
425
0
    debug_str += " " + std::to_string(truth_x);
426
0
    debug_str += "\n";
427
0
    if (curr_box_x > (truth_x + norm_box_tolerance_)) {
428
0
      break;                                                  // failed to find a matching box
429
0
    } else if (curr_box_x >= truth_x - norm_box_tolerance_ && // matched
430
0
               (blob_index + 1 >= num_blobs ||                // next box can't be included
431
0
                next_box_x > truth_x + norm_box_tolerance_)) {
432
0
      correct_segmentation_cols_.push_back(curr_box_col);
433
0
      correct_segmentation_rows_.push_back(next_box_col - 1);
434
0
      ++truth_idx;
435
0
      debug_str += "col=" + std::to_string(curr_box_col);
436
0
      debug_str += " row=" + std::to_string(next_box_col - 1);
437
0
      debug_str += "\n";
438
0
      curr_box_col = next_box_col;
439
0
    }
440
0
  }
441
0
  if (blob_index < num_blobs || // trailing blobs
442
0
      correct_segmentation_cols_.size() != norm_truth_word_.length()) {
443
0
    debug_str +=
444
0
        "Blamer failed to find correct segmentation"
445
0
        " (tolerance=" +
446
0
        std::to_string(norm_box_tolerance_);
447
0
    if (blob_index >= num_blobs) {
448
0
      debug_str += " blob == nullptr";
449
0
    }
450
0
    debug_str += ")\n";
451
0
    debug_str += " path length " + std::to_string(correct_segmentation_cols_.size());
452
0
    debug_str += " vs. truth " + std::to_string(norm_truth_word_.length());
453
0
    debug_str += "\n";
454
0
    SetBlame(IRR_UNKNOWN, debug_str, nullptr, debug);
455
0
    correct_segmentation_cols_.clear();
456
0
    correct_segmentation_rows_.clear();
457
0
  }
458
0
}
459
460
// Returns true if a guided segmentation search is needed.
461
0
bool BlamerBundle::GuidedSegsearchNeeded(const WERD_CHOICE *best_choice) const {
462
0
  return incorrect_result_reason_ == IRR_CORRECT && !segsearch_is_looking_for_blame_ &&
463
0
         truth_has_char_boxes_ && !ChoiceIsCorrect(best_choice);
464
0
}
465
466
#if !defined(DISABLED_LEGACY_ENGINE)
467
// Setup ready to guide the segmentation search to the correct segmentation.
468
void BlamerBundle::InitForSegSearch(const WERD_CHOICE *best_choice, MATRIX *ratings,
469
                                    bool debug, std::string &debug_str,
470
                                    tesseract::LMPainPoints *pain_points, double max_char_wh_ratio,
471
0
                                    WERD_RES *word_res) {
472
0
  segsearch_is_looking_for_blame_ = true;
473
0
  if (debug) {
474
0
    tprintf("segsearch starting to look for blame\n");
475
0
  }
476
  // Fill pain points for any unclassifed blob corresponding to the
477
  // correct segmentation state.
478
0
  debug_str += "Correct segmentation:\n";
479
0
  for (unsigned idx = 0; idx < correct_segmentation_cols_.size(); ++idx) {
480
0
    debug_str += "col=" + std::to_string(correct_segmentation_cols_[idx]);
481
0
    debug_str += " row=" + std::to_string(correct_segmentation_rows_[idx]);
482
0
    debug_str += "\n";
483
0
    if (!ratings->Classified(correct_segmentation_cols_[idx], correct_segmentation_rows_[idx]) &&
484
0
        !pain_points->GeneratePainPoint(
485
0
            correct_segmentation_cols_[idx], correct_segmentation_rows_[idx],
486
0
            tesseract::LM_PPTYPE_BLAMER, 0.0, false, max_char_wh_ratio, word_res)) {
487
0
      segsearch_is_looking_for_blame_ = false;
488
0
      debug_str += "\nFailed to insert pain point\n";
489
0
      SetBlame(IRR_SEGSEARCH_HEUR, debug_str, best_choice, debug);
490
0
      break;
491
0
    }
492
0
  } // end for blamer_bundle->correct_segmentation_cols/rows
493
0
}
494
#endif // !defined(DISABLED_LEGACY_ENGINE)
495
496
// Returns true if the guided segsearch is in progress.
497
0
bool BlamerBundle::GuidedSegsearchStillGoing() const {
498
0
  return segsearch_is_looking_for_blame_;
499
0
}
500
501
// The segmentation search has ended. Sets the blame appropriately.
502
0
void BlamerBundle::FinishSegSearch(const WERD_CHOICE *best_choice, bool debug, std::string &debug_str) {
503
  // If we are still looking for blame (i.e. best_choice is incorrect, but a
504
  // path representing the correct segmentation could be constructed), we can
505
  // blame segmentation search pain point prioritization if the rating of the
506
  // path corresponding to the correct segmentation is better than that of
507
  // best_choice (i.e. language model would have done the correct thing, but
508
  // because of poor pain point prioritization the correct segmentation was
509
  // never explored). Otherwise we blame the tradeoff between the language model
510
  // and the classifier, since even after exploring the path corresponding to
511
  // the correct segmentation incorrect best_choice would have been chosen.
512
  // One special case when we blame the classifier instead is when best choice
513
  // is incorrect, but it is a dictionary word and it classifier's top choice.
514
0
  if (segsearch_is_looking_for_blame_) {
515
0
    segsearch_is_looking_for_blame_ = false;
516
0
    if (best_choice_is_dict_and_top_choice_) {
517
0
      debug_str = "Best choice is: incorrect, top choice, dictionary word";
518
0
      debug_str += " with permuter ";
519
0
      debug_str += best_choice->permuter_name();
520
0
      SetBlame(IRR_CLASSIFIER, debug_str, best_choice, debug);
521
0
    } else if (best_correctly_segmented_rating_ < best_choice->rating()) {
522
0
      debug_str += "Correct segmentation state was not explored";
523
0
      SetBlame(IRR_SEGSEARCH_PP, debug_str, best_choice, debug);
524
0
    } else {
525
0
      if (best_correctly_segmented_rating_ >= WERD_CHOICE::kBadRating) {
526
0
        debug_str += "Correct segmentation paths were pruned by LM\n";
527
0
      } else {
528
0
        debug_str += "Best correct segmentation rating " +
529
0
                                  std::to_string(best_correctly_segmented_rating_);
530
0
        debug_str += " vs. best choice rating " + std::to_string(best_choice->rating());
531
0
      }
532
0
      SetBlame(IRR_CLASS_LM_TRADEOFF, debug_str, best_choice, debug);
533
0
    }
534
0
  }
535
0
}
536
537
// If the bundle is null or still does not indicate the correct result,
538
// fix it and use some backup reason for the blame.
539
0
void BlamerBundle::LastChanceBlame(bool debug, WERD_RES *word) {
540
0
  if (word->blamer_bundle == nullptr) {
541
0
    word->blamer_bundle = new BlamerBundle();
542
0
    word->blamer_bundle->SetBlame(IRR_PAGE_LAYOUT, "LastChanceBlame", word->best_choice, debug);
543
0
  } else if (word->blamer_bundle->incorrect_result_reason_ == IRR_NO_TRUTH) {
544
0
    word->blamer_bundle->SetBlame(IRR_NO_TRUTH, "Rejected truth", word->best_choice, debug);
545
0
  } else {
546
0
    bool correct = word->blamer_bundle->ChoiceIsCorrect(word->best_choice);
547
0
    IncorrectResultReason irr = word->blamer_bundle->incorrect_result_reason_;
548
0
    if (irr == IRR_CORRECT && !correct) {
549
0
      std::string debug_str = "Choice is incorrect after recognition";
550
0
      word->blamer_bundle->SetBlame(IRR_UNKNOWN, debug_str, word->best_choice, debug);
551
0
    } else if (irr != IRR_CORRECT && correct) {
552
0
      if (debug) {
553
0
        tprintf("Corrected %s\n", word->blamer_bundle->debug_.c_str());
554
0
      }
555
0
      word->blamer_bundle->incorrect_result_reason_ = IRR_CORRECT;
556
0
      word->blamer_bundle->debug_ = "";
557
0
    }
558
0
  }
559
0
}
560
561
// Sets the misadaption debug if this word is incorrect, as this word is
562
// being adapted to.
563
0
void BlamerBundle::SetMisAdaptionDebug(const WERD_CHOICE *best_choice, bool debug) {
564
0
  if (incorrect_result_reason_ != IRR_NO_TRUTH && !ChoiceIsCorrect(best_choice)) {
565
0
    misadaption_debug_ = "misadapt to word (";
566
0
    misadaption_debug_ += best_choice->permuter_name();
567
0
    misadaption_debug_ += "): ";
568
0
    FillDebugString("", best_choice, misadaption_debug_);
569
0
    if (debug) {
570
0
      tprintf("%s\n", misadaption_debug_.c_str());
571
0
    }
572
0
  }
573
0
}
574
575
} // namespace tesseract