//$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ // // Copyright (c) 2001 Microsoft Corporation. All rights reserved. // // Module: // volcano/dll/lattice.c // // Description: // Functions to implement the lattice search for the best // explanation of the input. // // Author: // hrowley // // Modified by: // ahmadab 11/05/01 // //$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$ #include #include #include "volcanop.h" #include "otterp.h" #include "bboxfeat.h" // If defined, then centipede is used #define USE_CENTIPEDE #ifdef USE_CENTIPEDE // If defined, all scoring apart from centipede is disabled, to isolate the // contribution of centipede relative to the old heuristics. //#define TEST_CENTIPEDE #endif //#define DUMP_BBOXES // This define controls whether to merge sequential strokes whose end and start points // are so close together that they were probably caused by a pen skip. This helps with // old data from flaky hardware. Note that this code is size dependent... #define MERGE_STROKES // This defines the maximum distance between the end point of one stroke and the start // of the next one in order for them to be merged. #define MERGE_THRESHOLD 10 // This defines the minimum number of characters which must be available to apply // IFELang3. For boxed mode, this is just a count of the number of distinct boxes // containing ink plus the number of context characters. For free mode, we use the // number of characters on the best path using the internal language model plus the // context. #define MIN_CHARS_FOR_IFELANG3 3 // There is a special case for boxed mode when there is only one character of ink, // in which we require that the pre- and post-context both have at least this many // characters. #define MIN_CONTEXT_FOR_IFELANG3 2 // Uncomment to dump out the lattice //#define DUMP_LATTICE // Name of lattice file #define LATTICE_FILENAME "c:/lattice.txt" // Whether to dump in a format readable by the DOTTY program #define DUMP_LATTICE_TO_DOTTY // Uncomment to dump out the DTW lattice used by SearchForTargetResult() //#define DUMP_DTW #ifdef HWX_TUNE #include #endif // These functions were copied and adapted from pathsrch.c in Tsunami #define TYPE_BASE_MASK (BYTE)0x0f #define TYPE_HEIGHT_MASK (BYTE)0xf0 #define BASE_NORMAL 0x00 // kanji, kana, numbers, etc #define BASE_QUOTE 0x01 // upper punctuation, etc #define BASE_DASH 0x02 // middle punctuation, etc #define BASE_DESCENDER 0x03 // gy, anything that descends. #define BASE_THIRD 0x04 // something that starts a third way up. #define XHEIGHT_HALF 0x00 // lower-case, small kana, etc #define XHEIGHT_FULL 0x10 // upper-case, kana, kanji, numbers, etc #define XHEIGHT_PUNC 0x20 // comma, quote, etc #define XHEIGHT_DASH 0x30 // dash, period, etc #define XHEIGHT_3Q 0x40 #define XHEIGHT_NORMAL 0x00 // lower-case, small kana, etc #define XHEIGHT_KANJI 0x10 // upper-case, kana, kanji, numbers, etc // This will go away when we use Greg's scoring code float FloatClippedLog2(COUNTER num, COUNTER denom) { double ratio, val; ASSERT(num>=0); ASSERT(denom>=0); if (denom==0) return Log2Range; if (num==0) return Log2Range; ratio=(double)num/(double)denom; val=log(ratio)/log(2.0); if (val0) val=0; return (FLOAT)val; } // Given the bounding box of the ink and the dense code of a character, // guess what the writing area was for the character. This is the centipede version. RECT GuessWritingBoxCentipede(RECT bbox, SYM sym) { int stats[INKSTAT_ALL]; RECT result; memset(stats,0,sizeof(int)*INKSTAT_ALL); stats[INKSTAT_W] = bbox.right - bbox.left; stats[INKSTAT_H] = bbox.bottom - bbox.top; // Get the inferred box enclosing the character. ShapeUnigramBaseline(&g_centipedeInfo, sym, stats); result.left = bbox.left - stats[INKSTAT_X]; result.top = bbox.top - stats[INKSTAT_Y]; result.right = result.left + stats[INKSTAT_BOX_W]; result.bottom = result.top + stats[INKSTAT_BOX_H]; if (result.right == result.left) result.right++; if (result.bottom == result.top) result.bottom++; ASSERT(result.bottom > result.top); return result; } // Given the bounding box of the ink and the dense code of a character, // guess what the writing area was for the character. RECT GuessWritingBox(RECT bbox, SYM sym) { return GuessWritingBoxCentipede(bbox,sym); } // Given a guide and a box number, get the drawn box RECT GetGuideDrawnBox(HWXGUIDE *guide, int iBox) { RECT box; box.top = (iBox / guide->cHorzBox) * guide->cyBox + guide->yOrigin + guide->cyOffset; box.bottom = box.top + guide->cyWriting; box.left = (iBox % guide->cHorzBox) * guide->cxBox + guide->xOrigin + guide->cxOffset; box.right = box.left + guide->cxWriting; return box; } // Get information about a given box in the guide, will go away // when the insurance version goes away. BOXINFO GetBoxinfo(HWXGUIDE *guide, int iBox) { RECT rect = GetGuideDrawnBox(guide,iBox); BOXINFO box; box.size = rect.bottom - rect.top; box.baseline = rect.bottom; box.xheight = box.size / 2; box.midline = box.baseline - box.xheight; return box; } // Functions copied from the old lattice search code, used by the insurance version. FLOAT BaselineTransitionCost(SYM symPrev, RECT rPrev, BOXINFO *biPrev, SYM sym, RECT r, BOXINFO *bi) { BYTE type, typePrev; int base; FLOAT cost; // ASSUMPTION: SYM_UNKNOWN should be the only sym if its present. // So there aren't any alternatives that could get a "better" cost // so it probably doesn't really matter what cost we return here if (sym == SYM_UNKNOWN) return (FLOAT) 0.0; type = LocRunDense2BLineHgt(&g_locRunInfo, sym); type = LOCBH_BASE_MASK & type; if (symPrev == SYM_UNKNOWN) symPrev = 0; if (symPrev) { typePrev = LocRunDense2BLineHgt(&g_locRunInfo, symPrev); typePrev = LOCBH_BASE_MASK & typePrev; } // // If the first and second chars are supposed to have the same baseline then // compute a penalty based on the difference in their baseline. // if (symPrev && type == typePrev) { cost = (FLOAT) (100L * abs((r.bottom-bi->baseline) - (rPrev.bottom-biPrev->baseline)) / (bi->size * 2)); // cost = (FLOAT) (100L * abs((r.bottom) - (rPrev.bottom)) / (bi->size * 2)); } else { switch (type) { case BASE_NORMAL: base = bi->baseline; break; case BASE_THIRD: base = bi->baseline - (bi->xheight / 2); break; case BASE_DASH: base = bi->baseline - bi->xheight; break; case BASE_QUOTE: base = bi->baseline - (7 * bi->xheight / 4); break; default: base = bi->baseline; break; } cost = (FLOAT) (100L * abs(r.bottom - base) / (2 * bi->size)); } cost = (-cost) / (FLOAT) 100.0; // ASSERT(cost <= 0.0); return(cost); } #define HEIGHT_DASH 20 FLOAT HeightTransitionCost(SYM symPrev, RECT rPrev, BOXINFO *biPrev, SYM sym, RECT r, BOXINFO *bi) { BYTE type, typePrev; int ht, htPrev; FLOAT cost; // ASSUMPTION: SYM_UNKNOWN should be the only sym if its present. // So there aren't any alternatives that could get a "better" cost // so it probably doesn't really matter what cost we return here if (sym == SYM_UNKNOWN) return (FLOAT) 0.0; if (symPrev == SYM_UNKNOWN) symPrev = 0; cost = (FLOAT) 0.0; type = LocRunDense2BLineHgt(&g_locRunInfo, sym); type = LOCBH_HEIGHT_MASK & type; ht = r.bottom - r.top; // we may want to handle XHEIGHT_PUNC in the same manner that // we do XHEIGHT_DASH, i.e. no relative sizing if (type == XHEIGHT_DASH) { cost = (FLOAT) (100L * abs(ht - HEIGHT_DASH) / (ht + HEIGHT_DASH)); } else { if (symPrev) { typePrev = LocRunDense2BLineHgt(&g_locRunInfo, symPrev); typePrev = LOCBH_HEIGHT_MASK & typePrev; } if (symPrev && typePrev != XHEIGHT_DASH) { // // We scale everything up to be normal (1/2) height. // htPrev = rPrev.bottom - rPrev.top; if (type == XHEIGHT_KANJI) ht = ht / 2; else if (type == XHEIGHT_PUNC) ht = ht * 3; if (typePrev == XHEIGHT_KANJI) htPrev = htPrev / 2; else if (typePrev == XHEIGHT_PUNC) htPrev = htPrev * 3; if ((ht + htPrev) == 0) { cost = (FLOAT) 0.0; } else { cost = (FLOAT) (100L * abs(ht - htPrev) / (ht + htPrev)); } } else { // // We scale everything up to be 1/2 height of box. // if (type == XHEIGHT_KANJI) { // 3/4 of the box ht = (2 * ht) / 3; } else if (type == XHEIGHT_NORMAL) { // 1/3 of the box ht = (3 * ht) / 2; } else if (type == XHEIGHT_PUNC) { // 1/8 of the box ht = 4 * ht; } // // Now we compute a cost based on how far off from what // we computed we should be. // cost = (FLOAT) (100L * abs(ht - bi->xheight) / (2 * bi->xheight)); } } cost = (-cost) / (FLOAT) 100.0; // ASSERT(cost <= 0.0); return(cost); } /******************************Public*Routine******************************\ * HeightBoxCost * * This function computes the likelihood of a character given the height of * the character written and the height of the box the person was supposed * to write in. * * History: * 05-May-1995 -by- Patrick Haluptzok patrickh * Wrote it. \**************************************************************************/ FLOAT HeightBoxCost(SYM sym, RECT r, BOXINFO *bi) { int heightShouldBe; // This is the height it should be given the box // they were told to write in. int heightIs; // This is the height the glyph is. BYTE type; FLOAT cost; heightIs = r.bottom - r.top; // ASSUMPTION: SYM_UNKNOWN should be the only sym if its present. // So there aren't any alternatives that could get a "better" cost // so it probably doesn't really matter what cost we return here if (sym == SYM_UNKNOWN) { return (FLOAT) 0.0; } type = LocRunDense2BLineHgt(&g_locRunInfo, sym); // Could not determine the code if (type == LOC_RUN_NO_BLINEHGT) { // ASSERT(0); return (FLOAT) 0.0; } type = LOCBH_HEIGHT_MASK & type; switch (type) { case XHEIGHT_KANJI: // full height "ABC" etc. heightShouldBe = (bi->size * 3) / 4; if (heightIs >= heightShouldBe) { // No cost, can't be anything bigger. cost = (FLOAT) 0.0; } else { cost = (FLOAT) ( ((float) (heightShouldBe - heightIs)) / (float) (bi->size)); } break; case XHEIGHT_NORMAL: // half height "ace" etc. heightShouldBe = (bi->size * 2) / 5; cost = (FLOAT) ( ((float) abs(heightIs - heightShouldBe)) / (float) (bi->size)); break; case XHEIGHT_PUNC: // small height "maru ," etc. heightShouldBe = bi->size / 6; cost = (FLOAT) (((float) abs(heightIs - heightShouldBe)) / (float) (bi->size)); break; case XHEIGHT_DASH: heightShouldBe = bi->xheight / 8; if (heightIs <= heightShouldBe) { // // It's below the minimum height, way down, so no penalty given. // cost = (FLOAT) 0.0; } else { cost = (FLOAT) (((float) (heightIs - heightShouldBe)) / (float) (bi->size)); } break; default: ASSERT(0); // We should not get here but if we do we have some default // behaviour that should be OK. cost = (FLOAT) 0.0; } cost = cost * cost * -5.0F; // ASSERT(cost <= 0.0); return(cost); } /******************************Public*Routine******************************\ * BaselineBoxCost * * This function computes a penalty given the baseline of the character * and where we thought the characters baseline should be given the box * they were told to write in. * * History: * 04-May-1995 -by- Patrick Haluptzok patrickh * Modify it. \**************************************************************************/ FLOAT BaselineBoxCost(SYM sym, RECT r, BOXINFO *bi) { BYTE type; FLOAT cost; int baselineShouldBe; // This is what the baseline should be for the // char we are proposing. int baselineIs; // This is what the baseline is for the char written. // ASSUMPTION: SYM_UNKNOWN should be the only sym if its present. // So there aren't any alternatives that could get a "better" cost // so it probably doesn't really matter what cost we return here. if (sym == SYM_UNKNOWN) { return (FLOAT) 0.0; } type = LocRunDense2BLineHgt(&g_locRunInfo, sym); // Could not determine the code if (type == LOC_RUN_NO_BLINEHGT) { // ASSERT(0); return (FLOAT) 0.0; } type = LOCBH_BASE_MASK & type; baselineIs = r.bottom; // This is what the baseline is for the glyph. switch (type) { case BASE_NORMAL: baselineShouldBe = bi->baseline - (bi->size / 8); cost = (FLOAT) (100L * abs(baselineIs - baselineShouldBe) / (bi->size)); break; case BASE_THIRD: baselineShouldBe = bi->baseline - (bi->size / 4); cost = (FLOAT) (100L * abs(baselineIs - baselineShouldBe) / (bi->size)); break; case BASE_DASH: baselineShouldBe = bi->midline; cost = (FLOAT) (100L * abs(baselineIs - baselineShouldBe) / (bi->size)); break; case BASE_QUOTE: ASSERT((bi->baseline - bi->midline) > 0); baselineShouldBe = ((bi->baseline - bi->size + bi->midline) / 2); if (baselineIs <= baselineShouldBe) { // // It's above the quote baseline, way up high, so no penalty // for any BASE_QUOTE chars. // cost = (FLOAT) 0.0; } else { cost = (FLOAT) (100L * (baselineIs - baselineShouldBe) / (bi->size)); } break; case BASE_DESCENDER: baselineShouldBe = bi->baseline; if (baselineIs >= baselineShouldBe) { // // It's below the descender baseline, way down, so no penalty // for any BASE_DESCENDER chars. // cost = (FLOAT) 0.0; } else { cost = (FLOAT) (100L * (baselineShouldBe - baselineIs) / (bi->size)); } break; default: ASSERT(0); // We should not get here but if we do we have some default // behaviour that should be OK. cost = (FLOAT) 0.0; break; } cost = (-cost) / (FLOAT) 100.0; // ASSERT(cost <= 0.0); return(cost); } // Given a character (dense code), return the probability of that character occuring in natural text. // If fProbMode is not set, then the value is returned as a score (used by the insurance recognizer). void LanguageModelUnigram(BOOL fProbMode, wchar_t wChar, int nStrokes, VOLCANO_WEIGHTS *pTuneScores) { #ifdef DISABLE_HEURISTICS return; #else pTuneScores->afl[VTUNE_UNIGRAM] += (float) (UnigramCost(&g_unigramInfo, wChar) * 100.0 / 10.0); // float fl = (float) (UnigramCost(&g_unigramInfo, wChar) * 100.0 / 10.0); // fl = log(0.5 * pow(2, fl) + 0.5 / g_locRunInfo.cCodePoints); // pTuneScores->aflWeights[VTUNE_UNIGRAM] = fl; #endif } // Given a pair of characters (dense codes), return the probability of that character sequence occurring // in natural text. // If fProbMode is not set, then the value is returned as a score (used by the insurance recognizer). // The fStringMode flag is used to adjust the tuning parameters for the insurance version. void LanguageModelBigram(BOOL fProbMode, BOOL fStringMode, BOOL fFreeMode, wchar_t wChar, int nStrokes, wchar_t wPrevChar, VOLCANO_WEIGHTS *pTuneScores) { #ifdef DISABLE_HEURISTICS return; #else if (wPrevChar==SYM_UNKNOWN) { LanguageModelUnigram(fProbMode, wChar, nStrokes, pTuneScores); return; } pTuneScores->afl[fFreeMode ? VTUNE_FREE_SMOOTHING_UNIGRAM : VTUNE_STRING_SMOOTHING_UNIGRAM] += (float)(UnigramCost(&g_unigramInfo, wChar) * 100.0 / 10.0); #if !defined(WINCE) && !defined(FAKE_WINCE) pTuneScores->afl[fFreeMode ? VTUNE_FREE_BIGRAM : VTUNE_STRING_BIGRAM] += (float)(BigramTransitionCost(&g_locRunInfo, &g_bigramInfo, wPrevChar, wChar) * 256.0 / 10.0); #endif pTuneScores->afl[fFreeMode ? VTUNE_FREE_CLASS_BIGRAM : VTUNE_STRING_CLASS_BIGRAM] += (float)(ClassBigramTransitionCost(&g_locRunInfo, &g_classBigramInfo, wPrevChar, wChar) * 256.0 / 10.0); #endif } // Allocate memory for a path of nChars characters. nChars can be zero. LATTICE_PATH *AllocatePath(int nChars) { LATTICE_PATH *path=(LATTICE_PATH*)ExternAlloc(sizeof(LATTICE_PATH)); if (path==NULL) return NULL; path->nChars=nChars; if (nChars==0) { path->pElem=NULL; } else { path->pElem=(LATTICE_PATH_ELEMENT*)ExternAlloc(sizeof(LATTICE_PATH_ELEMENT)*nChars); if (path->pElem==NULL) { ExternFree(path); path=NULL; } } return path; } // Create a stroke structure to store in the lattice. Makes a copy of the // array of points that is passed in. BOOL CreateStroke(STROKE *pStroke, int nInk, POINT *pts, long time) { int iInk; ASSERT(nInk>0); ASSERT(pts!=NULL); pStroke->pts=(POINT*)ExternAlloc(sizeof(POINT)*nInk); if (pStroke->pts!=NULL) { pStroke->nInk=nInk; memcpy(pStroke->pts,pts,sizeof(POINT)*nInk); } else { return FALSE; } pStroke->bbox.left=pStroke->bbox.right=pts[0].x; pStroke->bbox.top=pStroke->bbox.bottom=pts[0].y; for (iInk=1; iInkbbox.left=__min(pStroke->bbox.left,pts[iInk].x); pStroke->bbox.right=__max(pStroke->bbox.right,pts[iInk].x); pStroke->bbox.top=__min(pStroke->bbox.top,pts[iInk].y); pStroke->bbox.bottom=__max(pStroke->bbox.bottom,pts[iInk].y); } // Make the bottom and right sides exclusive, to match the old glyph code pStroke->bbox.right++; pStroke->bbox.bottom++; pStroke->timeStart=time; pStroke->timeEnd=time+10*nInk; pStroke->iBox=-1; return TRUE; } #ifdef MERGE_STROKES // Given two strokes, add the ink from the second stroke at the end of the first // stroke. The second stroke is freed. Returns FALSE if for some reason it fails, // in which case the second stroke is not freed. BOOL AddStrokeToStroke(STROKE *pStroke, STROKE *pAddStroke) { POINT *pNewPoints=(POINT*)ExternAlloc(sizeof(POINT)*(pStroke->nInk+pAddStroke->nInk)); if (pNewPoints==NULL) return FALSE; memcpy(pNewPoints,pStroke->pts,sizeof(POINT)*pStroke->nInk); memcpy(pNewPoints+pStroke->nInk,pAddStroke->pts,sizeof(POINT)*pAddStroke->nInk); UnionRect(&pStroke->bbox, &pStroke->bbox, &pAddStroke->bbox); pStroke->timeEnd=pAddStroke->timeEnd; pStroke->iLast = pAddStroke->iLast; pStroke->nInk+=pAddStroke->nInk; ExternFree(pStroke->pts); pStroke->pts=pNewPoints; return TRUE; } #endif // Free the memory for a stroke void FreeStroke(STROKE *stroke) { ASSERT(stroke!=NULL); ASSERT(stroke->pts!=NULL); ExternFree(stroke->pts); } // Delete the specified alternate from the specified stroke. void DeleteElement(LATTICE *lat, int iStroke, int iAlt) { // If there are any alternates after the specified one, move them up. if (lat->pAltList[iStroke].nUsed-iAlt-1>0) { memmove(&lat->pAltList[iStroke].alts[iAlt], &lat->pAltList[iStroke].alts[iAlt+1], sizeof(LATTICE_ELEMENT)*(lat->pAltList[iStroke].nUsed-iAlt-1)); } // Reduce the number of elements in the column. lat->pAltList[iStroke].nUsed--; } // Insert a new alternate into the specified lattice column. If the // column already contains a "matching" element (having the same character // label or number of strokes), then that element will get replaced if // the given one has a higher log prob path score. void InsertElement(LATTICE *lat, int iStroke, LATTICE_ELEMENT *elem) { int newLength; int iAlt; // make sure that by default the char detector score is set -1 for any new element elem->iCharDetectorScore = -1; // Log prob is too low (a case we haven't seen in training), so ignore this path // if (elem->logProb<=Log2Range) return; // Make sure that the element being inserted is connected to a path // (unless it is connecting to the start of the lattice). ASSERT(iStroke-elem->nStrokes+1==0 || elem->iPrevAlt!=-1); // For each alternate already present, check for a match with the given one for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { BOOL fCharMatch=elem->wChar==lat->pAltList[iStroke].alts[iAlt].wChar; // BOOL fPrevCharMatch=elem->wPrevChar==lat->pAltList[iStroke].alts[iAlt].wPrevChar; BOOL fnStrokesMatch=elem->nStrokes==lat->pAltList[iStroke].alts[iAlt].nStrokes; // BOOL fnPrevStrokesMatch=elem->nPrevStrokes==lat->pAltList[iStroke].alts[iAlt].nPrevStrokes; // BOOL fMatch=fCharMatch && fPrevCharMatch && fnStrokesMatch && fnPrevStrokesMatch; BOOL fMatch=fCharMatch && fnStrokesMatch; if (fMatch) { // If it matches, check the log probs if (elem->logProbPath > lat->pAltList[iStroke].alts[iAlt].logProbPath) { // Matched a previous entry, but with a higher log prob, so replace it. DeleteElement(lat,iStroke,iAlt); // Insert later as normal break; } else { // Matched a previous entry, but with a lower score, so drop the given element. return; } } } // Compute the new length of the alt list after the insertion. newLength=lat->pAltList[iStroke].nUsed; if (lat->pAltList[iStroke].nUsed lat->pAltList[iStroke].nUsed-1 || elem->logProbPath > lat->pAltList[iStroke].alts[iAlt].logProbPath) { // Shift all remaining elements (if any) down. if (newLength-iAlt-1>0) { memmove(&lat->pAltList[iStroke].alts[iAlt+1], &lat->pAltList[iStroke].alts[iAlt], sizeof(LATTICE_ELEMENT)*(newLength-iAlt-1)); } // Put in the new element lat->pAltList[iStroke].alts[iAlt] = *elem; break; } // Store thew new alt list length lat->pAltList[iStroke].nUsed=newLength; } // Clear out an alt list void ClearAltList(LATTICE_ALT_LIST *list) { int i; ASSERT(list!=NULL); list->nUsed=0; for (i=0; ialts[i].fUsed=FALSE; } // Create an empty lattice data structure. LATTICE *AllocateLattice() { LATTICE *lat=(LATTICE*)ExternAlloc(sizeof(LATTICE)); if (lat!=NULL) { lat->nStrokes=0; lat->nRealStrokes = 0; lat->nStrokesAllocated=0; lat->pStroke=NULL; lat->pAltList=NULL; lat->fUseFactoid = FALSE; lat->alcFactoid = 0; lat->pbFactoidChars = NULL; lat->recogSettings.alcValid=0xFFFFFFFF; lat->recogSettings.alcPriority=0; lat->recogSettings.pbAllowedChars = NULL; lat->recogSettings.pbPriorityChars = NULL; lat->recogSettings.partialMode=HWX_PARTIAL_ALL; lat->recogSettings.pAbort=(UINT *)0; lat->recogSettings.pbAllowedChars = NULL; lat->fUseGuide=FALSE; lat->wszBefore = NULL; lat->wszAfter = NULL; lat->nProcessed = 0; lat->fEndInput = FALSE; lat->fIncremental = FALSE; lat->nFixedResult = 0; lat->wszAnswer = NULL; lat->fWordMode = FALSE; lat->fSingleSeg = FALSE; lat->fCoerceMode = FALSE; lat->fSepMode = 0; lat->pvCache = NULL; lat->fUseLM = TRUE; } return lat; } // Creates a new lattice structure with the same settings as the old one. LATTICE *CreateCompatibleLattice(LATTICE *lat) { LATTICE *newlat=AllocateLattice(); if (newlat==NULL || lat==NULL) return newlat; newlat->recogSettings=lat->recogSettings; newlat->recogSettings.pbAllowedChars = CopyAllowedChars(&g_locRunInfo, lat->recogSettings.pbAllowedChars); newlat->recogSettings.pbPriorityChars = CopyAllowedChars(&g_locRunInfo, lat->recogSettings.pbPriorityChars); newlat->pbFactoidChars = CopyAllowedChars(&g_locRunInfo, lat->pbFactoidChars); newlat->alcFactoid = lat->alcFactoid; newlat->fUseFactoid = lat->fUseFactoid; newlat->fWordMode = lat->fWordMode; newlat->fSingleSeg = lat->fSingleSeg; newlat->fCoerceMode = lat->fCoerceMode; newlat->fUseGuide=lat->fUseGuide; newlat->guide=lat->guide; newlat->fUseLM = lat->fUseLM; if (lat->wszBefore != NULL) { newlat->wszBefore = Externwcsdup(lat->wszBefore); if (newlat->wszBefore == NULL) { ExternFree(newlat); return NULL; } } if (lat->wszAfter != NULL) { newlat->wszAfter = Externwcsdup(lat->wszAfter); if (newlat->wszAfter == NULL) { ExternFree(newlat->wszBefore); ExternFree(newlat); return NULL; } } return newlat; } // Sets the guide structure in the lattice void SetLatticeGuide(LATTICE *lat, HWXGUIDE *pGuide) { ASSERT(lat!=NULL); ASSERT(pGuide!=NULL); lat->guide=*pGuide; lat->fUseGuide=TRUE; } // Sets the ALC settings for the lattice (only used in the lower level of the character recognizer, // if at all) void SetLatticeALCValid(LATTICE *lat, ALC alcValid) { ASSERT(lat!=NULL); lat->recogSettings.alcValid=alcValid; } // Sets the ALC settings for the lattice (only used in the lower level of the character recognizer, // if at all) void SetLatticeALCPriority(LATTICE *lat, ALC alcPriority) { ASSERT(lat!=NULL); lat->recogSettings.alcPriority=alcPriority; } // Get one character of the pre-context wchar_t GetPreContextChar(LATTICE *lat) { if (lat->wszBefore != NULL) { return lat->wszBefore[0]; } return SYM_UNKNOWN; } // Get one character of the pre-context wchar_t GetPostContextChar(LATTICE *lat) { if (lat->wszAfter != NULL) { return lat->wszAfter[0]; } return SYM_UNKNOWN; } // Destroy lattice data structure. void FreeLattice(LATTICE *lat) { ASSERT(lat!=NULL); if (lat->pStroke!=NULL) { int i; for (i=0; inStrokes; i++) { FreeStroke(lat->pStroke+i); } ExternFree(lat->pStroke); } if (lat->pAltList!=NULL) ExternFree(lat->pAltList); ExternFree(lat->wszBefore); ExternFree(lat->wszAfter); if (lat->wszAnswer != NULL) { ExternFree(lat->wszAnswer); } ExternFree(lat->recogSettings.pbAllowedChars); ExternFree(lat->recogSettings.pbPriorityChars); ExternFree(lat->pbFactoidChars); FreeRecognizerCache(lat->pvCache); ExternFree(lat); } // Add a stroke to the lattice, returns TRUE if it succeeds. BOOL AddStrokeToLattice(LATTICE *lat, int nInk, POINT *pts, DWORD time) { int iStroke; ASSERT(nInk>0); ASSERT(lat!=NULL); if (lat->nStrokes+1>lat->nStrokesAllocated) { STROKE *pNewStroke; LATTICE_ALT_LIST *ppNewElem; int nNewAlloc=lat->nStrokesAllocated*2; if (nNewAlloc==0) nNewAlloc=32; pNewStroke=(STROKE*)ExternAlloc(sizeof(STROKE)*nNewAlloc); ppNewElem=(LATTICE_ALT_LIST*)ExternAlloc(sizeof(LATTICE_ALT_LIST)*nNewAlloc); if (pNewStroke==NULL || ppNewElem==NULL) { if (pNewStroke!=NULL) ExternFree(pNewStroke); if (ppNewElem!=NULL) ExternFree(ppNewElem); return FALSE; } for (iStroke=0; iStrokenStrokesAllocated>0) { memcpy(pNewStroke,lat->pStroke,sizeof(STROKE)*lat->nStrokesAllocated); memcpy(ppNewElem,lat->pAltList,sizeof(LATTICE_ALT_LIST)*lat->nStrokesAllocated); ExternFree(lat->pStroke); ExternFree(lat->pAltList); } lat->nStrokesAllocated=nNewAlloc; lat->pStroke=pNewStroke; lat->pAltList=ppNewElem; } iStroke=lat->nStrokes; ClearAltList(lat->pAltList+iStroke); if (CreateStroke(lat->pStroke+iStroke,nInk,pts,time)) { lat->pStroke[iStroke].iOrder=lat->nRealStrokes; lat->pStroke[iStroke].iLast=lat->nRealStrokes; lat->nStrokes++; lat->nRealStrokes++; return TRUE; } else return FALSE; } // Pre-segmentation probability for single characters. BOOL CheapUnaryProb(int nStrokes, RECT bbox, int cSpace, int cArea, VOLCANO_WEIGHTS *pTuneScores) { float logProb; int iUnary; STROKE_SET_STATS stats; stats.rect=bbox; stats.space=cSpace; stats.area=cArea; iUnary=ComputeUnaryFeatures(&stats,nStrokes); logProb = (float)g_pProbTable->unarySamples[iUnary]; pTuneScores->afl[VTUNE_FREE_SEG_UNIGRAM] = logProb; return (logProb > Log2Range); } // Pre-segmentation probability for character pairs BOOL CheapBinaryProb(int nStrokes, RECT bbox, int cSpace, int cArea, int nPrevStrokes, RECT prevBbox, int cPrevSpace, int cPrevArea, VOLCANO_WEIGHTS *pTuneScores) { float logProb; int iBinary; STROKE_SET_STATS stats, prevStats; stats.rect=bbox; stats.space=cSpace; stats.area=cArea; prevStats.rect=prevBbox; prevStats.space=cPrevSpace; prevStats.area=cPrevArea; iBinary=ComputeBinaryFeatures(&prevStats,&stats,nPrevStrokes,nStrokes); logProb=(float)g_pProbTable->binarySamples[iBinary]; // Probability of 7/8 of moving to the right, 1/8 for other cases. if (prevBbox.right < bbox.right) logProb += FloatClippedLog2(7,8); else logProb += FloatClippedLog2(1,8); pTuneScores->afl[VTUNE_FREE_SEG_BIGRAM] = logProb; return (logProb > Log2Range); } // Post-segmentation probability for single characters void RecogUnaryProb(LATTICE *lat, int iStroke, int nStrokes, RECT bbox, wchar_t wChar, VOLCANO_WEIGHTS *pTuneScores, BOOL fStringMode) { #ifdef DISABLE_HEURISTICS return; #else float sizePenalty=0; wchar_t unicode=LocRunDense2Unicode(&g_locRunInfo,wChar); // If there's a guide, compute the probability of this character appearing at // a particular location in the box. if (lat->fUseGuide) { SCORE score; int stats[INKSTAT_ALL]; float logprob; RECT guide=GetGuideDrawnBox(&lat->guide,lat->pStroke[iStroke].iBox); stats[INKSTAT_X] = bbox.left - guide.left; stats[INKSTAT_Y] = bbox.top - guide.top; stats[INKSTAT_CX] = (bbox.left + bbox.right + 1) / 2 - (guide.left + guide.right + 1) / 2; stats[INKSTAT_CY] = (bbox.top + bbox.bottom + 1) / 2 - (guide.top + guide.bottom + 1) / 2; stats[INKSTAT_W] = bbox.right - bbox.left; stats[INKSTAT_H] = bbox.bottom - bbox.top; stats[INKSTAT_BOX_W] = guide.right - guide.left; CentipedeNormalizeUnigram(stats); score=ShapeUnigramBoxCost(&g_centipedeInfo,wChar,stats); logprob= -(float)score / (float)SCORE_SCALE; pTuneScores->afl[fStringMode ? VTUNE_STRING_SHAPE_BOX_UNIGRAM : VTUNE_CHAR_SHAPE_BOX_UNIGRAM] = logprob; } else { // Otherwise compute the probability of this character having a particular // aspect ratio. SCORE score; int stats[INKSTAT_ALL]; float logprob; stats[INKSTAT_W] = bbox.right-bbox.left; stats[INKSTAT_H] = bbox.bottom-bbox.top; score=ShapeUnigramFreeCost(&g_centipedeInfo,wChar,stats); logprob= -(float)score / (float)SCORE_SCALE; // We'd also like to include a bias so that small ink gives punctuation. pTuneScores->afl[VTUNE_FREE_SHAPE_UNIGRAM] = logprob; } #endif } // Post-segmentation for character pairs void RecogBinaryProb(LATTICE *lat, int iStroke, int nStrokes, RECT bbox, wchar_t wChar, int iPrevStroke, int nPrevStrokes, RECT prevBbox, wchar_t wPrevChar, VOLCANO_WEIGHTS *pTuneScores) { #ifdef DISABLE_HEURISTICS return; #else float sizePenalty=0; wchar_t unicode = LocRunDense2Unicode(&g_locRunInfo,wChar); wchar_t prevUnicode; float logprob=0; // If we had to generate a fake alternate because there are no viable alternates, then the // previous character might be one of these fakes. If we pass this fake through to Centipede, // there will be problems. So let's just use the unary shape scores instead. if (wPrevChar == SYM_UNKNOWN) { RecogUnaryProb(lat, iStroke, nStrokes, bbox,wChar, pTuneScores, TRUE); return; } prevUnicode = LocRunDense2Unicode(&g_locRunInfo,wPrevChar); if (lat->fUseGuide) { SCORE score; int prevStats[INKSTAT_ALL], stats[INKSTAT_ALL]; RECT prevGuide=GetGuideDrawnBox(&lat->guide,lat->pStroke[iStroke-nStrokes].iBox); RECT guide=GetGuideDrawnBox(&lat->guide,lat->pStroke[iStroke].iBox); memset(prevStats,0,sizeof(int)*INKSTAT_ALL); prevStats[INKSTAT_X] = prevBbox.left - prevGuide.left; prevStats[INKSTAT_Y] = prevBbox.top - prevGuide.top; prevStats[INKSTAT_CX] = (prevBbox.left + prevBbox.right + 1) / 2 - (prevGuide.left + prevGuide.right + 1) / 2; prevStats[INKSTAT_CY] = (prevBbox.top + prevBbox.bottom + 1) / 2 - (prevGuide.top + prevGuide.bottom + 1) / 2; prevStats[INKSTAT_W] = prevBbox.right - prevBbox.left; prevStats[INKSTAT_H] = prevBbox.bottom - prevBbox.top; prevStats[INKSTAT_BOX_W] = prevGuide.right - prevGuide.left; prevStats[INKSTAT_MARGIN_W] = lat->guide.cxOffset; CentipedeNormalizeUnigram(prevStats); memset(stats,0,sizeof(int)*INKSTAT_ALL); stats[INKSTAT_X] = bbox.left - guide.left; stats[INKSTAT_Y] = bbox.top - guide.top; stats[INKSTAT_CX] = (bbox.left + bbox.right + 1) / 2 - (guide.left + guide.right + 1) / 2; stats[INKSTAT_CY] = (bbox.top + bbox.bottom + 1) / 2 - (guide.top + guide.bottom + 1) / 2; stats[INKSTAT_W] = bbox.right - bbox.left; stats[INKSTAT_H] = bbox.bottom - bbox.top; stats[INKSTAT_BOX_W] = guide.right - guide.left; stats[INKSTAT_MARGIN_W] = lat->guide.cxOffset; CentipedeNormalizeUnigram(stats); score = ShapeBigramBoxCost(&g_centipedeInfo,wPrevChar,wChar,prevStats,stats); logprob= -(float)score / (float)SCORE_SCALE; pTuneScores->afl[VTUNE_STRING_SHAPE_BOX_BIGRAM] = logprob; } else { int stats[INKBIN_ALL]; SCORE score; stats[INKBIN_W_LEFT] = prevBbox.right - prevBbox.left; stats[INKBIN_W_GAP] = bbox.left - prevBbox.right; stats[INKBIN_W_RIGHT] = bbox.right - bbox.left; stats[INKBIN_H_LEFT] = prevBbox.bottom - prevBbox.top; stats[INKBIN_H_GAP] = bbox.top - prevBbox.bottom; stats[INKBIN_H_RIGHT] = bbox.bottom - bbox.top; CentipedeNormalizeBigram(stats); score = ShapeBigramFreeCost(&g_centipedeInfo, wPrevChar, wChar, stats); logprob = -(float)score / (float)SCORE_SCALE; pTuneScores->afl[VTUNE_FREE_SHAPE_BIGRAM] = logprob; } #endif } #ifdef TRAIN_ZILLA_HOUND_COMBINER // Pass on label when training combiner. wchar_t g_CurCharAnswer; #endif // Call the underlying recognizer, and get a list of alternates with // associated probability (research version or insurance version in free mode), or // scores (for the insurance version in boxed mode). int RecognizerProbs(LATTICE *lat, int iStroke, int nStrokes, RECT bbox, wchar_t wAlts[MaxAltsPerStroke], FLOAT valueAlts[MaxAltsPerStroke], FLOAT *pScore, int *pnZillaStrokes, VOLCANO_WEIGHTS *pTuneScores, BOOL fStringMode) { int i; int space=0; RECOG_ALT scoreAlts[MaxAltsPerStroke], probAlts[MaxAltsPerStroke]; int nScoreAlts=0, nProbAlts=0; FLOAT probScore, score=0; RECT rGuide; SYM context; // If we have a guide, convert it into a RECT for the current box if (lat->fUseGuide) { rGuide = GetGuideDrawnBox(&lat->guide, lat->pStroke[iStroke].iBox); } else { int size; // If we don't have a guide, too bad, because we need one anyway. // We'll walk back through the lattice, averaging the box sizes along // the best path from the current node. int totalWidth = 0, totalHeight = 0, totalBoxes = 0; int currStroke = iStroke - nStrokes, iAlt, bestAlt = 0; BOOL fHavePrevBox = FALSE; RECT rPrevBox; // If there are more characters if (currStroke >= 0) { // Find the best alternate before the current character float bestProb = lat->pAltList[currStroke].alts[0].logProbPath; for (iAlt=1; iAltpAltList[currStroke].nUsed; iAlt++) { if (lat->pAltList[currStroke].alts[iAlt].logProbPath > bestProb) { bestAlt = iAlt; bestProb = lat->pAltList[currStroke].alts[iAlt].logProbPath; } } // If the previous character is on the same line, then record if (bbox.left > lat->pAltList[currStroke].alts[bestAlt].writingBox.left) { rPrevBox = lat->pAltList[currStroke].alts[bestAlt].writingBox; fHavePrevBox = TRUE; } // Follow the best path back through that alternate to the // start of the lattice adding in the box sizes as we go. while (currStroke >= 0) { int newStroke, newAlt; RECT box = lat->pAltList[currStroke].alts[bestAlt].writingBox; totalWidth += box.right-box.left; totalHeight += box.bottom-box.top; totalBoxes++; newStroke = currStroke - lat->pAltList[currStroke].alts[bestAlt].nStrokes; newAlt = lat->pAltList[currStroke].alts[bestAlt].iPrevAlt; currStroke=newStroke; bestAlt=newAlt; } } // Get the maximum dimension if (totalBoxes == 0) { size = max(bbox.right - bbox.left, bbox.bottom - bbox.top); } else { size = __max( totalWidth, totalHeight ) / totalBoxes; } // Set an arbitrary minimum on the box size to prevent divide by zeros size = __max( size, 10 ); // If there was no previous box on the line if (!fHavePrevBox) { // Then center the square box on top of the ink rGuide.top = ( bbox.top + bbox.bottom - size ) / 2; rGuide.bottom = ( bbox.top + bbox.bottom + size ) / 2; rGuide.left = ( bbox.left + bbox.right - size ) / 2; rGuide.right = ( bbox.left + bbox.right + size ) / 2; } else { // If there was a previous character on the same line, then try to // keep it lined up. // Center the box vertically with the previous character. rGuide.top = (rPrevBox.top + rPrevBox.bottom - size) / 2; rGuide.bottom = (rPrevBox.top + rPrevBox.bottom + size) / 2; // Center the box horizontally rGuide.left = ( bbox.left + bbox.right - size ) / 2; rGuide.right = ( bbox.left + bbox.right + size ) / 2; // If it overlaps with the previous box, then shift over to the right if (rGuide.left - rPrevBox.right < 0) { rGuide.right += (rPrevBox.right - rGuide.left); rGuide.left = rPrevBox.right; } // Expand to include the ink rGuide.top = min(rGuide.top, bbox.top); rGuide.bottom = max(rGuide.bottom, bbox.bottom); rGuide.left = min(rGuide.left, bbox.left); rGuide.right = max(rGuide.right, bbox.right); } } // If this is the first character in the lattice, then pass the context through. // Otherwise the context is unknown (at least at the single-character level // the recognizer is working at). if (iStroke-nStrokes+1==0) context = GetPreContextChar(lat); else context=SYM_UNKNOWN; #ifdef USE_OLD_DATABASES // Zero out the scores for (i = 0; i < MaxAltsPerStroke; i++) { VTuneZeroWeights(pTuneScores + i); } #ifdef TRAIN_ZILLA_HOUND_COMBINER // Pass on label when training combiner. g_CurCharAnswer = lat->wszAnswer[0]; #endif RecognizeCharInsurance(&lat->recogSettings, nStrokes, lat->nRealStrokes, lat->pStroke + iStroke - nStrokes + 1, &score, MaxAltsPerStroke, probAlts, &nProbAlts, scoreAlts, &nScoreAlts, &rGuide, context, &space, pTuneScores, fStringMode, lat->fProbMode, lat->pvCache, iStroke); #else nProbAlts=RecognizeChar(&lat->recogSettings, nStrokes, lat->nRealStrokes, lat->pStroke + iStroke - nStrokes + 1, &score,MaxAltsPerStroke,probAlts,&rGuide,&space); #endif if (pScore!=NULL) *pScore=score; if (pnZillaStrokes!=NULL) *pnZillaStrokes=space; // If we're in probability mode, return the probabilities if (lat->fProbMode) { if (nProbAlts<=0) return 0; // Zero out the scores for (i = 0; i < MaxAltsPerStroke; i++) { VTuneZeroWeights(pTuneScores + i); } // The probability that this is a character given the matcher score. // This code is disabled for the old databases, because the otter space // is not returned. For the new databases, we get space numbers for both // otter and zilla, so the match score can be interpreted meaningfully. #ifdef USE_OLD_DATABASES probScore=0; #else probScore=0; // probScore=g_pProbTable->scoreSamples[MatchSpaceScoreToFeature(nStrokes,score,space)]; #endif for (i=0; ifUseGuide) { pTuneScores[i].afl[VTUNE_FREE_PROB] = valueAlts[i]; } else if (fStringMode) { pTuneScores[i].afl[VTUNE_STRING_CORE] = valueAlts[i]; } else { pTuneScores[i].afl[VTUNE_CHAR_CORE] = valueAlts[i]; } } return nProbAlts; } else { // Otherwise return the matcher scores to emulate the legacy recognizer #ifndef USE_OLD_DATABASES ASSERT(("Trying to use matcher scores with the new recognizer",0)); #endif if (nScoreAlts<=0) return 0; for (i=0; irecogSettings.alcValid; charSet.recmaskPriority = lat->recogSettings.alcPriority; charSet.pbAllowedChars = lat->recogSettings.pbAllowedChars; charSet.pbPriorityChars = lat->recogSettings.pbPriorityChars; // If there are no priorities, don't change anything. if (charSet.recmaskPriority == 0 && charSet.pbPriorityChars == NULL) return; // If there are no alternates, don't do anything. if (nAlts == 0) return; // Scan through the alt list and copy all the priority characters into the // temporary list. for (i = 0; i < nAlts; i ++) { if (IsPriorityChar(&g_locRunInfo, &charSet, wAlts[i])) { wTmp[iUsed] = wAlts[i]; aTuneScoresTmp[iUsed] = pTuneScores[i]; iUsed ++; if (fNonPriorityFound) { fPriorityBelowNonPriority = TRUE; } } else { fNonPriorityFound = TRUE; } } // If there weren't any priority characters, then return if (iUsed == 0) return; // If there aren't any priority characters listed below non-priority ones if (!fPriorityBelowNonPriority) return; // Scan through the alt list and copy all the non-priority characters into the // temporary list. for (i = 0; i < nAlts; i ++) { if (!IsPriorityChar(&g_locRunInfo, &charSet, wAlts[i])) { // If the penalty has not been set yet. if (!fPenaltySet) { penalty = (VTuneComputeScore(&g_vtuneInfo.pTune->weights, &(aTuneScoresTmp[iUsed - 1])) - VTuneComputeScore(&g_vtuneInfo.pTune->weights, &(pTuneScores[i])) - PENALTY_ALC_PRIORITY); if (g_vtuneInfo.pTune->weights.afl[VTUNE_UNIGRAM] > 0) { penalty /= g_vtuneInfo.pTune->weights.afl[VTUNE_UNIGRAM]; } fPenaltySet = TRUE; } wTmp[iUsed] = wAlts[i]; aTuneScoresTmp[iUsed] = pTuneScores[i]; aTuneScoresTmp[iUsed].afl[VTUNE_UNIGRAM] += penalty; iUsed ++; } } ASSERT(iUsed == nAlts); // Then copy the temp lists back to the output for (i = 0; i < nAlts; i ++) { wAlts[i] = wTmp[i]; pTuneScores[i] = aTuneScoresTmp[i]; } } // See if two rectangles overlap one another BOOL IsOverlapped(RECT r1, RECT r2) { if (r1.left>r2.right || r1.rightr2.bottom || r1.bottompAltList[iPrevStroke].alts+iPrevAlt; if (IsOverlapped(elem->bbox,bbox)) return TRUE; iPrevStroke -= elem->nStrokes; iPrevAlt = elem->iPrevAlt; } return FALSE; } // Penalties used in separator mode #define PENALTY_SKIP_INK 200 #define PENALTY_SKIP_PROMPT 200 #define PENALTY_OVERLAP_CHAR 300 #define PENALTY_OVERLAP 100 // Given the proposed next character to be added to the lattice, and the next available prompt character, // is the proposed character allowed? The character is allowed if is either the "SYM_UNKNOWN" // character or a character which appears later in the string. If this required skipping prompt // characters, or the character is SYM_UNKNOWN, then a penalty is also returned. If fEndInput is set // then all characters remaining in the prompt are consumed (skipped) with a cost. If the proposed // character is not allowed, -1 is returned. Otherwise, we return the next available prompt character. int IsAllowedNextChar(LATTICE *lat, wchar_t wchNext, int iAnswerPtr, BOOL fEndInput, float *pflCost) { BOOL fMatched = FALSE; *pflCost = 0; // If we're not in separator mode, don't do anything. if (!lat->fSepMode) { return 0; } // If this character is unknown if (wchNext == SYM_UNKNOWN) { // At end of input, skip all remaining prompt characters if (fEndInput) { while (lat->wszAnswer[iAnswerPtr] != 0) { *pflCost += -PENALTY_SKIP_PROMPT; iAnswerPtr++; } } // Return a pointer to the end of the prompt return iAnswerPtr; } // Convert the dense code to unicode for comparison with the prompt wchNext = LocRunDense2Unicode(&g_locRunInfo, wchNext); // While we haven't reached the end of the prompt and haven't found a match, // skip a prompt character and add a penalty while (lat->wszAnswer[iAnswerPtr] != 0 && lat->wszAnswer[iAnswerPtr] != wchNext) { iAnswerPtr++; *pflCost += -PENALTY_SKIP_PROMPT; } // If we didn't find a match, return failure. if (lat->wszAnswer[iAnswerPtr] != wchNext) { return -1; } // Found a match iAnswerPtr++; // Now add a penalty for any unmatched characters at the end of the input. if (fEndInput) { while (lat->wszAnswer[iAnswerPtr] != 0) { *pflCost += -PENALTY_SKIP_PROMPT; iAnswerPtr++; } } // Return the prompt pointer. return iAnswerPtr; } // Sometimes the algorithm will not produce any alternates for a given stroke. This is bad, because it could // result in a disconnected lattice. The solution is to produce an alternate with the lowest possible // probability and a character code of SYM_UNKNOWN. This will never get used if there is another choice, // but if there is no other choice, the user might see it. It will get translated to a SYM_UNKNOWN when returned // to the user. void FakeRecogResult(LATTICE *lat, int iStroke, int nStrokes, float logProb) { int i, width, height; // Create the fake alternate, which is always alternate zero. LATTICE_ELEMENT elem; elem.iPathLength = 1; elem.fCurrentPath=FALSE; elem.fUsed=TRUE; elem.maxDist=0; elem.nPrevStrokes=0; elem.nStrokes=nStrokes; elem.logProb=logProb; elem.logProbPath=logProb; elem.iPrevAlt=-1; elem.score=0; elem.wChar=SYM_UNKNOWN; elem.wPrevChar=0; elem.iPromptPtr = 0; #ifdef HWX_TUNE VTuneZeroWeights(&elem.tuneScores); elem.tuneScores.afl[VTUNE_FREE_PROB] = logProb; #endif // Build a bounding box for this fake character. elem.bbox = lat->pStroke[iStroke].bbox; for (i = iStroke-nStrokes+1; i < iStroke; i ++) { RECT other = lat->pStroke[i].bbox; elem.bbox.left = __min(elem.bbox.left, other.left); elem.bbox.right = __max(elem.bbox.right, other.right); elem.bbox.top = __min(elem.bbox.top, other.top); elem.bbox.bottom = __max(elem.bbox.bottom, other.bottom); } // Copy bounding box to the writing box elem.writingBox = elem.bbox; // If the box is more than 4 times wider than high ... width = elem.writingBox.right - elem.writingBox.left; height = elem.writingBox.bottom - elem.writingBox.top; if (width / 4 > height) { // ... increase the height by width / 4 elem.writingBox.top -= width / 8; elem.writingBox.bottom += width / 8; } // If this is not the first character in the lattice, then hook it up to the best previous // alternate. if (iStroke-nStrokes>-1) { LATTICE_ELEMENT *pPrevElem; int iAlt; elem.iPrevAlt=0; for (iAlt=1; iAltpAltList[iStroke-nStrokes].nUsed; iAlt++) { if (lat->pAltList[iStroke-nStrokes].alts[elem.iPrevAlt].logProbPath < lat->pAltList[iStroke-nStrokes].alts[iAlt].logProbPath) { elem.iPrevAlt=iAlt; } } pPrevElem = &lat->pAltList[iStroke-nStrokes].alts[elem.iPrevAlt]; elem.iPathLength = pPrevElem->iPathLength + 1; elem.wPrevChar = pPrevElem->wChar; elem.logProbPath = (pPrevElem->logProbPath * pPrevElem->iPathLength + logProb) / elem.iPathLength; elem.iPromptPtr = pPrevElem->iPromptPtr; #ifdef HWX_TUNE { int iParam; for (iParam = 0; iParam < VTUNE_NUM_WEIGHTS; iParam++) { elem.tuneScores.afl[iParam] = (elem.tuneScores.afl[iParam] + pPrevElem->tuneScores.afl[iParam] * pPrevElem->iPathLength) / elem.iPathLength; } } #endif } InsertElement(lat, iStroke, &elem); } // Go through the pre-segmentation lattice, and run the recognizer on the top alternates of the // given column. The alternates will get replaced with ones containing actual characters. void BuildRecogAlts(LATTICE *lat, int iStroke, BOOL fStringMode) { VOLCANO_WEIGHTS aTuneScores[MaxAltsPerStroke]; LATTICE_ALT_LIST bboxAlts; int iAlt; ASSERT(lat!=NULL); ASSERT(iStroke>=0 && iStrokenStrokes); // Make a copy of the current alt-list. memcpy(&bboxAlts,&lat->pAltList[iStroke],sizeof(LATTICE_ALT_LIST)); // Clear out the alt-list in the lattice ClearAltList(&lat->pAltList[iStroke]); // For each segmentation lattice for (iAlt=0; iAltpAltList[iStroke].nUsedlat->pAltList[iStroke].alts[lat->pAltList[iStroke].nUsed-1].logProbPath*/) { int nZillaStrokes; wchar_t wAlts[MaxAltsPerStroke]; FLOAT probAlts[MaxAltsPerStroke]; FLOAT score; float flCost; // Run the recognizer, get scores/probabilities back int nRecogAlts=RecognizerProbs(lat,iStroke,bboxAlts.alts[iAlt].nStrokes,bboxAlts.alts[iAlt].bbox, wAlts,probAlts,&score,&nZillaStrokes, aTuneScores, fStringMode); int iRecogAlt; // In separator mode, add on an "unknown character" at the end of the alt list if (lat->fSepMode) { if (nRecogAlts < MaxAltsPerStroke) { wAlts[nRecogAlts] = SYM_UNKNOWN; aTuneScores[nRecogAlts].afl[VTUNE_FREE_PROB] = -PENALTY_SKIP_INK; if (nRecogAlts > 0) { aTuneScores[nRecogAlts].afl[VTUNE_FREE_PROB] += aTuneScores[nRecogAlts - 1].afl[VTUNE_FREE_PROB]; } nRecogAlts++; } } // Make sure we don't try to apply a priority during tuning. #ifdef HWX_TUNE if (g_pTuneFile == NULL) #endif { ApplyALCPriority(lat, nRecogAlts, wAlts, aTuneScores); } // For reach alternate from the recognizer for (iRecogAlt=0; iRecogAltnStrokes - 1) && lat->fEndInput), &flCost); if (iAnswerPtr >= 0) { elem.iPromptPtr = iAnswerPtr; aTuneScores[iRecogAlt].afl[VTUNE_FREE_PROB] += flCost; // We can only apply the language model when we are not in // separator mode, and we know what character this is. if (!lat->fSepMode && elem.wChar != SYM_UNKNOWN) { if (lat->fUseLM) { // Apply the language model LanguageModelBigram( lat->fProbMode, FALSE, !lat->fUseGuide, elem.wChar, elem.nStrokes, GetPreContextChar(lat), aTuneScores + iRecogAlt); // If we are at the end of the input, use the post-context if it is available if (lat->fEndInput && iStroke == lat->nStrokes - 1 && GetPostContextChar(lat) != SYM_UNKNOWN) { LanguageModelBigram( lat->fProbMode, TRUE, !lat->fUseGuide, GetPostContextChar(lat), elem.nStrokes, elem.wChar, aTuneScores + iRecogAlt); } } // Centipede only makes sense when the full character is written. // Also we may want to allow this in separator mode once it is // trained on free input data. if (lat->recogSettings.partialMode == HWX_PARTIAL_ALL) { RecogUnaryProb(lat, iStroke, elem.nStrokes, elem.bbox, elem.wChar, aTuneScores + iRecogAlt, fStringMode); } } // If in free mode, also add in the pre-segmentation probabilities. // We don't care about segmentation in word mode. if (!lat->fWordMode && !lat->fUseGuide) { CheapUnaryProb(elem.nStrokes, elem.bbox, elem.space, elem.area, aTuneScores + iRecogAlt); } #ifdef HWX_TUNE // Record values for tuning elem.tuneScores = aTuneScores[iRecogAlt]; #endif // Apply tuning parameters elem.logProb = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, aTuneScores + iRecogAlt); // Combined score includes the language model probability elem.logProbPath = VTuneComputeScore(&g_vtuneInfo.pTune->weights, aTuneScores + iRecogAlt); elem.iPathLength = 1; // Put it in the lattice InsertElement(lat, iStroke, &elem); } } else { BOOL first=TRUE; int iPrevAlt; // If there are previous characters in the lattice, then we loop over each previous // alternate, and use bigram probabilities. The best alternate is recorded, and // the probability associated with the best alternate is stored with the current // alternate. This essentially does a Viterbi search. for (iPrevAlt=0; iPrevAltpAltList[iStroke-elem.nStrokes].nUsed; iPrevAlt++) { LATTICE_ELEMENT *prevElem=&lat->pAltList[iStroke-elem.nStrokes].alts[iPrevAlt]; int iAnswerPtr = IsAllowedNextChar(lat, elem.wChar, prevElem->iPromptPtr, ((iStroke == lat->nStrokes - 1) && lat->fEndInput), &flCost); if (iAnswerPtr >= 0) { float prob, probPath; wchar_t prevUnicode = (prevElem->wChar == SYM_UNKNOWN) ? SYM_UNKNOWN : LocRunDense2Unicode(&g_locRunInfo,prevElem->wChar); VOLCANO_WEIGHTS altTuneScores = aTuneScores[iRecogAlt]; ASSERT(prevElem->fUsed); // If we're in free mode and this choice of a previous alternate would cause // an overlap, then don't consider it. // Don't use this heuristic in separator mode, because sometimes the characters // really do overlap and we want to be able to learn that in the future. if (!lat->fSepMode && !lat->fUseGuide && IsOverlappedPath(lat, iStroke - elem.nStrokes, iPrevAlt, elem.bbox)) { continue; } // Only use the language model if we are not in separator mode, // and we know what the current character is. if (!lat->fSepMode && elem.wChar != SYM_UNKNOWN) { if (lat->fUseLM) { // If we're in boxed mode, and the boxes are consequtive, then // use bigrams as the language model, otherwise use a unigram if (!lat->fUseGuide || lat->pStroke[iStroke].iBox-1 == lat->pStroke[iStroke-elem.nStrokes].iBox) { LanguageModelBigram( lat->fProbMode, TRUE, !lat->fUseGuide, elem.wChar, elem.nStrokes, prevElem->wChar, &altTuneScores); } else { LanguageModelBigram(lat->fProbMode, FALSE, !lat->fUseGuide, elem.wChar, elem.nStrokes, SYM_UNKNOWN, &altTuneScores); } // If we are at the end of the input, use the post-context if it is present if (lat->fEndInput && iStroke == lat->nStrokes - 1 && GetPostContextChar(lat) != SYM_UNKNOWN) { LanguageModelBigram( lat->fProbMode, TRUE, !lat->fUseGuide, GetPostContextChar(lat), elem.nStrokes, elem.wChar, &altTuneScores); } } // Centipede only makes sense when the whole character is written // Also we may want to allow this in separator mode once it is // trained on free input data. if (lat->recogSettings.partialMode == HWX_PARTIAL_ALL) { RecogBinaryProb(lat, iStroke, elem.nStrokes, elem.bbox, elem.wChar, iStroke-elem.nStrokes, prevElem->nStrokes, prevElem->bbox, prevElem->wChar, &altTuneScores); } } // In free mode, also include the pre-segmentation probabilities // We don't care about segmentation in word mode if (!lat->fWordMode && !lat->fUseGuide) { BOOL fSegOkay = CheapBinaryProb(elem.nStrokes, elem.bbox, elem.space, elem.area, prevElem->nStrokes, prevElem->bbox, prevElem->space, prevElem->area, &altTuneScores); // If we're not in separator mode, then prune based on the // segmentation score if (!lat->fSepMode && !fSegOkay) { continue; } // In separator mode, add a penalty for overlapping characters, rather than // making a hard decision as is done during normal recognition. if (lat->fSepMode && IsOverlappedPath(lat, iStroke - elem.nStrokes, iPrevAlt, elem.bbox)) { altTuneScores.afl[VTUNE_FREE_SEG_BIGRAM] += ((elem.wChar == SYM_UNKNOWN) ? -PENALTY_OVERLAP : -PENALTY_OVERLAP_CHAR); } } // Add in the prompt matching cost temporarily altTuneScores.afl[VTUNE_FREE_PROB] += flCost; // Apply tuning parameters prob = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, &altTuneScores); // Combined score includes the language model probability probPath = (VTuneComputeScore(&g_vtuneInfo.pTune->weights, &altTuneScores) + prevElem->logProbPath * prevElem->iPathLength) / (prevElem->iPathLength + 1); // If we found a better previous alternate, update. if (/*probPath > Log2Range &&*/ (first || probPath > elem.logProbPath)) { #ifdef HWX_TUNE int iParam; for (iParam = 0; iParam < VTUNE_NUM_WEIGHTS; iParam++) { elem.tuneScores.afl[iParam] = (altTuneScores.afl[iParam] + prevElem->tuneScores.afl[iParam] * prevElem->iPathLength) / (prevElem->iPathLength + 1); } #endif elem.logProb=prob; elem.logProbPath=probPath; elem.iPrevAlt=iPrevAlt; elem.nPrevStrokes=prevElem->nStrokes; elem.wPrevChar=prevElem->wChar; elem.iPathLength = prevElem->iPathLength + 1; elem.iPromptPtr = iAnswerPtr; first=FALSE; #ifdef USE_IFELANG3_BIGRAMS elem.bigramLogProbs[elem.nBigrams]=prob; elem.bigramAlts[elem.nBigrams]=iPrevAlt; elem.nBigrams++; #endif } } // put the alternate in the lattice if (!first) InsertElement(lat,iStroke,&elem); } } } } } } // This function does assignment of strokes to boxes, and tries to keep ink going into // consequtive sequences of boxes, even if it is not written in quite the right place. // This seems to make a difference on the test set. void GetBoxIndexSINFO(LATTICE *lat, STROKE *pStroke, int *pixBoxPrev, int *piyBoxPrev) { #define CYBASE_FUDGE 4 XY xy; int ixBox, iyBox, iyBoxBiased; RECT *rect; HWXGUIDE *lpguide = &lat->guide; ASSERT(lpguide); ASSERT(lpguide->cHorzBox > 0); ASSERT(lpguide->cVertBox > 0); rect = &pStroke->bbox; // Center of the stroke bounding box horizontally xy.x = (rect->left + rect->right) / 2; // 1/4 down from the top of the stroke's bounding box xy.y = (rect->top * (CYBASE_FUDGE - 1) + rect->bottom) / CYBASE_FUDGE; // Box number in X is just where xy.x falls in the guide. ixBox = (xy.x - lpguide->xOrigin) / (int)lpguide->cxBox; if (ixBox < 0) ixBox = 0; else if (ixBox > (int)lpguide->cHorzBox - 1) ixBox = lpguide->cHorzBox - 1; // Same for Y iyBox = (xy.y - lpguide->yOrigin) / (int)lpguide->cyBox; if (iyBox < 0) iyBox = 0; else if (iyBox > (int)lpguide->cVertBox - 1) iyBox = lpguide->cVertBox - 1; // If we have a previous stroke's Y box number if (*piyBoxPrev >= 0) { // Help dots, accents, etc land on their respective boxes... if (iyBox < *piyBoxPrev) { // If the stroke seems to be in a higher box, // shift the "hot spot" down 1/2 of the remaining distance to the // bottom of the box xy.y = (rect->bottom + xy.y) / 2; // compute the new Y box number iyBoxBiased = (xy.y - lpguide->yOrigin) / (int)lpguide->cyBox; if (iyBoxBiased > (int)(lpguide->cVertBox - 1)) iyBoxBiased = lpguide->cVertBox - 1; else if (iyBoxBiased < 0) iyBoxBiased = 0; // if this one matches the previous stroke, then keep it if (iyBoxBiased == *piyBoxPrev) iyBox = iyBoxBiased; } else if (iyBox > *piyBoxPrev && ixBox >= *pixBoxPrev) { // If the stroke seems to be in a lower box, // shift the "hot spot" up 1/2 of the remaining distance to the // top of the box xy.y = (rect->top + xy.y) / 2; // compute the new Y box number iyBoxBiased = (xy.y - lpguide->yOrigin) / (int)lpguide->cyBox; if (iyBoxBiased > (int)(lpguide->cVertBox - 1)) iyBoxBiased = lpguide->cVertBox - 1; else if (iyBoxBiased < 0) iyBoxBiased = 0; // if this one matches the previous stroke, then keep it if (iyBoxBiased == *piyBoxPrev) iyBox = iyBoxBiased; } } // Return the result *pixBoxPrev = ixBox; *piyBoxPrev = iyBox; pStroke->iBox = ixBox + (iyBox * lpguide->cHorzBox); } // This function contained my old algorithm for assigning strokes to boxes, which // has now been commented out to call the above routine from Tsunami. void AssignStrokeToBox(LATTICE *lat, STROKE *pStroke, int *pixBoxPrev, int *piyBoxPrev) { #if 0 int iInk; int totX=0, totY=0; int boxX, boxY; ASSERT(lat!=NULL); ASSERT(pStroke!=NULL); ASSERT(pStroke->nInk>0); if (pStroke->iBox != -1) return; for (iInk = 0; iInk < pStroke->nInk; iInk++) { totX += pStroke->pts[iInk].x; totY += pStroke->pts[iInk].y; } totX /= pStroke->nInk; totY /= pStroke->nInk; boxX = (totX - lat->guide.xOrigin) / lat->guide.cxBox; boxY = (totY - lat->guide.yOrigin) / lat->guide.cyBox; if (boxX < 0) boxX=0; if (boxX >= (int)lat->guide.cHorzBox) boxX = lat->guide.cHorzBox - 1; if (boxY < 0) boxY=0; if (boxY >= (int)lat->guide.cVertBox) boxY = lat->guide.cVertBox - 1; pStroke->iBox = boxX + boxY * lat->guide.cHorzBox; #endif GetBoxIndexSINFO(lat,pStroke,pixBoxPrev,piyBoxPrev); } // Function to compare the ordering of two strokes in qsort. Used to put // the strokes back in their original order if re-processing is requested. int __cdecl CompareStrokes(const void *p1, const void *p2) { STROKE *pStroke1=(STROKE*)p1; STROKE *pStroke2=(STROKE*)p2; ASSERT(pStroke1!=NULL); ASSERT(pStroke2!=NULL); // Boxes are in numerical order if (pStroke1->iBox < pStroke2->iBox) return -1; if (pStroke1->iBox > pStroke2->iBox) return 1; // Within a box, strokes are in time order. if (pStroke1->iOrder < pStroke2->iOrder) return -1; if (pStroke1->iOrder > pStroke2->iOrder) return 1; // Two strokes with the same iOrder values shouldn't occur ASSERT(0); return 0; } #ifdef MERGE_STROKES // This code merges adjacent strokes with one another, if the end point of // one is close enough to the starting point of the next. Note that this // code works on the whole list of strokes at once; it is possible to do // this incrementally also, by keeping track of the last stroke which was // added. void MergeStrokes(LATTICE *lat) { int iStroke, iDest; // First put the strokes back into time order, and clear the alt lists /* for (iStroke=0; iStrokenStrokes; iStroke++) { lat->pStroke[iStroke].iBox=-1; ClearAltList(lat->pAltList+iStroke); } qsort(lat->pStroke,lat->nStrokes,sizeof(STROKE),CompareStrokes); */ // Make sure we have some more strokes to process if (lat->nProcessed == lat->nStrokes) return; // Then go through and merge strokes, starting at the newly added ones. iDest=lat->nProcessed; for (iStroke=lat->nProcessed+1; iStrokenStrokes; iStroke++) { BOOL fMerged=FALSE; POINT ptFirst=lat->pStroke[iDest].pts[lat->pStroke[iDest].nInk-1]; POINT ptLast=lat->pStroke[iStroke].pts[0]; int dx=(ptFirst.x-ptLast.x); int dy=(ptFirst.y-ptLast.y); int dist=dx*dx+dy*dy; // fprintf(stderr,"%d(%d,%d-%d,%d),",dist,ptFirst.x,ptFirst.y,ptLast.x,ptLast.y); if (dist<=MERGE_THRESHOLD*MERGE_THRESHOLD) { // Merge the strokes, and free up the one that was merged in fMerged=AddStrokeToStroke(lat->pStroke+iDest,lat->pStroke+iStroke); // fprintf(stderr,"X,"); // fprintf(stderr,"(%d->%d)",iStroke,iDest); } if (fMerged) { // If we successfully merged, then free the merged stroke. FreeStroke(lat->pStroke+iStroke); } else { // If we didn't merge, just copy the stroke iDest++; if (iDestpStroke[iDest]=lat->pStroke[iStroke]; } } // fprintf(stderr,"\n"); // Record the new number of strokes lat->nStrokes=iDest+1; } #endif // Assign the strokes to boxes, then sort the strokes in order by their box numbers. // Note that the alternate lists are invalidated by this (since they depend on stroke // order), so the alt lists are cleared and must be recomputed. void SortStrokesByGuide(LATTICE *lat) { int iStroke, ixBoxPrev=-1, iyBoxPrev=-1; ASSERT(lat!=NULL); ASSERT(lat->fUseGuide); // If we have any unprocessed strokes left to assign if (lat->nStrokes-lat->nProcessed>0) { int iEarliestTouched = -1; // Assign the strokes to boxes and clear out their alt lists, // and record the lowest box number which has been touched. for (iStroke=lat->nProcessed; iStrokenStrokes; iStroke++) { AssignStrokeToBox(lat,lat->pStroke+iStroke,&ixBoxPrev,&iyBoxPrev); if (iEarliestTouched == -1 || lat->pStroke[iStroke].iBox < iEarliestTouched) { iEarliestTouched = lat->pStroke[iStroke].iBox; } } // If the writer touched a box which has already been processed, then // we will need to do some reprocessing. if (lat->nProcessed > 0) { if (lat->pStroke[lat->nProcessed - 1].iBox >= iEarliestTouched) { // Figure out how far back we actually need to process. This is done // by scanning the lattice to find the boundary for the earliest touched box. lat->nProcessed = 0; for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) { if (lat->pStroke[iStroke].iBox >= iEarliestTouched) { lat->nProcessed = iStroke; break; } } } } // Sort the strokes into box order qsort(lat->pStroke + lat->nProcessed, lat->nStrokes - lat->nProcessed, sizeof(STROKE), CompareStrokes); } } // Count the number of distinct boxes that are used in the ink int CountBoxes(LATTICE *lat) { int iStroke; int nBoxes=0; int prevBox=-1; for (iStroke=0; iStrokenStrokes; iStroke++) { if (prevBox!=lat->pStroke[iStroke].iBox) nBoxes++; prevBox=lat->pStroke[iStroke].iBox; } return nBoxes; } // Determine if the given stroke number iEnd is the valid end of a character. BOOL CandidateEnd(LATTICE *lat, int iEnd) { // If we're on the last stroke seen so far, then we don't know for sure that // it is the end of a character until we get the end of input signal. if (iEnd==lat->nStrokes-1) { return TRUE; /* if (lat->fEndInput) return TRUE; else return FALSE; */ } // If we're not at the last stroke, just check if the box numbers change. if (lat->pStroke[iEnd].iBox!=lat->pStroke[iEnd+1].iBox) return TRUE; return FALSE; } // Mark the "best path so far", ie the path through the lattice which will not change // if more input is added. This only works in boxed mode. void FindIncrementalPath(LATTICE *lat) { int iStroke, iAlt; int iStartAlt, iStartStroke = lat->nProcessed - 1; if (iStartStroke >= 0) { int nPaths = lat->pAltList[iStartStroke].nUsed; ASSERT(lat->fUseGuide); // First zero out the hit counts in each lattice element which doesn't have // its result determined yet. for (iStroke = lat->nFixedResult; iStroke < lat->nProcessed; iStroke++) for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) lat->pAltList[iStroke].alts[iAlt].nHits = 0; // Then trace back all the paths from the current end of lattice to the // point at which all paths converge, counting the number of times each // element occurs on a path. for (iStartAlt = 0; iStartAlt < nPaths; iStartAlt++) { iStroke = iStartStroke; iAlt = iStartAlt; while (iStroke >= lat->nFixedResult) { LATTICE_ELEMENT *elem = lat->pAltList[iStroke].alts + iAlt; elem->nHits++; iStroke -= elem->nStrokes; iAlt = elem->iPrevAlt; } } // All path elements that were hit by all nPaths paths are part of the // current path. for (iStroke = lat->nFixedResult; iStroke < lat->nProcessed; iStroke++) for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].nHits == nPaths) { lat->pAltList[iStroke].alts[iAlt].fCurrentPath = TRUE; lat->nFixedResult = iStroke + 1; } else lat->pAltList[iStroke].alts[iAlt].fCurrentPath = FALSE; } } } // Mark the best path through the lattice (assuming we're at the end of the input), // and return the number of characters along that path. int FindFullPath(LATTICE *lat) { int nChars=0; int iStroke, iAlt; // Wipe out the current path for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) lat->pAltList[iStroke].alts[iAlt].fCurrentPath = FALSE; // Trace back from the best path iStroke = lat->nProcessed - 1; iAlt = 0; while (iStroke!=-1) { int iNextStroke, iNextAlt; if (lat->pAltList[iStroke].nUsed==0) { ASSERT(lat->pAltList[iStroke].nUsed!=0); iStroke--; iAlt=0; continue; } lat->pAltList[iStroke].alts[iAlt].fCurrentPath=TRUE; nChars++; iNextStroke=iStroke-lat->pAltList[iStroke].alts[iAlt].nStrokes; iNextAlt=lat->pAltList[iStroke].alts[iAlt].iPrevAlt; iStroke=iNextStroke; iAlt=iNextAlt; } return nChars; } // This routine is the analogy of ProcessLattice for the boxed mode case. Since we have // the boxes, the segmentation problem is "deterministic". We create the lattice with // the minimal set of pre-segmentation lattice elements. void CreateLatticeForGuide(LATTICE *lat) { int cContextBefore = 0, cContextAfter = 0; int iStroke; int iPrevEnd; BOOL fStringMode; ASSERT(lat!=NULL); #ifdef MERGE_STROKES // First run the stroke merging algorithm MergeStrokes(lat); #endif // Assign the strokes to boxes, and put them in the appropriate order. Note that // this also clears the alternate lists for the strokes. SortStrokesByGuide(lat); if (lat->wszBefore != NULL) cContextBefore = wcslen(lat->wszBefore); if (lat->wszAfter != NULL) cContextAfter = wcslen(lat->wszAfter); fStringMode = (CountBoxes(lat) + cContextBefore + cContextAfter > 1); #ifdef USE_IFELANG3 // Decide whether to use IFELang3. The criterion is that we can't be doing incremental // processing, we must have IFELang3 available, and we must have at least MIN_CHARS_FOR_IFELANG3 boxes // of ink plus context. There is a special case for one character of ink, in which we require that // both the pre- and post-context each have at least MIN_CONTEXT_FOR_IFELANG3 characters. if (/* !lat->fIncremental && */ LatticeIFELang3Available()) { int cChars = CountBoxes(lat); if ((cChars == 1 && cContextBefore >= MIN_CONTEXT_FOR_IFELANG3 && cContextAfter >= MIN_CONTEXT_FOR_IFELANG3) || (cChars > 1 && cChars + cContextBefore + cContextAfter >= MIN_CHARS_FOR_IFELANG3)) { lat->fUseIFELang3 = TRUE; } else { lat->fUseIFELang3 = FALSE; } } else { lat->fUseIFELang3=FALSE; } // { FILE *f=fopen("/ife.log","w"); fprintf(f,"%s %d %d %d\n",(lat->fUseIFELang3?"YES":"NO"),cContextBefore,cContextAfter,CountBoxes(lat)); fclose(f); } #else lat->fUseIFELang3=FALSE; #endif #ifdef USE_OLD_DATABASES if (lat->fUseIFELang3) lat->fProbMode=TRUE; else lat->fProbMode=FALSE; // fprintf(stderr,"prob=%d, ife=%d, guide=%d\n",lat->fProbMode,lat->fUseIFELang3,lat->fUseGuide); // lat->fProbMode=FALSE; // lat->fUseIFELang3=FALSE; lat->fProbMode = FALSE; #else lat->fProbMode=TRUE; #endif iPrevEnd = lat->nProcessed-1; // For each stroke for (iStroke=lat->nProcessed; iStrokenStrokes; iStroke++) { ClearAltList(lat->pAltList + iStroke); // If this stroke is the end of a box, then create an alternate if (CandidateEnd(lat,iStroke)) { // Create the alternate, filling in all the features int i; LATTICE_ELEMENT *elem=&(lat->pAltList[iStroke].alts[0]); RECT bbox; // INTERVALS xIntervals, yIntervals; bbox=lat->pStroke[iStroke].bbox; // EmptyIntervals(&xIntervals,bbox.left,bbox.right); // EmptyIntervals(&yIntervals,bbox.top,bbox.bottom); for (i=iPrevEnd+1; i<=iStroke; i++) { RECT other=lat->pStroke[i].bbox; bbox.left=__min(bbox.left,other.left); bbox.right=__max(bbox.right,other.right); bbox.top=__min(bbox.top,other.top); bbox.bottom=__max(bbox.bottom,other.bottom); // ExpandIntervalsRange(&xIntervals,other.left,other.right); // ExpandIntervalsRange(&yIntervals,other.top,other.bottom); // RemoveInterval(&xIntervals,other.left,other.right); // RemoveInterval(&yIntervals,other.top,other.bottom); } lat->pAltList[iStroke].nUsed=1; elem->fCurrentPath=FALSE; elem->fUsed=TRUE; elem->iPrevAlt=0; if (iPrevEnd==-1) elem->iPrevAlt=-1; elem->maxDist=0; elem->nPrevStrokes=0; elem->nStrokes=iStroke-iPrevEnd; elem->logProb=0; elem->logProbPath=0; elem->score=0; elem->wChar=0; elem->wPrevChar=0; elem->area=0; //TotalRange(&xIntervals)+TotalRange(&yIntervals); elem->space=0; //TotalPresent(&xIntervals)+TotalPresent(&yIntervals); elem->bbox=bbox; elem->iPathLength = 1; // Convert this alternate to a list of recognition alternates. BuildRecogAlts(lat, iStroke, fStringMode); if (lat->pAltList[iStroke].nUsed==0) { // Well, the recognizer came back and send there were no candidates. // Let's put in a SYM_UNKNOWN so the user will see something, and also so // the language model will have a path to work with. // fprintf(stderr,"Lost a character in boxed mode (box %d).\n",lat->pStroke[iStroke].iBox); FakeRecogResult(lat,iStroke,iStroke-iPrevEnd,0); } // fprintf(stderr,"Found %d\n",lat->pStroke[iStroke].iBox); iPrevEnd=iStroke; // Record that we have processed up to this point, so that we don't // redo any work. lat->nProcessed = iStroke+1; } } // if (!lat->fEndInput) // FindIncrementalPath(lat); else FindFullPath(lat); } //****************************************************************************** //***** Time Bomb code //****************************************************************************** // If there is no setting from the project, then by default turn the time bomb on. #ifndef TIME_BOMB #define TIME_BOMB 0 #endif #if TIME_BOMB #include #define TIME_BOMB_MONTH 4 #define TIME_BOMB_DAY 10 #define TIME_BOMB_YEAR 2000 #define TIME_BOMB_NUM_WARNING_DAYS 14 BOOL TimeBombExpired() { SYSTEMTIME st; FILETIME ft; DWORD dwCurrent, dwExpired; // Get the expired day (relative to January 1, 1601) ZeroMemory(&st, sizeof(st)); st.wMonth = TIME_BOMB_MONTH; st.wDay = TIME_BOMB_DAY; st.wYear = TIME_BOMB_YEAR; SystemTimeToFileTime(&st, &ft); dwExpired = (DWORD)(*(DWORDLONG*)&ft / (DWORDLONG)864000000000); // Get the current day (relative to January 1, 1601) GetLocalTime(&st); SystemTimeToFileTime(&st, &ft); dwCurrent = (DWORD)(*(DWORDLONG*)&ft / (DWORDLONG)864000000000); // Check to see if we have expired. if (dwCurrent >= dwExpired) { static BOOL fWarned = FALSE; if (!fWarned) { MessageBox(NULL, L"This beta has expired. Please install our updated beta.", L"Expiration", MB_ICONWARNING); fWarned = TRUE; } return TRUE; } // Check to see if we are in the warning period. if ((dwCurrent + TIME_BOMB_NUM_WARNING_DAYS) >= dwExpired) { static BOOL fWarned = FALSE; if (!fWarned) { wchar_t wszMsg[256]; swprintf(wszMsg, L"This beta expires in %d day%s. Please install our updated beta.", (int)(dwExpired - dwCurrent), (dwExpired - dwCurrent == 1) ? L"" : L"s"); MessageBox(NULL, wszMsg, L"Expiration", MB_ICONWARNING); fWarned = TRUE; } } return FALSE; } #endif // TIME_BOMB // Figure out which strokes are valid end of character in paths that span the // whole lattice. static BOOL CheckIfValid(LATTICE *lat, int iStartStroke, int iStroke, BYTE *pValidEnd) { LATTICE_ALT_LIST *pAlts; int ii; BOOL fValidPath; // Have we already checked this position? if (pValidEnd[iStroke] == LCF_INVALID) { return FALSE; } else if (pValidEnd[iStroke] == LCF_VALID) { return TRUE; } // Assume no valid path, until proved otherwise. fValidPath = FALSE; // Process each element ending at this position in lattice. pAlts = lat->pAltList + iStroke; for (ii = 0; ii < pAlts->nUsed; ++ii) { LATTICE_ELEMENT *pElem; int prevEnd; pElem = pAlts->alts + ii; prevEnd = iStroke - pElem->nStrokes; ASSERT(prevEnd >= -1); if (prevEnd == iStartStroke - 1) { // Reached start. fValidPath = TRUE; } else if (CheckIfValid(lat, iStartStroke, prevEnd, pValidEnd)) { // Have path to start. fValidPath = TRUE; } } // Did one (or more) paths reach the start? if (fValidPath) { pValidEnd[iStroke] = LCF_VALID; return TRUE; } else { pValidEnd[iStroke] = LCF_INVALID; return FALSE; } } void PruneLattice(LATTICE *lat, int iStartStroke, int iEndStroke) { // Figure out valid character transition points by stroke. We don't want to include // pieces of lattice that don't actually connect up for the full run. This uses // a recursive algorithm to find valid paths. Since it marks the paths as it // finds them, it can quickly test and not redo work. int size = sizeof(BYTE) * (iEndStroke + 1); int iStroke; BYTE *pValidEnd = (BYTE *)ExternAlloc(size); // If we failed to allocate memory, just return if (pValidEnd == NULL) return; memset(pValidEnd, LCF_UNKNOWN, size); CheckIfValid(lat, iStartStroke, iEndStroke, pValidEnd); for (iStroke = iStartStroke; iStroke <= iEndStroke; iStroke++) { if (pValidEnd[iStroke] != LCF_VALID) { lat->pAltList[iStroke].nUsed = 0; } } ExternFree(pValidEnd); } // Evaluate all the elements on the best path. If any of them is a member of the big/small kana // pair, and the other element of that pair is also present, then switch the best path element // based on the position criterion. static void BigSmallKanaHack(LATTICE *lat) { int iStroke, iAlt; // List of all the small characters static wchar_t *wszSmallKana = L"copsuvwxz\x3041\x3043\x3045\x3047\x3049\x3083\x3085\x3087\x308e\x30a1\x30a3\x30a5\x30a7\x30a9\x30e3\x30e5\x30e7\x30ee\x30f5\x30f6"; // Corresponding list of all the big characters static wchar_t *wszBigKana = L"COPSUVWXZ\x3042\x3044\x3046\x3048\x304a\x3084\x3086\x3088\x308f\x30a2\x30a4\x30a6\x30a8\x30aa\x30e4\x30e6\x30e8\x30ef\x30ab\x30b1"; // Threshold on the fraction down from the top of the box below which // we always classify as a large character. static float aflBigThreshold[] = { (float) 17.910447761194, (float) 17.5257731958763, (float) 9.33333333333333, (float) 12, (float) 14.8648648648649, (float) 12.1621621621622, (float) 13.4328358208955, (float) 11, (float) 15.2777777777778, (float) 16.5532879818594, (float) 30.1666666666667, (float) 17.5, (float) 15.6666666666667, (float) 33.5, (float) 16, (float) 15.4639175257732, (float) 15.4639175257732, (float) 24.6666666666667, (float) 21.5, (float) 13.4020618556701, (float) 10.3092783505155, (float) 25, (float) 15.4639175257732, (float) 17.5257731958763, (float) 27.536231884058, (float) 23.3560090702948, (float) 27, (float) 4.21052631578947, (float) 12.3711340206186, }; // Threshold on the fraction down from the top of the box below which // we always classify as a small character. static float aflSmallThreshold[] = { (float) 58.1081081081081, (float) 57.7319587628866, (float) 50.5154639175258, (float) 51.5463917525773, (float) 54.0540540540541, (float) 56.7164179104478, (float) 54.639175257732, (float) 54.0540540540541, (float) 64.8148148148148, (float) 50.7246376811594, (float) 67.0103092783505, (float) 50.5154639175258, (float) 50.7246376811594, (float) 52.6315789473684, (float) 54.1666666666667, (float) 51.5789473684211, (float) 54.639175257732, (float) 53.6082474226804, (float) 55.6701030927835, (float) 52.5773195876289, (float) 46.3917525773196, (float) 61.9565217391304, (float) 49.4845360824742, (float) 53.6666666666667, (float) 64.9484536082474, (float) 57.1666666666667, (float) 55.6701030927835, (float) 55.6701030927835, (float) 50.5154639175258, }; // Sanity checks on the data tables above ASSERT(wcslen(wszBigKana) == wcslen(wszSmallKana)); ASSERT(wcslen(wszBigKana) == sizeof(aflSmallThreshold) / sizeof(float)); ASSERT(wcslen(wszBigKana) == sizeof(aflBigThreshold) / sizeof(float)); // For each stroke for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) { RECT rInk, rGuide; float flPosition; wchar_t *pwchBig, *pwchSmall, wch, wchOther; int iBestAlt = -1, iOtherAlt = -1; // Scan through the alt list looking for a node on the current path for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].fCurrentPath) { iBestAlt = iAlt; break; } } if (iBestAlt == -1) { continue; } // If we found one, look to see if it is in a big/small pair wch = LocRunDense2Unicode(&g_locRunInfo, lat->pAltList[iStroke].alts[iAlt].wChar); pwchBig = wcschr(wszBigKana, wch); pwchSmall = wcschr(wszSmallKana, wch); if (pwchBig == NULL && pwchSmall == NULL) { continue; } // If so, scan the alt list for another character with the same number of // strokes which is the other member of the pair. if (pwchBig == NULL) { wchOther = wszBigKana[pwchSmall - wszSmallKana]; } else { wchOther = wszSmallKana[pwchBig - wszBigKana]; } for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].nStrokes == lat->pAltList[iStroke].alts[iBestAlt].nStrokes && LocRunDense2Unicode(&g_locRunInfo, lat->pAltList[iStroke].alts[iAlt].wChar) == wchOther) { iOtherAlt = iAlt; break; } } if (iOtherAlt == -1) { continue; } // If found, then compute the position of the top of the ink in the writing box. rInk = lat->pAltList[iStroke].alts[iAlt].bbox; rGuide = GetGuideDrawnBox(&lat->guide, lat->pStroke[iStroke].iBox); flPosition = 100 * (float) (rInk.top - rGuide.top) / (float) max(1, rGuide.bottom - rGuide.top); #if 0 { FILE *f = fopen("c:/boxes.log", "a"); fprintf(f, "top %5d start %5d end %5d bottom %5d fract %f\n", rGuide.top, rInk.top, rInk.bottom, rGuide.bottom, flPosition); fclose(f); } #endif // Apply the thresholds. if ((pwchBig != NULL && flPosition > aflSmallThreshold[pwchBig - wszBigKana]) || (pwchSmall != NULL && flPosition < aflBigThreshold[pwchSmall - wszSmallKana])) { lat->pAltList[iStroke].alts[iBestAlt].fCurrentPath = FALSE; lat->pAltList[iStroke].alts[iOtherAlt].fCurrentPath = TRUE; } } } // Update the probabilities in the lattice, including setting current // path to the most likely path so far (including language model). // Can be called repeatedly for incremental processing after each stroke. BOOL ProcessLattice(LATTICE *lat, BOOL fEndInput) { int iStroke, iAlt, nChars; //DebugBreak(); ASSERT(lat!=NULL); #if TIME_BOMB if (TimeBombExpired()) { return FALSE; } #endif lat->fEndInput = fEndInput; if (!fEndInput) { lat->fIncremental = TRUE; } if (lat->nStrokes==0) { return TRUE; } if (lat->fUseFactoid) { // Figure out which parameters we're updating if (lat->fCoerceMode) { lat->recogSettings.alcValid = lat->alcFactoid; lat->recogSettings.pbAllowedChars = CopyAllowedChars(&g_locRunInfo, lat->pbFactoidChars); } else { lat->recogSettings.alcPriority = lat->alcFactoid; lat->recogSettings.pbPriorityChars = CopyAllowedChars(&g_locRunInfo, lat->pbFactoidChars); } } if (lat->fUseGuide) { CreateLatticeForGuide(lat); } else { // We're in free mode, so create the cache if (lat->pvCache == NULL) { lat->pvCache = AllocateRecognizerCache(); } // Always use probability mode for free input lat->fProbMode=TRUE; // Word mode processing if (lat->fWordMode) { for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) { lat->pAltList[iStroke].nUsed = 0; } // Create a placeholder for the correct segmentation. FakeRecogResult(lat, lat->nStrokes - 1, lat->nStrokes, 0); BuildRecogAlts(lat, lat->nStrokes - 1, TRUE); if (lat->pAltList[lat->nStrokes - 1].nUsed==0) { // Well, the recognizer came back and said there were no candidates. // Let's put in a dummy alt so the user will see something, and also so // the language model will have a path to work with. // fprintf(stderr,"Lost a stroke in free mode.\n"); FakeRecogResult(lat, lat->nStrokes - 1, lat->nStrokes, -PENALTY_SKIP_INK); } // Indicate how much processing has been done so far lat->nProcessed = lat->nStrokes; } else { // For each stroke in the lattice for (iStroke = lat->nProcessed; iStroke < lat->nStrokes; iStroke++) { int maxDist=0; int nChar1; RECT bbox; INTERVALS xIntervals, yIntervals; // Wipe out the current path ClearAltList(lat->pAltList + iStroke); bbox=lat->pStroke[iStroke].bbox; EmptyIntervals(&xIntervals,bbox.left,bbox.right); EmptyIntervals(&yIntervals,bbox.top,bbox.bottom); // Run through all possible numbers of strokes for this // character for (nChar1=1; nChar1<=MaxStrokesPerCharacter && iStroke-nChar1+1>=0; nChar1++) { VOLCANO_WEIGHTS tuneScores; // Determine the features for this proposed character LATTICE_ELEMENT elem; if (nChar1>1) { RECT other=lat->pStroke[iStroke-nChar1+1].bbox; int xdist=__max(other.left-bbox.right,bbox.left-other.right); int ydist=__max(other.top-bbox.bottom,bbox.top-other.bottom); int dist=__max(xdist,ydist); bbox.left=__min(bbox.left,other.left); bbox.right=__max(bbox.right,other.right); bbox.top=__min(bbox.top,other.top); bbox.bottom=__max(bbox.bottom,other.bottom); dist=xdist/(bbox.right-bbox.left); // dist=xdist; maxDist=__max(dist,maxDist); ExpandIntervalsRange(&xIntervals,other.left,other.right); ExpandIntervalsRange(&yIntervals,other.top,other.bottom); RemoveInterval(&xIntervals,other.left,other.right); RemoveInterval(&yIntervals,other.top,other.bottom); } elem.fUsed=TRUE; elem.fCurrentPath=FALSE; elem.nStrokes=nChar1; elem.nPrevStrokes=0; elem.wChar=SYM_UNKNOWN; elem.wPrevChar=0; elem.bbox=bbox; ASSERT(bbox.top<=bbox.bottom); ASSERT(bbox.left<=bbox.right); elem.iPrevAlt=-1; elem.area=TotalRange(&xIntervals)+TotalRange(&yIntervals); elem.space=TotalPresent(&xIntervals)+TotalPresent(&yIntervals); elem.maxDist=maxDist; // If this is the first character in the lattice, use single // character statistics if (iStroke-nChar1+1==0) { BOOL fSegOkay = FALSE; VTuneZeroWeights(&tuneScores); fSegOkay = CheapUnaryProb(nChar1,bbox,elem.space,elem.area, &tuneScores); // If we're not in separator mode, then prune based on the // segmentation score if (!lat->fSepMode && !fSegOkay) { continue; } // Equalize probs across different numbers of strokes elem.logProb = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, &tuneScores); elem.logProbPath = elem.logProb; elem.iPathLength = 1; InsertElement(lat,iStroke,&elem); } else { // Otherwise go through all the previous characters in the // lattice as alternates. BOOL first=TRUE; for (iAlt=0; iAltpAltList[iStroke-nChar1].nUsed; iAlt++) { LATTICE_ELEMENT *prevElem=&lat->pAltList[iStroke-nChar1].alts[iAlt]; float prob, probPath; BOOL fSegOkay; // Only disallow overlapping boxes in normal recognition mode if (!lat->fSepMode && IsOverlappedPath(lat,iStroke-nChar1,iAlt,bbox)) { continue; } VTuneZeroWeights(&tuneScores); fSegOkay = CheapBinaryProb(nChar1,bbox,elem.space,elem.area, prevElem->nStrokes,prevElem->bbox,prevElem->space,prevElem->area, &tuneScores); // If we're not in separator mode, then prune based on the // segmentation score if (!lat->fSepMode && !fSegOkay) { continue; } // In separator mode, just add a penalty for overlapping boxes if (lat->fSepMode && IsOverlappedPath(lat,iStroke-nChar1,iAlt,bbox)) { tuneScores.afl[VTUNE_FREE_SEG_BIGRAM] += -PENALTY_OVERLAP_CHAR; } prob = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, &tuneScores); // Equalize probs across different numbers of strokes probPath = (prob + prevElem->logProbPath * prevElem->iPathLength) / (prevElem->iPathLength + 1); if (first || probPath > elem.logProbPath) { first=FALSE; elem.iPathLength = prevElem->iPathLength + 1; elem.logProb=prob; elem.logProbPath=probPath; elem.nPrevStrokes=prevElem->nStrokes; elem.iPrevAlt=iAlt; } } if (!first) { InsertElement(lat,iStroke,&elem); } } } if (lat->pAltList[iStroke].nUsed==0) { // Well, the pre-segmentation came back and said there were no candidates. // Let's put in a dummy alt so the pruning will still have a connected lattice // to work with. // fprintf(stderr,"Lost a stroke in free mode.\n"); FakeRecogResult(lat, iStroke, 1, -PENALTY_SKIP_INK); } } PruneLattice(lat, 0, lat->nStrokes - 1); // For each unprocessed stroke in the lattice for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) { if (iStroke >= lat->nProcessed) { BuildRecogAlts(lat, iStroke, TRUE); } if (lat->pAltList[iStroke].nUsed==0) { // Well, the recognizer came back and said there were no candidates. // Let's put in a dummy alt so the user will see something, and also so // the language model will have a path to work with. // fprintf(stderr,"Lost a stroke in free mode.\n"); FakeRecogResult(lat, iStroke, 1, -PENALTY_SKIP_INK); } ASSERT(lat->pAltList[iStroke].nUsed>0); } // Mark this stroke as processed so we don't redo work. lat->nProcessed = lat->nStrokes; } // Mark the best path so far, and count how many characters it has nChars = FindFullPath(lat); // If we're at the end of the input if (lat->fEndInput) { int cContext = 0; // If there are enough, use IFELang3 #ifdef USE_IFELANG3 if (lat->wszBefore != NULL) cContext += wcslen(lat->wszBefore); if (lat->wszAfter != NULL) cContext += wcslen(lat->wszAfter); if (nChars + cContext >= MIN_CHARS_FOR_IFELANG3 && LatticeIFELang3Available()) { lat->fUseIFELang3=TRUE; } else { lat->fUseIFELang3=FALSE; } #else lat->fUseIFELang3 = FALSE; #endif // Until we can get it tuned correctly, force IFELang3 off for free mode. lat->fUseIFELang3 = FALSE; } } // Hack for big and small kana characters if (lat->fUseGuide) { BigSmallKanaHack(lat); } return TRUE; } #ifdef DUMP_LATTICE #include #ifdef DUMP_LATTICE_TO_DOTTY void DumpLatticeElement(FILE *f, LATTICE *lat, int iStroke, int iAlt) { LATTICE_ELEMENT *elem=&(lat->pAltList[iStroke].alts[iAlt]); if (elem->fUsed) { wchar_t wch = elem->wChar == SYM_UNKNOWN ? SYM_UNKNOWN : LocRunDense2Unicode(&g_locRunInfo, elem->wChar); wchar_t wchPrev = elem->wPrevChar == SYM_UNKNOWN ? SYM_UNKNOWN : LocRunDense2Unicode(&g_locRunInfo, elem->wPrevChar); int nPrevStrokes = elem->iPrevAlt == -1 ? 0 : lat->pAltList[iStroke - elem->nStrokes].alts[elem->iPrevAlt].nStrokes; if (0 && wch == 0) { char *color=",color=green,fontcolor=green,weight=2"; if (elem->fCurrentPath) { color=",color=red,fontcolor=red,weight=2"; } fprintf(f, "\"%d\" -> \"%d\" [ label = \"DUMMY (prev U+%04X/%d)\\npr=%.2f,pp=%.2f\"%s ];\n", iStroke-elem->nStrokes,iStroke, wchPrev, nPrevStrokes, elem->logProb, elem->logProbPath, color); } else { char *color=",color=gray,fontcolor=gray,weight=2"; if (elem->fCurrentPath) { color=",color=black,fontcolor=black,weight=2"; } fprintf(f, "\"%d\" -> \"%d\" [ label = \"U+%04X (prev U+%04X/%d)\\nfr=%.2f,ch=%.2f\\nsh1=%.2f,sh2=%.2f\\nun1=%.2f,un2=%.2f\\nbi=%.2f,cb=%.2f\\npr=%.2f,pp=%.2f\"%s ];\n", iStroke-elem->nStrokes,iStroke, wch, wchPrev, nPrevStrokes, (elem->tuneScores.afl[VTUNE_FREE_SEG_UNIGRAM] + elem->tuneScores.afl[VTUNE_FREE_SEG_BIGRAM]) * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_PROB] * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_SHAPE_UNIGRAM] * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_SHAPE_BIGRAM] * elem->iPathLength, elem->tuneScores.afl[VTUNE_UNIGRAM] * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_SMOOTHING_UNIGRAM] * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_BIGRAM] * elem->iPathLength, elem->tuneScores.afl[VTUNE_FREE_CLASS_BIGRAM] * elem->iPathLength, elem->logProb, elem->logProbPath * elem->iPathLength, color); } } } void DumpLattice(char *filename, LATTICE *lat) { if (lat->fSepMode) { int iStroke, iAlt, i; FILE *f; ASSERT(filename!=NULL); ASSERT(lat!=NULL); f=fopen(filename,"w"); if (f == NULL) { fprintf(stderr,"Couldn't open '%s' to dump the lattice.",filename); return; } fprintf(f, "digraph lattice {\n"); // Dump out the answer for (i = 0; i < (int)wcslen(lat->wszAnswer); i++) { if (i > 0) { fprintf(f, " -> "); } fprintf(f, "\"U+%04X\"", lat->wszAnswer[i]); } fprintf(f, "\n"); // This produces a timeline fprintf(f, "\"Stroke -1\""); for (iStroke = 0; iStroke < lat->nStrokes; iStroke++) { fprintf(f, " -> \"Stroke %d\"", iStroke); } fprintf(f, "\n"); // This forces the nodes to be shown in time order for (iStroke = -1; iStroke < lat->nStrokes; iStroke++) { fprintf(f, "{rank=same; \"%d\"; \"Stroke %d\";}\n", iStroke, iStroke); if (iStroke >= 0) { fprintf(f, "\"Stroke %d\" [ label=\"%d-%d\" ];\n", iStroke, lat->pStroke[iStroke].iOrder, lat->pStroke[iStroke].iLast); } } // This produces the actual lattice for (iStroke = lat->nStrokes - 1; iStroke >= 0; iStroke--) { for (iAlt = 0; iAlt < lat->pAltList[iStroke].nUsed; iAlt++) { DumpLatticeElement(f,lat,iStroke,iAlt); } } fprintf(f,"}\n"); fclose(f); } } #else void DumpLatticeElement(FILE *f, LATTICE *lat, int iStroke, int iAlt) { LATTICE_ELEMENT *elem=&(lat->pAltList[iStroke].alts[iAlt]); if (elem->fUsed) { fprintf(f,"Stroke %d alternate %d",iStroke,iAlt); if (elem->fCurrentPath) fprintf(f," on current path\n"); else fprintf(f,"\n"); fprintf(f," nStrokes =%2d wChar =U+%04X score=%g\n",elem->nStrokes,LocRunDense2Unicode(&g_locRunInfo,elem->wChar),elem->score); fprintf(f," nPrevStrokes=%2d wPrevChar=U+%04X\n",elem->nPrevStrokes,LocRunDense2Unicode(&g_locRunInfo,elem->wPrevChar)); fprintf(f," prob=%g probpath=%g\n",elem->logProb,elem->logProbPath); fprintf(f," iPrevAlt=%d\n",elem->iPrevAlt); fprintf(f," bbox xrange=%d-%d, yrange=%d-%d\n",elem->bbox.left,elem->bbox.right,elem->bbox.top,elem->bbox.bottom); fprintf(f," space=%d area=%d\n",elem->space,elem->area); } } void DumpLattice(char *filename, LATTICE *lat) { int iStroke, iAlt; FILE *f; ASSERT(filename!=NULL); ASSERT(lat!=NULL); f=fopen(filename,"w"); if (f==NULL) { fprintf(stderr,"Couldn't open '%s' to dump the lattice.",filename); return; } for (iStroke=lat->nStrokes-1; iStroke>=0; iStroke--) { for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { DumpLatticeElement(f,lat,iStroke,iAlt); fprintf(f,"\n"); } fprintf(f,"\n"); } fclose(f); } #endif #endif // Return the current path. // When called after ProcessLattice, returns the highest probability path. // The memory for the path should be freed by the caller. BOOL GetCurrentPath(LATTICE *lat, LATTICE_PATH **pPath) { int nChars, iStroke, iChar, iAlt, iTestStroke=-1; LATTICE_PATH *path; RECT rectBox, rectPrevBox; BOOL bSpaceEnabled; // currently spaces are only enabled in korean free mode if (!lat->fUseGuide && !lat->fSepMode && !wcsicmp (g_szRecognizerLanguage, L"KOR")) { bSpaceEnabled = TRUE; } else { bSpaceEnabled = FALSE; } ASSERT(lat!=NULL); #ifdef DUMP_LATTICE DumpLattice(LATTICE_FILENAME,lat); #endif #ifdef DUMP_BBOXES { FILE *f = fopen("e:/bbox.txt", "w"); fprintf(f, "canvas .c -width 1000 -height 1000;\n"); fprintf(f, "pack .c;\n"); fclose(f); } #endif nChars = 0; for (iStroke=0; iStrokenStrokes; iStroke++) { // Wipe out all spaces so far lat->pAltList[iStroke].fSpaceAfterStroke = FALSE; for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].fCurrentPath) { nChars++; // Make sure this element of the path connects up to the previous one. ASSERT(iStroke - lat->pAltList[iStroke].alts[iAlt].nStrokes == iTestStroke); iTestStroke = iStroke; } } } // we'll allocate memory for 2 * nChars in anticipation of spaces between chars path=AllocatePath(2 * nChars); if (path==NULL) { return FALSE; } iChar = 0; for (iStroke=0; iStrokenStrokes; iStroke++) { for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].fCurrentPath) { wchar_t dch; // are spaces enabled if (bSpaceEnabled) { // do we have a space before us rectBox = lat->pAltList[iStroke].alts[iAlt].bbox; // we can only have a space before us if we are not the 1st char if (iChar > 0) { int xDel; int iAvgHgt; xDel = rectBox.left - rectPrevBox.right; iAvgHgt = 1 + ( (rectBox.right - rectBox.left) + (rectPrevBox.right - rectPrevBox.left) ) / 2; if ( xDel >= ((int)(iAvgHgt * SPACE_RATIO)) || rectBox.right < rectPrevBox.left ) { // create a phantom path element path->pElem[iChar].iBoxNum = -1; path->pElem[iChar].iStroke = iStroke - lat->pAltList[iStroke].alts[iAlt].nStrokes; path->pElem[iChar].nStrokes = 0; path->pElem[iChar].iAlt = SPACE_ALT_ID; path->pElem[iChar].wChar = SYM_SPACE; // mark the location of the space in the lattice lat->pAltList[path->pElem[iChar].iStroke].fSpaceAfterStroke = TRUE; iChar++; } } // save the rectangle of the current char rectPrevBox = rectBox; } dch = lat->pAltList[iStroke].alts[iAlt].wChar; if (dch==SYM_UNKNOWN) { // In separator mode, we need to return a special symbol for the // skipped ink cases. path->pElem[iChar].wChar = SYM_UNKNOWN; } else { ASSERT(LocRunIsDenseCode(&g_locRunInfo, dch)); path->pElem[iChar].wChar = LocRunDense2Unicode(&g_locRunInfo,dch); } path->pElem[iChar].nStrokes = lat->pAltList[iStroke].alts[iAlt].nStrokes; path->pElem[iChar].iBoxNum = lat->pStroke[iStroke].iBox; path->pElem[iChar].iStroke = iStroke; path->pElem[iChar].iAlt = iAlt; // path->pElem[iChar].scores = lat->pAltList[iStroke].alts[iAlt].score; // path->pElem[iChar].bbox=lat->pAltList[iStroke].alts[iAlt].bbox; #ifdef DUMP_BBOXES { int i; RECT r = lat->pAltList[iStroke].alts[iAlt].writingBox; double s = 1.0 / 100.0; FILE *f = fopen("e:/bbox.txt","a"); fprintf(f, ".c create rect %f %f %f %f -outline black -fill {};\n", r.left * s, r.top * s, r.right * s, r.bottom * s); for (i = iStroke - lat->pAltList[iStroke].alts[iAlt].nStrokes + 1; i <= iStroke; i++) { int j; fprintf(f, ".c create line"); for (j = 0; j < lat->pStroke[i].nInk; j++) { fprintf(f, " %f %f", lat->pStroke[i].pts[j].x * s, lat->pStroke[i].pts[j].y * s); } fprintf(f, " -fill red\n"); } fclose(f); } #endif iChar++; } } } path->nChars = iChar; *pPath=path; return TRUE; } // Given a lattice and a path through it, for characters iStartChar (inclusive) through iEndChar // (exclusive), return the time stamps of the first and last strokes in those characters. // Returns FALSE if there are no strokes associated with the characters (eg, spaces) BOOL GetCharacterTimeRange(LATTICE *lat, LATTICE_PATH *path, int iStartChar, int iEndChar, DWORD *piStartTime, DWORD *piEndTime) { int iStartStroke=-1, iEndStroke=-1; ASSERT(lat!=NULL); ASSERT(path!=NULL); ASSERT(iStartChar>=0 && iStartCharnChars); ASSERT(iEndChar>iStartChar && iEndChar<=path->nChars); while (iStartChar < path->nChars && path->pElem[iStartChar].iAlt == SPACE_ALT_ID) { iStartChar++; } while (iEndChar > 0 && path->pElem[iEndChar - 1].iAlt == SPACE_ALT_ID) { iEndChar--; } if (iStartChar >= iEndChar) { return FALSE; } iStartStroke=path->pElem[iStartChar].iStroke - path->pElem[iStartChar].nStrokes + 1; iEndStroke=path->pElem[iEndChar-1].iStroke; ASSERT(iStartStroke>=0); ASSERT(iEndStroke>=0); ASSERT(iStartStrokenStrokes); ASSERT(iEndStrokenStrokes); *piStartTime=lat->pStroke[iStartStroke].timeStart; *piEndTime=lat->pStroke[iEndStroke].timeEnd; return TRUE; } // Get the number of strokes which have been added to the lattice. int GetLatticeStrokeCount(LATTICE *lat) { ASSERT(lat!=NULL); return lat->nStrokes; } // Given a character in the current path, determine a "guide box" around that character. BOOL GetBoxOfAlternateInCurrentPath(LATTICE *lat, LATTICE_PATH *path, int iChar, RECT *pRect) { int iStroke = path->pElem[iChar].iStroke; int iAlt = path->pElem[iChar].iAlt; // Get the inferred writing box, using either centipede or the old baseline/height database. RECT writingBox = lat->pAltList[iStroke].alts[iAlt].writingBox; // Adjust the writing box so the top is the midline: writingBox.top = (writingBox.top + writingBox.bottom + 1) / 2; // Make sure the box isn't zero or negative sized: if (writingBox.bottom <= writingBox.top) writingBox.bottom = writingBox.top + 1; if (writingBox.right <= writingBox.left) writingBox.right = writingBox.left + 1; *pRect = writingBox; return TRUE; } // Look at the lattice column containing the given character, and find alternates with the same number // of strokes. The alternates are returned in the given array, and the number of alternates found is // returned. int GetAlternatesForCharacterInCurrentPath(LATTICE *lat, LATTICE_PATH *path, int iChar, int nAlts, wchar_t *pwAlts) { int iStroke = path->pElem[iChar].iStroke; int nStrokesInChar = path->pElem[iChar].nStrokes; int cAlts = 0, iAlt; ASSERT(lat!=NULL); ASSERT(lat->nStrokes>0); ASSERT(path!=NULL); // if this is a space char, then we will produce a single entry alt list if (path->pElem[iChar].iAlt == SPACE_ALT_ID) { pwAlts[0] = path->pElem[iChar].wChar; return 1; } // Run through the list of alternates, and find those with // the same number of strokes for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { if (lat->pAltList[iStroke].alts[iAlt].nStrokes==nStrokesInChar) { int i; BOOL found=FALSE; for (i=0; ipAltList[iStroke].alts[iAlt].wChar) { found=TRUE; } } if (!found) { pwAlts[cAlts++]=lat->pAltList[iStroke].alts[iAlt].wChar; if (cAlts==nAlts) { goto breakLoop; } } } } breakLoop: // Now convert the dense codes to Unicode. for (iAlt = 0; iAlt < cAlts; ++iAlt) { // We need to return a special symbol // for the skipped ink case. if (pwAlts[iAlt] != SYM_UNKNOWN) { ASSERT(LocRunIsDenseCode(&g_locRunInfo, pwAlts[iAlt])); pwAlts[iAlt] = LocRunDense2Unicode(&g_locRunInfo, pwAlts[iAlt]); } } return cAlts; } void FreeLatticePath(LATTICE_PATH *path) { ASSERT(path!=NULL); if (path->pElem!=NULL) ExternFree(path->pElem); ExternFree(path); } // Structure used to hold information for the DTW. typedef struct tagDTW { float logProb; // Score for this location in the table int nSubstitutions; // How many character substitutions occurred along this path int iAlt; // Alt list entry in the lattice for this table entry int iPrevChar, iPrevStroke; // Prev character and score for trace back } DTW; // Given a lattice and a string of unicode characters, find the best path through the lattice // which gives that sequence of characters. Baring that, it will find the most likely path // through the lattice with the same number of characters and the minimum number of mismatches // to the prompt. In case no such path can be found, the current path becomes empty. // The function returns the number of substitutions used, or -1 if there is no path with // the desired number of characters, -2 if a memory allocation error occurs, or -3 if a // file write error occurs. int SearchForTargetResultInternal(LATTICE *lat, wchar_t *wszTarget) { wchar_t *wsz; int i, j; int iAlt, iStroke, iChar, nSubs = -1; DTW *pTable, *dtw; ASSERT(lat!=NULL); ASSERT(wszTarget!=NULL); if (wcslen(wszTarget)==0 || lat->nStrokes == 0) { return -1; } // fprintf(stderr,"Beginning SearchForTargetResult\n"); // Allocate the string wsz = (wchar_t*)ExternAlloc((wcslen(wszTarget)+1)*sizeof(wchar_t)); if (wsz==NULL) return -2; // Get rid of unwanted characters (spaces) j=0; for (i = 0; i <= (int)wcslen(wszTarget); i++) if (wszTarget[i]==0 || !iswspace(wszTarget[i])) wsz[j++] = wszTarget[i]; if (wcslen(wsz) == 0) { ExternFree(wsz); return -1; } // Allocate the DTW table pTable=(DTW*)ExternAlloc(sizeof(DTW)*lat->nStrokes*wcslen(wsz)); if (pTable==NULL) { ExternFree(wsz); return -2; } // For each table entry, find the best path to get there for (iStroke=0; iStrokenStrokes; iStroke++) { for (iChar=0; iChar<(int)wcslen(wsz); iChar++) { // Convert the character we're looking for to a dense code SYM dense=LocRunUnicode2Dense(&g_locRunInfo,wsz[iChar]); // To record the best value for this table entry BOOL first=TRUE; int bestAlt=-1; float bestLogProb=0; int bestNSubstitutions=0; int bestPrevChar=-1, bestPrevStroke=-1; // Run through the alternates at this stroke, and see if anything matches for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) { float prevLogProb=0; int prevNSubstitutions=0; // Get the information about the best path leading to this alternate int iPrevChar=iChar-1; int iPrevStroke=iStroke-lat->pAltList[iStroke].alts[iAlt].nStrokes; // Make sure the first character starts at the first stroke. if (iPrevChar==-1 && iPrevStroke!=-1) continue; if (iPrevStroke==-1 && iPrevChar!=-1) continue; // if (iPrevStroke>=0 && iPrevChar==-1) prevLogProb=(float)iPrevStroke+1; // if (iPrevStroke==-1 && iPrevChar>=0) prevLogProb=(float)iPrevChar+1; // Check the previous best path if (iPrevStroke>=0 && iPrevChar>=0) { // If there wasn't a path, then skip this alternative if (pTable[iPrevChar*lat->nStrokes+iPrevStroke].iAlt==-1) continue; prevLogProb=pTable[iPrevChar*lat->nStrokes+iPrevStroke].logProb; prevNSubstitutions=pTable[iPrevChar*lat->nStrokes+iPrevStroke].nSubstitutions; } // If the alternate matches, then record it without increasing the number of substitutions if (lat->pAltList[iStroke].alts[iAlt].wChar==dense) { float thisLogProb=prevLogProb; int thisNSubstitutions=prevNSubstitutions; if (first || thisNSubstitutionsbestLogProb)) { bestAlt=iAlt; bestLogProb=thisLogProb; bestPrevStroke=iPrevStroke; bestPrevChar=iPrevChar; bestNSubstitutions=thisNSubstitutions; first=FALSE; } } else { // Otherwise it is a substitution, so record as such float thisLogProb=/*lat->pAltList[iStroke].alts[iAlt].nStrokes*/prevLogProb; int thisNSubstitutions=prevNSubstitutions+1; if (first || thisNSubstitutionsbestLogProb)) { bestAlt=iAlt; bestLogProb=thisLogProb; bestPrevStroke=iPrevStroke; bestPrevChar=iPrevChar; bestNSubstitutions=thisNSubstitutions; first=FALSE; } } // skip ink /* prevLogProb=(float)lat->pAltList[iStroke].alts[iAlt].nStrokes; if (iPrevStroke>=0) prevLogProb+=pTable[iChar*lat->nStrokes+iPrevStroke].logProb; if (first || prevLogProb=0) prevLogProb+=pTable[iPrevChar*lat->nStrokes+iStroke].logProb; if (first || prevLogProbnStrokes+iStroke].logProb=bestLogProb; pTable[iChar*lat->nStrokes+iStroke].iAlt=bestAlt; pTable[iChar*lat->nStrokes+iStroke].iPrevStroke=bestPrevStroke; pTable[iChar*lat->nStrokes+iStroke].iPrevChar=bestPrevChar; pTable[iChar*lat->nStrokes+iStroke].nSubstitutions=bestNSubstitutions; } } // Dump out some debugging information about which characters were matched. #ifdef DUMP_DTW { int iChar, iStroke; FILE *f=fopen("/dtw.txt","w"); iStroke = lat->nStrokes-1; iChar = wcslen(wsz)-1; // Go back through the DTW lattice to mark the matched path while (iStroke!=-1 && iChar!=-1) { DTW *dtw=pTable+(iChar*lat->nStrokes+iStroke); if (dtw->iAlt!=-1) { fprintf(f,"Character %d (U+%04X) found at iStroke=%d iAlt=%d\n", iChar,wsz[iChar],iStroke,dtw->iAlt); } else { fprintf(f,"Character %d (U+%04X) not found (iAlt=%d)\n", iChar,wsz[iChar],dtw->iAlt); } iStroke=dtw->iPrevStroke; iChar=dtw->iPrevChar; } fclose(f); } #endif // Go back through the DTW lattice to mark the best matched path iStroke = lat->nStrokes-1; iChar = wcslen(wsz)-1; dtw=pTable+(iChar*lat->nStrokes+iStroke); if (dtw->iAlt==-1) { // If we didn't find any path all the way through, don't return any path. nSubs=-1; } else { #ifdef HWX_TUNE if (g_pTuneFile == NULL || g_iTuneMode == 3) #endif { // Wipe out the old path for (iStroke=0; iStrokenStrokes; iStroke++) for (iAlt=0; iAltpAltList[iStroke].nUsed; iAlt++) lat->pAltList[iStroke].alts[iAlt].fCurrentPath=FALSE; } // Get the first step in the path iStroke = lat->nStrokes-1; iChar = wcslen(wsz)-1; dtw=pTable+(iChar*lat->nStrokes+iStroke); nSubs=dtw->nSubstitutions; while (iStroke!=-1 && iChar!=-1) { dtw = pTable + (iChar * lat->nStrokes + iStroke); if (dtw->iAlt!=-1) { #ifdef HWX_TUNE if (g_pTuneFile == NULL || g_iTuneMode == 3) #endif { // If there was a path to this point, mark the alternate lat->pAltList[iStroke].alts[dtw->iAlt].fCurrentPath=TRUE; } #ifdef HWX_TUNE // If there was an correct path up to this point, then dump it out along // with the wrong alternates. if (g_pTuneFile != NULL && g_iTuneMode != 3) { if (lat->fUseGuide || dtw->nSubstitutions == 0) { wchar_t dchCorrect = LocRunUnicode2Dense(&g_locRunInfo, wsz[iChar]); INT idch = dchCorrect; INT iCorrect = -1; int iAlt; BOOL okay; INT nAlts = lat->pAltList[iStroke].nUsed; for (iAlt = 0; iAlt < nAlts; iAlt++) { LATTICE_ELEMENT *elem = lat->pAltList[iStroke].alts + iAlt; if (elem->wChar == dchCorrect) { iCorrect = iAlt; } } // Write out the correct answer okay = (fwrite(&idch, sizeof(INT), 1, g_pTuneFile) == 1); // Write out the index of the correct alternate okay = okay && (fwrite(&iCorrect, sizeof(INT), 1, g_pTuneFile) == 1); // Write out the number of alternates okay = okay && (fwrite(&nAlts, sizeof(INT), 1, g_pTuneFile) == 1); // Write out the alternates for (iAlt = 0; iAlt < nAlts; iAlt++) { LATTICE_ELEMENT *elem = lat->pAltList[iStroke].alts + iAlt; // int iTem = (iAlt == dtw->iAlt); // int iTem = (elem->wChar == dchCorrect); // okay = okay && (fwrite(&iTem, sizeof(int), 1, g_pTuneFile) == 1); // okay = okay && (fwrite(&elem->logProbPath, sizeof(FLOAT), 1, g_pTuneFile) == 1); // okay = okay && (fwrite(&elem->tuneScores, sizeof(VOLCANO_WEIGHTS), 1, g_pTuneFile) == 1); if (g_iTuneMode == 1) { okay = okay && VTuneCompressTuningRecord(g_pTuneFile, &elem->tuneScores); } if (g_iTuneMode == 2) { okay = okay && (fwrite(&elem->logProb, sizeof(FLOAT), 1, g_pTuneFile) == 1); } } if (!okay) { ExternFree(wsz); ExternFree(pTable); return -3; } } } #endif } iStroke=dtw->iPrevStroke; iChar=dtw->iPrevChar; } } ExternFree(wsz); ExternFree(pTable); return nSubs; } // Configuration info VOLCANO_CONFIG g_latticeConfigInfo; // Initialize the lattice configuration to default values. // These defaults may in the future depend on the language // that is loaded. void LatticeConfigInit() { int i; // Also go through the stroke counts setting no recognizer. for (i = 0; i <= VOLCANO_CONFIG_MAX_STROKE_COUNT; i++) { if (i == 0 || i > GLYPH_CFRAMEMAX) { g_latticeConfigInfo.iRecognizers[i] = VOLCANO_CONFIG_ZILLA; } else { g_latticeConfigInfo.iRecognizers[i] = VOLCANO_CONFIG_OTTER; } } } // Update the probabilities in the lattice, including setting current // path to the most likely path so far (including language model). BOOL ProcessLatticeRange (LATTICE *lat, int iStrtStroke, int iEndStroke) { int iStroke, iAlt; ASSERT(lat!=NULL); // invalid stroke range if (iStrtStroke < 0 || iEndStroke >= lat->nStrokes || iStrtStroke > iEndStroke) { return FALSE; } // can only be called in panel free mode if (lat->fUseGuide || lat->fWordMode || lat->fSepMode) { return FALSE; } // Always use probability mode for free input lat->fProbMode=TRUE; // For each stroke within the range for (iStroke = iStrtStroke; iStroke <= iEndStroke; iStroke++) { int maxDist = 0; int nChar1; RECT bbox; INTERVALS xIntervals, yIntervals; // Wipe out the current path ClearAltList(lat->pAltList + iStroke); bbox = lat->pStroke[iStroke].bbox; EmptyIntervals(&xIntervals,bbox.left,bbox.right); EmptyIntervals(&yIntervals,bbox.top,bbox.bottom); // Run through all possible numbers of strokes for this // character for (nChar1=1; nChar1<=MaxStrokesPerCharacter && iStroke-nChar1+1>=iStrtStroke; nChar1++) { VOLCANO_WEIGHTS tuneScores; // Determine the features for this proposed character LATTICE_ELEMENT elem; if (nChar1>1) { RECT other=lat->pStroke[iStroke-nChar1+1].bbox; int xdist=__max(other.left-bbox.right,bbox.left-other.right); int ydist=__max(other.top-bbox.bottom,bbox.top-other.bottom); int dist=__max(xdist,ydist); bbox.left=__min(bbox.left,other.left); bbox.right=__max(bbox.right,other.right); bbox.top=__min(bbox.top,other.top); bbox.bottom=__max(bbox.bottom,other.bottom); dist=xdist/(bbox.right-bbox.left); maxDist=__max(dist,maxDist); ExpandIntervalsRange(&xIntervals,other.left,other.right); ExpandIntervalsRange(&yIntervals,other.top,other.bottom); RemoveInterval(&xIntervals,other.left,other.right); RemoveInterval(&yIntervals,other.top,other.bottom); } elem.fUsed=TRUE; elem.fCurrentPath=FALSE; elem.nStrokes=nChar1; elem.nPrevStrokes=0; elem.wChar=SYM_UNKNOWN; elem.wPrevChar=0; elem.bbox=bbox; ASSERT(bbox.top<=bbox.bottom); ASSERT(bbox.left<=bbox.right); elem.iPrevAlt=-1; elem.area=TotalRange(&xIntervals)+TotalRange(&yIntervals); elem.space=TotalPresent(&xIntervals)+TotalPresent(&yIntervals); elem.maxDist=maxDist; // If this is the first character in the lattice, use single // character statistics if (iStroke-nChar1+1==0) { VTuneZeroWeights(&tuneScores); CheapUnaryProb(nChar1,bbox,elem.space,elem.area, &tuneScores); // Equalize probs across different numbers of strokes elem.logProb = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, &tuneScores); elem.logProbPath = elem.logProb; elem.iPathLength = 1; InsertElement(lat,iStroke,&elem); } else { // Otherwise go through all the previous characters in the // lattice as alternates. BOOL first=TRUE; for (iAlt=0; iAltpAltList[iStroke-nChar1].nUsed; iAlt++) { LATTICE_ELEMENT *prevElem = &lat->pAltList[iStroke-nChar1].alts[iAlt]; float prob, probPath; // Only disallow overlapping boxes in normal recognition mode if (IsOverlappedPath(lat, iStroke - nChar1, iAlt, bbox)) { continue; } VTuneZeroWeights(&tuneScores); // If we're not in separator mode, then prune based on the // segmentation score CheapBinaryProb(nChar1,bbox,elem.space,elem.area, prevElem->nStrokes,prevElem->bbox,prevElem->space,prevElem->area, &tuneScores); prob = VTuneComputeScoreNoLM(&g_vtuneInfo.pTune->weights, &tuneScores); // Equalize probs across different numbers of strokes probPath = (prob + prevElem->logProbPath * prevElem->iPathLength) / (prevElem->iPathLength + 1); if (first || probPath > elem.logProbPath) { first=FALSE; elem.iPathLength = prevElem->iPathLength + 1; elem.logProb=prob; elem.logProbPath=probPath; elem.nPrevStrokes=prevElem->nStrokes; elem.iPrevAlt=iAlt; } } if (!first) { InsertElement(lat,iStroke,&elem); } } } } PruneLattice(lat, iStrtStroke, iEndStroke); // For each stroke in the lattice for (iStroke = iStrtStroke; iStroke <= iEndStroke; iStroke++) { BuildRecogAlts(lat, iStroke, TRUE); } if (lat->pAltList[iEndStroke].nUsed==0) { // Well, the recognizer came back and said there were no candidates. // Let's put in a dummy alt so the user will see something, and also so // the language model will have a path to work with. FakeRecogResult(lat, iEndStroke, iEndStroke - iStrtStroke + 1, -PENALTY_SKIP_INK); } return TRUE; } void FixupBackPointers (LATTICE *pLat) { int iStrk, iAlt, iPrevStrk, iPrevAlt, cChar; iPrevStrk = 0; iPrevAlt = -1; cChar = 0; // all strokes for (iStrk = 0; iStrk < pLat->nStrokes; iStrk++) { // every alt at this stroke for (iAlt = 0; iAlt < pLat->pAltList[iStrk].nUsed; iAlt++) { // is it part of the best path if (pLat->pAltList[iStrk].alts[iAlt].fCurrentPath) { // the number of strokes should be correct because this is // how we set the fCurrent flags, so assert otherwise ASSERT (pLat->pAltList[iStrk].alts[iAlt].nStrokes == (iStrk - iPrevStrk + 1)); // set the prev alt pLat->pAltList[iStrk].alts[iAlt].iPrevAlt = iPrevAlt; pLat->pAltList[iStrk].alts[iAlt].iPathLength = (++cChar); // update the prev strk and alt iPrevStrk = iStrk + 1; iPrevAlt = iAlt; } } } }