Coverage Report

Created: 2026-09-14 06:59

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/tesseract/src/lstm/recodebeam.h
Line
Count
Source
1
///////////////////////////////////////////////////////////////////////
2
// File:        recodebeam.h
3
// Description: Beam search to decode from the re-encoded CJK as a sequence of
4
//              smaller numbers in place of a single large code.
5
// Author:      Ray Smith
6
//
7
// (C) Copyright 2015, Google Inc.
8
// Licensed under the Apache License, Version 2.0 (the "License");
9
// you may not use this file except in compliance with the License.
10
// You may obtain a copy of the License at
11
// http://www.apache.org/licenses/LICENSE-2.0
12
// Unless required by applicable law or agreed to in writing, software
13
// distributed under the License is distributed on an "AS IS" BASIS,
14
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
// See the License for the specific language governing permissions and
16
// limitations under the License.
17
//
18
///////////////////////////////////////////////////////////////////////
19
20
#ifndef THIRD_PARTY_TESSERACT_LSTM_RECODEBEAM_H_
21
#define THIRD_PARTY_TESSERACT_LSTM_RECODEBEAM_H_
22
23
#include "dawg.h"
24
#include "dict.h"
25
#include "genericheap.h"
26
#include "genericvector.h"
27
#include "kdpair.h"
28
#include "networkio.h"
29
#include "ratngs.h"
30
#include "unicharcompress.h"
31
32
#include <unordered_set> // for std::unordered_set
33
#include <vector>        // for std::vector
34
35
namespace tesseract {
36
37
// Enum describing what can follow the current node.
38
// Consider the following softmax outputs:
39
// Timestep    0    1    2    3    4    5    6    7    8
40
// X-score    0.01 0.55 0.98 0.42 0.01 0.01 0.40 0.95 0.01
41
// Y-score    0.00 0.01 0.01 0.01 0.01 0.97 0.59 0.04 0.01
42
// Null-score 0.99 0.44 0.01 0.57 0.98 0.02 0.01 0.01 0.98
43
// Then the correct CTC decoding (in which adjacent equal classes are folded,
44
// and then all nulls are dropped) is clearly XYX, but simple decoding (taking
45
// the max at each timestep) leads to:
46
// Null@0.99 X@0.55 X@0.98 Null@0.57 Null@0.98 Y@0.97 Y@0.59 X@0.95 Null@0.98,
47
// which folds to the correct XYX. The conversion to Tesseract rating and
48
// certainty uses the sum of the log probs (log of the product of probabilities)
49
// for the Rating and the minimum log prob for the certainty, but that yields a
50
// minimum certainty of log(0.55), which is poor for such an obvious case.
51
// CTC says that the probability of the result is the SUM of the products of the
52
// probabilities over ALL PATHS that decode to the same result, which includes:
53
// NXXNNYYXN, NNXNNYYN, NXXXNYYXN, NNXXNYXXN, and others including XXXXXYYXX.
54
// That is intractable, so some compromise between simple and ideal is needed.
55
// Observing that evenly split timesteps rarely happen next to each other, we
56
// allow scores at a transition between classes to be added for decoding thus:
57
// N@0.99 (N+X)@0.99 X@0.98 (N+X)@0.99 N@0.98 Y@0.97 (X+Y+N)@1.00 X@0.95 N@0.98.
58
// This works because NNX and NXX both decode to X, so in the middle we can use
59
// N+X. Note that the classes either side of a sum must stand alone, i.e. use a
60
// single score, to force all paths to pass through them and decode to the same
61
// result. Also in the special case of a transition from X to Y, with only one
62
// timestep between, it is possible to add X+Y+N, since XXY, XYY, and XNY all
63
// decode to XY.
64
// An important condition is that we cannot combine X and Null between two
65
// stand-alone Xs, since that can decode as XNX->XX or XXX->X, so the scores for
66
// X and Null have to go in separate paths. Combining scores in this way
67
// provides a much better minimum certainty of log(0.95).
68
// In the implementation of the beam search, we have to place the possibilities
69
// X, X+N and X+Y+N in the beam under appropriate conditions of the previous
70
// node, and constrain what can follow, to enforce the rules explained above.
71
// We therefore have 3 different types of node determined by what can follow:
72
enum NodeContinuation {
73
  NC_ANYTHING, // This node used just its own score, so anything can follow.
74
  NC_ONLY_DUP, // The current node combined another score with the score for
75
               // itself, without a stand-alone duplicate before, so must be
76
               // followed by a stand-alone duplicate.
77
  NC_NO_DUP,   // The current node combined another score with the score for
78
               // itself, after a stand-alone, so can only be followed by
79
               // something other than a duplicate of the current node.
80
  NC_COUNT
81
};
82
83
// Enum describing the top-n status of a code.
84
enum TopNState {
85
  TN_TOP2,     // Winner or 2nd.
86
  TN_TOPN,     // Runner up in top-n, but not 1st or 2nd.
87
  TN_ALSO_RAN, // Not in the top-n.
88
  TN_COUNT
89
};
90
91
// Lattice element for Re-encode beam search.
92
struct RecodeNode {
93
  RecodeNode()
94
1.56M
      : code(-1)
95
1.56M
      , unichar_id(INVALID_UNICHAR_ID)
96
1.56M
      , permuter(TOP_CHOICE_PERM)
97
1.56M
      , start_of_dawg(false)
98
1.56M
      , start_of_word(false)
99
1.56M
      , end_of_word(false)
100
1.56M
      , duplicate(false)
101
1.56M
      , certainty(0.0f)
102
1.56M
      , score(0.0f)
103
1.56M
      , prev(nullptr)
104
1.56M
      , dawgs(nullptr)
105
1.56M
      , code_hash(0) {}
106
  RecodeNode(int c, int uni_id, PermuterType perm, bool dawg_start, bool word_start, bool end,
107
             bool dup, float cert, float s, const RecodeNode *p, DawgPositionVector *d,
108
             uint64_t hash)
109
28.5M
      : code(c)
110
28.5M
      , unichar_id(uni_id)
111
28.5M
      , permuter(perm)
112
28.5M
      , start_of_dawg(dawg_start)
113
28.5M
      , start_of_word(word_start)
114
28.5M
      , end_of_word(end)
115
28.5M
      , duplicate(dup)
116
28.5M
      , certainty(cert)
117
28.5M
      , score(s)
118
28.5M
      , prev(p)
119
28.5M
      , dawgs(d)
120
28.5M
      , code_hash(hash) {}
121
  // NOTE: If we could use C++11, then this would be a move constructor.
122
  // Instead we have copy constructor that does a move!! This is because we
123
  // don't want to copy the whole DawgPositionVector each time, and true
124
  // copying isn't necessary for this struct. It does get moved around a lot
125
  // though inside the heap and during heap push, hence the move semantics.
126
98.0M
  RecodeNode(const RecodeNode &src) : dawgs(nullptr) {
127
98.0M
    *this = src;
128
98.0M
    ASSERT_HOST(src.dawgs == nullptr);
129
98.0M
  }
130
177M
  RecodeNode &operator=(const RecodeNode &src) {
131
177M
    if (this != &src) {
132
177M
      delete dawgs;
133
177M
      code = src.code;
134
177M
      unichar_id = src.unichar_id;
135
177M
      permuter = src.permuter;
136
177M
      start_of_dawg = src.start_of_dawg;
137
177M
      start_of_word = src.start_of_word;
138
177M
      end_of_word = src.end_of_word;
139
177M
      duplicate = src.duplicate;
140
177M
      certainty = src.certainty;
141
177M
      score = src.score;
142
177M
      prev = src.prev;
143
177M
      dawgs = src.dawgs;
144
177M
      code_hash = src.code_hash;
145
177M
      const_cast<RecodeNode &>(src).dawgs = nullptr;
146
177M
    }
147
177M
    return *this;
148
177M
  }
149
128M
  ~RecodeNode() {
150
128M
    delete dawgs;
151
128M
  }
152
  // Prints details of the node.
153
  void Print(int null_char, const UNICHARSET &unicharset, int depth) const;
154
155
  // The re-encoded code here = index to network output.
156
  int code;
157
  // The decoded unichar_id is only valid for the final code of a sequence.
158
  int unichar_id;
159
  // The type of permuter active at this point. Intervals between start_of_word
160
  // and end_of_word make valid words of type given by permuter where
161
  // end_of_word is true. These aren't necessarily delimited by spaces.
162
  PermuterType permuter;
163
  // True if this is the initial dawg state. May be attached to a space or,
164
  // in a non-space-delimited lang, the end of the previous word.
165
  bool start_of_dawg;
166
  // True if this is the first node in a dictionary word.
167
  bool start_of_word;
168
  // True if this represents a valid candidate end of word position. Does not
169
  // necessarily mark the end of a word, since a word can be extended beyond a
170
  // candidate end by a continuation, eg 'the' continues to 'these'.
171
  bool end_of_word;
172
  // True if this->code is a duplicate of prev->code. Some training modes
173
  // allow the network to output duplicate characters and crush them with CTC,
174
  // but that would mess up the dictionary search, so we just smash them
175
  // together on the fly using the duplicate flag.
176
  bool duplicate;
177
  // Certainty (log prob) of (just) this position.
178
  float certainty;
179
  // Total certainty of the path to this position.
180
  float score;
181
  // The previous node in this chain. Borrowed pointer.
182
  const RecodeNode *prev;
183
  // The currently active dawgs at this position. Owned pointer.
184
  DawgPositionVector *dawgs;
185
  // A hash of all codes in the prefix and this->code as well. Used for
186
  // duplicate path removal.
187
  uint64_t code_hash;
188
};
189
190
using RecodePair = KDPairInc<double, RecodeNode>;
191
using RecodeHeap = GenericHeap<RecodePair>;
192
193
// Class that holds the entire beam search for recognition of a text line.
194
class TESS_API RecodeBeamSearch {
195
public:
196
  // Borrows the pointer, which is expected to survive until *this is deleted.
197
  RecodeBeamSearch(const UnicharCompress &recoder, int null_char, bool simple_text, Dict *dict);
198
  ~RecodeBeamSearch();
199
200
  // Decodes the set of network outputs, storing the lattice internally.
201
  // If charset is not null, it enables detailed debugging of the beam search.
202
  void Decode(const NetworkIO &output, double dict_ratio, double cert_offset,
203
              double worst_dict_cert, const UNICHARSET *charset, int lstm_choice_mode = 0);
204
  void Decode(const GENERIC_2D_ARRAY<float> &output, double dict_ratio, double cert_offset,
205
              double worst_dict_cert, const UNICHARSET *charset);
206
207
  void DecodeSecondaryBeams(const NetworkIO &output, double dict_ratio, double cert_offset,
208
                            double worst_dict_cert, const UNICHARSET *charset);
209
210
  // Returns the best path as labels/scores/xcoords similar to simple CTC.
211
  void ExtractBestPathAsLabels(std::vector<int> *labels, std::vector<int> *xcoords) const;
212
  // Returns the best path as unichar-ids/certs/ratings/xcoords skipping
213
  // duplicates, nulls and intermediate parts.
214
  void ExtractBestPathAsUnicharIds(bool debug, const UNICHARSET *unicharset,
215
                                   std::vector<int> *unichar_ids, std::vector<float> *certs,
216
                                   std::vector<float> *ratings, std::vector<int> *xcoords) const;
217
218
  // Returns the best path as a set of WERD_RES.
219
  void ExtractBestPathAsWords(const TBOX &line_box, float scale_factor, bool debug,
220
                              const UNICHARSET *unicharset, PointerVector<WERD_RES> *words);
221
222
  // Generates debug output of the content of the beams after a Decode.
223
  void DebugBeams(const UNICHARSET &unicharset) const;
224
225
  // Extract the best characters from the current decode iteration and block
226
  // those symbols for the next iteration. In contrast to Tesseract's standard
227
  // method to chose the best overall node chain, this methods looks at a short
228
  // node chain segmented by the character boundaries and chooses the best
229
  // option independent of the remaining node chain.
230
  void extractSymbolChoices(const UNICHARSET *unicharset);
231
232
  // Generates debug output of the content of the beams after a Decode.
233
  void PrintBeam2(bool uids, const UNICHARSET *charset, bool secondary) const;
234
  // Segments the timestep bundle by the character_boundaries.
235
  void segmentTimestepsByCharacters();
236
  std::vector<std::vector<std::pair<const char *, float>>>
237
  // Unions the segmented timestep character bundles to one big bundle.
238
  combineSegmentedTimesteps(
239
      std::vector<std::vector<std::vector<std::pair<const char *, float>>>> *segmentedTimesteps);
240
  // Stores the alternative characters of every timestep together with their
241
  // probability.
242
  std::vector<std::vector<std::pair<const char *, float>>> timesteps;
243
  std::vector<std::vector<std::vector<std::pair<const char *, float>>>> segmentedTimesteps;
244
  // Stores the character choices found in the ctc algorithm
245
  std::vector<std::vector<std::pair<const char *, float>>> ctc_choices;
246
  // Stores all unicharids which are excluded for future iterations
247
  std::vector<std::unordered_set<int>> excludedUnichars;
248
  // Stores the character boundaries regarding timesteps.
249
  std::vector<int> character_boundaries_;
250
  // Clipping value for certainty inside Tesseract. Reflects the minimum value
251
  // of certainty that will be returned by ExtractBestPathAsUnicharIds.
252
  // Supposedly on a uniform scale that can be compared across languages and
253
  // engines.
254
  static constexpr float kMinCertainty = -20.0f;
255
  // Number of different code lengths for which we have a separate beam.
256
  static const int kNumLengths = RecodedCharID::kMaxCodeLen + 1;
257
  // Total number of beams: dawg/nodawg * number of NodeContinuation * number
258
  // of different lengths.
259
  static const int kNumBeams = 2 * NC_COUNT * kNumLengths;
260
  // Returns the relevant factor in the beams_ index.
261
19.2M
  static int LengthFromBeamsIndex(int index) {
262
19.2M
    return index % kNumLengths;
263
19.2M
  }
264
104M
  static NodeContinuation ContinuationFromBeamsIndex(int index) {
265
104M
    return static_cast<NodeContinuation>((index / kNumLengths) % NC_COUNT);
266
104M
  }
267
19.2M
  static bool IsDawgFromBeamsIndex(int index) {
268
19.2M
    return index / (kNumLengths * NC_COUNT) > 0;
269
19.2M
  }
270
  // Computes a beams_ index from the given factors.
271
50.4M
  static int BeamIndex(bool is_dawg, NodeContinuation cont, int length) {
272
50.4M
    return (is_dawg * NC_COUNT + cont) * kNumLengths + length;
273
50.4M
  }
274
275
private:
276
  // Struct for the Re-encode beam search. This struct holds the data for
277
  // a single time-step position of the output. Use a vector<RecodeBeam>
278
  // to hold all the timesteps and prevent reallocation of the individual heaps.
279
  struct RecodeBeam {
280
    // Resets to the initial state without deleting all the memory.
281
1.55M
    void Clear() {
282
93.5M
      for (auto &beam : beams_) {
283
93.5M
        beam.clear();
284
93.5M
      }
285
1.55M
      RecodeNode empty;
286
4.67M
      for (auto &best_initial_dawg : best_initial_dawgs_) {
287
4.67M
        best_initial_dawg = empty;
288
4.67M
      }
289
1.55M
    }
290
291
    // A separate beam for each combination of code length,
292
    // NodeContinuation, and dictionary flag. Separating out all these types
293
    // allows the beam to be quite narrow, and yet still have a low chance of
294
    // losing the best path.
295
    // We have to keep all these beams separate, since the highest scoring paths
296
    // come from the paths that are most likely to dead-end at any time, like
297
    // dawg paths, NC_ONLY_DUP etc.
298
    // Each heap is stored with the WORST result at the top, so we can quickly
299
    // get the top-n values.
300
    RecodeHeap beams_[kNumBeams];
301
    // While the language model is only a single word dictionary, we can use
302
    // word starts as a choke point in the beam, and keep only a single dict
303
    // start node at each step (for each NodeContinuation type), so we find the
304
    // best one here and push it on the heap, if it qualifies, after processing
305
    // all of the step.
306
    RecodeNode best_initial_dawgs_[NC_COUNT];
307
  };
308
  using TopPair = KDPairInc<float, int>;
309
310
  // Generates debug output of the content of a single beam position.
311
  void DebugBeamPos(const UNICHARSET &unicharset, const RecodeHeap &heap) const;
312
313
  // Returns the given best_nodes as unichar-ids/certs/ratings/xcoords skipping
314
  // duplicates, nulls and intermediate parts.
315
  static void ExtractPathAsUnicharIds(const std::vector<const RecodeNode *> &best_nodes,
316
                                      std::vector<int> *unichar_ids, std::vector<float> *certs,
317
                                      std::vector<float> *ratings, std::vector<int> *xcoords,
318
                                      std::vector<int> *character_boundaries = nullptr);
319
320
  // Sets up a word with the ratings matrix and fake blobs with boxes in the
321
  // right places.
322
  WERD_RES *InitializeWord(bool leading_space, const TBOX &line_box, int word_start, int word_end,
323
                           float space_certainty, const UNICHARSET *unicharset,
324
                           float scale_factor);
325
326
  // Fills top_n_flags_ with bools that are true iff the corresponding output
327
  // is one of the top_n.
328
  void ComputeTopN(const float *outputs, int num_outputs, int top_n);
329
330
  void ComputeSecTopN(std::unordered_set<int> *exList, const float *outputs, int num_outputs,
331
                      int top_n);
332
333
  // Adds the computation for the current time-step to the beam. Call at each
334
  // time-step in sequence from left to right. outputs is the activation vector
335
  // for the current timestep.
336
  void DecodeStep(const float *outputs, int t, double dict_ratio, double cert_offset,
337
                  double worst_dict_cert, const UNICHARSET *charset, bool debug = false);
338
339
  void DecodeSecondaryStep(const float *outputs, int t, double dict_ratio, double cert_offset,
340
                           double worst_dict_cert, const UNICHARSET *charset, bool debug = false);
341
342
  // Saves the most certain choices for the current time-step.
343
  void SaveMostCertainChoices(const float *outputs, int num_outputs, const UNICHARSET *charset);
344
345
  // Calculates more accurate character boundaries which can be used to
346
  // provide more accurate alternative symbol choices.
347
  static void calculateCharBoundaries(std::vector<int> *starts, std::vector<int> *ends,
348
                                      std::vector<int> *character_boundaries_, int maxWidth);
349
350
  // Adds to the appropriate beams the legal (according to recoder)
351
  // continuations of context prev, which is from the given index to beams_,
352
  // using the given network outputs to provide scores to the choices. Uses only
353
  // those choices for which top_n_flags[code] == top_n_flag.
354
  void ContinueContext(const RecodeNode *prev, int index, const float *outputs,
355
                       TopNState top_n_flag, const UNICHARSET *unicharset, double dict_ratio,
356
                       double cert_offset, double worst_dict_cert, RecodeBeam *step);
357
  // Continues for a new unichar, using dawg or non-dawg as per flag.
358
  void ContinueUnichar(int code, int unichar_id, float cert, float worst_dict_cert,
359
                       float dict_ratio, bool use_dawgs, NodeContinuation cont,
360
                       const RecodeNode *prev, RecodeBeam *step);
361
  // Adds a RecodeNode composed of the args to the correct heap in step if
362
  // unichar_id is a valid dictionary continuation of whatever is in prev.
363
  void ContinueDawg(int code, int unichar_id, float cert, NodeContinuation cont,
364
                    const RecodeNode *prev, RecodeBeam *step);
365
  // Sets the correct best_initial_dawgs_ with a RecodeNode composed of the args
366
  // if better than what is already there.
367
  void PushInitialDawgIfBetter(int code, int unichar_id, PermuterType permuter, bool start,
368
                               bool end, float cert, NodeContinuation cont, const RecodeNode *prev,
369
                               RecodeBeam *step);
370
  // Adds a RecodeNode composed of the args to the correct heap in step for
371
  // partial unichar or duplicate if there is room or if better than the
372
  // current worst element if already full.
373
  void PushDupOrNoDawgIfBetter(int length, bool dup, int code, int unichar_id, float cert,
374
                               float worst_dict_cert, float dict_ratio, bool use_dawgs,
375
                               NodeContinuation cont, const RecodeNode *prev, RecodeBeam *step);
376
  // Adds a RecodeNode composed of the args to the correct heap in step if there
377
  // is room or if better than the current worst element if already full.
378
  void PushHeapIfBetter(int max_size, int code, int unichar_id, PermuterType permuter,
379
                        bool dawg_start, bool word_start, bool end, bool dup, float cert,
380
                        const RecodeNode *prev, DawgPositionVector *d, RecodeHeap *heap);
381
  // Adds a RecodeNode to heap if there is room
382
  // or if better than the current worst element if already full.
383
  void PushHeapIfBetter(int max_size, RecodeNode *node, RecodeHeap *heap);
384
  // Searches the heap for an entry matching new_node, and updates the entry
385
  // with reshuffle if needed. Returns true if there was a match.
386
  bool UpdateHeapIfMatched(RecodeNode *new_node, RecodeHeap *heap);
387
  // Computes and returns the code-hash for the given code and prev.
388
  uint64_t ComputeCodeHash(int code, bool dup, const RecodeNode *prev) const;
389
  // Backtracks to extract the best path through the lattice that was built
390
  // during Decode. On return the best_nodes vector essentially contains the set
391
  // of code, score pairs that make the optimal path with the constraint that
392
  // the recoder can decode the code sequence back to a sequence of unichar-ids.
393
  void ExtractBestPaths(std::vector<const RecodeNode *> *best_nodes,
394
                        std::vector<const RecodeNode *> *second_nodes) const;
395
  // Helper backtracks through the lattice from the given node, storing the
396
  // path and reversing it.
397
  void ExtractPath(const RecodeNode *node, std::vector<const RecodeNode *> *path) const;
398
  void ExtractPath(const RecodeNode *node, std::vector<const RecodeNode *> *path,
399
                   int limiter) const;
400
  // Helper prints debug information on the given lattice path.
401
  void DebugPath(const UNICHARSET *unicharset, const std::vector<const RecodeNode *> &path) const;
402
  // Helper prints debug information on the given unichar path.
403
  void DebugUnicharPath(const UNICHARSET *unicharset, const std::vector<const RecodeNode *> &path,
404
                        const std::vector<int> &unichar_ids, const std::vector<float> &certs,
405
                        const std::vector<float> &ratings, const std::vector<int> &xcoords) const;
406
407
  static const int kBeamWidths[RecodedCharID::kMaxCodeLen + 1];
408
409
  // The encoder/decoder that we will be using.
410
  const UnicharCompress &recoder_;
411
  // The beam for each timestep in the output.
412
  std::vector<RecodeBeam *> beam_;
413
  // Secondary Beam for Results with less Probability
414
  std::vector<RecodeBeam *> secondary_beam_;
415
  // The number of timesteps valid in beam_;
416
  int beam_size_;
417
  // A flag to indicate which outputs are the top-n choices. Current timestep
418
  // only.
419
  std::vector<TopNState> top_n_flags_;
420
  // A record of the highest and second scoring codes.
421
  int top_code_;
422
  int second_code_;
423
  // Heap used to compute the top_n_flags_.
424
  GenericHeap<TopPair> top_heap_;
425
  // Borrowed pointer to the dictionary to use in the search.
426
  Dict *dict_;
427
  // True if the language is space-delimited, which is true for most languages
428
  // except chi*, jpn, tha.
429
  bool space_delimited_;
430
  // True if the input is simple text, ie adjacent equal chars are not to be
431
  // eliminated.
432
  bool is_simple_text_;
433
  // The encoded (class label) of the null/reject character.
434
  int null_char_;
435
};
436
437
} // namespace tesseract.
438
439
#endif // THIRD_PARTY_TESSERACT_LSTM_RECODEBEAM_H_