3877 lines
126 KiB
C
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;
|
|
}
|
|
}
|
|
}
|
|
}
|