windows-nt/Source/XPSP1/NT/drivers/tpg/hwx/volcano/dll/lattice.c
2020-09-26 16:20:57 +08:00

3877 lines
126 KiB
C

//$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
//
// 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 <math.h>
#include <stdlib.h>
#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 <stdio.h>
#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 (val<Log2Range) val=Log2Range;
if (val>0) 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; iInk<nInk; iInk++) {
pStroke->bbox.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; iAlt<lat->pAltList[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<MaxAltsPerStroke)
newLength++;
// Go through all the potential places for the given element.
for (iAlt=0; iAlt<newLength; iAlt++)
// If we are beyond the end of the existing elements in the list, or we beat the current element
if (iAlt > 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; i<MaxAltsPerStroke; i++) list->alts[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; i<lat->nStrokes; 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; iStroke<nNewAlloc; iStroke++) ClearAltList(&ppNewElem[iStroke]);
if (lat->nStrokesAllocated>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; iAlt<lat->pAltList[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; i<nProbAlts; i++) {
#ifdef USE_OLD_DATABASES
double logProbAlt = FloatClippedLog2((int)probAlts[i].prob,65535);
#else
double logProbAlt = -(double)probAlts[i].prob/(double)SCORE_SCALE;
ASSERT(SCORE_BASE==2);
#endif
wAlts[i]=probAlts[i].wch;
valueAlts[i]=(FLOAT)(probScore+logProbAlt);
if (!lat->fUseGuide)
{
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; i<nScoreAlts; i++) {
wAlts[i]=scoreAlts[i].wch;
valueAlts[i]=scoreAlts[i].prob;
}
return nScoreAlts;
}
}
#define PENALTY_ALC_PRIORITY 1000
// Sort the alt list so that all characters matching alcPriority are above
// all those that do not.
void ApplyALCPriority(LATTICE *lat, int nAlts, wchar_t *wAlts, VOLCANO_WEIGHTS *pTuneScores)
{
wchar_t wTmp[MaxAltsPerStroke];
VOLCANO_WEIGHTS aTuneScoresTmp[MaxAltsPerStroke];
FLOAT penalty;
BOOL fPenaltySet = FALSE;
int iUsed = 0, i;
BOOL fNonPriorityFound = FALSE, fPriorityBelowNonPriority = FALSE;
CHARSET charSet;
charSet.recmask = lat->recogSettings.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.right<r2.left || r1.top>r2.bottom || r1.bottom<r2.top)
return FALSE;
return TRUE;
}
// See if the given bounding box overlaps with any previous character in the
// best path, starting from alternate iPrevAlt at iPrevStroke.
BOOL IsOverlappedPath(LATTICE *lat, int iPrevStroke, int iPrevAlt, RECT bbox)
{
while (iPrevStroke!=-1) {
LATTICE_ELEMENT *elem=lat->pAltList[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; iAlt<lat->pAltList[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 && iStroke<lat->nStrokes);
// 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; iAlt<bboxAlts.nUsed; iAlt++) {
// We continue only if either there is more space in the alt list, or if it is filled, only
// if there is a chance that the alternates we are going to insert will have a high enough
// probability to get on to the list.
if (1 /*lat->pAltList[iStroke].nUsed<MaxAltsPerStroke ||
bboxAlts.alts[iAlt].logProbPath>lat->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; iRecogAlt<nRecogAlts; iRecogAlt++)
{
LATTICE_ELEMENT elem;
wchar_t unicode = (wAlts[iRecogAlt] != SYM_UNKNOWN) ? LocRunDense2Unicode(&g_locRunInfo,wAlts[iRecogAlt]) : SYM_UNKNOWN;
// Set up the alternate
elem.fUsed=TRUE;
elem.fCurrentPath=FALSE;
elem.wChar=wAlts[iRecogAlt];
elem.wPrevChar=0;
elem.nStrokes=bboxAlts.alts[iAlt].nStrokes;
elem.nPrevStrokes=0;
elem.iPrevAlt=-1;
elem.space=bboxAlts.alts[iAlt].space;
elem.area=bboxAlts.alts[iAlt].area;
elem.bbox=bboxAlts.alts[iAlt].bbox;
elem.writingBox=GuessWritingBox(elem.bbox,elem.wChar);
elem.maxDist=bboxAlts.alts[iAlt].maxDist;
elem.score=score;
#ifdef USE_IFELANG3_BIGRAMS
elem.nBigrams=0;
#endif
// If this alternate is the first character in the lattice, then it
// gets handled differently.
if (iStroke-elem.nStrokes+1==0)
{
int iAnswerPtr = IsAllowedNextChar(lat, elem.wChar, 0,
((iStroke == lat->nStrokes - 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; iPrevAlt<lat->pAltList[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; iStroke<lat->nStrokes; 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; iStroke<lat->nStrokes; 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 (iDest<iStroke)
lat->pStroke[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; iStroke<lat->nStrokes; 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; iStroke<lat->nStrokes; 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; iStroke<lat->nStrokes; 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 <stdio.h>
#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; iAlt<lat->pAltList[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 <stdio.h>
#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; iAlt<lat->pAltList[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; iStroke<lat->nStrokes; iStroke++)
{
// Wipe out all spaces so far
lat->pAltList[iStroke].fSpaceAfterStroke = FALSE;
for (iAlt=0; iAlt<lat->pAltList[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; iStroke<lat->nStrokes; iStroke++)
{
for (iAlt=0; iAlt<lat->pAltList[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 && iStartChar<path->nChars);
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(iStartStroke<lat->nStrokes);
ASSERT(iEndStroke<lat->nStrokes);
*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; iAlt<lat->pAltList[iStroke].nUsed; iAlt++)
{
if (lat->pAltList[iStroke].alts[iAlt].nStrokes==nStrokesInChar)
{
int i;
BOOL found=FALSE;
for (i=0; i<cAlts; i++)
{
if (pwAlts[i]==lat->pAltList[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; iStroke<lat->nStrokes; 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; iAlt<lat->pAltList[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 || thisNSubstitutions<bestNSubstitutions || (thisNSubstitutions==bestNSubstitutions && thisLogProb>bestLogProb)) {
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 || thisNSubstitutions<bestNSubstitutions || (thisNSubstitutions==bestNSubstitutions && thisLogProb>bestLogProb)) {
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<bestLogProb) {
bestAlt=-1;
bestLogProb=prevLogProb;
bestPrevStroke=iPrevStroke;
bestPrevChar=iChar;
first=FALSE;
} */
}
// skip char
/* prevLogProb=1;
if (iPrevChar>=0) prevLogProb+=pTable[iPrevChar*lat->nStrokes+iStroke].logProb;
if (first || prevLogProb<bestLogProb) {
bestAlt=-1;
bestLogProb=prevLogProb;
bestPrevStroke=iStroke;
bestPrevChar=iPrevChar;
first=FALSE;
} */
// Record the best path
pTable[iChar*lat->nStrokes+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; iStroke<lat->nStrokes; iStroke++)
for (iAlt=0; iAlt<lat->pAltList[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; iAlt<lat->pAltList[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;
}
}
}
}