sclass SimpleRecognizer { bool useCache1 = true, useCache2 = true; S initial = [[ The images "9176714c9a935fc91e14595dbb3adddf 35731666e72dd0c9448f616ff3a7464a da9bc5a24bd503e898f69ad43bb9b92e" are the characters "yet". The images "412f9e0f8e177817a4fa285415c5a13f 386b289407599f31e3a7c57c3adb2587 6cd2a2284a5a6fda3a7cad3e3a048671 b0c47c014d665ec5b658f510c258dc47 b273ff598fe6afa04cfed9f9e8fb4109" are the grouped characters "[Ob]ject". The images "175116b749670b7b65707c10c935b42c ddaf2b9c198818c49628387ecc2910ed 3777597e39eb0de16c022293c8c91cc0 fad86bda3f716ef4e7fa9d199f6c4383 35731666e72dd0c9448f616ff3a7464a 03b96c30adaaed5b60cea4ab9bc37263" are the characters "images". ]]; Lock lock = lock(); class GlyphInfo { S meaning; bool multi; // multiple meanings seen } // key = md5 new Map glyphInfos; // optional for full similarity search - character image to MD5 Map fullSearchMap; S unknownCharacter = diamond(); // "\u2666" - diamond suit symbol; used for unknown characters *() { load(initial); } void load(S info) { lock lock; recognizeGrouped_cache.clear(); new Matches m; for (S s : toLinesFullTrim(info)) { if (find3("the images * are the characters *", s, m)) { L md5s = splitAtSpace($1); L characters = eachCharAsString(dropSpaces($2)); saveMeanings(md5s, characters); } else if (find3("the images * are the grouped characters *", s, m)) { L md5s = splitAtSpace($1); L characters = ocr_parseGlyphs(dropSpaces($2)); saveMeanings(md5s, characters); } else if (nempty(javaTokC(s))) { print("huh? " + s); } } //print("Have " + n(l(glyphInfos), "glyph info")); //psl(glyphInfos); } void saveMeaning(S md5, S meaning) { GlyphInfo info = getGlyphInfo(md5); if (info.multi) ret; if (hasDifferent(info.meaning, meaning)) { //info.meaning = null; info.meaning = meaning; info.multi = true; //print("multi"); } else info.meaning = meaning; } // gets or creates GlyphInfo GlyphInfo getGlyphInfo(S md5) { GlyphInfo info = glyphInfos.get(md5); if (info == null) glyphInfos.put(md5, info = new GlyphInfo); ret info; } void saveMeanings(L md5s, L characters) { if (l(md5s) != l(characters)) { print("huh?"); ret; } for i over md5s: saveMeaning(md5s.get(i), characters.get(i)); } // lookup stored recognition of whole image Scored recognizeCheat(BWImage img) { lock lock; Scored scored = recognizeGlyph(img, false); GlyphInfo info = getVar(scored); if (info != null && info.meaning != null) ret scored(info.meaning, scored); null; } S recognize(BWImage img) { ret ocr_joinGroups(recognizeGrouped(img)); } Scored recognizeScored(BWImage img) { Scored> s = recognizeGrouped(img, null); ret scored(ocr_joinGroups(s!), s); } L recognizeGrouped(BWImage img) { ret getVar(recognizeGrouped(img, null)); } // md5 -> recognition result Map>> recognizeGrouped_cache = synchroMap(); int cantCache, cacheHits, cacheMisses; Scored> recognizeGrouped(BWImage img, L clips_out) { S md5 = null; if (clips_out != null || !useCache1) ++cantCache; else { md5 = md5OfBWImage(img); Scored> result = recognizeGrouped_cache.get(md5); if (result != null) { ++cacheHits; ret result; } else ++cacheMisses; } Scored> result = recognizeGrouped_uncached(img, clips_out); if (md5 != null) recognizeGrouped_cache.put(md5, result); ret result; } Scored> recognizeGrouped_uncached(BWImage img, L clips_out) { lock lock; // TODO: auto-crop here? Scored s = recognizeCheat(img); if (s != null) { if (clips_out != null) clips_out.add(new Rect(0, 0, img.w(), img.h())); ret scored(ll(ocr_escapeMeaning(s!)), s); // hmm... how to group? does it matter? } new L buf; L rects = horizontalAutoSplit2ThenAutoCrop(img); if (empty(rects)) ret scored(ll(unknownCharacter), 0.5); new L scores; iLoop: for (int i = 0; i < l(rects); i++) { Rect r = null; for (int j = i; j < l(rects); j++) { r = rectUnion(r, rects.get(j)); BWImage cImg = img.clip(r); Scored scored = recognizeGlyph(cImg, false); GlyphInfo info = getVar(scored); if (info != null && info.meaning != null) { buf.add(info.meaning); buf.addAll(rep("_", j-i)); if (clips_out != null) clips_out.addAll(rep(r, j-i+1)); scores.add(scored); i = j; continue iLoop; } } r = rects.get(i); Scored scored = recognizeGlyph(img.clip(r), true); GlyphInfo info = getVar(scored); if (info != null && info.meaning != null) buf.add(info.meaning); else buf.add(unknownCharacter); if (clips_out != null) clips_out.add(r); scores.add(scored); } ret scored(buf, averageScore(scores)); } // md5 -> recognition result Map> recognizeGlyph_cache = synchroMap(); static int cacheHits2, cacheMisses2; Scored recognizeGlyph(BWImage img, bool fullSearch) { S md5 = md5OfBWImage(img); GlyphInfo info = glyphInfos.get(md5); if (info != null || !fullSearch || fullSearchMap == null) ret fullScored(info); if (useCache2) { Scored result = recognizeGlyph_cache.get(md5); if (result != null) { ++cacheHits2; ret result; } cacheMisses2++; } new Best best; for (BWImage cImg : keys(fullSearchMap)) { float sim = bwImageSimilarityResized(img, cImg, (float) best.bestScore()); best.put(fullSearchMap.get(cImg), sim); } Scored result = !best.has() ? null : scored(glyphInfos.get(best!), best.score()); if (useCache2) recognizeGlyph_cache.put(md5, result); ret result; } S cacheStats() { //ret "Cache size: " + l(recognizeGrouped_cache) + ", hits: " + cacheHits + ", misses: " + cacheMisses + ", uncachable: " + cantCache; ret "Cache size: " + l(recognizeGlyph_cache) + ", hits: " + cacheHits2 + ", misses: " + cacheMisses2 + ", full search map: " + l(fullSearchMap); } }