2007-03-08 04:03:40 +08:00
|
|
|
/* -*-C-*-
|
|
|
|
********************************************************************************
|
|
|
|
*
|
|
|
|
* File: permdawg.c (Formerly permdawg.c)
|
2009-07-11 10:20:33 +08:00
|
|
|
* Description: Scale word choices by a dictionary
|
2007-03-08 04:03:40 +08:00
|
|
|
* Author: Mark Seaman, OCR Technology
|
|
|
|
* Created: Fri Oct 16 14:37:00 1987
|
|
|
|
* Modified: Tue Jul 9 15:43:18 1991 (Mark Seaman) marks@hpgrlt
|
|
|
|
* Language: C
|
|
|
|
* Package: N/A
|
|
|
|
* Status: Reusable Software Component
|
|
|
|
*
|
|
|
|
* (c) Copyright 1987, Hewlett-Packard Company.
|
|
|
|
** Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
** you may not use this file except in compliance with the License.
|
|
|
|
** You may obtain a copy of the License at
|
|
|
|
** http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
** Unless required by applicable law or agreed to in writing, software
|
|
|
|
** distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
** See the License for the specific language governing permissions and
|
|
|
|
** limitations under the License.
|
|
|
|
*
|
|
|
|
*********************************************************************************/
|
|
|
|
/*----------------------------------------------------------------------
|
|
|
|
I n c l u d e s
|
|
|
|
----------------------------------------------------------------------*/
|
2009-07-11 10:20:33 +08:00
|
|
|
|
2007-03-08 04:03:40 +08:00
|
|
|
#include "context.h"
|
2009-07-11 10:20:33 +08:00
|
|
|
#include "conversion.h"
|
|
|
|
#include "cutil.h"
|
|
|
|
#include "dawg.h"
|
2007-03-08 04:03:40 +08:00
|
|
|
#include "freelist.h"
|
|
|
|
#include "globals.h"
|
2009-07-11 10:20:33 +08:00
|
|
|
#include "ndminx.h"
|
|
|
|
#include "permdawg.h"
|
|
|
|
#include "permute.h"
|
|
|
|
#include "stopper.h"
|
|
|
|
#include "tordvars.h"
|
2008-04-22 08:42:51 +08:00
|
|
|
#include "tprintf.h"
|
2009-07-11 10:20:33 +08:00
|
|
|
#include "varable.h"
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
#include <ctype.h>
|
|
|
|
#include "dict.h"
|
|
|
|
#include "image.h"
|
2007-03-08 04:03:40 +08:00
|
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
|
|
V a r i a b l e s
|
|
|
|
----------------------------------------------------------------------*/
|
2009-07-11 10:20:33 +08:00
|
|
|
BOOL_VAR(segment_dawg_debug, 0, "Debug mode for word segmentation");
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
double_VAR(segment_penalty_dict_case_bad, OK_WERD,
|
|
|
|
"Default score multiplier for word matches, which may have "
|
|
|
|
"case issues (lower is better).");
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
double_VAR(segment_penalty_dict_case_ok, GOOD_WERD,
|
|
|
|
"Score multiplier for word matches that have good case "
|
|
|
|
"(lower is better).");
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
double_VAR(segment_penalty_dict_frequent_word, FREQ_WERD,
|
|
|
|
"Score multiplier for word matches which have good case and are "
|
|
|
|
"frequent in the given language (lower is better).");
|
2007-03-08 04:03:40 +08:00
|
|
|
|
|
|
|
/*----------------------------------------------------------------------
|
|
|
|
F u n c t i o n s
|
|
|
|
----------------------------------------------------------------------*/
|
2009-07-11 10:20:33 +08:00
|
|
|
namespace tesseract {
|
|
|
|
|
|
|
|
static const float kPermDawgRatingPad = 5.0;
|
|
|
|
|
2010-08-02 08:05:57 +08:00
|
|
|
/**
|
|
|
|
* @name adjust_word
|
2007-03-08 04:03:40 +08:00
|
|
|
*
|
2009-07-11 10:20:33 +08:00
|
|
|
* Assign an adjusted value to a string that is a word. The value
|
2007-03-08 04:03:40 +08:00
|
|
|
* that this word choice has is based on case and punctuation rules.
|
2010-08-02 08:05:57 +08:00
|
|
|
*/
|
2009-07-11 10:20:33 +08:00
|
|
|
void Dict::adjust_word(WERD_CHOICE *word,
|
|
|
|
float *certainty_array) {
|
2007-03-08 04:03:40 +08:00
|
|
|
float adjust_factor;
|
2009-07-11 10:20:33 +08:00
|
|
|
float new_rating = word->rating();
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("Word: %s %4.2f ",
|
|
|
|
word->debug_string(getUnicharset()).string(), word->rating());
|
|
|
|
}
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
new_rating += RATING_PAD;
|
|
|
|
if (Context::case_ok(*word, getUnicharset())) {
|
|
|
|
if (freq_dawg_ != NULL && freq_dawg_->word_in_dawg(*word)) {
|
|
|
|
word->set_permuter(FREQ_DAWG_PERM);
|
|
|
|
new_rating *= segment_penalty_dict_frequent_word;
|
|
|
|
adjust_factor = segment_penalty_dict_frequent_word;
|
|
|
|
if (segment_dawg_debug)
|
|
|
|
tprintf(", F, %4.2f ", (double)segment_penalty_dict_frequent_word);
|
|
|
|
} else {
|
|
|
|
new_rating *= segment_penalty_dict_case_ok;
|
|
|
|
adjust_factor = segment_penalty_dict_case_ok;
|
|
|
|
if (segment_dawg_debug)
|
|
|
|
tprintf(", %4.2f ", (double)segment_penalty_dict_case_ok);
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
2009-07-11 10:20:33 +08:00
|
|
|
} else {
|
|
|
|
new_rating *= segment_penalty_dict_case_bad;
|
|
|
|
adjust_factor = segment_penalty_dict_case_bad;
|
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf(", C %4.2f ", (double)segment_penalty_dict_case_bad);
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
}
|
2009-07-11 10:20:33 +08:00
|
|
|
new_rating -= RATING_PAD;
|
|
|
|
word->set_rating(new_rating);
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
LogNewChoice(*word, adjust_factor, certainty_array, false);
|
2007-03-08 04:03:40 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
if (segment_dawg_debug)
|
|
|
|
tprintf(" --> %4.2f\n", new_rating);
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
|
2010-08-02 08:05:57 +08:00
|
|
|
/**
|
|
|
|
* @name go_deeper_dawg_fxn
|
2007-03-08 04:03:40 +08:00
|
|
|
*
|
2009-07-11 10:20:33 +08:00
|
|
|
* If the choice being composed so far could be a dictionary word
|
|
|
|
* keep exploring choices.
|
2010-08-02 08:05:57 +08:00
|
|
|
*
|
|
|
|
* There are two modes for deciding whether to go deeper: regular dawg
|
|
|
|
* permuter mode and the special ambigs mode. If *limit is <= 0.0 the
|
|
|
|
* function switches to the ambigs mode (this is the case when
|
|
|
|
* dawg_permute_and_select() function is called from NoDangerousAmbigs()) and
|
|
|
|
* only searches for the first choice that has a rating better than *limit
|
|
|
|
* (in this case ratings are fake, since the real ratings can not be < 0).
|
|
|
|
* Modification of the hyphen state is turned off in the ambigs mode.
|
|
|
|
* When in the regular dawg permuter mode, the function explores all the
|
|
|
|
* possible words and chooses the one with the best rating. The letters with
|
|
|
|
* ratings that are far worse than the ones seen so far are pruned out.
|
|
|
|
*/
|
2009-07-11 10:20:33 +08:00
|
|
|
void Dict::go_deeper_dawg_fxn(
|
|
|
|
const char *debug, const BLOB_CHOICE_LIST_VECTOR &char_choices,
|
|
|
|
int char_choice_index,
|
|
|
|
const CHAR_FRAGMENT_INFO *prev_char_frag_info,
|
|
|
|
bool word_ending, WERD_CHOICE *word, float certainties[],
|
|
|
|
float *limit, WERD_CHOICE *best_choice, void *void_more_args) {
|
|
|
|
DawgArgs *more_args = reinterpret_cast<DawgArgs*>(void_more_args);
|
|
|
|
int word_index = word->length() - 1;
|
|
|
|
|
|
|
|
bool ambigs_mode = (*limit <= 0.0);
|
|
|
|
if (ambigs_mode) {
|
|
|
|
if (best_choice->rating() < *limit) return;
|
|
|
|
} else {
|
|
|
|
// Prune bad subwords
|
|
|
|
if (more_args->rating_array[word_index] == NO_RATING) {
|
|
|
|
more_args->rating_array[word_index] = word->rating();
|
|
|
|
} else {
|
|
|
|
float permdawg_limit = more_args->rating_array[word_index] *
|
|
|
|
more_args->rating_margin + kPermDawgRatingPad;
|
|
|
|
if (permdawg_limit < word->rating()) {
|
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("early pruned word rating=%4.2f,"
|
|
|
|
" permdawg_limit=%4.2f, word=%s\n", word->rating(),
|
|
|
|
permdawg_limit, word->debug_string(getUnicharset()).string());
|
|
|
|
}
|
|
|
|
return;
|
2008-04-22 08:42:51 +08:00
|
|
|
}
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
}
|
2009-07-11 10:20:33 +08:00
|
|
|
// Deal with hyphens
|
|
|
|
if (word_ending && has_hyphen_end(*word) && !ambigs_mode) {
|
|
|
|
if (segment_dawg_debug)
|
|
|
|
tprintf("new hyphen choice = %s\n",
|
|
|
|
word->debug_string(getUnicharset()).string());
|
|
|
|
word->set_permuter(more_args->permuter);
|
|
|
|
adjust_word(word, certainties);
|
|
|
|
set_hyphen_word(*word, *(more_args->active_dawgs),
|
|
|
|
*(more_args->constraints));
|
|
|
|
update_best_choice(*word, best_choice);
|
|
|
|
} else { // Look up char in DAWG
|
|
|
|
// TODO(daria): update the rest of the code that specifies alternative
|
|
|
|
// letter_is_okay_ functions (e.g. TessCharNgram class) to work with
|
|
|
|
// multi-byte unichars and/or unichar ids.
|
|
|
|
|
|
|
|
// If the current unichar is an ngram first try calling
|
|
|
|
// letter_is_okay() for each unigram it contains separately.
|
|
|
|
UNICHAR_ID orig_uch_id = word->unichar_id(word_index);
|
|
|
|
bool checked_unigrams = false;
|
|
|
|
if (getUnicharset().get_isngram(orig_uch_id)) {
|
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("checking unigrams in an ngram %s\n",
|
|
|
|
getUnicharset().debug_str(orig_uch_id).string());
|
|
|
|
}
|
|
|
|
int orig_num_fragments = word->fragment_length(word_index);
|
|
|
|
int num_unigrams = 0;
|
|
|
|
word->remove_last_unichar_id();
|
|
|
|
const char *ngram_str = getUnicharset().id_to_unichar(orig_uch_id);
|
|
|
|
const char *ngram_str_end = ngram_str + strlen(ngram_str);
|
|
|
|
const char *ngram_ptr = ngram_str;
|
|
|
|
bool unigrams_ok = true;
|
|
|
|
// Construct DawgArgs that reflect the current state.
|
|
|
|
DawgInfoVector unigram_active_dawgs = *(more_args->active_dawgs);
|
|
|
|
DawgInfoVector unigram_constraints = *(more_args->constraints);
|
|
|
|
DawgInfoVector unigram_updated_active_dawgs;
|
|
|
|
DawgInfoVector unigram_updated_constraints;
|
|
|
|
DawgArgs unigram_dawg_args(&unigram_active_dawgs, &unigram_constraints,
|
|
|
|
&unigram_updated_active_dawgs,
|
|
|
|
&unigram_updated_constraints, 0.0);
|
|
|
|
unigram_dawg_args.permuter = more_args->permuter;
|
|
|
|
// Check unigrams in the ngram with letter_is_okay().
|
|
|
|
while (unigrams_ok && ngram_ptr < ngram_str_end) {
|
|
|
|
int step = getUnicharset().step(ngram_ptr);
|
|
|
|
UNICHAR_ID uch_id = (step <= 0) ? INVALID_UNICHAR_ID :
|
|
|
|
getUnicharset().unichar_to_id(ngram_ptr, step);
|
|
|
|
ngram_ptr += step;
|
|
|
|
++num_unigrams;
|
|
|
|
word->append_unichar_id(uch_id, 1, 0.0, 0.0);
|
|
|
|
unigrams_ok = unigrams_ok && (this->*letter_is_okay_)(
|
|
|
|
&unigram_dawg_args, word_index+num_unigrams-1, word,
|
|
|
|
word_ending && (ngram_ptr == ngram_str_end));
|
|
|
|
(*unigram_dawg_args.active_dawgs) =
|
|
|
|
*(unigram_dawg_args.updated_active_dawgs);
|
|
|
|
(*unigram_dawg_args.constraints) =
|
|
|
|
*(unigram_dawg_args.updated_constraints);
|
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("unigram %s is %s\n",
|
|
|
|
getUnicharset().debug_str(uch_id).string(),
|
|
|
|
unigrams_ok ? "OK" : "not OK");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// Restore the word and copy the updated dawg state if needed.
|
|
|
|
while (num_unigrams-- > 0) word->remove_last_unichar_id();
|
|
|
|
word->append_unichar_id_space_allocated(
|
|
|
|
orig_uch_id, orig_num_fragments, 0.0, 0.0);
|
|
|
|
if (unigrams_ok) {
|
|
|
|
checked_unigrams = true;
|
|
|
|
more_args->permuter = unigram_dawg_args.permuter;
|
|
|
|
*(more_args->updated_active_dawgs) =
|
|
|
|
*(unigram_dawg_args.updated_active_dawgs);
|
|
|
|
*(more_args->updated_constraints) =
|
|
|
|
*(unigram_dawg_args.updated_constraints);
|
|
|
|
}
|
2008-12-24 09:08:34 +08:00
|
|
|
}
|
2007-07-18 09:15:07 +08:00
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
// Check which dawgs from dawgs_ vector contain the word
|
|
|
|
// up to and including the current unichar.
|
|
|
|
if (checked_unigrams ||
|
|
|
|
(this->*letter_is_okay_)(more_args, word_index, word, word_ending)) {
|
|
|
|
// Add a new word choice
|
|
|
|
if (word_ending) {
|
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("found word = %s\n",
|
|
|
|
word->debug_string(getUnicharset()).string());
|
|
|
|
}
|
|
|
|
WERD_CHOICE *adjusted_word = word;
|
|
|
|
WERD_CHOICE hyphen_tail_word;
|
|
|
|
if (!ambigs_mode && hyphen_base_size() > 0) {
|
|
|
|
hyphen_tail_word = *word;
|
|
|
|
remove_hyphen_head(&hyphen_tail_word);
|
|
|
|
adjusted_word = &hyphen_tail_word;
|
|
|
|
}
|
|
|
|
adjusted_word->set_permuter(more_args->permuter);
|
|
|
|
if (!ambigs_mode) {
|
|
|
|
adjust_word(adjusted_word, &certainties[hyphen_base_size()]);
|
|
|
|
}
|
|
|
|
update_best_choice(*adjusted_word, best_choice);
|
|
|
|
} else { // search the next letter
|
|
|
|
// Make updated_* point to the next entries in the DawgInfoVector
|
|
|
|
// arrays (that were originally created in dawg_permute_and_select)
|
|
|
|
++(more_args->updated_active_dawgs);
|
|
|
|
++(more_args->updated_constraints);
|
|
|
|
// Make active_dawgs and constraints point to the updated ones.
|
|
|
|
++(more_args->active_dawgs);
|
|
|
|
++(more_args->constraints);
|
|
|
|
permute_choices(debug, char_choices, char_choice_index + 1,
|
|
|
|
prev_char_frag_info, word, certainties, limit,
|
|
|
|
best_choice, more_args);
|
|
|
|
// Restore previous state to explore another letter in this position.
|
|
|
|
--(more_args->updated_active_dawgs);
|
|
|
|
--(more_args->updated_constraints);
|
|
|
|
--(more_args->active_dawgs);
|
|
|
|
--(more_args->constraints);
|
2007-07-18 09:15:07 +08:00
|
|
|
}
|
|
|
|
} else {
|
2009-07-11 10:20:33 +08:00
|
|
|
if (segment_dawg_debug) {
|
|
|
|
tprintf("last unichar not OK at index %d in %s\n",
|
|
|
|
word_index, word->debug_string(getUnicharset()).string());
|
2008-04-22 08:42:51 +08:00
|
|
|
}
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-02 08:05:57 +08:00
|
|
|
/**
|
2009-07-11 10:20:33 +08:00
|
|
|
* dawg_permute_and_select
|
2007-03-08 04:03:40 +08:00
|
|
|
*
|
2009-07-11 10:20:33 +08:00
|
|
|
* Recursively explore all the possible character combinations in
|
|
|
|
* the given char_choices. Use go_deeper_dawg_fxn() to search all the
|
|
|
|
* dawgs in the dawgs_ vector in parallel and discard invalid words.
|
|
|
|
*
|
|
|
|
* Allocate and return a WERD_CHOICE with the best valid word found.
|
2010-08-02 08:05:57 +08:00
|
|
|
*/
|
2009-07-11 10:20:33 +08:00
|
|
|
WERD_CHOICE *Dict::dawg_permute_and_select(
|
|
|
|
const BLOB_CHOICE_LIST_VECTOR &char_choices, float rating_limit) {
|
|
|
|
WERD_CHOICE *best_choice = new WERD_CHOICE();
|
|
|
|
best_choice->make_bad();
|
|
|
|
best_choice->set_rating(rating_limit);
|
|
|
|
if (char_choices.length() == 0) return best_choice;
|
|
|
|
DawgInfoVector *active_dawgs = new DawgInfoVector[char_choices.length() + 1];
|
|
|
|
DawgInfoVector *constraints = new DawgInfoVector[char_choices.length() + 1];
|
|
|
|
init_active_dawgs(&(active_dawgs[0]));
|
|
|
|
init_constraints(&(constraints[0]));
|
|
|
|
DawgArgs dawg_args(&(active_dawgs[0]), &(constraints[0]),
|
|
|
|
&(active_dawgs[1]), &(constraints[1]),
|
|
|
|
(segment_penalty_dict_case_bad /
|
|
|
|
segment_penalty_dict_case_ok));
|
|
|
|
WERD_CHOICE word(MAX_WERD_LENGTH);
|
|
|
|
copy_hyphen_info(&word);
|
|
|
|
// Discard rating and certainty of the hyphen base (if any).
|
|
|
|
word.set_rating(0.0);
|
|
|
|
word.set_certainty(0.0);
|
|
|
|
if (word.length() + char_choices.length() > MAX_WERD_LENGTH) {
|
2010-05-20 02:48:59 +08:00
|
|
|
delete[] active_dawgs;
|
|
|
|
delete[] constraints;
|
2009-07-11 10:20:33 +08:00
|
|
|
return best_choice; // the word is too long to permute
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
2009-07-11 10:20:33 +08:00
|
|
|
float certainties[MAX_WERD_LENGTH];
|
|
|
|
this->go_deeper_fxn_ = &tesseract::Dict::go_deeper_dawg_fxn;
|
|
|
|
permute_choices(segment_dawg_debug ? "segment_dawg_debug" : NULL,
|
|
|
|
char_choices, 0, NULL, &word, certainties,
|
|
|
|
&rating_limit, best_choice, &dawg_args);
|
|
|
|
delete[] active_dawgs;
|
|
|
|
delete[] constraints;
|
|
|
|
return best_choice;
|
|
|
|
}
|
2007-07-18 09:15:07 +08:00
|
|
|
|
2010-08-02 08:05:57 +08:00
|
|
|
/**
|
|
|
|
* Fill the given active_dawgs vector with dawgs that could contain the
|
|
|
|
* beginning of the word. If hyphenated() returns true, copy the entries
|
|
|
|
* from hyphen_active_dawgs_ instead.
|
|
|
|
*/
|
2009-07-11 10:20:33 +08:00
|
|
|
void Dict::init_active_dawgs(DawgInfoVector *active_dawgs) {
|
|
|
|
int i;
|
|
|
|
if (hyphenated()) {
|
|
|
|
*active_dawgs = hyphen_active_dawgs_;
|
|
|
|
if (dawg_debug_level >= 3) {
|
|
|
|
for (i = 0; i < hyphen_active_dawgs_.size(); ++i) {
|
|
|
|
tprintf("Adding hyphen beginning dawg [%d, " REFFORMAT "]\n",
|
|
|
|
hyphen_active_dawgs_[i].dawg_index,
|
|
|
|
hyphen_active_dawgs_[i].ref);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
for (i = 0; i < dawgs_.length(); ++i) {
|
|
|
|
if (kBeginningDawgsType[(dawgs_[i])->type()]) {
|
|
|
|
*active_dawgs += DawgInfo(i, NO_EDGE);
|
|
|
|
if (dawg_debug_level >= 3) {
|
|
|
|
tprintf("Adding beginning dawg [%d, " REFFORMAT "]\n", i, NO_EDGE);
|
|
|
|
}
|
|
|
|
}
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2010-08-02 08:05:57 +08:00
|
|
|
/**
|
|
|
|
* If hyphenated() returns true, copy the entries from hyphen_constraints_
|
|
|
|
* into the given constraints vector.
|
|
|
|
*/
|
2009-07-11 10:20:33 +08:00
|
|
|
void Dict::init_constraints(DawgInfoVector *constraints) {
|
|
|
|
if (hyphenated()) {
|
|
|
|
*constraints = hyphen_constraints_;
|
|
|
|
if (dawg_debug_level >= 3) {
|
|
|
|
for (int i = 0; i < hyphen_constraints_.size(); ++i) {
|
|
|
|
tprintf("Adding hyphen constraint [%d, " REFFORMAT "]\n",
|
|
|
|
hyphen_constraints_[i].dawg_index,
|
|
|
|
hyphen_constraints_[i].ref);
|
|
|
|
}
|
2007-03-08 04:03:40 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2009-07-11 10:20:33 +08:00
|
|
|
} // namespace tesseract
|