/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 |