Coverage Report

Created: 2025-07-12 06:44

/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