/src/tesseract/src/classify/adaptmatch.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | /****************************************************************************** |
2 | | ** Filename: adaptmatch.cpp |
3 | | ** Purpose: High level adaptive matcher. |
4 | | ** Author: Dan Johnson |
5 | | ** |
6 | | ** (c) Copyright Hewlett-Packard Company, 1988. |
7 | | ** Licensed under the Apache License, Version 2.0 (the "License"); |
8 | | ** you may not use this file except in compliance with the License. |
9 | | ** You may obtain a copy of the License at |
10 | | ** http://www.apache.org/licenses/LICENSE-2.0 |
11 | | ** Unless required by applicable law or agreed to in writing, software |
12 | | ** distributed under the License is distributed on an "AS IS" BASIS, |
13 | | ** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
14 | | ** See the License for the specific language governing permissions and |
15 | | ** limitations under the License. |
16 | | ******************************************************************************/ |
17 | | |
18 | | /*----------------------------------------------------------------------------- |
19 | | Include Files and Type Defines |
20 | | -----------------------------------------------------------------------------*/ |
21 | | #ifdef HAVE_CONFIG_H |
22 | | # include "config_auto.h" |
23 | | #endif |
24 | | |
25 | | #include "adaptive.h" // for ADAPT_CLASS |
26 | | #include "ambigs.h" // for UnicharIdVector, UnicharAmbigs |
27 | | #include "bitvec.h" // for FreeBitVector, NewBitVector, BIT_VECTOR |
28 | | #include "blobs.h" // for TBLOB, TWERD |
29 | | #include "classify.h" // for Classify, CST_FRAGMENT, CST_WHOLE |
30 | | #include "dict.h" // for Dict |
31 | | #include "errcode.h" // for ASSERT_HOST |
32 | | #include "featdefs.h" // for CharNormDesc |
33 | | #include "float2int.h" // for BASELINE_Y_SHIFT |
34 | | #include "fontinfo.h" // for ScoredFont, FontSet |
35 | | #include "intfx.h" // for BlobToTrainingSample, INT_FX_RESULT_S... |
36 | | #include "intmatcher.h" // for CP_RESULT_STRUCT, IntegerMatcher |
37 | | #include "intproto.h" // for INT_FEATURE_STRUCT, (anonymous), Clas... |
38 | | #include "matchdefs.h" // for CLASS_ID, FEATURE_ID, PROTO_ID, NO_PROTO |
39 | | #include "mfoutline.h" // for baseline, character, MF_SCALE_FACTOR |
40 | | #include "normalis.h" // for DENORM, kBlnBaselineOffset, kBlnXHeight |
41 | | #include "normfeat.h" // for ActualOutlineLength, CharNormLength |
42 | | #include "ocrfeatures.h" // for FEATURE_STRUCT, FEATURE |
43 | | #include "oldlist.h" // for push, delete_d |
44 | | #include "outfeat.h" // for OutlineFeatDir, OutlineFeatLength |
45 | | #include "pageres.h" // for WERD_RES |
46 | | #include "params.h" // for IntParam, BoolParam, DoubleParam, Str... |
47 | | #include "picofeat.h" // for PicoFeatDir, PicoFeatX, PicoFeatY |
48 | | #include "protos.h" // for PROTO_STRUCT, FillABC |
49 | | #include "ratngs.h" // for BLOB_CHOICE_IT, BLOB_CHOICE_LIST, BLO... |
50 | | #include "rect.h" // for TBOX |
51 | | #include "scrollview.h" // for ScrollView, ScrollView::BROWN, Scroll... |
52 | | #include "seam.h" // for SEAM |
53 | | #include "shapeclassifier.h" // for ShapeClassifier |
54 | | #include "shapetable.h" // for UnicharRating, ShapeTable, Shape, Uni... |
55 | | #include "tessclassifier.h" // for TessClassifier |
56 | | #include "tessdatamanager.h" // for TessdataManager, TESSDATA_INTTEMP |
57 | | #include "tprintf.h" // for tprintf |
58 | | #include "trainingsample.h" // for TrainingSample |
59 | | #include "unicharset.h" // for UNICHARSET, CHAR_FRAGMENT, UNICHAR_SPACE |
60 | | #include "unicity_table.h" // for UnicityTable |
61 | | |
62 | | #include <tesseract/unichar.h> // for UNICHAR_ID, INVALID_UNICHAR_ID |
63 | | #include "helpers.h" // for IntCastRounded, ClipToRange |
64 | | #include "serialis.h" // for TFile |
65 | | |
66 | | #include <algorithm> // for max, min |
67 | | #include <cassert> // for assert |
68 | | #include <cmath> // for fabs |
69 | | #include <cstdint> // for INT32_MAX, UINT8_MAX |
70 | | #include <cstdio> // for fflush, fclose, fopen, stdout, FILE |
71 | | #include <cstring> // for strstr, memset, strcmp |
72 | | |
73 | | namespace tesseract { |
74 | | |
75 | | // TODO: The parameter classify_enable_adaptive_matcher can cause |
76 | | // a segmentation fault if it is set to false (issue #256), |
77 | | // so override it here. |
78 | 4 | #define classify_enable_adaptive_matcher true |
79 | | |
80 | 0 | #define ADAPT_TEMPLATE_SUFFIX ".a" |
81 | | |
82 | 6.05M | #define MAX_MATCHES 10 |
83 | 6.42k | #define UNLIKELY_NUM_FEAT 200 |
84 | 3.51k | #define NO_DEBUG 0 |
85 | 105k | #define MAX_ADAPTABLE_WERD_SIZE 40 |
86 | | |
87 | 52.7k | #define ADAPTABLE_WERD_ADJUSTMENT (0.05) |
88 | | |
89 | 23.6k | #define Y_DIM_OFFSET (Y_SHIFT - BASELINE_Y_SHIFT) |
90 | | |
91 | 19.4M | #define WORST_POSSIBLE_RATING (0.0f) |
92 | | |
93 | | struct ADAPT_RESULTS { |
94 | | int32_t BlobLength; |
95 | | bool HasNonfragment; |
96 | | UNICHAR_ID best_unichar_id; |
97 | | int best_match_index; |
98 | | float best_rating; |
99 | | std::vector<UnicharRating> match; |
100 | | std::vector<CP_RESULT_STRUCT> CPResults; |
101 | | |
102 | | /// Initializes data members to the default values. Sets the initial |
103 | | /// rating of each class to be the worst possible rating (1.0). |
104 | 4.03M | inline void Initialize() { |
105 | 4.03M | BlobLength = INT32_MAX; |
106 | 4.03M | HasNonfragment = false; |
107 | 4.03M | ComputeBest(); |
108 | 4.03M | } |
109 | | // Computes best_unichar_id, best_match_index and best_rating. |
110 | 6.05M | void ComputeBest() { |
111 | 6.05M | best_unichar_id = INVALID_UNICHAR_ID; |
112 | 6.05M | best_match_index = -1; |
113 | 6.05M | best_rating = WORST_POSSIBLE_RATING; |
114 | 15.8M | for (unsigned i = 0; i < match.size(); ++i) { |
115 | 9.78M | if (match[i].rating > best_rating) { |
116 | 2.01M | best_rating = match[i].rating; |
117 | 2.01M | best_unichar_id = match[i].unichar_id; |
118 | 2.01M | best_match_index = i; |
119 | 2.01M | } |
120 | 9.78M | } |
121 | 6.05M | } |
122 | | }; |
123 | | |
124 | | struct PROTO_KEY { |
125 | | ADAPT_TEMPLATES_STRUCT *Templates; |
126 | | CLASS_ID ClassId; |
127 | | int ConfigId; |
128 | | }; |
129 | | |
130 | | // Sort function to sort ratings appropriately by descending rating. |
131 | 29.9M | static bool SortDescendingRating(const UnicharRating &a, const UnicharRating &b) { |
132 | 29.9M | if (a.rating != b.rating) { |
133 | 29.9M | return a.rating > b.rating; |
134 | 29.9M | } else { |
135 | 2.93k | return a.unichar_id < b.unichar_id; |
136 | 2.93k | } |
137 | 29.9M | } |
138 | | |
139 | | /*----------------------------------------------------------------------------- |
140 | | Private Macros |
141 | | -----------------------------------------------------------------------------*/ |
142 | 442k | inline bool MarginalMatch(float confidence, float matcher_great_threshold) { |
143 | 442k | return (1.0f - confidence) > matcher_great_threshold; |
144 | 442k | } |
145 | | |
146 | | /*----------------------------------------------------------------------------- |
147 | | Private Function Prototypes |
148 | | -----------------------------------------------------------------------------*/ |
149 | | // Returns the index of the given id in results, if present, or the size of the |
150 | | // vector (index it will go at) if not present. |
151 | 23.9M | static unsigned FindScoredUnichar(UNICHAR_ID id, const ADAPT_RESULTS &results) { |
152 | 105M | for (unsigned i = 0; i < results.match.size(); i++) { |
153 | 81.3M | if (results.match[i].unichar_id == id) { |
154 | 51.4k | return i; |
155 | 51.4k | } |
156 | 81.3M | } |
157 | 23.8M | return results.match.size(); |
158 | 23.9M | } |
159 | | |
160 | | // Returns the current rating for a unichar id if we have rated it, defaulting |
161 | | // to WORST_POSSIBLE_RATING. |
162 | 0 | static float ScoredUnichar(UNICHAR_ID id, const ADAPT_RESULTS &results) { |
163 | 0 | unsigned index = FindScoredUnichar(id, results); |
164 | 0 | if (index >= results.match.size()) { |
165 | 0 | return WORST_POSSIBLE_RATING; |
166 | 0 | } |
167 | 0 | return results.match[index].rating; |
168 | 0 | } |
169 | | |
170 | | void InitMatcherRatings(float *Rating); |
171 | | |
172 | | int MakeTempProtoPerm(void *item1, void *item2); |
173 | | |
174 | | void SetAdaptiveThreshold(float Threshold); |
175 | | |
176 | | /*----------------------------------------------------------------------------- |
177 | | Public Code |
178 | | -----------------------------------------------------------------------------*/ |
179 | | /** |
180 | | * This routine calls the adaptive matcher |
181 | | * which returns (in an array) the class id of each |
182 | | * class matched. |
183 | | * |
184 | | * It also returns the number of classes matched. |
185 | | * For each class matched it places the best rating |
186 | | * found for that class into the Ratings array. |
187 | | * |
188 | | * Bad matches are then removed so that they don't |
189 | | * need to be sorted. The remaining good matches are |
190 | | * then sorted and converted to choices. |
191 | | * |
192 | | * This routine also performs some simple speckle |
193 | | * filtering. |
194 | | * |
195 | | * @param Blob blob to be classified |
196 | | * @param[out] Choices List of choices found by adaptive matcher. |
197 | | * filled on return with the choices found by the |
198 | | * class pruner and the ratings there from. Also |
199 | | * contains the detailed results of the integer matcher. |
200 | | * |
201 | | */ |
202 | 2.01M | void Classify::AdaptiveClassifier(TBLOB *Blob, BLOB_CHOICE_LIST *Choices) { |
203 | 2.01M | assert(Choices != nullptr); |
204 | 2.01M | auto *Results = new ADAPT_RESULTS; |
205 | 2.01M | Results->Initialize(); |
206 | | |
207 | 2.01M | ASSERT_HOST(AdaptedTemplates != nullptr); |
208 | | |
209 | 2.01M | DoAdaptiveMatch(Blob, Results); |
210 | | |
211 | 2.01M | RemoveBadMatches(Results); |
212 | 2.01M | std::sort(Results->match.begin(), Results->match.end(), SortDescendingRating); |
213 | 2.01M | RemoveExtraPuncs(Results); |
214 | 2.01M | Results->ComputeBest(); |
215 | 2.01M | ConvertMatchesToChoices(Blob->denorm(), Blob->bounding_box(), Results, Choices); |
216 | | |
217 | | // TODO(rays) Move to before ConvertMatchesToChoices! |
218 | 2.01M | if (LargeSpeckle(*Blob) || Choices->empty()) { |
219 | 73.8k | AddLargeSpeckleTo(Results->BlobLength, Choices); |
220 | 73.8k | } |
221 | | |
222 | 2.01M | if (matcher_debug_level >= 1) { |
223 | 0 | tprintf("AD Matches = "); |
224 | 0 | PrintAdaptiveMatchResults(*Results); |
225 | 0 | } |
226 | | |
227 | | #ifndef GRAPHICS_DISABLED |
228 | | if (classify_enable_adaptive_debugger) { |
229 | | DebugAdaptiveClassifier(Blob, Results); |
230 | | } |
231 | | #endif |
232 | | |
233 | 2.01M | delete Results; |
234 | 2.01M | } /* AdaptiveClassifier */ |
235 | | |
236 | | #ifndef GRAPHICS_DISABLED |
237 | | |
238 | | // If *win is nullptr, sets it to a new ScrollView() object with title msg. |
239 | | // Clears the window and draws baselines. |
240 | | void Classify::RefreshDebugWindow(ScrollView **win, const char *msg, int y_offset, |
241 | | const TBOX &wbox) { |
242 | | const int kSampleSpaceWidth = 500; |
243 | | if (*win == nullptr) { |
244 | | *win = new ScrollView(msg, 100, y_offset, kSampleSpaceWidth * 2, 200, kSampleSpaceWidth * 2, |
245 | | 200, true); |
246 | | } |
247 | | (*win)->Clear(); |
248 | | (*win)->Pen(64, 64, 64); |
249 | | (*win)->Line(-kSampleSpaceWidth, kBlnBaselineOffset, kSampleSpaceWidth, kBlnBaselineOffset); |
250 | | (*win)->Line(-kSampleSpaceWidth, kBlnXHeight + kBlnBaselineOffset, kSampleSpaceWidth, |
251 | | kBlnXHeight + kBlnBaselineOffset); |
252 | | (*win)->ZoomToRectangle(wbox.left(), wbox.top(), wbox.right(), wbox.bottom()); |
253 | | } |
254 | | |
255 | | #endif // !GRAPHICS_DISABLED |
256 | | |
257 | | // Learns the given word using its chopped_word, seam_array, denorm, |
258 | | // box_word, best_state, and correct_text to learn both correctly and |
259 | | // incorrectly segmented blobs. If fontname is not nullptr, then LearnBlob |
260 | | // is called and the data will be saved in an internal buffer. |
261 | | // Otherwise AdaptToBlob is called for adaption within a document. |
262 | 2.40k | void Classify::LearnWord(const char *fontname, WERD_RES *word) { |
263 | 2.40k | int word_len = word->correct_text.size(); |
264 | 2.40k | if (word_len == 0) { |
265 | 0 | return; |
266 | 0 | } |
267 | | |
268 | 2.40k | float *thresholds = nullptr; |
269 | 2.40k | if (fontname == nullptr) { |
270 | | // Adaption mode. |
271 | 2.40k | if (!EnableLearning || word->best_choice == nullptr) { |
272 | 0 | return; // Can't or won't adapt. |
273 | 0 | } |
274 | | |
275 | 2.40k | if (classify_learning_debug_level >= 1) { |
276 | 0 | tprintf("\n\nAdapting to word = %s\n", word->best_choice->debug_string().c_str()); |
277 | 0 | } |
278 | 2.40k | thresholds = new float[word_len]; |
279 | 2.40k | word->ComputeAdaptionThresholds(getDict().certainty_scale, matcher_perfect_threshold, |
280 | 2.40k | matcher_good_threshold, matcher_rating_margin, thresholds); |
281 | 2.40k | } |
282 | 2.40k | int start_blob = 0; |
283 | | |
284 | | #ifndef GRAPHICS_DISABLED |
285 | | if (classify_debug_character_fragments) { |
286 | | if (learn_fragmented_word_debug_win_ != nullptr) { |
287 | | learn_fragmented_word_debug_win_->Wait(); |
288 | | } |
289 | | RefreshDebugWindow(&learn_fragments_debug_win_, "LearnPieces", 400, |
290 | | word->chopped_word->bounding_box()); |
291 | | RefreshDebugWindow(&learn_fragmented_word_debug_win_, "LearnWord", 200, |
292 | | word->chopped_word->bounding_box()); |
293 | | word->chopped_word->plot(learn_fragmented_word_debug_win_); |
294 | | ScrollView::Update(); |
295 | | } |
296 | | #endif // !GRAPHICS_DISABLED |
297 | | |
298 | 4.82k | for (int ch = 0; ch < word_len; ++ch) { |
299 | 2.41k | if (classify_debug_character_fragments) { |
300 | 0 | tprintf("\nLearning %s\n", word->correct_text[ch].c_str()); |
301 | 0 | } |
302 | 2.41k | if (word->correct_text[ch].length() > 0) { |
303 | 2.41k | float threshold = thresholds != nullptr ? thresholds[ch] : 0.0f; |
304 | | |
305 | 2.41k | LearnPieces(fontname, start_blob, word->best_state[ch], threshold, CST_WHOLE, |
306 | 2.41k | word->correct_text[ch].c_str(), word); |
307 | | |
308 | 2.41k | if (word->best_state[ch] > 1 && !disable_character_fragments) { |
309 | | // Check that the character breaks into meaningful fragments |
310 | | // that each match a whole character with at least |
311 | | // classify_character_fragments_garbage_certainty_threshold |
312 | 0 | bool garbage = false; |
313 | 0 | int frag; |
314 | 0 | for (frag = 0; frag < word->best_state[ch]; ++frag) { |
315 | 0 | TBLOB *frag_blob = word->chopped_word->blobs[start_blob + frag]; |
316 | 0 | if (classify_character_fragments_garbage_certainty_threshold < 0) { |
317 | 0 | garbage |= LooksLikeGarbage(frag_blob); |
318 | 0 | } |
319 | 0 | } |
320 | | // Learn the fragments. |
321 | 0 | if (!garbage) { |
322 | 0 | bool pieces_all_natural = word->PiecesAllNatural(start_blob, word->best_state[ch]); |
323 | 0 | if (pieces_all_natural || !prioritize_division) { |
324 | 0 | for (frag = 0; frag < word->best_state[ch]; ++frag) { |
325 | 0 | std::vector<std::string> tokens = split(word->correct_text[ch], ' '); |
326 | |
|
327 | 0 | tokens[0] = CHAR_FRAGMENT::to_string(tokens[0].c_str(), frag, word->best_state[ch], |
328 | 0 | pieces_all_natural); |
329 | |
|
330 | 0 | std::string full_string; |
331 | 0 | for (unsigned i = 0; i < tokens.size(); i++) { |
332 | 0 | full_string += tokens[i]; |
333 | 0 | if (i != tokens.size() - 1) { |
334 | 0 | full_string += ' '; |
335 | 0 | } |
336 | 0 | } |
337 | 0 | LearnPieces(fontname, start_blob + frag, 1, threshold, CST_FRAGMENT, |
338 | 0 | full_string.c_str(), word); |
339 | 0 | } |
340 | 0 | } |
341 | 0 | } |
342 | 0 | } |
343 | | |
344 | | // TODO(rays): re-enable this part of the code when we switch to the |
345 | | // new classifier that needs to see examples of garbage. |
346 | | /* |
347 | | if (word->best_state[ch] > 1) { |
348 | | // If the next blob is good, make junk with the rightmost fragment. |
349 | | if (ch + 1 < word_len && word->correct_text[ch + 1].length() > 0) { |
350 | | LearnPieces(fontname, start_blob + word->best_state[ch] - 1, |
351 | | word->best_state[ch + 1] + 1, |
352 | | threshold, CST_IMPROPER, INVALID_UNICHAR, word); |
353 | | } |
354 | | // If the previous blob is good, make junk with the leftmost fragment. |
355 | | if (ch > 0 && word->correct_text[ch - 1].length() > 0) { |
356 | | LearnPieces(fontname, start_blob - word->best_state[ch - 1], |
357 | | word->best_state[ch - 1] + 1, |
358 | | threshold, CST_IMPROPER, INVALID_UNICHAR, word); |
359 | | } |
360 | | } |
361 | | // If the next blob is good, make a join with it. |
362 | | if (ch + 1 < word_len && word->correct_text[ch + 1].length() > 0) { |
363 | | std::string joined_text = word->correct_text[ch]; |
364 | | joined_text += word->correct_text[ch + 1]; |
365 | | LearnPieces(fontname, start_blob, |
366 | | word->best_state[ch] + word->best_state[ch + 1], |
367 | | threshold, CST_NGRAM, joined_text.c_str(), word); |
368 | | } |
369 | | */ |
370 | 2.41k | } |
371 | 2.41k | start_blob += word->best_state[ch]; |
372 | 2.41k | } |
373 | 2.40k | delete[] thresholds; |
374 | 2.40k | } // LearnWord. |
375 | | |
376 | | // Builds a blob of length fragments, from the word, starting at start, |
377 | | // and then learns it, as having the given correct_text. |
378 | | // If fontname is not nullptr, then LearnBlob is called and the data will be |
379 | | // saved in an internal buffer for static training. |
380 | | // Otherwise AdaptToBlob is called for adaption within a document. |
381 | | // threshold is a magic number required by AdaptToChar and generated by |
382 | | // ComputeAdaptionThresholds. |
383 | | // Although it can be partly inferred from the string, segmentation is |
384 | | // provided to explicitly clarify the character segmentation. |
385 | | void Classify::LearnPieces(const char *fontname, int start, int length, float threshold, |
386 | | CharSegmentationType segmentation, const char *correct_text, |
387 | 2.41k | WERD_RES *word) { |
388 | | // TODO(daria) Remove/modify this if/when we want |
389 | | // to train and/or adapt to n-grams. |
390 | 2.41k | if (segmentation != CST_WHOLE && (segmentation != CST_FRAGMENT || disable_character_fragments)) { |
391 | 0 | return; |
392 | 0 | } |
393 | | |
394 | 2.41k | if (length > 1) { |
395 | 87 | SEAM::JoinPieces(word->seam_array, word->chopped_word->blobs, start, start + length - 1); |
396 | 87 | } |
397 | 2.41k | TBLOB *blob = word->chopped_word->blobs[start]; |
398 | | // Rotate the blob if needed for classification. |
399 | 2.41k | TBLOB *rotated_blob = blob->ClassifyNormalizeIfNeeded(); |
400 | 2.41k | if (rotated_blob == nullptr) { |
401 | 2.41k | rotated_blob = blob; |
402 | 2.41k | } |
403 | | |
404 | | #ifndef GRAPHICS_DISABLED |
405 | | // Draw debug windows showing the blob that is being learned if needed. |
406 | | if (strcmp(classify_learn_debug_str.c_str(), correct_text) == 0) { |
407 | | RefreshDebugWindow(&learn_debug_win_, "LearnPieces", 600, word->chopped_word->bounding_box()); |
408 | | rotated_blob->plot(learn_debug_win_, ScrollView::GREEN, ScrollView::BROWN); |
409 | | learn_debug_win_->Update(); |
410 | | learn_debug_win_->Wait(); |
411 | | } |
412 | | if (classify_debug_character_fragments && segmentation == CST_FRAGMENT) { |
413 | | ASSERT_HOST(learn_fragments_debug_win_ != nullptr); // set up in LearnWord |
414 | | blob->plot(learn_fragments_debug_win_, ScrollView::BLUE, ScrollView::BROWN); |
415 | | learn_fragments_debug_win_->Update(); |
416 | | } |
417 | | #endif // !GRAPHICS_DISABLED |
418 | | |
419 | 2.41k | if (fontname != nullptr) { |
420 | 0 | classify_norm_method.set_value(character); // force char norm spc 30/11/93 |
421 | 0 | tess_bn_matching.set_value(false); // turn it off |
422 | 0 | tess_cn_matching.set_value(false); |
423 | 0 | DENORM bl_denorm, cn_denorm; |
424 | 0 | INT_FX_RESULT_STRUCT fx_info; |
425 | 0 | SetupBLCNDenorms(*rotated_blob, classify_nonlinear_norm, &bl_denorm, &cn_denorm, &fx_info); |
426 | 0 | LearnBlob(fontname, rotated_blob, cn_denorm, fx_info, correct_text); |
427 | 2.41k | } else if (unicharset.contains_unichar(correct_text)) { |
428 | 2.41k | UNICHAR_ID class_id = unicharset.unichar_to_id(correct_text); |
429 | 2.41k | int font_id = word->fontinfo != nullptr ? fontinfo_table_.get_index(*word->fontinfo) : 0; |
430 | 2.41k | if (classify_learning_debug_level >= 1) { |
431 | 0 | tprintf("Adapting to char = %s, thr= %g font_id= %d\n", unicharset.id_to_unichar(class_id), |
432 | 0 | threshold, font_id); |
433 | 0 | } |
434 | | // If filename is not nullptr we are doing recognition |
435 | | // (as opposed to training), so we must have already set word fonts. |
436 | 2.41k | AdaptToChar(rotated_blob, class_id, font_id, threshold, AdaptedTemplates); |
437 | 2.41k | if (BackupAdaptedTemplates != nullptr) { |
438 | | // Adapt the backup templates too. They will be used if the primary gets |
439 | | // too full. |
440 | 2.36k | AdaptToChar(rotated_blob, class_id, font_id, threshold, BackupAdaptedTemplates); |
441 | 2.36k | } |
442 | 2.41k | } else if (classify_debug_level >= 1) { |
443 | 0 | tprintf("Can't adapt to %s not in unicharset\n", correct_text); |
444 | 0 | } |
445 | 2.41k | if (rotated_blob != blob) { |
446 | 0 | delete rotated_blob; |
447 | 0 | } |
448 | | |
449 | 2.41k | SEAM::BreakPieces(word->seam_array, word->chopped_word->blobs, start, start + length - 1); |
450 | 2.41k | } // LearnPieces. |
451 | | |
452 | | /*---------------------------------------------------------------------------*/ |
453 | | /** |
454 | | * This routine performs cleanup operations |
455 | | * on the adaptive classifier. It should be called |
456 | | * before the program is terminated. Its main function |
457 | | * is to save the adapted templates to a file. |
458 | | * |
459 | | * Globals: |
460 | | * - #AdaptedTemplates current set of adapted templates |
461 | | * - #classify_save_adapted_templates true if templates should be saved |
462 | | * - #classify_enable_adaptive_matcher true if adaptive matcher is enabled |
463 | | */ |
464 | 0 | void Classify::EndAdaptiveClassifier() { |
465 | 0 | std::string Filename; |
466 | 0 | FILE *File; |
467 | |
|
468 | 0 | if (AdaptedTemplates != nullptr && classify_enable_adaptive_matcher && |
469 | 0 | classify_save_adapted_templates) { |
470 | 0 | Filename = imagefile + ADAPT_TEMPLATE_SUFFIX; |
471 | 0 | File = fopen(Filename.c_str(), "wb"); |
472 | 0 | if (File == nullptr) { |
473 | 0 | tprintf("Unable to save adapted templates to %s!\n", Filename.c_str()); |
474 | 0 | } else { |
475 | 0 | tprintf("\nSaving adapted templates to %s ...", Filename.c_str()); |
476 | 0 | fflush(stdout); |
477 | 0 | WriteAdaptedTemplates(File, AdaptedTemplates); |
478 | 0 | tprintf("\n"); |
479 | 0 | fclose(File); |
480 | 0 | } |
481 | 0 | } |
482 | |
|
483 | 0 | delete AdaptedTemplates; |
484 | 0 | AdaptedTemplates = nullptr; |
485 | 0 | delete BackupAdaptedTemplates; |
486 | 0 | BackupAdaptedTemplates = nullptr; |
487 | |
|
488 | 0 | if (PreTrainedTemplates != nullptr) { |
489 | 0 | delete PreTrainedTemplates; |
490 | 0 | PreTrainedTemplates = nullptr; |
491 | 0 | } |
492 | 0 | getDict().EndDangerousAmbigs(); |
493 | 0 | FreeNormProtos(); |
494 | 0 | if (AllProtosOn != nullptr) { |
495 | 0 | FreeBitVector(AllProtosOn); |
496 | 0 | FreeBitVector(AllConfigsOn); |
497 | 0 | FreeBitVector(AllConfigsOff); |
498 | 0 | FreeBitVector(TempProtoMask); |
499 | 0 | AllProtosOn = nullptr; |
500 | 0 | AllConfigsOn = nullptr; |
501 | 0 | AllConfigsOff = nullptr; |
502 | 0 | TempProtoMask = nullptr; |
503 | 0 | } |
504 | 0 | delete shape_table_; |
505 | 0 | shape_table_ = nullptr; |
506 | 0 | delete static_classifier_; |
507 | 0 | static_classifier_ = nullptr; |
508 | 0 | } /* EndAdaptiveClassifier */ |
509 | | |
510 | | /*---------------------------------------------------------------------------*/ |
511 | | /** |
512 | | * This routine reads in the training |
513 | | * information needed by the adaptive classifier |
514 | | * and saves it into global variables. |
515 | | * Parameters: |
516 | | * load_pre_trained_templates Indicates whether the pre-trained |
517 | | * templates (inttemp, normproto and pffmtable components) |
518 | | * should be loaded. Should only be set to true if the |
519 | | * necessary classifier components are present in the |
520 | | * [lang].traineddata file. |
521 | | * Globals: |
522 | | * BuiltInTemplatesFile file to get built-in temps from |
523 | | * BuiltInCutoffsFile file to get avg. feat per class from |
524 | | * classify_use_pre_adapted_templates |
525 | | * enables use of pre-adapted templates |
526 | | */ |
527 | 4 | void Classify::InitAdaptiveClassifier(TessdataManager *mgr) { |
528 | 4 | if (!classify_enable_adaptive_matcher) { |
529 | 0 | return; |
530 | 0 | } |
531 | 4 | if (AllProtosOn != nullptr) { |
532 | 0 | EndAdaptiveClassifier(); // Don't leak with multiple inits. |
533 | 0 | } |
534 | | |
535 | | // If there is no language_data_path_prefix, the classifier will be |
536 | | // adaptive only. |
537 | 4 | if (language_data_path_prefix.length() > 0 && mgr != nullptr) { |
538 | 4 | TFile fp; |
539 | 4 | ASSERT_HOST(mgr->GetComponent(TESSDATA_INTTEMP, &fp)); |
540 | 4 | PreTrainedTemplates = ReadIntTemplates(&fp); |
541 | | |
542 | 4 | if (mgr->GetComponent(TESSDATA_SHAPE_TABLE, &fp)) { |
543 | 4 | shape_table_ = new ShapeTable(unicharset); |
544 | 4 | if (!shape_table_->DeSerialize(&fp)) { |
545 | 0 | tprintf("Error loading shape table!\n"); |
546 | 0 | delete shape_table_; |
547 | 0 | shape_table_ = nullptr; |
548 | 0 | } |
549 | 4 | } |
550 | | |
551 | 4 | ASSERT_HOST(mgr->GetComponent(TESSDATA_PFFMTABLE, &fp)); |
552 | 4 | ReadNewCutoffs(&fp, CharNormCutoffs); |
553 | | |
554 | 4 | ASSERT_HOST(mgr->GetComponent(TESSDATA_NORMPROTO, &fp)); |
555 | 4 | NormProtos = ReadNormProtos(&fp); |
556 | 4 | static_classifier_ = new TessClassifier(false, this); |
557 | 4 | } |
558 | | |
559 | 4 | InitIntegerFX(); |
560 | | |
561 | 4 | AllProtosOn = NewBitVector(MAX_NUM_PROTOS); |
562 | 4 | AllConfigsOn = NewBitVector(MAX_NUM_CONFIGS); |
563 | 4 | AllConfigsOff = NewBitVector(MAX_NUM_CONFIGS); |
564 | 4 | TempProtoMask = NewBitVector(MAX_NUM_PROTOS); |
565 | 4 | set_all_bits(AllProtosOn, WordsInVectorOfSize(MAX_NUM_PROTOS)); |
566 | 4 | set_all_bits(AllConfigsOn, WordsInVectorOfSize(MAX_NUM_CONFIGS)); |
567 | 4 | zero_all_bits(AllConfigsOff, WordsInVectorOfSize(MAX_NUM_CONFIGS)); |
568 | | |
569 | 131k | for (uint16_t &BaselineCutoff : BaselineCutoffs) { |
570 | 131k | BaselineCutoff = 0; |
571 | 131k | } |
572 | | |
573 | 4 | if (classify_use_pre_adapted_templates) { |
574 | 0 | TFile fp; |
575 | 0 | std::string Filename = imagefile; |
576 | 0 | Filename += ADAPT_TEMPLATE_SUFFIX; |
577 | 0 | if (!fp.Open(Filename.c_str(), nullptr)) { |
578 | 0 | AdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset); |
579 | 0 | } else { |
580 | 0 | tprintf("\nReading pre-adapted templates from %s ...\n", Filename.c_str()); |
581 | 0 | fflush(stdout); |
582 | 0 | AdaptedTemplates = ReadAdaptedTemplates(&fp); |
583 | 0 | tprintf("\n"); |
584 | 0 | PrintAdaptedTemplates(stdout, AdaptedTemplates); |
585 | |
|
586 | 0 | for (unsigned i = 0; i < AdaptedTemplates->Templates->NumClasses; i++) { |
587 | 0 | BaselineCutoffs[i] = CharNormCutoffs[i]; |
588 | 0 | } |
589 | 0 | } |
590 | 4 | } else { |
591 | 4 | delete AdaptedTemplates; |
592 | 4 | AdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset); |
593 | 4 | } |
594 | 4 | } /* InitAdaptiveClassifier */ |
595 | | |
596 | 0 | void Classify::ResetAdaptiveClassifierInternal() { |
597 | 0 | if (classify_learning_debug_level > 0) { |
598 | 0 | tprintf("Resetting adaptive classifier (NumAdaptationsFailed=%d)\n", NumAdaptationsFailed); |
599 | 0 | } |
600 | 0 | delete AdaptedTemplates; |
601 | 0 | AdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset); |
602 | 0 | delete BackupAdaptedTemplates; |
603 | 0 | BackupAdaptedTemplates = nullptr; |
604 | 0 | NumAdaptationsFailed = 0; |
605 | 0 | } |
606 | | |
607 | | // If there are backup adapted templates, switches to those, otherwise resets |
608 | | // the main adaptive classifier (because it is full.) |
609 | 2 | void Classify::SwitchAdaptiveClassifier() { |
610 | 2 | if (BackupAdaptedTemplates == nullptr) { |
611 | 0 | ResetAdaptiveClassifierInternal(); |
612 | 0 | return; |
613 | 0 | } |
614 | 2 | if (classify_learning_debug_level > 0) { |
615 | 0 | tprintf("Switch to backup adaptive classifier (NumAdaptationsFailed=%d)\n", |
616 | 0 | NumAdaptationsFailed); |
617 | 0 | } |
618 | 2 | delete AdaptedTemplates; |
619 | 2 | AdaptedTemplates = BackupAdaptedTemplates; |
620 | 2 | BackupAdaptedTemplates = nullptr; |
621 | 2 | NumAdaptationsFailed = 0; |
622 | 2 | } |
623 | | |
624 | | // Resets the backup adaptive classifier to empty. |
625 | 11.6k | void Classify::StartBackupAdaptiveClassifier() { |
626 | 11.6k | delete BackupAdaptedTemplates; |
627 | 11.6k | BackupAdaptedTemplates = new ADAPT_TEMPLATES_STRUCT(unicharset); |
628 | 11.6k | } |
629 | | |
630 | | /*---------------------------------------------------------------------------*/ |
631 | | /** |
632 | | * This routine prepares the adaptive |
633 | | * matcher for the start |
634 | | * of the first pass. Learning is enabled (unless it |
635 | | * is disabled for the whole program). |
636 | | * |
637 | | * @note this is somewhat redundant, it simply says that if learning is |
638 | | * enabled then it will remain enabled on the first pass. If it is |
639 | | * disabled, then it will remain disabled. This is only put here to |
640 | | * make it very clear that learning is controlled directly by the global |
641 | | * setting of EnableLearning. |
642 | | * |
643 | | * Globals: |
644 | | * - #EnableLearning |
645 | | * set to true by this routine |
646 | | */ |
647 | 52.7k | void Classify::SetupPass1() { |
648 | 52.7k | EnableLearning = classify_enable_learning; |
649 | | |
650 | 52.7k | getDict().SetupStopperPass1(); |
651 | | |
652 | 52.7k | } /* SetupPass1 */ |
653 | | |
654 | | /*---------------------------------------------------------------------------*/ |
655 | | /** |
656 | | * This routine prepares the adaptive |
657 | | * matcher for the start of the second pass. Further |
658 | | * learning is disabled. |
659 | | * |
660 | | * Globals: |
661 | | * - #EnableLearning set to false by this routine |
662 | | */ |
663 | 76.6k | void Classify::SetupPass2() { |
664 | 76.6k | EnableLearning = false; |
665 | 76.6k | getDict().SetupStopperPass2(); |
666 | | |
667 | 76.6k | } /* SetupPass2 */ |
668 | | |
669 | | /*---------------------------------------------------------------------------*/ |
670 | | /** |
671 | | * This routine creates a new adapted |
672 | | * class and uses Blob as the model for the first |
673 | | * config in that class. |
674 | | * |
675 | | * @param Blob blob to model new class after |
676 | | * @param ClassId id of the class to be initialized |
677 | | * @param FontinfoId font information inferred from pre-trained templates |
678 | | * @param Class adapted class to be initialized |
679 | | * @param Templates adapted templates to add new class to |
680 | | * |
681 | | * Globals: |
682 | | * - #AllProtosOn dummy mask with all 1's |
683 | | * - BaselineCutoffs kludge needed to get cutoffs |
684 | | * - #PreTrainedTemplates kludge needed to get cutoffs |
685 | | */ |
686 | | void Classify::InitAdaptedClass(TBLOB *Blob, CLASS_ID ClassId, int FontinfoId, ADAPT_CLASS_STRUCT *Class, |
687 | 1.63k | ADAPT_TEMPLATES_STRUCT *Templates) { |
688 | 1.63k | FEATURE_SET Features; |
689 | 1.63k | int Fid, Pid; |
690 | 1.63k | FEATURE Feature; |
691 | 1.63k | int NumFeatures; |
692 | 1.63k | PROTO_STRUCT *Proto; |
693 | 1.63k | INT_CLASS_STRUCT *IClass; |
694 | 1.63k | TEMP_CONFIG_STRUCT *Config; |
695 | | |
696 | 1.63k | classify_norm_method.set_value(baseline); |
697 | 1.63k | Features = ExtractOutlineFeatures(Blob); |
698 | 1.63k | NumFeatures = Features->NumFeatures; |
699 | 1.63k | if (NumFeatures > UNLIKELY_NUM_FEAT || NumFeatures <= 0) { |
700 | 0 | delete Features; |
701 | 0 | return; |
702 | 0 | } |
703 | | |
704 | 1.63k | Config = new TEMP_CONFIG_STRUCT(NumFeatures - 1, FontinfoId); |
705 | 1.63k | TempConfigFor(Class, 0) = Config; |
706 | | |
707 | | /* this is a kludge to construct cutoffs for adapted templates */ |
708 | 1.63k | if (Templates == AdaptedTemplates) { |
709 | 10 | BaselineCutoffs[ClassId] = CharNormCutoffs[ClassId]; |
710 | 10 | } |
711 | | |
712 | 1.63k | IClass = ClassForClassId(Templates->Templates, ClassId); |
713 | | |
714 | 21.0k | for (Fid = 0; Fid < Features->NumFeatures; Fid++) { |
715 | 19.4k | Pid = AddIntProto(IClass); |
716 | 19.4k | assert(Pid != NO_PROTO); |
717 | | |
718 | 19.4k | Feature = Features->Features[Fid]; |
719 | 19.4k | auto TempProto = new TEMP_PROTO_STRUCT; |
720 | 19.4k | Proto = &(TempProto->Proto); |
721 | | |
722 | | /* compute proto params - NOTE that Y_DIM_OFFSET must be used because |
723 | | ConvertProto assumes that the Y dimension varies from -0.5 to 0.5 |
724 | | instead of the -0.25 to 0.75 used in baseline normalization */ |
725 | 19.4k | Proto->Angle = Feature->Params[OutlineFeatDir]; |
726 | 19.4k | Proto->X = Feature->Params[OutlineFeatX]; |
727 | 19.4k | Proto->Y = Feature->Params[OutlineFeatY] - Y_DIM_OFFSET; |
728 | 19.4k | Proto->Length = Feature->Params[OutlineFeatLength]; |
729 | 19.4k | FillABC(Proto); |
730 | | |
731 | 19.4k | TempProto->ProtoId = Pid; |
732 | 19.4k | SET_BIT(Config->Protos, Pid); |
733 | | |
734 | 19.4k | ConvertProto(Proto, Pid, IClass); |
735 | 19.4k | AddProtoToProtoPruner(Proto, Pid, IClass, classify_learning_debug_level >= 2); |
736 | | |
737 | 19.4k | Class->TempProtos = push(Class->TempProtos, TempProto); |
738 | 19.4k | } |
739 | 1.63k | delete Features; |
740 | | |
741 | 1.63k | AddIntConfig(IClass); |
742 | 1.63k | ConvertConfig(AllProtosOn, 0, IClass); |
743 | | |
744 | 1.63k | if (classify_learning_debug_level >= 1) { |
745 | 0 | tprintf("Added new class '%s' with class id %d and %d protos.\n", |
746 | 0 | unicharset.id_to_unichar(ClassId), ClassId, NumFeatures); |
747 | | #ifndef GRAPHICS_DISABLED |
748 | | if (classify_learning_debug_level > 1) { |
749 | | DisplayAdaptedChar(Blob, IClass); |
750 | | } |
751 | | #endif |
752 | 0 | } |
753 | | |
754 | 1.63k | if (IsEmptyAdaptedClass(Class)) { |
755 | 0 | (Templates->NumNonEmptyClasses)++; |
756 | 0 | } |
757 | 1.63k | } /* InitAdaptedClass */ |
758 | | |
759 | | /*---------------------------------------------------------------------------*/ |
760 | | /** |
761 | | * This routine sets up the feature |
762 | | * extractor to extract baseline normalized |
763 | | * pico-features. |
764 | | * |
765 | | * The extracted pico-features are converted |
766 | | * to integer form and placed in IntFeatures. The |
767 | | * original floating-pt. features are returned in |
768 | | * FloatFeatures. |
769 | | * |
770 | | * Globals: none |
771 | | * @param Blob blob to extract features from |
772 | | * @param[out] IntFeatures array to fill with integer features |
773 | | * @param[out] FloatFeatures place to return actual floating-pt features |
774 | | * |
775 | | * @return Number of pico-features returned (0 if |
776 | | * an error occurred) |
777 | | */ |
778 | | int Classify::GetAdaptiveFeatures(TBLOB *Blob, INT_FEATURE_ARRAY IntFeatures, |
779 | 3.14k | FEATURE_SET *FloatFeatures) { |
780 | 3.14k | FEATURE_SET Features; |
781 | 3.14k | int NumFeatures; |
782 | | |
783 | 3.14k | classify_norm_method.set_value(baseline); |
784 | 3.14k | Features = ExtractPicoFeatures(Blob); |
785 | | |
786 | 3.14k | NumFeatures = Features->NumFeatures; |
787 | 3.14k | if (NumFeatures == 0 || NumFeatures > UNLIKELY_NUM_FEAT) { |
788 | 47 | delete Features; |
789 | 47 | return 0; |
790 | 47 | } |
791 | | |
792 | 3.09k | ComputeIntFeatures(Features, IntFeatures); |
793 | 3.09k | *FloatFeatures = Features; |
794 | | |
795 | 3.09k | return NumFeatures; |
796 | 3.14k | } /* GetAdaptiveFeatures */ |
797 | | |
798 | | /*----------------------------------------------------------------------------- |
799 | | Private Code |
800 | | -----------------------------------------------------------------------------*/ |
801 | | /*---------------------------------------------------------------------------*/ |
802 | | /** |
803 | | * Return true if the specified word is acceptable for adaptation. |
804 | | * |
805 | | * Globals: none |
806 | | * |
807 | | * @param word current word |
808 | | * |
809 | | * @return true or false |
810 | | */ |
811 | 52.7k | bool Classify::AdaptableWord(WERD_RES *word) { |
812 | 52.7k | if (word->best_choice == nullptr) { |
813 | 0 | return false; |
814 | 0 | } |
815 | 52.7k | auto BestChoiceLength = word->best_choice->length(); |
816 | 52.7k | float adaptable_score = getDict().segment_penalty_dict_case_ok + ADAPTABLE_WERD_ADJUSTMENT; |
817 | 52.7k | return // rules that apply in general - simplest to compute first |
818 | 52.7k | BestChoiceLength > 0 && BestChoiceLength == word->rebuild_word->NumBlobs() && |
819 | 52.7k | BestChoiceLength <= MAX_ADAPTABLE_WERD_SIZE && |
820 | | // This basically ensures that the word is at least a dictionary match |
821 | | // (freq word, user word, system dawg word, etc). |
822 | | // Since all the other adjustments will make adjust factor higher |
823 | | // than higher than adaptable_score=1.1+0.05=1.15 |
824 | | // Since these are other flags that ensure that the word is dict word, |
825 | | // this check could be at times redundant. |
826 | 52.7k | word->best_choice->adjust_factor() <= adaptable_score && |
827 | | // Make sure that alternative choices are not dictionary words. |
828 | 52.7k | word->AlternativeChoiceAdjustmentsWorseThan(adaptable_score); |
829 | 52.7k | } |
830 | | |
831 | | /*---------------------------------------------------------------------------*/ |
832 | | /** |
833 | | * @param Blob blob to add to templates for ClassId |
834 | | * @param ClassId class to add blob to |
835 | | * @param FontinfoId font information from pre-trained templates |
836 | | * @param Threshold minimum match rating to existing template |
837 | | * @param adaptive_templates current set of adapted templates |
838 | | * |
839 | | * Globals: |
840 | | * - AllProtosOn dummy mask to match against all protos |
841 | | * - AllConfigsOn dummy mask to match against all configs |
842 | | */ |
843 | | void Classify::AdaptToChar(TBLOB *Blob, CLASS_ID ClassId, int FontinfoId, float Threshold, |
844 | 4.78k | ADAPT_TEMPLATES_STRUCT *adaptive_templates) { |
845 | 4.78k | int NumFeatures; |
846 | 4.78k | INT_FEATURE_ARRAY IntFeatures; |
847 | 4.78k | UnicharRating int_result; |
848 | 4.78k | INT_CLASS_STRUCT *IClass; |
849 | 4.78k | ADAPT_CLASS_STRUCT *Class; |
850 | 4.78k | TEMP_CONFIG_STRUCT *TempConfig; |
851 | 4.78k | FEATURE_SET FloatFeatures; |
852 | 4.78k | int NewTempConfigId; |
853 | | |
854 | 4.78k | if (!LegalClassId(ClassId)) { |
855 | 0 | return; |
856 | 0 | } |
857 | | |
858 | 4.78k | int_result.unichar_id = ClassId; |
859 | 4.78k | Class = adaptive_templates->Class[ClassId]; |
860 | 4.78k | assert(Class != nullptr); |
861 | 4.78k | if (IsEmptyAdaptedClass(Class)) { |
862 | 1.63k | InitAdaptedClass(Blob, ClassId, FontinfoId, Class, adaptive_templates); |
863 | 3.14k | } else { |
864 | 3.14k | IClass = ClassForClassId(adaptive_templates->Templates, ClassId); |
865 | | |
866 | 3.14k | NumFeatures = GetAdaptiveFeatures(Blob, IntFeatures, &FloatFeatures); |
867 | 3.14k | if (NumFeatures <= 0) { |
868 | 47 | return; // Features already freed by GetAdaptiveFeatures. |
869 | 47 | } |
870 | | |
871 | | // Only match configs with the matching font. |
872 | 3.09k | BIT_VECTOR MatchingFontConfigs = NewBitVector(MAX_NUM_PROTOS); |
873 | 51.4k | for (int cfg = 0; cfg < IClass->NumConfigs; ++cfg) { |
874 | 48.3k | if (GetFontinfoId(Class, cfg) == FontinfoId) { |
875 | 43.3k | SET_BIT(MatchingFontConfigs, cfg); |
876 | 43.3k | } else { |
877 | 5.00k | reset_bit(MatchingFontConfigs, cfg); |
878 | 5.00k | } |
879 | 48.3k | } |
880 | 3.09k | im_.Match(IClass, AllProtosOn, MatchingFontConfigs, NumFeatures, IntFeatures, &int_result, |
881 | 3.09k | classify_adapt_feature_threshold, NO_DEBUG, matcher_debug_separate_windows); |
882 | 3.09k | FreeBitVector(MatchingFontConfigs); |
883 | | |
884 | 3.09k | SetAdaptiveThreshold(Threshold); |
885 | | |
886 | 3.09k | if (1.0f - int_result.rating <= Threshold) { |
887 | 2.68k | if (ConfigIsPermanent(Class, int_result.config)) { |
888 | 2.16k | if (classify_learning_debug_level >= 1) { |
889 | 0 | tprintf("Found good match to perm config %d = %4.1f%%.\n", int_result.config, |
890 | 0 | int_result.rating * 100.0); |
891 | 0 | } |
892 | 2.16k | delete FloatFeatures; |
893 | 2.16k | return; |
894 | 2.16k | } |
895 | | |
896 | 522 | TempConfig = TempConfigFor(Class, int_result.config); |
897 | 522 | IncreaseConfidence(TempConfig); |
898 | 522 | if (TempConfig->NumTimesSeen > Class->MaxNumTimesSeen) { |
899 | 371 | Class->MaxNumTimesSeen = TempConfig->NumTimesSeen; |
900 | 371 | } |
901 | 522 | if (classify_learning_debug_level >= 1) { |
902 | 0 | tprintf("Increasing reliability of temp config %d to %d.\n", int_result.config, |
903 | 0 | TempConfig->NumTimesSeen); |
904 | 0 | } |
905 | | |
906 | 522 | if (TempConfigReliable(ClassId, TempConfig)) { |
907 | 177 | MakePermanent(adaptive_templates, ClassId, int_result.config, Blob); |
908 | 177 | UpdateAmbigsGroup(ClassId, Blob); |
909 | 177 | } |
910 | 522 | } else { |
911 | 415 | if (classify_learning_debug_level >= 1) { |
912 | 0 | tprintf("Found poor match to temp config %d = %4.1f%%.\n", int_result.config, |
913 | 0 | int_result.rating * 100.0); |
914 | | #ifndef GRAPHICS_DISABLED |
915 | | if (classify_learning_debug_level > 2) { |
916 | | DisplayAdaptedChar(Blob, IClass); |
917 | | } |
918 | | #endif |
919 | 0 | } |
920 | 415 | NewTempConfigId = MakeNewTemporaryConfig(adaptive_templates, ClassId, FontinfoId, NumFeatures, |
921 | 415 | IntFeatures, FloatFeatures); |
922 | 415 | if (NewTempConfigId >= 0 && |
923 | 415 | TempConfigReliable(ClassId, TempConfigFor(Class, NewTempConfigId))) { |
924 | 0 | MakePermanent(adaptive_templates, ClassId, NewTempConfigId, Blob); |
925 | 0 | UpdateAmbigsGroup(ClassId, Blob); |
926 | 0 | } |
927 | | |
928 | | #ifndef GRAPHICS_DISABLED |
929 | | if (classify_learning_debug_level > 1) { |
930 | | DisplayAdaptedChar(Blob, IClass); |
931 | | } |
932 | | #endif |
933 | 415 | } |
934 | 937 | delete FloatFeatures; |
935 | 937 | } |
936 | 4.78k | } /* AdaptToChar */ |
937 | | |
938 | | #ifndef GRAPHICS_DISABLED |
939 | | |
940 | | void Classify::DisplayAdaptedChar(TBLOB *blob, INT_CLASS_STRUCT *int_class) { |
941 | | INT_FX_RESULT_STRUCT fx_info; |
942 | | std::vector<INT_FEATURE_STRUCT> bl_features; |
943 | | TrainingSample *sample = |
944 | | BlobToTrainingSample(*blob, classify_nonlinear_norm, &fx_info, &bl_features); |
945 | | if (sample == nullptr) { |
946 | | return; |
947 | | } |
948 | | |
949 | | UnicharRating int_result; |
950 | | im_.Match(int_class, AllProtosOn, AllConfigsOn, bl_features.size(), &bl_features[0], &int_result, |
951 | | classify_adapt_feature_threshold, NO_DEBUG, matcher_debug_separate_windows); |
952 | | tprintf("Best match to temp config %d = %4.1f%%.\n", int_result.config, |
953 | | int_result.rating * 100.0); |
954 | | if (classify_learning_debug_level >= 2) { |
955 | | uint32_t ConfigMask; |
956 | | ConfigMask = 1 << int_result.config; |
957 | | ShowMatchDisplay(); |
958 | | im_.Match(int_class, AllProtosOn, static_cast<BIT_VECTOR>(&ConfigMask), bl_features.size(), |
959 | | &bl_features[0], &int_result, classify_adapt_feature_threshold, 6 | 0x19, |
960 | | matcher_debug_separate_windows); |
961 | | UpdateMatchDisplay(); |
962 | | } |
963 | | |
964 | | delete sample; |
965 | | } |
966 | | |
967 | | #endif |
968 | | |
969 | | /** |
970 | | * This routine adds the result of a classification into |
971 | | * Results. If the new rating is much worse than the current |
972 | | * best rating, it is not entered into results because it |
973 | | * would end up being stripped later anyway. If the new rating |
974 | | * is better than the old rating for the class, it replaces the |
975 | | * old rating. If this is the first rating for the class, the |
976 | | * class is added to the list of matched classes in Results. |
977 | | * If the new rating is better than the best so far, it |
978 | | * becomes the best so far. |
979 | | * |
980 | | * Globals: |
981 | | * - #matcher_bad_match_pad defines limits of an acceptable match |
982 | | * |
983 | | * @param new_result new result to add |
984 | | * @param[out] results results to add new result to |
985 | | */ |
986 | 23.9M | void Classify::AddNewResult(const UnicharRating &new_result, ADAPT_RESULTS *results) { |
987 | 23.9M | auto old_match = FindScoredUnichar(new_result.unichar_id, *results); |
988 | | |
989 | 23.9M | if (new_result.rating + matcher_bad_match_pad < results->best_rating || |
990 | 23.9M | (old_match < results->match.size() && |
991 | 20.7M | new_result.rating <= results->match[old_match].rating)) { |
992 | 3.16M | return; // New one not good enough. |
993 | 3.16M | } |
994 | | |
995 | 20.7M | if (!unicharset.get_fragment(new_result.unichar_id)) { |
996 | 20.7M | results->HasNonfragment = true; |
997 | 20.7M | } |
998 | | |
999 | 20.7M | if (old_match < results->match.size()) { |
1000 | 17.0k | results->match[old_match].rating = new_result.rating; |
1001 | 20.7M | } else { |
1002 | 20.7M | results->match.push_back(new_result); |
1003 | 20.7M | } |
1004 | | |
1005 | 20.7M | if (new_result.rating > results->best_rating && |
1006 | | // Ensure that fragments do not affect best rating, class and config. |
1007 | | // This is needed so that at least one non-fragmented character is |
1008 | | // always present in the results. |
1009 | | // TODO(daria): verify that this helps accuracy and does not |
1010 | | // hurt performance. |
1011 | 20.7M | !unicharset.get_fragment(new_result.unichar_id)) { |
1012 | 5.96M | results->best_match_index = old_match; |
1013 | 5.96M | results->best_rating = new_result.rating; |
1014 | 5.96M | results->best_unichar_id = new_result.unichar_id; |
1015 | 5.96M | } |
1016 | 20.7M | } /* AddNewResult */ |
1017 | | |
1018 | | /*---------------------------------------------------------------------------*/ |
1019 | | /** |
1020 | | * This routine is identical to CharNormClassifier() |
1021 | | * except that it does no class pruning. It simply matches |
1022 | | * the unknown blob against the classes listed in |
1023 | | * Ambiguities. |
1024 | | * |
1025 | | * Globals: |
1026 | | * - #AllProtosOn mask that enables all protos |
1027 | | * - #AllConfigsOn mask that enables all configs |
1028 | | * |
1029 | | * @param blob blob to be classified |
1030 | | * @param templates built-in templates to classify against |
1031 | | * @param classes adapted class templates |
1032 | | * @param ambiguities array of unichar id's to match against |
1033 | | * @param[out] results place to put match results |
1034 | | * @param int_features |
1035 | | * @param fx_info |
1036 | | */ |
1037 | | void Classify::AmbigClassifier(const std::vector<INT_FEATURE_STRUCT> &int_features, |
1038 | | const INT_FX_RESULT_STRUCT &fx_info, const TBLOB *blob, |
1039 | | INT_TEMPLATES_STRUCT *templates, ADAPT_CLASS_STRUCT **classes, |
1040 | 0 | UNICHAR_ID *ambiguities, ADAPT_RESULTS *results) { |
1041 | 0 | if (int_features.empty()) { |
1042 | 0 | return; |
1043 | 0 | } |
1044 | 0 | auto *CharNormArray = new uint8_t[unicharset.size()]; |
1045 | 0 | UnicharRating int_result; |
1046 | |
|
1047 | 0 | results->BlobLength = GetCharNormFeature(fx_info, templates, nullptr, CharNormArray); |
1048 | 0 | bool debug = matcher_debug_level >= 2 || classify_debug_level > 1; |
1049 | 0 | if (debug) { |
1050 | 0 | tprintf("AM Matches = "); |
1051 | 0 | } |
1052 | |
|
1053 | 0 | int top = blob->bounding_box().top(); |
1054 | 0 | int bottom = blob->bounding_box().bottom(); |
1055 | 0 | while (*ambiguities >= 0) { |
1056 | 0 | CLASS_ID class_id = *ambiguities; |
1057 | |
|
1058 | 0 | int_result.unichar_id = class_id; |
1059 | 0 | im_.Match(ClassForClassId(templates, class_id), AllProtosOn, AllConfigsOn, int_features.size(), |
1060 | 0 | &int_features[0], &int_result, classify_adapt_feature_threshold, NO_DEBUG, |
1061 | 0 | matcher_debug_separate_windows); |
1062 | |
|
1063 | 0 | ExpandShapesAndApplyCorrections(nullptr, debug, class_id, bottom, top, 0, results->BlobLength, |
1064 | 0 | classify_integer_matcher_multiplier, CharNormArray, &int_result, |
1065 | 0 | results); |
1066 | 0 | ambiguities++; |
1067 | 0 | } |
1068 | 0 | delete[] CharNormArray; |
1069 | 0 | } /* AmbigClassifier */ |
1070 | | |
1071 | | /*---------------------------------------------------------------------------*/ |
1072 | | /// Factored-out calls to IntegerMatcher based on class pruner results. |
1073 | | /// Returns integer matcher results inside CLASS_PRUNER_RESULTS structure. |
1074 | | void Classify::MasterMatcher(INT_TEMPLATES_STRUCT *templates, int16_t num_features, |
1075 | | const INT_FEATURE_STRUCT *features, const uint8_t *norm_factors, |
1076 | | ADAPT_CLASS_STRUCT **classes, int debug, int matcher_multiplier, |
1077 | | const TBOX &blob_box, const std::vector<CP_RESULT_STRUCT> &results, |
1078 | 2.51M | ADAPT_RESULTS *final_results) { |
1079 | 2.51M | int top = blob_box.top(); |
1080 | 2.51M | int bottom = blob_box.bottom(); |
1081 | 2.51M | UnicharRating int_result; |
1082 | 13.3M | for (auto &&result : results) { |
1083 | 13.3M | CLASS_ID class_id = result.Class; |
1084 | 13.3M | BIT_VECTOR protos = classes != nullptr ? classes[class_id]->PermProtos : AllProtosOn; |
1085 | 13.3M | BIT_VECTOR configs = classes != nullptr ? classes[class_id]->PermConfigs : AllConfigsOn; |
1086 | | |
1087 | 13.3M | int_result.unichar_id = class_id; |
1088 | 13.3M | im_.Match(ClassForClassId(templates, class_id), protos, configs, num_features, features, |
1089 | 13.3M | &int_result, classify_adapt_feature_threshold, debug, matcher_debug_separate_windows); |
1090 | 13.3M | bool is_debug = matcher_debug_level >= 2 || classify_debug_level > 1; |
1091 | 13.3M | ExpandShapesAndApplyCorrections(classes, is_debug, class_id, bottom, top, result.Rating, |
1092 | 13.3M | final_results->BlobLength, matcher_multiplier, norm_factors, |
1093 | 13.3M | &int_result, final_results); |
1094 | 13.3M | } |
1095 | 2.51M | } |
1096 | | |
1097 | | // Converts configs to fonts, and if the result is not adapted, and a |
1098 | | // shape_table_ is present, the shape is expanded to include all |
1099 | | // unichar_ids represented, before applying a set of corrections to the |
1100 | | // distance rating in int_result, (see ComputeCorrectedRating.) |
1101 | | // The results are added to the final_results output. |
1102 | | void Classify::ExpandShapesAndApplyCorrections(ADAPT_CLASS_STRUCT **classes, bool debug, int class_id, |
1103 | | int bottom, int top, float cp_rating, |
1104 | | int blob_length, int matcher_multiplier, |
1105 | | const uint8_t *cn_factors, UnicharRating *int_result, |
1106 | 13.3M | ADAPT_RESULTS *final_results) { |
1107 | 13.3M | if (classes != nullptr) { |
1108 | | // Adapted result. Convert configs to fontinfo_ids. |
1109 | 676k | int_result->adapted = true; |
1110 | 25.1M | for (auto &font : int_result->fonts) { |
1111 | 25.1M | font.fontinfo_id = GetFontinfoId(classes[class_id], font.fontinfo_id); |
1112 | 25.1M | } |
1113 | 12.6M | } else { |
1114 | | // Pre-trained result. Map fonts using font_sets_. |
1115 | 12.6M | int_result->adapted = false; |
1116 | 405M | for (auto &font : int_result->fonts) { |
1117 | 405M | font.fontinfo_id = ClassAndConfigIDToFontOrShapeID(class_id, font.fontinfo_id); |
1118 | 405M | } |
1119 | 12.6M | if (shape_table_ != nullptr) { |
1120 | | // Two possible cases: |
1121 | | // 1. Flat shapetable. All unichar-ids of the shapes referenced by |
1122 | | // int_result->fonts are the same. In this case build a new vector of |
1123 | | // mapped fonts and replace the fonts in int_result. |
1124 | | // 2. Multi-unichar shapetable. Variable unichars in the shapes referenced |
1125 | | // by int_result. In this case, build a vector of UnicharRating to |
1126 | | // gather together different font-ids for each unichar. Also covers case1. |
1127 | 12.6M | std::vector<UnicharRating> mapped_results; |
1128 | 405M | for (auto &f : int_result->fonts) { |
1129 | 405M | int shape_id = f.fontinfo_id; |
1130 | 405M | const Shape &shape = shape_table_->GetShape(shape_id); |
1131 | 810M | for (int c = 0; c < shape.size(); ++c) { |
1132 | 405M | int unichar_id = shape[c].unichar_id; |
1133 | 405M | if (!unicharset.get_enabled(unichar_id)) { |
1134 | 0 | continue; |
1135 | 0 | } |
1136 | | // Find the mapped_result for unichar_id. |
1137 | 405M | unsigned r = 0; |
1138 | 405M | for (r = 0; r < mapped_results.size() && mapped_results[r].unichar_id != unichar_id; |
1139 | 405M | ++r) { |
1140 | 0 | } |
1141 | 405M | if (r == mapped_results.size()) { |
1142 | 12.6M | mapped_results.push_back(*int_result); |
1143 | 12.6M | mapped_results[r].unichar_id = unichar_id; |
1144 | 12.6M | mapped_results[r].fonts.clear(); |
1145 | 12.6M | } |
1146 | 405M | for (int font_id : shape[c].font_ids) { |
1147 | 405M | mapped_results[r].fonts.emplace_back(font_id, f.score); |
1148 | 405M | } |
1149 | 405M | } |
1150 | 405M | } |
1151 | 12.6M | for (auto &m : mapped_results) { |
1152 | 12.6M | m.rating = ComputeCorrectedRating(debug, m.unichar_id, cp_rating, int_result->rating, |
1153 | 12.6M | int_result->feature_misses, bottom, top, blob_length, |
1154 | 12.6M | matcher_multiplier, cn_factors); |
1155 | 12.6M | AddNewResult(m, final_results); |
1156 | 12.6M | } |
1157 | 12.6M | return; |
1158 | 12.6M | } |
1159 | 12.6M | } |
1160 | 676k | if (unicharset.get_enabled(class_id)) { |
1161 | 676k | int_result->rating = ComputeCorrectedRating(debug, class_id, cp_rating, int_result->rating, |
1162 | 676k | int_result->feature_misses, bottom, top, |
1163 | 676k | blob_length, matcher_multiplier, cn_factors); |
1164 | 676k | AddNewResult(*int_result, final_results); |
1165 | 676k | } |
1166 | 676k | } |
1167 | | |
1168 | | // Applies a set of corrections to the confidence im_rating, |
1169 | | // including the cn_correction, miss penalty and additional penalty |
1170 | | // for non-alnums being vertical misfits. Returns the corrected confidence. |
1171 | | double Classify::ComputeCorrectedRating(bool debug, int unichar_id, double cp_rating, |
1172 | | double im_rating, int feature_misses, int bottom, int top, |
1173 | | int blob_length, int matcher_multiplier, |
1174 | 13.3M | const uint8_t *cn_factors) { |
1175 | | // Compute class feature corrections. |
1176 | 13.3M | double cn_corrected = im_.ApplyCNCorrection(1.0 - im_rating, blob_length, cn_factors[unichar_id], |
1177 | 13.3M | matcher_multiplier); |
1178 | 13.3M | double miss_penalty = tessedit_class_miss_scale * feature_misses; |
1179 | 13.3M | double vertical_penalty = 0.0; |
1180 | | // Penalize non-alnums for being vertical misfits. |
1181 | 13.3M | if (!unicharset.get_isalpha(unichar_id) && !unicharset.get_isdigit(unichar_id) && |
1182 | 13.3M | cn_factors[unichar_id] != 0 && classify_misfit_junk_penalty > 0.0) { |
1183 | 0 | int min_bottom, max_bottom, min_top, max_top; |
1184 | 0 | unicharset.get_top_bottom(unichar_id, &min_bottom, &max_bottom, &min_top, &max_top); |
1185 | 0 | if (debug) { |
1186 | 0 | tprintf("top=%d, vs [%d, %d], bottom=%d, vs [%d, %d]\n", top, min_top, max_top, bottom, |
1187 | 0 | min_bottom, max_bottom); |
1188 | 0 | } |
1189 | 0 | if (top < min_top || top > max_top || bottom < min_bottom || bottom > max_bottom) { |
1190 | 0 | vertical_penalty = classify_misfit_junk_penalty; |
1191 | 0 | } |
1192 | 0 | } |
1193 | 13.3M | double result = 1.0 - (cn_corrected + miss_penalty + vertical_penalty); |
1194 | 13.3M | if (result < WORST_POSSIBLE_RATING) { |
1195 | 83.0k | result = WORST_POSSIBLE_RATING; |
1196 | 83.0k | } |
1197 | 13.3M | if (debug) { |
1198 | 0 | tprintf("%s: %2.1f%%(CP%2.1f, IM%2.1f + CN%.2f(%d) + MP%2.1f + VP%2.1f)\n", |
1199 | 0 | unicharset.id_to_unichar(unichar_id), result * 100.0, cp_rating * 100.0, |
1200 | 0 | (1.0 - im_rating) * 100.0, (cn_corrected - (1.0 - im_rating)) * 100.0, |
1201 | 0 | cn_factors[unichar_id], miss_penalty * 100.0, vertical_penalty * 100.0); |
1202 | 0 | } |
1203 | 13.3M | return result; |
1204 | 13.3M | } |
1205 | | |
1206 | | /*---------------------------------------------------------------------------*/ |
1207 | | /** |
1208 | | * This routine extracts baseline normalized features |
1209 | | * from the unknown character and matches them against the |
1210 | | * specified set of templates. The classes which match |
1211 | | * are added to Results. |
1212 | | * |
1213 | | * Globals: |
1214 | | * - BaselineCutoffs expected num features for each class |
1215 | | * |
1216 | | * @param Blob blob to be classified |
1217 | | * @param Templates current set of adapted templates |
1218 | | * @param Results place to put match results |
1219 | | * @param int_features |
1220 | | * @param fx_info |
1221 | | * |
1222 | | * @return Array of possible ambiguous chars that should be checked. |
1223 | | */ |
1224 | | UNICHAR_ID *Classify::BaselineClassifier(TBLOB *Blob, |
1225 | | const std::vector<INT_FEATURE_STRUCT> &int_features, |
1226 | | const INT_FX_RESULT_STRUCT &fx_info, |
1227 | 494k | ADAPT_TEMPLATES_STRUCT *Templates, ADAPT_RESULTS *Results) { |
1228 | 494k | if (int_features.empty()) { |
1229 | 128 | return nullptr; |
1230 | 128 | } |
1231 | 494k | auto *CharNormArray = new uint8_t[unicharset.size()]; |
1232 | 494k | ClearCharNormArray(CharNormArray); |
1233 | | |
1234 | 494k | Results->BlobLength = IntCastRounded(fx_info.Length / kStandardFeatureLength); |
1235 | 494k | PruneClasses(Templates->Templates, int_features.size(), -1, &int_features[0], CharNormArray, |
1236 | 494k | BaselineCutoffs, &Results->CPResults); |
1237 | | |
1238 | 494k | if (matcher_debug_level >= 2 || classify_debug_level > 1) { |
1239 | 0 | tprintf("BL Matches = "); |
1240 | 0 | } |
1241 | | |
1242 | 494k | MasterMatcher(Templates->Templates, int_features.size(), &int_features[0], CharNormArray, |
1243 | 494k | Templates->Class, matcher_debug_flags, 0, Blob->bounding_box(), Results->CPResults, |
1244 | 494k | Results); |
1245 | | |
1246 | 494k | delete[] CharNormArray; |
1247 | 494k | CLASS_ID ClassId = Results->best_unichar_id; |
1248 | 494k | if (ClassId == INVALID_UNICHAR_ID || Results->best_match_index < 0) { |
1249 | 117k | return nullptr; |
1250 | 117k | } |
1251 | | |
1252 | 376k | return Templates->Class[ClassId] |
1253 | 376k | ->Config[Results->match[Results->best_match_index].config] |
1254 | 376k | .Perm->Ambigs; |
1255 | 494k | } /* BaselineClassifier */ |
1256 | | |
1257 | | /*---------------------------------------------------------------------------*/ |
1258 | | /** |
1259 | | * This routine extracts character normalized features |
1260 | | * from the unknown character and matches them against the |
1261 | | * specified set of templates. The classes which match |
1262 | | * are added to Results. |
1263 | | * |
1264 | | * @param blob blob to be classified |
1265 | | * @param sample templates to classify unknown against |
1266 | | * @param adapt_results place to put match results |
1267 | | * |
1268 | | * Globals: |
1269 | | * - CharNormCutoffs expected num features for each class |
1270 | | * - AllProtosOn mask that enables all protos |
1271 | | * - AllConfigsOn mask that enables all configs |
1272 | | */ |
1273 | | int Classify::CharNormClassifier(TBLOB *blob, const TrainingSample &sample, |
1274 | 2.01M | ADAPT_RESULTS *adapt_results) { |
1275 | | // This is the length that is used for scaling ratings vs certainty. |
1276 | 2.01M | adapt_results->BlobLength = IntCastRounded(sample.outline_length() / kStandardFeatureLength); |
1277 | 2.01M | std::vector<UnicharRating> unichar_results; |
1278 | 2.01M | static_classifier_->UnicharClassifySample(sample, blob->denorm().pix(), 0, -1, &unichar_results); |
1279 | | // Convert results to the format used internally by AdaptiveClassifier. |
1280 | 10.5M | for (auto &r : unichar_results) { |
1281 | 10.5M | AddNewResult(r, adapt_results); |
1282 | 10.5M | } |
1283 | 2.01M | return sample.num_features(); |
1284 | 2.01M | } /* CharNormClassifier */ |
1285 | | |
1286 | | // As CharNormClassifier, but operates on a TrainingSample and outputs to |
1287 | | // a vector of ShapeRating without conversion to classes. |
1288 | | int Classify::CharNormTrainingSample(bool pruner_only, int keep_this, const TrainingSample &sample, |
1289 | 2.01M | std::vector<UnicharRating> *results) { |
1290 | 2.01M | results->clear(); |
1291 | 2.01M | std::unique_ptr<ADAPT_RESULTS> adapt_results(new ADAPT_RESULTS()); |
1292 | 2.01M | adapt_results->Initialize(); |
1293 | | // Compute the bounding box of the features. |
1294 | 2.01M | uint32_t num_features = sample.num_features(); |
1295 | | // Only the top and bottom of the blob_box are used by MasterMatcher, so |
1296 | | // fabricate right and left using top and bottom. |
1297 | 2.01M | TBOX blob_box(sample.geo_feature(GeoBottom), sample.geo_feature(GeoBottom), |
1298 | 2.01M | sample.geo_feature(GeoTop), sample.geo_feature(GeoTop)); |
1299 | | // Compute the char_norm_array from the saved cn_feature. |
1300 | 2.01M | FEATURE norm_feature = sample.GetCNFeature(); |
1301 | 2.01M | std::vector<uint8_t> char_norm_array(unicharset.size()); |
1302 | 2.01M | auto num_pruner_classes = std::max(static_cast<unsigned>(unicharset.size()), PreTrainedTemplates->NumClasses); |
1303 | 2.01M | std::vector<uint8_t> pruner_norm_array(num_pruner_classes); |
1304 | 2.01M | adapt_results->BlobLength = static_cast<int>(ActualOutlineLength(norm_feature) * 20 + 0.5f); |
1305 | 2.01M | ComputeCharNormArrays(norm_feature, PreTrainedTemplates, &char_norm_array[0], &pruner_norm_array[0]); |
1306 | | |
1307 | 2.01M | PruneClasses(PreTrainedTemplates, num_features, keep_this, sample.features(), &pruner_norm_array[0], |
1308 | 2.01M | shape_table_ != nullptr ? &shapetable_cutoffs_[0] : CharNormCutoffs, |
1309 | 2.01M | &adapt_results->CPResults); |
1310 | 2.01M | if (keep_this >= 0) { |
1311 | 0 | adapt_results->CPResults[0].Class = keep_this; |
1312 | 0 | adapt_results->CPResults.resize(1); |
1313 | 0 | } |
1314 | 2.01M | if (pruner_only) { |
1315 | | // Convert pruner results to output format. |
1316 | 0 | for (auto &it : adapt_results->CPResults) { |
1317 | 0 | int class_id = it.Class; |
1318 | 0 | results->push_back(UnicharRating(class_id, 1.0f - it.Rating)); |
1319 | 0 | } |
1320 | 2.01M | } else { |
1321 | 2.01M | MasterMatcher(PreTrainedTemplates, num_features, sample.features(), &char_norm_array[0], nullptr, |
1322 | 2.01M | matcher_debug_flags, classify_integer_matcher_multiplier, blob_box, |
1323 | 2.01M | adapt_results->CPResults, adapt_results.get()); |
1324 | | // Convert master matcher results to output format. |
1325 | 10.5M | for (auto &i : adapt_results->match) { |
1326 | 10.5M | results->push_back(i); |
1327 | 10.5M | } |
1328 | 2.01M | if (results->size() > 1) { |
1329 | 1.76M | std::sort(results->begin(), results->end(), SortDescendingRating); |
1330 | 1.76M | } |
1331 | 2.01M | } |
1332 | 2.01M | return num_features; |
1333 | 2.01M | } /* CharNormTrainingSample */ |
1334 | | |
1335 | | /*---------------------------------------------------------------------------*/ |
1336 | | /** |
1337 | | * This routine computes a rating which reflects the |
1338 | | * likelihood that the blob being classified is a noise |
1339 | | * blob. NOTE: assumes that the blob length has already been |
1340 | | * computed and placed into Results. |
1341 | | * |
1342 | | * @param results results to add noise classification to |
1343 | | * |
1344 | | * Globals: |
1345 | | * - matcher_avg_noise_size avg. length of a noise blob |
1346 | | */ |
1347 | 836 | void Classify::ClassifyAsNoise(ADAPT_RESULTS *results) { |
1348 | 836 | float rating = results->BlobLength / matcher_avg_noise_size; |
1349 | 836 | rating *= rating; |
1350 | 836 | rating /= 1 + rating; |
1351 | | |
1352 | 836 | AddNewResult(UnicharRating(UNICHAR_SPACE, 1.0f - rating), results); |
1353 | 836 | } /* ClassifyAsNoise */ |
1354 | | |
1355 | | /// The function converts the given match ratings to the list of blob |
1356 | | /// choices with ratings and certainties (used by the context checkers). |
1357 | | /// If character fragments are present in the results, this function also makes |
1358 | | /// sure that there is at least one non-fragmented classification included. |
1359 | | /// For each classification result check the unicharset for "definite" |
1360 | | /// ambiguities and modify the resulting Choices accordingly. |
1361 | | void Classify::ConvertMatchesToChoices(const DENORM &denorm, const TBOX &box, |
1362 | 2.01M | ADAPT_RESULTS *Results, BLOB_CHOICE_LIST *Choices) { |
1363 | 2.01M | assert(Choices != nullptr); |
1364 | 2.01M | float Rating; |
1365 | 2.01M | float Certainty; |
1366 | 2.01M | BLOB_CHOICE_IT temp_it; |
1367 | 2.01M | bool contains_nonfrag = false; |
1368 | 2.01M | temp_it.set_to_list(Choices); |
1369 | 2.01M | int choices_length = 0; |
1370 | | // With no shape_table_ maintain the previous MAX_MATCHES as the maximum |
1371 | | // number of returned results, but with a shape_table_ we want to have room |
1372 | | // for at least the biggest shape (which might contain hundreds of Indic |
1373 | | // grapheme fragments) and more, so use double the size of the biggest shape |
1374 | | // if that is more than the default. |
1375 | 2.01M | int max_matches = MAX_MATCHES; |
1376 | 2.01M | if (shape_table_ != nullptr) { |
1377 | 2.01M | max_matches = shape_table_->MaxNumUnichars() * 2; |
1378 | 2.01M | if (max_matches < MAX_MATCHES) { |
1379 | 2.01M | max_matches = MAX_MATCHES; |
1380 | 2.01M | } |
1381 | 2.01M | } |
1382 | | |
1383 | 2.01M | float best_certainty = -FLT_MAX; |
1384 | 9.37M | for (auto &it : Results->match) { |
1385 | 9.37M | const UnicharRating &result = it; |
1386 | 9.37M | bool adapted = result.adapted; |
1387 | 9.37M | bool current_is_frag = (unicharset.get_fragment(result.unichar_id) != nullptr); |
1388 | 9.37M | if (temp_it.length() + 1 == max_matches && !contains_nonfrag && current_is_frag) { |
1389 | 0 | continue; // look for a non-fragmented character to fill the |
1390 | | // last spot in Choices if only fragments are present |
1391 | 0 | } |
1392 | | // BlobLength can never be legally 0, this means recognition failed. |
1393 | | // But we must return a classification result because some invoking |
1394 | | // functions (chopper/permuter) do not anticipate a null blob choice. |
1395 | | // So we need to assign a poor, but not infinitely bad score. |
1396 | 9.37M | if (Results->BlobLength == 0) { |
1397 | 160 | Certainty = -20; |
1398 | 160 | Rating = 100; // should be -certainty * real_blob_length |
1399 | 9.37M | } else { |
1400 | 9.37M | Rating = Certainty = (1.0f - result.rating); |
1401 | 9.37M | Rating *= rating_scale * Results->BlobLength; |
1402 | 9.37M | Certainty *= -(getDict().certainty_scale); |
1403 | 9.37M | } |
1404 | | // Adapted results, by their very nature, should have good certainty. |
1405 | | // Those that don't are at best misleading, and often lead to errors, |
1406 | | // so don't accept adapted results that are too far behind the best result, |
1407 | | // whether adapted or static. |
1408 | | // TODO(rays) find some way of automatically tuning these constants. |
1409 | 9.37M | if (Certainty > best_certainty) { |
1410 | 2.02M | best_certainty = std::min(Certainty, static_cast<float>(classify_adapted_pruning_threshold)); |
1411 | 7.34M | } else if (adapted && Certainty / classify_adapted_pruning_factor < best_certainty) { |
1412 | 1.38k | continue; // Don't accept bad adapted results. |
1413 | 1.38k | } |
1414 | | |
1415 | 9.37M | float min_xheight, max_xheight, yshift; |
1416 | 9.37M | denorm.XHeightRange(result.unichar_id, unicharset, box, &min_xheight, &max_xheight, &yshift); |
1417 | 9.37M | auto *choice = new BLOB_CHOICE( |
1418 | 9.37M | result.unichar_id, Rating, Certainty, unicharset.get_script(result.unichar_id), min_xheight, |
1419 | 9.37M | max_xheight, yshift, adapted ? BCC_ADAPTED_CLASSIFIER : BCC_STATIC_CLASSIFIER); |
1420 | 9.37M | choice->set_fonts(result.fonts); |
1421 | 9.37M | temp_it.add_to_end(choice); |
1422 | 9.37M | contains_nonfrag |= !current_is_frag; // update contains_nonfrag |
1423 | 9.37M | choices_length++; |
1424 | 9.37M | if (choices_length >= max_matches) { |
1425 | 202k | break; |
1426 | 202k | } |
1427 | 9.37M | } |
1428 | 2.01M | Results->match.resize(choices_length); |
1429 | 2.01M | } // ConvertMatchesToChoices |
1430 | | |
1431 | | /*---------------------------------------------------------------------------*/ |
1432 | | #ifndef GRAPHICS_DISABLED |
1433 | | /** |
1434 | | * |
1435 | | * @param blob blob whose classification is being debugged |
1436 | | * @param Results results of match being debugged |
1437 | | * |
1438 | | * Globals: none |
1439 | | */ |
1440 | | void Classify::DebugAdaptiveClassifier(TBLOB *blob, ADAPT_RESULTS *Results) { |
1441 | | if (static_classifier_ == nullptr) { |
1442 | | return; |
1443 | | } |
1444 | | INT_FX_RESULT_STRUCT fx_info; |
1445 | | std::vector<INT_FEATURE_STRUCT> bl_features; |
1446 | | TrainingSample *sample = BlobToTrainingSample(*blob, false, &fx_info, &bl_features); |
1447 | | if (sample == nullptr) { |
1448 | | return; |
1449 | | } |
1450 | | static_classifier_->DebugDisplay(*sample, blob->denorm().pix(), Results->best_unichar_id); |
1451 | | } /* DebugAdaptiveClassifier */ |
1452 | | #endif |
1453 | | |
1454 | | /*---------------------------------------------------------------------------*/ |
1455 | | /** |
1456 | | * This routine performs an adaptive classification. |
1457 | | * If we have not yet adapted to enough classes, a simple |
1458 | | * classification to the pre-trained templates is performed. |
1459 | | * Otherwise, we match the blob against the adapted templates. |
1460 | | * If the adapted templates do not match well, we try a |
1461 | | * match against the pre-trained templates. If an adapted |
1462 | | * template match is found, we do a match to any pre-trained |
1463 | | * templates which could be ambiguous. The results from all |
1464 | | * of these classifications are merged together into Results. |
1465 | | * |
1466 | | * @param Blob blob to be classified |
1467 | | * @param Results place to put match results |
1468 | | * |
1469 | | * Globals: |
1470 | | * - PreTrainedTemplates built-in training templates |
1471 | | * - AdaptedTemplates templates adapted for this page |
1472 | | * - matcher_reliable_adaptive_result rating limit for a great match |
1473 | | */ |
1474 | 2.01M | void Classify::DoAdaptiveMatch(TBLOB *Blob, ADAPT_RESULTS *Results) { |
1475 | 2.01M | UNICHAR_ID *Ambiguities; |
1476 | | |
1477 | 2.01M | INT_FX_RESULT_STRUCT fx_info; |
1478 | 2.01M | std::vector<INT_FEATURE_STRUCT> bl_features; |
1479 | 2.01M | TrainingSample *sample = |
1480 | 2.01M | BlobToTrainingSample(*Blob, classify_nonlinear_norm, &fx_info, &bl_features); |
1481 | 2.01M | if (sample == nullptr) { |
1482 | 44 | return; |
1483 | 44 | } |
1484 | | |
1485 | | // TODO: With LSTM, static_classifier_ is nullptr. |
1486 | | // Return to avoid crash in CharNormClassifier. |
1487 | 2.01M | if (static_classifier_ == nullptr) { |
1488 | 0 | delete sample; |
1489 | 0 | return; |
1490 | 0 | } |
1491 | | |
1492 | 2.01M | if (AdaptedTemplates->NumPermClasses < matcher_permanent_classes_min || tess_cn_matching) { |
1493 | 1.52M | CharNormClassifier(Blob, *sample, Results); |
1494 | 1.52M | } else { |
1495 | 494k | Ambiguities = BaselineClassifier(Blob, bl_features, fx_info, AdaptedTemplates, Results); |
1496 | 494k | if ((!Results->match.empty() && |
1497 | 494k | MarginalMatch(Results->best_rating, matcher_reliable_adaptive_result) && |
1498 | 494k | !tess_bn_matching) || |
1499 | 494k | Results->match.empty()) { |
1500 | 494k | CharNormClassifier(Blob, *sample, Results); |
1501 | 494k | } else if (Ambiguities && *Ambiguities >= 0 && !tess_bn_matching) { |
1502 | 0 | AmbigClassifier(bl_features, fx_info, Blob, PreTrainedTemplates, AdaptedTemplates->Class, |
1503 | 0 | Ambiguities, Results); |
1504 | 0 | } |
1505 | 494k | } |
1506 | | |
1507 | | // Force the blob to be classified as noise |
1508 | | // if the results contain only fragments. |
1509 | | // TODO(daria): verify that this is better than |
1510 | | // just adding a nullptr classification. |
1511 | 2.01M | if (!Results->HasNonfragment || Results->match.empty()) { |
1512 | 836 | ClassifyAsNoise(Results); |
1513 | 836 | } |
1514 | 2.01M | delete sample; |
1515 | 2.01M | } /* DoAdaptiveMatch */ |
1516 | | |
1517 | | /*---------------------------------------------------------------------------*/ |
1518 | | /** |
1519 | | * This routine matches blob to the built-in templates |
1520 | | * to find out if there are any classes other than the correct |
1521 | | * class which are potential ambiguities. |
1522 | | * |
1523 | | * @param Blob blob to get classification ambiguities for |
1524 | | * @param CorrectClass correct class for Blob |
1525 | | * |
1526 | | * Globals: |
1527 | | * - CurrentRatings used by qsort compare routine |
1528 | | * - PreTrainedTemplates built-in templates |
1529 | | * |
1530 | | * @return String containing all possible ambiguous classes. |
1531 | | */ |
1532 | 177 | UNICHAR_ID *Classify::GetAmbiguities(TBLOB *Blob, CLASS_ID CorrectClass) { |
1533 | 177 | auto *Results = new ADAPT_RESULTS(); |
1534 | 177 | UNICHAR_ID *Ambiguities; |
1535 | | |
1536 | 177 | Results->Initialize(); |
1537 | 177 | INT_FX_RESULT_STRUCT fx_info; |
1538 | 177 | std::vector<INT_FEATURE_STRUCT> bl_features; |
1539 | 177 | TrainingSample *sample = |
1540 | 177 | BlobToTrainingSample(*Blob, classify_nonlinear_norm, &fx_info, &bl_features); |
1541 | 177 | if (sample == nullptr) { |
1542 | 0 | delete Results; |
1543 | 0 | return nullptr; |
1544 | 0 | } |
1545 | | |
1546 | 177 | CharNormClassifier(Blob, *sample, Results); |
1547 | 177 | delete sample; |
1548 | 177 | RemoveBadMatches(Results); |
1549 | 177 | std::sort(Results->match.begin(), Results->match.end(), SortDescendingRating); |
1550 | | |
1551 | | /* copy the class id's into an string of ambiguities - don't copy if |
1552 | | the correct class is the only class id matched */ |
1553 | 177 | Ambiguities = new UNICHAR_ID[Results->match.size() + 1]; |
1554 | 177 | if (Results->match.size() > 1 || |
1555 | 177 | (Results->match.size() == 1 && Results->match[0].unichar_id != CorrectClass)) { |
1556 | 157 | unsigned i; |
1557 | 900 | for (i = 0; i < Results->match.size(); i++) { |
1558 | 743 | Ambiguities[i] = Results->match[i].unichar_id; |
1559 | 743 | } |
1560 | 157 | Ambiguities[i] = -1; |
1561 | 157 | } else { |
1562 | 20 | Ambiguities[0] = -1; |
1563 | 20 | } |
1564 | | |
1565 | 177 | delete Results; |
1566 | 177 | return Ambiguities; |
1567 | 177 | } /* GetAmbiguities */ |
1568 | | |
1569 | | // Returns true if the given blob looks too dissimilar to any character |
1570 | | // present in the classifier templates. |
1571 | 0 | bool Classify::LooksLikeGarbage(TBLOB *blob) { |
1572 | 0 | auto *ratings = new BLOB_CHOICE_LIST(); |
1573 | 0 | AdaptiveClassifier(blob, ratings); |
1574 | 0 | BLOB_CHOICE_IT ratings_it(ratings); |
1575 | 0 | const UNICHARSET &unicharset = getDict().getUnicharset(); |
1576 | 0 | if (classify_debug_character_fragments) { |
1577 | 0 | print_ratings_list("======================\nLooksLikeGarbage() got ", ratings, unicharset); |
1578 | 0 | } |
1579 | 0 | for (ratings_it.mark_cycle_pt(); !ratings_it.cycled_list(); ratings_it.forward()) { |
1580 | 0 | if (unicharset.get_fragment(ratings_it.data()->unichar_id()) != nullptr) { |
1581 | 0 | continue; |
1582 | 0 | } |
1583 | 0 | float certainty = ratings_it.data()->certainty(); |
1584 | 0 | delete ratings; |
1585 | 0 | return certainty < classify_character_fragments_garbage_certainty_threshold; |
1586 | 0 | } |
1587 | 0 | delete ratings; |
1588 | 0 | return true; // no whole characters in ratings |
1589 | 0 | } |
1590 | | |
1591 | | /*---------------------------------------------------------------------------*/ |
1592 | | /** |
1593 | | * This routine calls the integer (Hardware) feature |
1594 | | * extractor if it has not been called before for this blob. |
1595 | | * |
1596 | | * The results from the feature extractor are placed into |
1597 | | * globals so that they can be used in other routines without |
1598 | | * re-extracting the features. |
1599 | | * |
1600 | | * It then copies the char norm features into the IntFeatures |
1601 | | * array provided by the caller. |
1602 | | * |
1603 | | * @param templates used to compute char norm adjustments |
1604 | | * @param pruner_norm_array Array of factors from blob normalization |
1605 | | * process |
1606 | | * @param char_norm_array array to fill with dummy char norm adjustments |
1607 | | * @param fx_info |
1608 | | * |
1609 | | * Globals: |
1610 | | * |
1611 | | * @return Number of features extracted or 0 if an error occurred. |
1612 | | */ |
1613 | | int Classify::GetCharNormFeature(const INT_FX_RESULT_STRUCT &fx_info, INT_TEMPLATES_STRUCT *templates, |
1614 | 0 | uint8_t *pruner_norm_array, uint8_t *char_norm_array) { |
1615 | 0 | auto norm_feature = new FEATURE_STRUCT(&CharNormDesc); |
1616 | 0 | float baseline = kBlnBaselineOffset; |
1617 | 0 | float scale = MF_SCALE_FACTOR; |
1618 | 0 | norm_feature->Params[CharNormY] = (fx_info.Ymean - baseline) * scale; |
1619 | 0 | norm_feature->Params[CharNormLength] = fx_info.Length * scale / LENGTH_COMPRESSION; |
1620 | 0 | norm_feature->Params[CharNormRx] = fx_info.Rx * scale; |
1621 | 0 | norm_feature->Params[CharNormRy] = fx_info.Ry * scale; |
1622 | | // Deletes norm_feature. |
1623 | 0 | ComputeCharNormArrays(norm_feature, templates, char_norm_array, pruner_norm_array); |
1624 | 0 | return IntCastRounded(fx_info.Length / kStandardFeatureLength); |
1625 | 0 | } /* GetCharNormFeature */ |
1626 | | |
1627 | | // Computes the char_norm_array for the unicharset and, if not nullptr, the |
1628 | | // pruner_array as appropriate according to the existence of the shape_table. |
1629 | | void Classify::ComputeCharNormArrays(FEATURE_STRUCT *norm_feature, INT_TEMPLATES_STRUCT *templates, |
1630 | 2.01M | uint8_t *char_norm_array, uint8_t *pruner_array) { |
1631 | 2.01M | ComputeIntCharNormArray(*norm_feature, char_norm_array); |
1632 | | //if (pruner_array != nullptr) { |
1633 | 2.01M | if (shape_table_ == nullptr) { |
1634 | 0 | ComputeIntCharNormArray(*norm_feature, pruner_array); |
1635 | 2.01M | } else { |
1636 | 2.01M | memset(&pruner_array[0], UINT8_MAX, templates->NumClasses * sizeof(pruner_array[0])); |
1637 | | // Each entry in the pruner norm array is the MIN of all the entries of |
1638 | | // the corresponding unichars in the CharNormArray. |
1639 | 230M | for (unsigned id = 0; id < templates->NumClasses; ++id) { |
1640 | 228M | int font_set_id = templates->Class[id]->font_set_id; |
1641 | 228M | const FontSet &fs = fontset_table_.at(font_set_id); |
1642 | 7.10G | for (auto f : fs) { |
1643 | 7.10G | const Shape &shape = shape_table_->GetShape(f); |
1644 | 14.2G | for (int c = 0; c < shape.size(); ++c) { |
1645 | 7.10G | if (char_norm_array[shape[c].unichar_id] < pruner_array[id]) { |
1646 | 152M | pruner_array[id] = char_norm_array[shape[c].unichar_id]; |
1647 | 152M | } |
1648 | 7.10G | } |
1649 | 7.10G | } |
1650 | 228M | } |
1651 | 2.01M | } |
1652 | | //} |
1653 | 2.01M | delete norm_feature; |
1654 | 2.01M | } |
1655 | | |
1656 | | /*---------------------------------------------------------------------------*/ |
1657 | | /** |
1658 | | * |
1659 | | * @param Templates adapted templates to add new config to |
1660 | | * @param ClassId class id to associate with new config |
1661 | | * @param FontinfoId font information inferred from pre-trained templates |
1662 | | * @param NumFeatures number of features in IntFeatures |
1663 | | * @param Features features describing model for new config |
1664 | | * @param FloatFeatures floating-pt representation of features |
1665 | | * |
1666 | | * @return The id of the new config created, a negative integer in |
1667 | | * case of error. |
1668 | | */ |
1669 | | int Classify::MakeNewTemporaryConfig(ADAPT_TEMPLATES_STRUCT *Templates, CLASS_ID ClassId, int FontinfoId, |
1670 | | int NumFeatures, INT_FEATURE_ARRAY Features, |
1671 | 415 | FEATURE_SET FloatFeatures) { |
1672 | 415 | INT_CLASS_STRUCT *IClass; |
1673 | 415 | ADAPT_CLASS_STRUCT *Class; |
1674 | 415 | PROTO_ID OldProtos[MAX_NUM_PROTOS]; |
1675 | 415 | FEATURE_ID BadFeatures[MAX_NUM_INT_FEATURES]; |
1676 | 415 | int NumOldProtos; |
1677 | 415 | int NumBadFeatures; |
1678 | 415 | int MaxProtoId, OldMaxProtoId; |
1679 | 415 | int MaskSize; |
1680 | 415 | int ConfigId; |
1681 | 415 | int i; |
1682 | 415 | int debug_level = NO_DEBUG; |
1683 | | |
1684 | 415 | if (classify_learning_debug_level >= 3) { |
1685 | 0 | debug_level = PRINT_MATCH_SUMMARY | PRINT_FEATURE_MATCHES | PRINT_PROTO_MATCHES; |
1686 | 0 | } |
1687 | | |
1688 | 415 | IClass = ClassForClassId(Templates->Templates, ClassId); |
1689 | 415 | Class = Templates->Class[ClassId]; |
1690 | | |
1691 | 415 | if (IClass->NumConfigs >= MAX_NUM_CONFIGS) { |
1692 | 0 | ++NumAdaptationsFailed; |
1693 | 0 | if (classify_learning_debug_level >= 1) { |
1694 | 0 | tprintf("Cannot make new temporary config: maximum number exceeded.\n"); |
1695 | 0 | } |
1696 | 0 | return -1; |
1697 | 0 | } |
1698 | | |
1699 | 415 | OldMaxProtoId = IClass->NumProtos - 1; |
1700 | | |
1701 | 415 | NumOldProtos = im_.FindGoodProtos(IClass, AllProtosOn, AllConfigsOff, NumFeatures, Features, |
1702 | 415 | OldProtos, classify_adapt_proto_threshold, debug_level); |
1703 | | |
1704 | 415 | MaskSize = WordsInVectorOfSize(MAX_NUM_PROTOS); |
1705 | 415 | zero_all_bits(TempProtoMask, MaskSize); |
1706 | 6.55k | for (i = 0; i < NumOldProtos; i++) { |
1707 | 6.13k | SET_BIT(TempProtoMask, OldProtos[i]); |
1708 | 6.13k | } |
1709 | | |
1710 | 415 | NumBadFeatures = im_.FindBadFeatures(IClass, TempProtoMask, AllConfigsOn, NumFeatures, Features, |
1711 | 415 | BadFeatures, classify_adapt_feature_threshold, debug_level); |
1712 | | |
1713 | 415 | MaxProtoId = |
1714 | 415 | MakeNewTempProtos(FloatFeatures, NumBadFeatures, BadFeatures, IClass, Class, TempProtoMask); |
1715 | 415 | if (MaxProtoId == NO_PROTO) { |
1716 | 2 | ++NumAdaptationsFailed; |
1717 | 2 | if (classify_learning_debug_level >= 1) { |
1718 | 0 | tprintf("Cannot make new temp protos: maximum number exceeded.\n"); |
1719 | 0 | } |
1720 | 2 | return -1; |
1721 | 2 | } |
1722 | | |
1723 | 413 | ConfigId = AddIntConfig(IClass); |
1724 | 413 | ConvertConfig(TempProtoMask, ConfigId, IClass); |
1725 | 413 | auto Config = new TEMP_CONFIG_STRUCT(MaxProtoId, FontinfoId); |
1726 | 413 | TempConfigFor(Class, ConfigId) = Config; |
1727 | 413 | copy_all_bits(TempProtoMask, Config->Protos, Config->ProtoVectorSize); |
1728 | | |
1729 | 413 | if (classify_learning_debug_level >= 1) { |
1730 | 0 | tprintf( |
1731 | 0 | "Making new temp config %d fontinfo id %d" |
1732 | 0 | " using %d old and %d new protos.\n", |
1733 | 0 | ConfigId, Config->FontinfoId, NumOldProtos, MaxProtoId - OldMaxProtoId); |
1734 | 0 | } |
1735 | | |
1736 | 413 | return ConfigId; |
1737 | 415 | } /* MakeNewTemporaryConfig */ |
1738 | | |
1739 | | /*---------------------------------------------------------------------------*/ |
1740 | | /** |
1741 | | * This routine finds sets of sequential bad features |
1742 | | * that all have the same angle and converts each set into |
1743 | | * a new temporary proto. The temp proto is added to the |
1744 | | * proto pruner for IClass, pushed onto the list of temp |
1745 | | * protos in Class, and added to TempProtoMask. |
1746 | | * |
1747 | | * @param Features floating-pt features describing new character |
1748 | | * @param NumBadFeat number of bad features to turn into protos |
1749 | | * @param BadFeat feature id's of bad features |
1750 | | * @param IClass integer class templates to add new protos to |
1751 | | * @param Class adapted class templates to add new protos to |
1752 | | * @param TempProtoMask proto mask to add new protos to |
1753 | | * |
1754 | | * Globals: none |
1755 | | * |
1756 | | * @return Max proto id in class after all protos have been added. |
1757 | | */ |
1758 | | PROTO_ID Classify::MakeNewTempProtos(FEATURE_SET Features, int NumBadFeat, FEATURE_ID BadFeat[], |
1759 | | INT_CLASS_STRUCT *IClass, ADAPT_CLASS_STRUCT *Class, |
1760 | 415 | BIT_VECTOR TempProtoMask) { |
1761 | 415 | FEATURE_ID *ProtoStart; |
1762 | 415 | FEATURE_ID *ProtoEnd; |
1763 | 415 | FEATURE_ID *LastBad; |
1764 | 415 | PROTO_STRUCT *Proto; |
1765 | 415 | FEATURE F1, F2; |
1766 | 415 | float X1, X2, Y1, Y2; |
1767 | 415 | float A1, A2, AngleDelta; |
1768 | 415 | float SegmentLength; |
1769 | 415 | PROTO_ID Pid; |
1770 | | |
1771 | 4.62k | for (ProtoStart = BadFeat, LastBad = ProtoStart + NumBadFeat; ProtoStart < LastBad; |
1772 | 4.21k | ProtoStart = ProtoEnd) { |
1773 | 4.21k | F1 = Features->Features[*ProtoStart]; |
1774 | 4.21k | X1 = F1->Params[PicoFeatX]; |
1775 | 4.21k | Y1 = F1->Params[PicoFeatY]; |
1776 | 4.21k | A1 = F1->Params[PicoFeatDir]; |
1777 | | |
1778 | 7.29k | for (ProtoEnd = ProtoStart + 1, SegmentLength = GetPicoFeatureLength(); ProtoEnd < LastBad; |
1779 | 6.89k | ProtoEnd++, SegmentLength += GetPicoFeatureLength()) { |
1780 | 6.89k | F2 = Features->Features[*ProtoEnd]; |
1781 | 6.89k | X2 = F2->Params[PicoFeatX]; |
1782 | 6.89k | Y2 = F2->Params[PicoFeatY]; |
1783 | 6.89k | A2 = F2->Params[PicoFeatDir]; |
1784 | | |
1785 | 6.89k | AngleDelta = std::fabs(A1 - A2); |
1786 | 6.89k | if (AngleDelta > 0.5f) { |
1787 | 318 | AngleDelta = 1 - AngleDelta; |
1788 | 318 | } |
1789 | | |
1790 | 6.89k | if (AngleDelta > matcher_clustering_max_angle_delta || std::fabs(X1 - X2) > SegmentLength || |
1791 | 6.89k | std::fabs(Y1 - Y2) > SegmentLength) { |
1792 | 3.81k | break; |
1793 | 3.81k | } |
1794 | 6.89k | } |
1795 | | |
1796 | 4.21k | F2 = Features->Features[*(ProtoEnd - 1)]; |
1797 | 4.21k | X2 = F2->Params[PicoFeatX]; |
1798 | 4.21k | Y2 = F2->Params[PicoFeatY]; |
1799 | 4.21k | A2 = F2->Params[PicoFeatDir]; |
1800 | | |
1801 | 4.21k | Pid = AddIntProto(IClass); |
1802 | 4.21k | if (Pid == NO_PROTO) { |
1803 | 2 | return (NO_PROTO); |
1804 | 2 | } |
1805 | | |
1806 | 4.21k | auto TempProto = new TEMP_PROTO_STRUCT; |
1807 | 4.21k | Proto = &(TempProto->Proto); |
1808 | | |
1809 | | /* compute proto params - NOTE that Y_DIM_OFFSET must be used because |
1810 | | ConvertProto assumes that the Y dimension varies from -0.5 to 0.5 |
1811 | | instead of the -0.25 to 0.75 used in baseline normalization */ |
1812 | 4.21k | Proto->Length = SegmentLength; |
1813 | 4.21k | Proto->Angle = A1; |
1814 | 4.21k | Proto->X = (X1 + X2) / 2; |
1815 | 4.21k | Proto->Y = (Y1 + Y2) / 2 - Y_DIM_OFFSET; |
1816 | 4.21k | FillABC(Proto); |
1817 | | |
1818 | 4.21k | TempProto->ProtoId = Pid; |
1819 | 4.21k | SET_BIT(TempProtoMask, Pid); |
1820 | | |
1821 | 4.21k | ConvertProto(Proto, Pid, IClass); |
1822 | 4.21k | AddProtoToProtoPruner(Proto, Pid, IClass, classify_learning_debug_level >= 2); |
1823 | | |
1824 | 4.21k | Class->TempProtos = push(Class->TempProtos, TempProto); |
1825 | 4.21k | } |
1826 | 413 | return IClass->NumProtos - 1; |
1827 | 415 | } /* MakeNewTempProtos */ |
1828 | | |
1829 | | /*---------------------------------------------------------------------------*/ |
1830 | | /** |
1831 | | * |
1832 | | * @param Templates current set of adaptive templates |
1833 | | * @param ClassId class containing config to be made permanent |
1834 | | * @param ConfigId config to be made permanent |
1835 | | * @param Blob current blob being adapted to |
1836 | | * |
1837 | | * Globals: none |
1838 | | */ |
1839 | | void Classify::MakePermanent(ADAPT_TEMPLATES_STRUCT *Templates, CLASS_ID ClassId, int ConfigId, |
1840 | 177 | TBLOB *Blob) { |
1841 | 177 | UNICHAR_ID *Ambigs; |
1842 | 177 | PROTO_KEY ProtoKey; |
1843 | | |
1844 | 177 | auto Class = Templates->Class[ClassId]; |
1845 | 177 | auto Config = TempConfigFor(Class, ConfigId); |
1846 | | |
1847 | 177 | MakeConfigPermanent(Class, ConfigId); |
1848 | 177 | if (Class->NumPermConfigs == 0) { |
1849 | 114 | Templates->NumPermClasses++; |
1850 | 114 | } |
1851 | 177 | Class->NumPermConfigs++; |
1852 | | |
1853 | | // Initialize permanent config. |
1854 | 177 | Ambigs = GetAmbiguities(Blob, ClassId); |
1855 | 177 | auto Perm = new PERM_CONFIG_STRUCT; |
1856 | 177 | Perm->Ambigs = Ambigs; |
1857 | 177 | Perm->FontinfoId = Config->FontinfoId; |
1858 | | |
1859 | | // Free memory associated with temporary config (since ADAPTED_CONFIG |
1860 | | // is a union we need to clean up before we record permanent config). |
1861 | 177 | ProtoKey.Templates = Templates; |
1862 | 177 | ProtoKey.ClassId = ClassId; |
1863 | 177 | ProtoKey.ConfigId = ConfigId; |
1864 | 177 | Class->TempProtos = delete_d(Class->TempProtos, &ProtoKey, MakeTempProtoPerm); |
1865 | 177 | delete Config; |
1866 | | |
1867 | | // Record permanent config. |
1868 | 177 | PermConfigFor(Class, ConfigId) = Perm; |
1869 | | |
1870 | 177 | if (classify_learning_debug_level >= 1) { |
1871 | 0 | tprintf( |
1872 | 0 | "Making config %d for %s (ClassId %d) permanent:" |
1873 | 0 | " fontinfo id %d, ambiguities '", |
1874 | 0 | ConfigId, getDict().getUnicharset().debug_str(ClassId).c_str(), ClassId, |
1875 | 0 | PermConfigFor(Class, ConfigId)->FontinfoId); |
1876 | 0 | for (UNICHAR_ID *AmbigsPointer = Ambigs; *AmbigsPointer >= 0; ++AmbigsPointer) { |
1877 | 0 | tprintf("%s", unicharset.id_to_unichar(*AmbigsPointer)); |
1878 | 0 | } |
1879 | 0 | tprintf("'.\n"); |
1880 | 0 | } |
1881 | 177 | } /* MakePermanent */ |
1882 | | |
1883 | | /*---------------------------------------------------------------------------*/ |
1884 | | /** |
1885 | | * This routine converts TempProto to be permanent if |
1886 | | * its proto id is used by the configuration specified in |
1887 | | * ProtoKey. |
1888 | | * |
1889 | | * @param item1 (TEMP_PROTO) temporary proto to compare to key |
1890 | | * @param item2 (PROTO_KEY) defines which protos to make permanent |
1891 | | * |
1892 | | * Globals: none |
1893 | | * |
1894 | | * @return true if TempProto is converted, false otherwise |
1895 | | */ |
1896 | 5.67k | int MakeTempProtoPerm(void *item1, void *item2) { |
1897 | 5.67k | auto TempProto = static_cast<TEMP_PROTO_STRUCT *>(item1); |
1898 | 5.67k | auto ProtoKey = static_cast<PROTO_KEY *>(item2); |
1899 | | |
1900 | 5.67k | auto Class = ProtoKey->Templates->Class[ProtoKey->ClassId]; |
1901 | 5.67k | auto Config = TempConfigFor(Class, ProtoKey->ConfigId); |
1902 | | |
1903 | 5.67k | if (TempProto->ProtoId > Config->MaxProtoId || !test_bit(Config->Protos, TempProto->ProtoId)) { |
1904 | 4.45k | return false; |
1905 | 4.45k | } |
1906 | | |
1907 | 1.21k | MakeProtoPermanent(Class, TempProto->ProtoId); |
1908 | 1.21k | AddProtoToClassPruner(&(TempProto->Proto), ProtoKey->ClassId, ProtoKey->Templates->Templates); |
1909 | 1.21k | delete TempProto; |
1910 | | |
1911 | 1.21k | return true; |
1912 | 5.67k | } /* MakeTempProtoPerm */ |
1913 | | |
1914 | | /*---------------------------------------------------------------------------*/ |
1915 | | /** |
1916 | | * This routine writes the matches in Results to File. |
1917 | | * |
1918 | | * @param results match results to write to File |
1919 | | * |
1920 | | * Globals: none |
1921 | | */ |
1922 | 0 | void Classify::PrintAdaptiveMatchResults(const ADAPT_RESULTS &results) { |
1923 | 0 | for (auto &it : results.match) { |
1924 | 0 | tprintf("%s ", unicharset.debug_str(it.unichar_id).c_str()); |
1925 | 0 | it.Print(); |
1926 | 0 | } |
1927 | 0 | } /* PrintAdaptiveMatchResults */ |
1928 | | |
1929 | | /*---------------------------------------------------------------------------*/ |
1930 | | /** |
1931 | | * This routine steps through each matching class in Results |
1932 | | * and removes it from the match list if its rating |
1933 | | * is worse than the BestRating plus a pad. In other words, |
1934 | | * all good matches get moved to the front of the classes |
1935 | | * array. |
1936 | | * |
1937 | | * @param Results contains matches to be filtered |
1938 | | * |
1939 | | * Globals: |
1940 | | * - matcher_bad_match_pad defines a "bad match" |
1941 | | */ |
1942 | 2.01M | void Classify::RemoveBadMatches(ADAPT_RESULTS *Results) { |
1943 | 2.01M | unsigned Next, NextGood; |
1944 | 2.01M | float BadMatchThreshold; |
1945 | 2.01M | static const char *romans = "i v x I V X"; |
1946 | 2.01M | BadMatchThreshold = Results->best_rating - matcher_bad_match_pad; |
1947 | | |
1948 | 2.01M | if (classify_bln_numeric_mode) { |
1949 | 0 | UNICHAR_ID unichar_id_one = |
1950 | 0 | unicharset.contains_unichar("1") ? unicharset.unichar_to_id("1") : -1; |
1951 | 0 | UNICHAR_ID unichar_id_zero = |
1952 | 0 | unicharset.contains_unichar("0") ? unicharset.unichar_to_id("0") : -1; |
1953 | 0 | float scored_one = ScoredUnichar(unichar_id_one, *Results); |
1954 | 0 | float scored_zero = ScoredUnichar(unichar_id_zero, *Results); |
1955 | |
|
1956 | 0 | for (Next = NextGood = 0; Next < Results->match.size(); Next++) { |
1957 | 0 | const UnicharRating &match = Results->match[Next]; |
1958 | 0 | if (match.rating >= BadMatchThreshold) { |
1959 | 0 | if (!unicharset.get_isalpha(match.unichar_id) || |
1960 | 0 | strstr(romans, unicharset.id_to_unichar(match.unichar_id)) != nullptr) { |
1961 | 0 | } else if (unicharset.eq(match.unichar_id, "l") && scored_one < BadMatchThreshold) { |
1962 | 0 | Results->match[Next].unichar_id = unichar_id_one; |
1963 | 0 | } else if (unicharset.eq(match.unichar_id, "O") && scored_zero < BadMatchThreshold) { |
1964 | 0 | Results->match[Next].unichar_id = unichar_id_zero; |
1965 | 0 | } else { |
1966 | 0 | Results->match[Next].unichar_id = INVALID_UNICHAR_ID; // Don't copy. |
1967 | 0 | } |
1968 | 0 | if (Results->match[Next].unichar_id != INVALID_UNICHAR_ID) { |
1969 | 0 | if (NextGood == Next) { |
1970 | 0 | ++NextGood; |
1971 | 0 | } else { |
1972 | 0 | Results->match[NextGood++] = Results->match[Next]; |
1973 | 0 | } |
1974 | 0 | } |
1975 | 0 | } |
1976 | 0 | } |
1977 | 2.01M | } else { |
1978 | 12.1M | for (Next = NextGood = 0; Next < Results->match.size(); Next++) { |
1979 | 10.1M | if (Results->match[Next].rating >= BadMatchThreshold) { |
1980 | 9.93M | if (NextGood == Next) { |
1981 | 8.94M | ++NextGood; |
1982 | 8.94M | } else { |
1983 | 994k | Results->match[NextGood++] = Results->match[Next]; |
1984 | 994k | } |
1985 | 9.93M | } |
1986 | 10.1M | } |
1987 | 2.01M | } |
1988 | 2.01M | Results->match.resize(NextGood); |
1989 | 2.01M | } /* RemoveBadMatches */ |
1990 | | |
1991 | | /*----------------------------------------------------------------------------*/ |
1992 | | /** |
1993 | | * This routine discards extra digits or punctuation from the results. |
1994 | | * We keep only the top 2 punctuation answers and the top 1 digit answer if |
1995 | | * present. |
1996 | | * |
1997 | | * @param Results contains matches to be filtered |
1998 | | */ |
1999 | 2.01M | void Classify::RemoveExtraPuncs(ADAPT_RESULTS *Results) { |
2000 | 2.01M | unsigned Next, NextGood; |
2001 | 2.01M | int punc_count; /*no of garbage characters */ |
2002 | 2.01M | int digit_count; |
2003 | | /*garbage characters */ |
2004 | 2.01M | static char punc_chars[] = ". , ; : / ` ~ ' - = \\ | \" ! _ ^"; |
2005 | 2.01M | static char digit_chars[] = "0 1 2 3 4 5 6 7 8 9"; |
2006 | | |
2007 | 2.01M | punc_count = 0; |
2008 | 2.01M | digit_count = 0; |
2009 | 11.9M | for (Next = NextGood = 0; Next < Results->match.size(); Next++) { |
2010 | 9.93M | const UnicharRating &match = Results->match[Next]; |
2011 | 9.93M | bool keep = true; |
2012 | 9.93M | if (strstr(punc_chars, unicharset.id_to_unichar(match.unichar_id)) != nullptr) { |
2013 | 750k | if (punc_count >= 2) { |
2014 | 64.7k | keep = false; |
2015 | 64.7k | } |
2016 | 750k | punc_count++; |
2017 | 9.18M | } else { |
2018 | 9.18M | if (strstr(digit_chars, unicharset.id_to_unichar(match.unichar_id)) != nullptr) { |
2019 | 374k | if (digit_count >= 1) { |
2020 | 88.3k | keep = false; |
2021 | 88.3k | } |
2022 | 374k | digit_count++; |
2023 | 374k | } |
2024 | 9.18M | } |
2025 | 9.93M | if (keep) { |
2026 | 9.78M | if (NextGood == Next) { |
2027 | 9.36M | ++NextGood; |
2028 | 9.36M | } else { |
2029 | 417k | Results->match[NextGood++] = match; |
2030 | 417k | } |
2031 | 9.78M | } |
2032 | 9.93M | } |
2033 | 2.01M | Results->match.resize(NextGood); |
2034 | 2.01M | } /* RemoveExtraPuncs */ |
2035 | | |
2036 | | /*---------------------------------------------------------------------------*/ |
2037 | | /** |
2038 | | * This routine resets the internal thresholds inside |
2039 | | * the integer matcher to correspond to the specified |
2040 | | * threshold. |
2041 | | * |
2042 | | * @param Threshold threshold for creating new templates |
2043 | | * |
2044 | | * Globals: |
2045 | | * - matcher_good_threshold default good match rating |
2046 | | */ |
2047 | 3.09k | void Classify::SetAdaptiveThreshold(float Threshold) { |
2048 | 3.09k | Threshold = (Threshold == matcher_good_threshold) ? 0.9f : (1 - Threshold); |
2049 | 3.09k | classify_adapt_proto_threshold.set_value(ClipToRange<int>(255 * Threshold, 0, 255)); |
2050 | 3.09k | classify_adapt_feature_threshold.set_value(ClipToRange<int>(255 * Threshold, 0, 255)); |
2051 | 3.09k | } /* SetAdaptiveThreshold */ |
2052 | | |
2053 | | #ifndef GRAPHICS_DISABLED |
2054 | | |
2055 | | /*---------------------------------------------------------------------------*/ |
2056 | | /** |
2057 | | * This routine displays debug information for the best config |
2058 | | * of the given shape_id for the given set of features. |
2059 | | * |
2060 | | * @param shape_id classifier id to work with |
2061 | | * @param features features of the unknown character |
2062 | | * @param num_features Number of features in the features array. |
2063 | | */ |
2064 | | |
2065 | | void Classify::ShowBestMatchFor(int shape_id, const INT_FEATURE_STRUCT *features, |
2066 | | int num_features) { |
2067 | | uint32_t config_mask; |
2068 | | if (UnusedClassIdIn(PreTrainedTemplates, shape_id)) { |
2069 | | tprintf("No built-in templates for class/shape %d\n", shape_id); |
2070 | | return; |
2071 | | } |
2072 | | if (num_features <= 0) { |
2073 | | tprintf("Illegal blob (char norm features)!\n"); |
2074 | | return; |
2075 | | } |
2076 | | UnicharRating cn_result; |
2077 | | classify_norm_method.set_value(character); |
2078 | | im_.Match(ClassForClassId(PreTrainedTemplates, shape_id), AllProtosOn, AllConfigsOn, num_features, |
2079 | | features, &cn_result, classify_adapt_feature_threshold, NO_DEBUG, |
2080 | | matcher_debug_separate_windows); |
2081 | | tprintf("\n"); |
2082 | | config_mask = 1 << cn_result.config; |
2083 | | |
2084 | | tprintf("Static Shape ID: %d\n", shape_id); |
2085 | | ShowMatchDisplay(); |
2086 | | im_.Match(ClassForClassId(PreTrainedTemplates, shape_id), AllProtosOn, &config_mask, num_features, |
2087 | | features, &cn_result, classify_adapt_feature_threshold, matcher_debug_flags, |
2088 | | matcher_debug_separate_windows); |
2089 | | UpdateMatchDisplay(); |
2090 | | } /* ShowBestMatchFor */ |
2091 | | |
2092 | | #endif // !GRAPHICS_DISABLED |
2093 | | |
2094 | | // Returns a string for the classifier class_id: either the corresponding |
2095 | | // unicharset debug_str or the shape_table_ debug str. |
2096 | | std::string Classify::ClassIDToDebugStr(const INT_TEMPLATES_STRUCT *templates, int class_id, |
2097 | 0 | int config_id) const { |
2098 | 0 | std::string class_string; |
2099 | 0 | if (templates == PreTrainedTemplates && shape_table_ != nullptr) { |
2100 | 0 | int shape_id = ClassAndConfigIDToFontOrShapeID(class_id, config_id); |
2101 | 0 | class_string = shape_table_->DebugStr(shape_id); |
2102 | 0 | } else { |
2103 | 0 | class_string = unicharset.debug_str(class_id); |
2104 | 0 | } |
2105 | 0 | return class_string; |
2106 | 0 | } |
2107 | | |
2108 | | // Converts a classifier class_id index to a shape_table_ index |
2109 | 405M | int Classify::ClassAndConfigIDToFontOrShapeID(int class_id, int int_result_config) const { |
2110 | 405M | int font_set_id = PreTrainedTemplates->Class[class_id]->font_set_id; |
2111 | | // Older inttemps have no font_ids. |
2112 | 405M | if (font_set_id < 0) { |
2113 | 0 | return kBlankFontinfoId; |
2114 | 0 | } |
2115 | 405M | const FontSet &fs = fontset_table_.at(font_set_id); |
2116 | 405M | return fs.at(int_result_config); |
2117 | 405M | } |
2118 | | |
2119 | | // Converts a shape_table_ index to a classifier class_id index (not a |
2120 | | // unichar-id!). Uses a search, so not fast. |
2121 | 0 | int Classify::ShapeIDToClassID(int shape_id) const { |
2122 | 0 | for (unsigned id = 0; id < PreTrainedTemplates->NumClasses; ++id) { |
2123 | 0 | int font_set_id = PreTrainedTemplates->Class[id]->font_set_id; |
2124 | 0 | ASSERT_HOST(font_set_id >= 0); |
2125 | 0 | const FontSet &fs = fontset_table_.at(font_set_id); |
2126 | 0 | for (auto f : fs) { |
2127 | 0 | if (f == shape_id) { |
2128 | 0 | return id; |
2129 | 0 | } |
2130 | 0 | } |
2131 | 0 | } |
2132 | 0 | tprintf("Shape %d not found\n", shape_id); |
2133 | 0 | return -1; |
2134 | 0 | } |
2135 | | |
2136 | | // Returns true if the given TEMP_CONFIG_STRUCT is good enough to make it |
2137 | | // a permanent config. |
2138 | 935 | bool Classify::TempConfigReliable(CLASS_ID class_id, const TEMP_CONFIG_STRUCT *config) { |
2139 | 935 | if (classify_learning_debug_level >= 1) { |
2140 | 0 | tprintf("NumTimesSeen for config of %s is %d\n", |
2141 | 0 | getDict().getUnicharset().debug_str(class_id).c_str(), config->NumTimesSeen); |
2142 | 0 | } |
2143 | 935 | if (config->NumTimesSeen >= matcher_sufficient_examples_for_prototyping) { |
2144 | 0 | return true; |
2145 | 935 | } else if (config->NumTimesSeen < matcher_min_examples_for_prototyping) { |
2146 | 758 | return false; |
2147 | 758 | } else if (use_ambigs_for_adaption) { |
2148 | | // Go through the ambigs vector and see whether we have already seen |
2149 | | // enough times all the characters represented by the ambigs vector. |
2150 | 0 | const UnicharIdVector *ambigs = getDict().getUnicharAmbigs().AmbigsForAdaption(class_id); |
2151 | 0 | int ambigs_size = (ambigs == nullptr) ? 0 : ambigs->size(); |
2152 | 0 | for (int ambig = 0; ambig < ambigs_size; ++ambig) { |
2153 | 0 | ADAPT_CLASS_STRUCT *ambig_class = AdaptedTemplates->Class[(*ambigs)[ambig]]; |
2154 | 0 | assert(ambig_class != nullptr); |
2155 | 0 | if (ambig_class->NumPermConfigs == 0 && |
2156 | 0 | ambig_class->MaxNumTimesSeen < matcher_min_examples_for_prototyping) { |
2157 | 0 | if (classify_learning_debug_level >= 1) { |
2158 | 0 | tprintf( |
2159 | 0 | "Ambig %s has not been seen enough times," |
2160 | 0 | " not making config for %s permanent\n", |
2161 | 0 | getDict().getUnicharset().debug_str((*ambigs)[ambig]).c_str(), |
2162 | 0 | getDict().getUnicharset().debug_str(class_id).c_str()); |
2163 | 0 | } |
2164 | 0 | return false; |
2165 | 0 | } |
2166 | 0 | } |
2167 | 0 | } |
2168 | 177 | return true; |
2169 | 935 | } |
2170 | | |
2171 | 177 | void Classify::UpdateAmbigsGroup(CLASS_ID class_id, TBLOB *Blob) { |
2172 | 177 | const UnicharIdVector *ambigs = getDict().getUnicharAmbigs().ReverseAmbigsForAdaption(class_id); |
2173 | 177 | int ambigs_size = (ambigs == nullptr) ? 0 : ambigs->size(); |
2174 | 177 | if (classify_learning_debug_level >= 1) { |
2175 | 0 | tprintf("Running UpdateAmbigsGroup for %s class_id=%d\n", |
2176 | 0 | getDict().getUnicharset().debug_str(class_id).c_str(), class_id); |
2177 | 0 | } |
2178 | 177 | for (int ambig = 0; ambig < ambigs_size; ++ambig) { |
2179 | 0 | CLASS_ID ambig_class_id = (*ambigs)[ambig]; |
2180 | 0 | const ADAPT_CLASS_STRUCT *ambigs_class = AdaptedTemplates->Class[ambig_class_id]; |
2181 | 0 | for (int cfg = 0; cfg < MAX_NUM_CONFIGS; ++cfg) { |
2182 | 0 | if (ConfigIsPermanent(ambigs_class, cfg)) { |
2183 | 0 | continue; |
2184 | 0 | } |
2185 | 0 | const TEMP_CONFIG_STRUCT *config = TempConfigFor(AdaptedTemplates->Class[ambig_class_id], cfg); |
2186 | 0 | if (config != nullptr && TempConfigReliable(ambig_class_id, config)) { |
2187 | 0 | if (classify_learning_debug_level >= 1) { |
2188 | 0 | tprintf("Making config %d of %s permanent\n", cfg, |
2189 | 0 | getDict().getUnicharset().debug_str(ambig_class_id).c_str()); |
2190 | 0 | } |
2191 | 0 | MakePermanent(AdaptedTemplates, ambig_class_id, cfg, Blob); |
2192 | 0 | } |
2193 | 0 | } |
2194 | 0 | } |
2195 | 177 | } |
2196 | | |
2197 | | } // namespace tesseract |