#include "crane.h" #include "algo.h" // Must include first to get new definitions #include "common.h" #include "cranep.h" QHEAD *gapqhList[30]; QNODE *gapqnList[30]; #define FRAME_MAXSTEPS 128 // Index of the four points that make up the bounds of the line in a self relitive // bounding box. We store the index of each point and its offset. typedef struct BOUNDS { int dX; // Delta X and Y of line segment int dY; int iAlongPlus; // Along the line in plus direction int oAlongPlus; int iAlongMinus; // Along to the line in minus direction int oAlongMinus; int iAwayPlus; // Away from the line in plus direction int oAwayPlus; int iAwayMinus; // Away from the line in minus direction int oAwayMinus; } BOUNDS; // Map simple features into index used for two feature map table static const aMapFeat[] = { 0, 1, 2, 3, 4, // FEAT_RIGHT, FEAT_DOWN_RIGHT, FEAT_DOWN, FEAT_OTHER, FEAT_COMPLEX 5, 5, 5, 5, // FEAT_CLOCKWISE_4, FEAT_CLOCKWISE_3, FEAT_CLOCKWISE_2, FEAT_CLOCKWISE_1 6, 6, 6, 6, // FEAT_C_CLOCKWISE_1, FEAT_C_CLOCKWISE_2, FEAT_C_CLOCKWISE_3, FEAT_C_CLOCKWISE_4 }; // Map two feature features into correct feature codes static const aMapTwoFeat[7][7] = { // Right Down-Right Down Other Complex Clockwise Counter-Clockwise { FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_COMPLEX, FEAT_2F_RI_CW, FEAT_2F_RI_CC }, // Right { FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_COMPLEX, FEAT_2F_DR_CW, FEAT_2F_DR_CC }, // Down-Right { FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_COMPLEX, FEAT_2F_DN_CW, FEAT_2F_DN_CC }, // Down { FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_2F_OTHER, FEAT_COMPLEX, FEAT_2F_OT_CW, FEAT_2F_OT_CC }, // Other { FEAT_COMPLEX, FEAT_COMPLEX, FEAT_COMPLEX, FEAT_COMPLEX, FEAT_COMPLEX, FEAT_COMPLEX, FEAT_COMPLEX }, // Complex { FEAT_2F_CW_RI, FEAT_2F_CW_DR, FEAT_2F_CW_DN, FEAT_2F_CW_OT, FEAT_COMPLEX, FEAT_2F_CW_CW, FEAT_2F_CW_CC }, // Clockwise { FEAT_2F_OTHER, FEAT_2F_CC_DR, FEAT_2F_CC_DN, FEAT_2F_CC_OT, FEAT_COMPLEX, FEAT_2F_CC_CW, FEAT_2F_CC_CC }, // C-Clockwise }; // Find the points that define the bounding box reletive to the line between // the start and end points. void FindBounds(int cXYStart, int cXYEnd, XY *pXY, BOUNDS *pBounds) { int iXY; int dX, dY; BOUNDS initial; // Store initial values for later normalization ASSERT(cXYStart <= cXYEnd); // Get delta X and delta Y of (end - start) for our calculations. dX = pXY[cXYEnd].x - pXY[cXYStart].x; dY = pXY[cXYEnd].y - pXY[cXYStart].y; if (dX == 0 && dY == 0) { dX = 1; // If both dX and dY are 0, force dX to be 1 so we have a real line } // Set initial limits. initial.dX = dX; initial.dY = dY; initial.iAlongPlus = cXYEnd; initial.oAlongPlus = pXY[cXYEnd].y * dY + pXY[cXYEnd].x * dX; initial.iAlongMinus = cXYStart; initial.oAlongMinus = pXY[cXYStart].y * dY + pXY[cXYStart].x * dX; initial.iAwayPlus = cXYStart; initial.oAwayPlus = pXY[cXYStart].y * dX - pXY[cXYStart].x * dY; initial.iAwayMinus = cXYStart; initial.oAwayMinus = initial.oAwayPlus; *pBounds = initial; // Now process all the other points to see how they fall relitive to the line between // the start and the end. for (iXY = cXYStart + 1; iXY < cXYEnd; ++iXY) { int along, away; // First calculate the along and away offset. along = pXY[iXY].y * dY + pXY[iXY].x * dX; away = pXY[iXY].y * dX - pXY[iXY].x * dY; // Now see how it compares to the current limits. if (along > pBounds->oAlongPlus) { pBounds->oAlongPlus = along; pBounds->iAlongPlus = iXY; } else if (along < pBounds->oAlongMinus) { pBounds->oAlongMinus = along; pBounds->iAlongMinus = iXY; } if (away > pBounds->oAwayPlus) { pBounds->oAwayPlus = away; pBounds->iAwayPlus = iXY; } else if (away < pBounds->oAwayMinus) { pBounds->oAwayMinus = away; pBounds->iAwayMinus = iXY; } } // Finally normalize the offsets. pBounds->oAlongPlus -= initial.oAlongPlus; pBounds->oAlongMinus -= initial.oAlongMinus; pBounds->oAwayPlus -= initial.oAwayPlus; pBounds->oAwayMinus -= initial.oAwayMinus; } // Recursively step line segements // iRecursion is available for debug/tunning use, and not otherwise used. void StepLineSegment(int cXYStart, int cXYEnd, XY *pXY, int *pCStep, STEP *rgStep, int iRecursion) { BOUNDS bounds; int points[6]; int ii; int normalize; int ratio; // Check for 1 or two point lines. if (cXYEnd - cXYStart < 2) { int dX, dY; int deltaAngle; ASSERT(cXYEnd >= cXYStart); if (cXYEnd == cXYStart) { ASSERT(cXYStart == 0); // Only should be passed one point if stroke is one point. ASSERT(*pCStep == 0); // We have to fake a feature for one point. rgStep[*pCStep].length = 0; rgStep[*pCStep].angle = 0; ANGLEDIFF(0, rgStep[*pCStep].angle, deltaAngle); rgStep[*pCStep].deltaAngle = (short)deltaAngle; // Delta from 0. if (*pCStep < FRAME_MAXSTEPS - 1) { ++*pCStep; } else { //ASSERT(*pCStep < FRAME_MAXSTEPS - 1); } rgStep[*pCStep].x = pXY[cXYEnd].x; rgStep[*pCStep].y = pXY[cXYEnd].y; return; } // Two points always form a line (as long as they arn't the same point). // So go ahead and add it. Calculate slope times 10 and the direction of // segment. Also length and angle dX = pXY[cXYEnd].x - pXY[cXYStart].x; dY = pXY[cXYEnd].y - pXY[cXYStart].y; rgStep[*pCStep].length = (short)Distance(dX, dY); rgStep[*pCStep].angle = (short)Arctan2(dY, dX); if (*pCStep == 0) { ANGLEDIFF(0, rgStep[*pCStep].angle, deltaAngle); rgStep[*pCStep].deltaAngle = (short)deltaAngle; // Delta from 0. } else { ANGLEDIFF(rgStep[*pCStep - 1].angle, rgStep[*pCStep].angle, deltaAngle); rgStep[*pCStep].deltaAngle = (short)deltaAngle; // Delta from last angle } if (*pCStep < FRAME_MAXSTEPS - 1) { ++*pCStep; } else { //ASSERT(*pCStep < FRAME_MAXSTEPS - 1); } rgStep[*pCStep].x = pXY[cXYEnd].x; rgStep[*pCStep].y = pXY[cXYEnd].y; return; } // Find initial bounds. FindBounds(cXYStart, cXYEnd, pXY, &bounds); // Figure out which points to use as boundries. Build them up in an array. points[0] = cXYStart; ii = 1; // Start with 'along' points. We assume any extent in the 'along' direction // requires new points. if (bounds.iAlongMinus != cXYStart) { points[ii++] = bounds.iAlongMinus; } if (bounds.iAlongPlus != cXYEnd) { points[ii++] = bounds.iAlongPlus; } // Now the 'away' points. These require a minimum ratio to be selected. // Since the ration is usually a fraction, and we want to handle integers, // multiply by 1000. normalize = bounds.dX * bounds.dX + bounds.dY * bounds.dY; normalize = max(1,normalize); ratio = (bounds.oAwayPlus * 1000) / normalize; if (ratio >= 180) // Ratio > ?? { points[ii++] = bounds.iAwayPlus; } ratio = -(bounds.oAwayMinus * 1000) / normalize; if (ratio >= 180) // Ratio > ?? { points[ii++] = bounds.iAwayMinus; } // See if we need to recurse or not. if (ii == 1) { int deltaAngle; // We have a straight line. Add the end point, and we are done with this path down. // Calculate slope times 10 and the direction of segment rgStep[*pCStep].length = (short)Distance(bounds.dX, bounds.dY); rgStep[*pCStep].angle = (short)Arctan2(bounds.dY, bounds.dX); if (*pCStep == 0) { ANGLEDIFF(0, rgStep[*pCStep].angle, deltaAngle); rgStep[*pCStep].deltaAngle = (short)deltaAngle; // Delta from 0. } else { ANGLEDIFF(rgStep[*pCStep - 1].angle, rgStep[*pCStep].angle, deltaAngle); rgStep[*pCStep].deltaAngle = (short)deltaAngle; // Delta from last angle } if (*pCStep < FRAME_MAXSTEPS - 1) { ++*pCStep; } else { //ASSERT(*pCStep < FRAME_MAXSTEPS - 1); } rgStep[*pCStep].x = pXY[cXYEnd].x; rgStep[*pCStep].y = pXY[cXYEnd].y; } else { int jj; // Have at least two sub-segments, process them. First sort into index order. // Note that start point is already in the correct position. // // privsort(points + 1, ii - 1); // // The following code replaces the call to privsort because // it's such a tiny array (5 or less I think) and privsort is so big. // { int bSort; do { bSort = FALSE; for (jj = 1; jj < ii - 1; jj++) { if (points[jj] > points[jj + 1]) { int tmp; tmp = points[jj]; points[jj] = points[jj + 1]; points[jj + 1] = tmp; bSort = TRUE; } } } while (bSort); } // Add end point, it will also be automatically in the correct position. points[ii] = cXYEnd; // Sequence through each adjacent pair of point to process the segments. for (jj = 0; jj < ii; ++jj) { // We can have duplicate points, ignore them. if (points[jj] != points[jj + 1]) { StepLineSegment(points[jj], points[jj + 1], pXY, pCStep, rgStep, iRecursion + 1); } } } } // Remove the 'splash' of random points sometimes caused by pen down or pen up. UINT RemovePenNoise(XY *rgxy, UINT cxy, BOOL fBegin, UINT ixyStop) { UINT ixy; int dx, dy; BOOL fSmall; UINT ixyEnd; ixyEnd = cxy - 1; // Limit splash zone if the stroke is smaller than 4 times the normal splash zone. dx = rgxy[0].x - rgxy[ixyEnd].x; dy = rgxy[0].y - rgxy[ixyEnd].y; dx /= 4; dy /= 4; fSmall = LineInSplash(dx, dy) ? 1 : 0; if (fBegin) { for (ixy = 0; ixy < ixyStop; ixy++) { dx = rgxy[ixy].x - rgxy[ixy + 1].x; dy = rgxy[ixy].y - rgxy[ixy + 1].y; if (!LineSmall(dx, dy)) { // Next jump is too big to just remove, check distance from start point. dx = rgxy[0].x - rgxy[ixy + 1].x; dy = rgxy[0].y - rgxy[ixy + 1].y; if (fSmall || !LineInSplash(dx, dy)) { // Doesn't fall in splash zone. break; } } } } else { for (ixy = ixyEnd; ixy > ixyStop; ixy--) { dx = rgxy[ixy].x - rgxy[ixy - 1].x; dy = rgxy[ixy].y - rgxy[ixy - 1].y; if (!LineSmall(dx, dy)) { // Next jump is too big to just remove, check distance from end point. dx = rgxy[ixyEnd].x - rgxy[ixy - 1].x; dy = rgxy[ixyEnd].y - rgxy[ixy - 1].y; if (fSmall || !LineInSplash(dx, dy)) { // Doesn't fall in splash zone. break; } } } } return(ixy); } UINT DebounceStrokePoints(XY *rgxy, UINT cxy) { UINT iBounceBegin, iBounceEnd; // Verify that we have enough points that we can debounce. if (cxy < 3) { return(cxy); } // Remove pen noise from pen-down and pen-up iBounceBegin = RemovePenNoise(rgxy, cxy, TRUE, cxy - 2); iBounceEnd = RemovePenNoise(rgxy, cxy, FALSE, iBounceBegin + 1); if (iBounceBegin > 0 || iBounceEnd < cxy - 1) { ASSERT(iBounceBegin < iBounceEnd); ASSERT(iBounceEnd < cxy); cxy = iBounceEnd - iBounceBegin + 1; memmove((VOID *)rgxy, (VOID *)(&rgxy[iBounceBegin]), sizeof(XY) * cxy); } return(cxy); } int StepsFromFRAME(FRAME *frame, STEP *rgStep, int cstepmax) { int cStep; int cXY; XY *pXY; // Get pointer to data and count of points cXY = frame->info.cPnt; pXY = frame->rgrawxy; // If a previous recognizer used this buffer, release it first if (frame->rgsmoothxy) ExternFree(frame->rgsmoothxy); // Smoothing has been replaced by de-splashing. JRB: Clean up to call de-splash directly. frame->rgsmoothxy = (XY *) ExternAlloc(cXY * sizeof(XY)); if (frame->rgsmoothxy == (XY *) NULL) return 0; memcpy(frame->rgsmoothxy, pXY, cXY * sizeof(XY)); frame->csmoothxy = cXY; frame->csmoothxy = DebounceStrokePoints(frame->rgsmoothxy, frame->csmoothxy); cXY = frame->csmoothxy; pXY = frame->rgsmoothxy; // Recursivly segment the line. cStep = 0; rgStep[0].x = pXY[0].x; rgStep[0].y = pXY[0].y; StepLineSegment(0, cXY - 1, pXY, &cStep, rgStep, 0); return(cStep); } // Convert an angle into a feature. BYTE Code(int angle) { if (angle < 12) return FEAT_RIGHT; else if (angle < 70) return FEAT_DOWN_RIGHT; else if (angle < 160) return FEAT_DOWN; else if (angle < 300) return FEAT_OTHER; // very rare directions else return FEAT_RIGHT; } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // We set this to 0xFF, this should be changed if the number of valid codes is excpected // to be more than 255 #define FEATURE_NULL 0xFF // avoid any valid code. // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // Convert steps into first pass at features. // Note that cFrame is not used. // JRB: Rewrite so that we just create one feature, instead of creating many and undoing it!!! VOID AddStepsKPROTO(KPROTO *pKProto, STEP *rgStep, int cStep, int cFrame, FRAME *frame) { int iStep; // Step being processed int cFeat; // Count of features so far in character int iStartStep; // Start of current feature. int iEndStep; // Current quess at end of step. int cumAngle; // Cumulitive change in angle for the current feature. int netAngle; // Net angle traversed by stroke (sum of step angles) int absAngle; // Total angle of stroke (sum of abs. val. of step angles) int nLength; // Total length of stroke int x, y; PRIMITIVE *pFeat = NULL; ASSERT(cStep > 0); // Spot to add next feature. cFeat = pKProto->cfeat; pFeat = (PRIMITIVE *)0; // Loop over all the steps finding features and gathering information about them. iStartStep = 0; iEndStep = 0; x = rgStep[0].x; y = rgStep[0].y; netAngle = 0; absAngle = 0; nLength = 0; for (iStep = 0; iStep < cStep; iStep++) { // Handle start of a feature. if (iStep == iEndStep) { // Get pointer to feature structure. pFeat = &(pKProto->rgfeat[cFeat]); // The array of primitives is CPRIMMAX in size so // the largest index it can handle is (CPRIMMAX-1). if (cFeat < (CPRIMMAX - 1)) { cFeat++; } else { //ASSERT(cFeat < (CPRIMMAX - 1)); } // Record start of feature as initial point or end of last feature. pFeat->x1 = (short)x; pFeat->y1 = (short)y; // Feature not known yet pFeat->code = FEATURE_NULL; // Default to making feature the rest of the line. iStartStep = iStep; iEndStep = cStep; cumAngle = 0; } ASSERT(pFeat); // End of current step x = rgStep[iStep + 1].x; y = rgStep[iStep + 1].y; // Add the step length to the total length nLength += Distance(x - rgStep[iStep].x, y - rgStep[iStep].y); // If not first step in this feature, add angle from last step. if (iStep > iStartStep) { cumAngle += rgStep[iStep].deltaAngle; } // If not first step, add angle from last step. if (iStep > 0) { netAngle += rgStep[iStep].deltaAngle; if (rgStep[iStep].deltaAngle >= 0) { absAngle += rgStep[iStep].deltaAngle; } else { absAngle -= rgStep[iStep].deltaAngle; } } // Decide if we need to split and start a new step. Can only be an inflection // if there are at least three segements left. Also if we are already breaking after // this step, we shouldn't check until we are past the break. if (cStep - iStep >= 3 && iEndStep != iStep + 1) { int angle1_2, angle2_3; // Figure angles between first & second segments and second & third segments. angle1_2 = rgStep[iStep + 1].deltaAngle; angle2_3 = rgStep[iStep + 2].deltaAngle; // Should never have two consecutive steps with exactly the same angle. // ASSERT(angle1_2 != 0); // ASSERT(angle2_3 != 0); // Are they in different directions? E.g. are signs different. // JRB: What about 180 or angles close to it? if (angle1_2 * angle2_3 < 0) { int abs1_2, abs2_3; // We have an inflection, which side do we break on? First get absolute // value of both angles. if (angle1_2 < 0) { abs1_2 = -angle1_2; abs2_3 = angle2_3; } else { abs1_2 = angle1_2; abs2_3 = -angle2_3; } // Now which is larger (e.g. tighter turn) if (abs1_2 >= abs2_3) { iEndStep = iStep + 1; // First is tighter, break there. } else { iEndStep = iStep + 2; // Second is tighter, break there. } } } // Handle end of a feature. if (iStep == iEndStep - 1) { int dX, dY; // Record end of feature. pFeat->x2 = (short)x; pFeat->y2 = (short)y; // Figure out feature code. if (cumAngle == 0) { pFeat->code = Code(rgStep[iStep].angle); // Streight line } else if (cumAngle > 0) { // Clockwise, how strong? if (cumAngle >= 390) { pFeat->code = FEAT_COMPLEX; } else if (cumAngle >= 300) { pFeat->code = FEAT_CLOCKWISE_4; } else if (cumAngle >= 182) { pFeat->code = FEAT_CLOCKWISE_3; } else if (cumAngle >= 120) { pFeat->code = FEAT_CLOCKWISE_2; } else if (cumAngle >= 38) { pFeat->code = FEAT_CLOCKWISE_1; } else { // Close enough to streight. dX = pFeat->x2 - pFeat->x1; dY = pFeat->y2 - pFeat->y1; pFeat->code = Code(Arctan2(dY, dX)); } } else // cumAngle < 0 { // Counter clockwise, how strong? if (cumAngle <= -360) { pFeat->code = FEAT_COMPLEX; } else if (cumAngle <= -182) { pFeat->code = FEAT_C_CLOCKWISE_4; } else if (cumAngle <= -115) { pFeat->code = FEAT_C_CLOCKWISE_3; } else if (cumAngle <= -60) { pFeat->code = FEAT_C_CLOCKWISE_2; } else if (cumAngle <= -38) { pFeat->code = FEAT_C_CLOCKWISE_1; } else { // Close enough to streight. dX = pFeat->x2 - pFeat->x1; dY = pFeat->y2 - pFeat->y1; pFeat->code = Code(Arctan2(dY, dX)); } } } } ASSERT(pFeat->code != FEATURE_NULL); // Map multi-feature strokes down to a single feature. pFeat = &(pKProto->rgfeat[pKProto->cfeat]); pFeat->cFeatures = cFeat - pKProto->cfeat; pFeat->cSteps = (BYTE)cStep; pFeat->fDakuTen = 0; pFeat->nLength = nLength; pFeat->netAngle = netAngle; pFeat->absAngle = absAngle; pFeat->boundingBox = *RectFRAME(frame); switch (pFeat->cFeatures) { case 0 : case 1 : // Zero or one feature. Zero should not actually happen, but if it does ... goto hadSingle; case 2 : { BYTE feat0, feat1; // Two features, look up what the replacement feature is. feat0 = pFeat[0].code; feat1 = pFeat[1].code; pFeat->code = (BYTE)aMapTwoFeat[aMapFeat[feat0]][aMapFeat[feat1]]; break; } case 3 : { BYTE feat0, feat1, feat2; // A few special cases, otherwise call it complex. feat0 = pFeat[0].code; feat1 = pFeat[1].code; feat2 = pFeat[2].code; switch (aMapFeat[feat0]) { case 0 : // Right switch (aMapFeat[feat1]) { case 3 : // Down pFeat->code = (aMapFeat[feat2] == 5) ? FEAT_3F_RI_DN_CW : FEAT_COMPLEX; // Clockwise break; case 5 : // Clockwise pFeat->code = (feat2 == FEAT_DOWN) ? FEAT_3F_RI_CW_DN : FEAT_COMPLEX; // Down break; case 6 : // Counter clockwise pFeat->code = (feat2 == FEAT_DOWN) // Down ? FEAT_3F_RI_CC_DN : (aMapFeat[feat2] == 5) ? FEAT_3F_RI_CC_CW : FEAT_COMPLEX; // Clockwise break; default : pFeat->code = FEAT_COMPLEX; break; } break; case 2 : // Down if (feat1 == FEAT_RIGHT) { // Right // Counter clockwise pFeat->code = (aMapFeat[feat2] == 6) ? FEAT_3F_DN_RI_CC : FEAT_COMPLEX; } else if (aMapFeat[feat1] == 5) { // Clockwise switch (aMapFeat[feat2]) { case 0 : pFeat->code = FEAT_3F_DN_CW_RI; break; // Right case 5 : pFeat->code = FEAT_3F_DN_CW_CW; break; // Clockwise case 6 : pFeat->code = FEAT_3F_DN_CW_CC; break; // Counter clockwise default : pFeat->code = FEAT_COMPLEX; break; } } else { pFeat->code = FEAT_COMPLEX; } break; case 5 : // Clockwise // Any and Clockwise pFeat->code = (aMapFeat[feat2] == 5) ? FEAT_3F_CW_XX_CW : FEAT_COMPLEX; break; case 6 : // Counter Clockwise // Clockwise and Right pFeat->code = (aMapFeat[feat1] == 5 && feat2 == FEAT_RIGHT) ? FEAT_3F_CC_CW_RI : FEAT_COMPLEX; break; default: pFeat->code = FEAT_COMPLEX; } } default: // Anything else (> 3) is always complex. pKProto->rgfeat[cFeat].code = FEAT_COMPLEX; break; } // Actually merge the position information and fix cFeat. pFeat->x2 = pKProto->rgfeat[cFeat - 1].x2; pFeat->y2 = pKProto->rgfeat[cFeat - 1].y2; cFeat = pKProto->cfeat + 1; hadSingle: // Jump here to skip merging of features. // Record number of features after our additions. pKProto->cfeat = (WORD)cFeat; } // Clean up the stepped stroke. cFrame is not used. int CraneSmoothSteps(STEP *rgStep, int cStep, int cFrame) { int ii; int cRemove; int totalLength, hookLength; int maxLength, maxSegment; int length; int lengths[6]; // First three and last three segment lengths. JRB: already calculated // Make sure we have enough segments to think about removing some. if (cStep < 2) { return cStep; // Only one segment, don't want to remove it. } else if (cStep == 2) { int percentLength; int angle; int partA, partB; int experA, experB; // Special processing for two step strokes. The only thing we may do // is drop one of the steps. We calculate the length of the first step // as a percent of the total line length. We also calculate the angle // between the lines. This gives us a rectangle with the ranges, // percentLength 0 - 100 and angle -179 to 180. We want to find if // this sample falls in one of the four corners of this that indicates // an extranious hook. The four lines (and there equations) we use are: // Lower left -180, 40 -> -30, 0 -- angle, percentLength // Lower right 180, 40 -> 30, 0 // Upper left -180, 60 -> -30, 100 // Upper right 180, 60 -> 30, 100 // The matching formulas for the areas beyound the lines are: // Lower left 4 * angle + 15 * percentLength <= - 120 // Lower right 4 * angle - 15 * percentLength >= 120 // Upper left 4 * angle - 15 * percentLength <= -1620 // Upper right 4 * angle + 15 * percentLength >= 1620 totalLength = rgStep[0].length + rgStep[1].length; percentLength = (rgStep[0].length * 100) / totalLength; angle = rgStep[1].deltaAngle; // Precalculate the pieces. partA = 4 * angle; partB = 15 * percentLength; experA = partA + partB; experB = partA - partB; // Looks OK, keep both segments. // Test the two corners that indicate the first segment is two small. // Then test the other two, to check the second segment. if (experA <= -120 || experB >= 120) { // Delete the first segment. rgStep[0] = rgStep[1]; rgStep[1] = rgStep[2]; return 1; } else if (experB <= -1620 || experA >= 1620) { // Delete the second segment. return 1; } return 2; } // Check for hooks. totalLength = 0; maxLength = 0; maxSegment = -1; for (ii = 0; ii < cStep; ++ii) { int fromEnd; // Figure segment length length = rgStep[ii].length; // Keep track of longest segment. if (maxLength < length) { maxLength = length; maxSegment = ii; } // Sum all segment lengths. totalLength += length; // Record the lengths that we will need later. if (ii < 3) { lengths[ii] = length; } fromEnd = cStep - 1 - ii; if (fromEnd < 3) { lengths[5 - fromEnd] = length; } } // Check for a streight line with small curves at one or both ends. ASSERT(maxSegment >= 0); if ((maxLength * 100) / totalLength > 80) { // throw away all other segments. rgStep[0] = rgStep[maxSegment]; rgStep[1] = rgStep[maxSegment + 1]; return 1; } // Try to find hook at the end. First try to remove two segments. cRemove = 0; if (cStep >= 3) { hookLength = lengths[5] + lengths[4]; if (hookLength * 10 < totalLength) { cRemove = 2; } } // If we couldn't remove two segments, try one. if (cRemove == 0 && lengths[5] * 10 < totalLength) { cRemove = 1; } // Drop points at the end as needed. Verify that there are enough points to continue. cStep -= cRemove; if (cStep < 2) { return cStep; // Only one segment left. } // Try to find hook at the start. First try to remove two segments. cRemove = 0; if (cStep >= 3) { hookLength = lengths[0] + lengths[1]; if (hookLength * 10 < totalLength) { cRemove = 2; } } // If we couldn't remove two segments, try one. if (cRemove == 0 && lengths[0] * 10 < totalLength) { cRemove = 1; } // Remove extra steps from start of array. if (cRemove > 0) { int from, to; to = 0; from = cRemove; while (from <= cStep) { rgStep[to++] = rgStep[from++]; } cStep -= cRemove; } return cStep; } int IsDakuten(RECT *pBoundingBox, FRAME *pFrame1, FRAME *pFrame2, KPROTO *pKProto) { int cXY; XY *pXY; int x1_1, y1_1, x2_1, y2_1; // Start and end of first stroke. int x1_2, y1_2, x2_2, y2_2; // Start and end of second stroke. int x1Per, y1Per, x2Per, y2Per; int dX, dY; int cFeat; PRIMITIVE *pFeat; // Figure size of bounding box. dX = pBoundingBox->right - pBoundingBox->left; dY = pBoundingBox->bottom - pBoundingBox->top; // Get pointer to data and count of points of first stroke. cXY = pFrame1->info.cPnt; pXY = pFrame1->rgrawxy; // Get end points x1_1 = pXY[0].x; y1_1 = pXY[0].y; x2_1 = pXY[cXY - 1].x; y2_1 = pXY[cXY - 1].y; // Get pointer to data and count of points of second stroke. cXY = pFrame2->info.cPnt; pXY = pFrame2->rgrawxy; // Get end points x1_2 = pXY[0].x; y1_2 = pXY[0].y; x2_2 = pXY[cXY - 1].x; y2_2 = pXY[cXY - 1].y; // Sometimesd strokes are reveresed. if (x1_1 > x1_2 && x2_1 > x2_2) { // Try switching strokes. return IsDakuten(pBoundingBox, pFrame2, pFrame1, pKProto); } // Get end positions relitive to the upper right corner of the bounding box. // Scale by 100 so we can get percents as integers. x1Per = ((pBoundingBox->right - x1_1) * 100) / dX; y1Per = ((y1_1 - pBoundingBox->top) * 100) / dY; x2Per = ((pBoundingBox->right - x2_1) * 100) / dX; y2Per = ((y2_1 - pBoundingBox->top) * 100) / dY; // Check if end points match expected locations for first stroke. if (x1Per > 70 || y1Per > 40 || x2Per > 60 || y2Per > 60) { // First stroke wrong position. return FALSE; } // Compare relitive positions of strokes. x1Per = ((x1_2 - x1_1) * 100) / dX; y1Per = ((y1_2 - y1_1) * 100) / dY; x2Per = ((x2_2 - x2_1) * 100) / dX; y2Per = ((y2_2 - y2_1) * 100) / dY; if (x1Per <= 0 || 50 <= x1Per || y1Per <= -30 || 25 <= y1Per || x2Per <= 0 || 55 <= x2Per || y2Per <= -35 || 30 <= y2Per ) { // Reletive positions of the strokes wrong. return FALSE; } // Get end positions relitive to the upper right corner of the bounding box. // Scale by 100 so we can get percents as integers. x1Per = ((pBoundingBox->right - x1_2) * 100) / dX; y1Per = ((y1_2 - pBoundingBox->top) * 100) / dY; x2Per = ((pBoundingBox->right - x2_2) * 100) / dX; y2Per = ((y2_2 - pBoundingBox->top) * 100) / dY; // Check if end points match expected locations for second stroke. if (x1Per > 40 || y1Per > 45 || x2Per > 25 || y2Per > 55) { // Second stroke wrong position. return FALSE; } { BOUNDS bounds; int normalize; int ratioPlus, ratioMinus; // End point tests pass, verify that we really have stokes that look mostly like lines. // We ignore the along direction because that does not seem to be a problem // We keep the away in loose bound since we only need to catch ones way out of line. // Since the ratio is usually a fraction, and we want to handle integers, // multiply by 1000. // Note that for very small lines, we don't care what the look like. FindBounds(0, pFrame1->info.cPnt - 1, pFrame1->rgrawxy, &bounds); if (bounds.dX > 20 || bounds.dX < -20 || bounds.dY > 20 || bounds.dY < -20) { // Is it large? normalize = bounds.dX * bounds.dX + bounds.dY * bounds.dY; normalize = max(1,normalize); ratioPlus = (bounds.oAwayPlus * 1000) / normalize; ratioMinus = -(bounds.oAwayMinus * 1000) / normalize; if (ratioPlus >= 1000 || ratioMinus >= 1000) { // Ratio > 1/1 return FALSE; } } FindBounds(0, pFrame2->info.cPnt - 1, pFrame2->rgrawxy, &bounds); if (bounds.dX > 20 || bounds.dX < -20 || bounds.dY > 20 || bounds.dY < -20) { // Is it large? normalize = bounds.dX * bounds.dX + bounds.dY * bounds.dY; normalize = max(1,normalize); ratioPlus = (bounds.oAwayPlus * 1000) / normalize; ratioMinus = -(bounds.oAwayMinus * 1000) / normalize; if (ratioPlus >= 1000 || ratioMinus >= 1000) { // Ratio > 1/1 return FALSE; } } } // Passes all the test. So add the two stokes to the feature list. cFeat = pKProto->cfeat; //// Handle first stroke // The array of primitives is CPRIMMAX in size so // the largest index it can handle is (CPRIMMAX-1). pFeat = &(pKProto->rgfeat[cFeat]); if (cFeat < (CPRIMMAX - 1)) { cFeat++; } else { //ASSERT(cFeat < (CPRIMMAX - 1)); } // Record start & end of feature. pFeat->x1 = (short)x1_1; pFeat->y1 = (short)y1_1; pFeat->x2 = (short)x2_1; pFeat->y2 = (short)y2_2; // Feature is small down right. pFeat->code = FEAT_DOWN_RIGHT; // Record CART info pFeat->cFeatures = 1; pFeat->cSteps = 1; pFeat->fDakuTen = 1; pFeat->nLength = Distance(pFeat->x2 - pFeat->x1, pFeat->y2 - pFeat->y1); pFeat->netAngle = 0; pFeat->absAngle = 0; pFeat->boundingBox = *RectFRAME(pFrame1); /// Now on to stroke two // The array of primitives is CPRIMMAX in size so // the largest index it can handle is (CPRIMMAX-1). pFeat = &(pKProto->rgfeat[cFeat]); if (cFeat < (CPRIMMAX - 1)) { cFeat++; } else { //ASSERT(cFeat < (CPRIMMAX - 1)); } // Record start & end of feature. pFeat->x1 = (short)x1_2; pFeat->y1 = (short)y1_2; pFeat->x2 = (short)x2_2; pFeat->y2 = (short)y2_2; // Feature is small down right. pFeat->code = FEAT_DOWN_RIGHT; // Record number of features after our additions. pKProto->cfeat = (WORD)cFeat; // Record CART info pFeat->cFeatures = 1; pFeat->cSteps = 1; pFeat->fDakuTen = 1; pFeat->nLength = Distance(pFeat->x2 - pFeat->x1, pFeat->y2 - pFeat->y1); pFeat->netAngle = 0; pFeat->absAngle = 0; pFeat->boundingBox = *RectFRAME(pFrame2); return TRUE; } int FeaturizeGLYPH(GLYPH *glyph, PRIMITIVE *rgprim, RECT *pRect) { KPROTO *kproto; int cstepmax, cframe, cstep, cprim; STEP *rgstep; FRAME *frame; int iframe; ASSERT(glyph); kproto = (KPROTO *) ExternAlloc(sizeof(KPROTO)); if (kproto == (KPROTO *) NULL) return 0; memset(kproto, '\0', sizeof(KPROTO)); kproto->rgfeat = rgprim; GetRectGLYPH(glyph, &kproto->rect); *pRect = kproto->rect; // Pass bounding rect up. rgstep = (STEP *) ExternAlloc(FRAME_MAXSTEPS * sizeof(STEP)); if (rgstep == (STEP *) NULL) goto cleanup; cstepmax = FRAME_MAXSTEPS; cframe = CframeGLYPH(glyph); for (iframe = 0; iframe < cframe; iframe++) { frame = FrameAtGLYPH(glyph, iframe); // Special check for dakuten. If we have 3 to 6 strokes and are looking // at the next to last stroke, we need to check if the last two strokes form // a dakuten. if (3 <= cframe && cframe <= 6 && iframe == cframe - 2) { FRAME *frame2; // Get the last frame as well frame2 = FrameAtGLYPH(glyph, iframe + 1); // Check the strokes, adds two features if it is a match. if (IsDakuten(&kproto->rect, frame, frame2, kproto)) { // OK, Have it abort the rest of the processing on the last two strokes. break; } } cstep = StepsFromFRAME(frame, rgstep, cstepmax); if (cstep == 0) { kproto->cfeat = 0; // Indicate a failure condition goto cleanu2; } cstep = CraneSmoothSteps(rgstep, cstep, cframe); AddStepsKPROTO(kproto, rgstep, cstep, cframe, frame); } cleanu2: ExternFree(rgstep); cleanup: cprim = kproto->cfeat; ExternFree(kproto); return cprim; } BOOL AllocFeature(SAMPLE *, int); BOOL MakeFeatures(SAMPLE *_this, void *pv) { PRIMITIVE rgprim[CPRIMMAX]; RECT rc; int ifeat; int iprim, cprim; int xShift, yShift; // Normalize position double xScale, yScale; // Normalize size double xyRatio; // Ratio of xy (scaled to fit in a WORD) cprim = FeaturizeGLYPH((GLYPH *) pv, rgprim, &rc); if (!cprim) return FALSE; // Figure normilization constants. xShift = -rc.left; xScale = 1000.0 / (double)(rc.right - rc.left); yShift = -rc.top; yScale = 1000.0 / (double)(rc.bottom - rc.top); xyRatio = (xScale / yScale) * 256.0; // Stroke count MUST be set before any per stroke data can be allocated _this->cstrk = (short)CframeGLYPH((GLYPH *) pv); // Now allocate per stroke features (currently, this is all of them) for (ifeat = 0; ifeat < FEATURE_COUNT; ifeat++) { if (!AllocFeature(_this, ifeat)) return FALSE; } // Set per character information (stroke count, dakuten, et.al.) _this->fDakuten = rgprim[cprim - 1].fDakuTen; // Set per stroke information (angle, stepping, end point position, et.al.) for (iprim = 0; iprim < cprim; iprim++) { ((SHORT *) _this->apfeat[FEATURE_ANGLE_NET]->data)[iprim] = max(min(rgprim[iprim].netAngle, 32767), -32768); ((USHORT *) _this->apfeat[FEATURE_ANGLE_ABS]->data)[iprim] = min(rgprim[iprim].absAngle, 0x0000ffff); ((USHORT *) _this->apfeat[FEATURE_LENGTH]->data)[iprim] = min(rgprim[iprim].nLength, 0x0000ffff); ((BYTE *) _this->apfeat[FEATURE_STEPS]->data)[iprim] = rgprim[iprim].cSteps; ((BYTE *) _this->apfeat[FEATURE_FEATURES]->data)[iprim] = rgprim[iprim].cFeatures; ((END_POINTS *) _this->apfeat[FEATURE_XPOS]->data)[iprim].start = (short) ((double) (rgprim[iprim].x1 + xShift) * xScale); ((END_POINTS *) _this->apfeat[FEATURE_XPOS]->data)[iprim].end = (short) ((double) (rgprim[iprim].x2 + xShift) * xScale); ((END_POINTS *) _this->apfeat[FEATURE_YPOS]->data)[iprim].start = (short) ((double) (rgprim[iprim].y1 + yShift) * yScale); ((END_POINTS *) _this->apfeat[FEATURE_YPOS]->data)[iprim].end = (short) ((double) (rgprim[iprim].y2 + yShift) * yScale); ((RECTS *) _this->apfeat[FEATURE_STROKE_BBOX]->data)[iprim].x1 = (short) ((double) (rgprim[iprim].boundingBox.left + xShift) * xScale); ((RECTS *) _this->apfeat[FEATURE_STROKE_BBOX]->data)[iprim].y1 = (short) ((double) (rgprim[iprim].boundingBox.top + yShift) * yScale); ((RECTS *) _this->apfeat[FEATURE_STROKE_BBOX]->data)[iprim].x2 = (short) ((double) (rgprim[iprim].boundingBox.right + xShift) * xScale); ((RECTS *) _this->apfeat[FEATURE_STROKE_BBOX]->data)[iprim].y2 = (short) ((double) (rgprim[iprim].boundingBox.bottom + yShift) * yScale); }; return TRUE; } BOOL CraneLoadFromPointer(LOCRUN_INFO *pLocRunInfo, QHEAD ** ppHead,QNODE **ppNode,BYTE *pbRes) { int *pOffsetQBT; int *pOffsetQBH; int iLoad; const CRANEDB_HEADER *pHeader = (CRANEDB_HEADER *)pbRes; if ( (pHeader->fileType != CRANEDB_FILE_TYPE) || (pHeader->headerSize < sizeof(*pHeader)) || (pHeader->minFileVer > CRANEDB_CUR_FILE_VERSION) || (pHeader->curFileVer < CRANEDB_OLD_FILE_VERSION) || memcmp (pHeader->adwSignature, pLocRunInfo->adwSignature,sizeof(pHeader->adwSignature)) ) { return FALSE; } pOffsetQBT = (int *)(pbRes + pHeader->headerSize); pOffsetQBH = pOffsetQBT + 29; for (iLoad = 1; iLoad <= 30; iLoad++) { ppHead[iLoad - 1] = (QHEAD *) (((BYTE *) pOffsetQBT) + pOffsetQBH[iLoad - 1]); ppNode[iLoad - 1] = (QNODE *) (((BYTE *) pOffsetQBT) + pOffsetQBT[iLoad - 1]); } return TRUE; } BOOL CraneLoadRes(HINSTANCE hInst, int resNumber, int resType, LOCRUN_INFO *pLocRunInfo) { HANDLE hres; HGLOBAL hglb; BYTE * pBase; hres = FindResource(hInst, (LPCTSTR) resNumber, (LPCTSTR) resType); ASSERT(hres); if (hres == NULL) return FALSE; hglb = LoadResource(hInst, hres); ASSERT(hglb); if (hglb == NULL) return FALSE; pBase = (BYTE *) LockResource(hglb); if (pBase == NULL) return FALSE; return CraneLoadFromPointer(pLocRunInfo, gapqhList, gapqnList, pBase); } QNODE *SkipAltList(UNIQART *puni) { while (puni->unicode) puni++; return (QNODE *) ++puni; } //FILE *pDebugFile = 0; QNODE *CraneFindAnswer(SAMPLE *psample, QNODE *pnode) { SAMPLE_INFO sample; if ((pnode->uniqart.qart.flag & 0xf0) != QART_QUESTION) return pnode; // Now ask the question sample.pSample = psample; AnswerQuestion(pnode->uniqart.qart.question, pnode->param1, pnode->param2, &sample, 1); //if (pDebugFile) { // fwprintf(pDebugFile, L" CART: (%2d:%5d:%5d) -> %5d ?< %5d\n", pnode->uniqart.qart, // pnode->param1, pnode->param2, sample.iAnswer, pnode->value // ); //} if (sample.iAnswer <= pnode->value) { if (pnode->uniqart.qart.flag & QART_NOBRANCH) return CraneFindAnswer(psample, SkipAltList((UNIQART *) pnode->extra)); else return CraneFindAnswer(psample, (QNODE *) pnode->extra); } else { if (pnode->uniqart.qart.flag & QART_NOBRANCH) return CraneFindAnswer(psample, (QNODE *) &pnode->offset); else return CraneFindAnswer(psample, (QNODE *) (((BYTE *) pnode->extra) + pnode->offset)); } } BOOL CraneMatch(ALT_LIST *pAlt, int cAlt, GLYPH *pGlyph, CHARSET *pCS, DRECTS *pdrcs, FLOAT eCARTWeight, LOCRUN_INFO *pLocRunInfo) { int cstrk = CframeGLYPH(pGlyph) - 1; // Our arrays are zero based int index; WORD wStart; WORD wFinal; QNODE *pqnode; SAMPLE sample; // See if Afterburner even has something with which to work if ((!pAlt->cAlt) || (cstrk < 0) || (cstrk >= 30)) return FALSE; // Get the page number of the top choice from the previous shape matcher index = HIGH_INDEX(pAlt->awchList[0]); if ((index < 0) || (index >= HIGH_INDEX_LIMIT - 1)) return FALSE; wStart = gapqhList[cstrk]->awIndex[index]; wFinal = gapqhList[cstrk]->awIndex[index + 1]; // Note that wStart may equal wFinal, in which case there are no valid selections. // Next get the low byte from the top choice, shift it to the left-most byte position. // Also, assume we didn't find a CART to run. index = (pAlt->awchList[0] & 0x00ff) << 24; pqnode = (QNODE *) NULL; while (wStart < wFinal) { if ((gapqhList[cstrk]->aqIndex[wStart] & 0xff000000) == ((DWORD) index)) { pqnode = (QNODE *) &(((BYTE *) gapqnList[cstrk])[gapqhList[cstrk]->aqIndex[wStart] & 0x00ffffff]); break; } if (gapqhList[cstrk]->aqIndex[wStart] > ((DWORD) index)) break; wStart++; } // If no tree was found, exit if (pqnode == (QNODE *) NULL) return FALSE; // OK, now we know we have a CART to run. Featurize the ink and run the tree InitFeatures(&sample); sample.drcs = *pdrcs; wFinal = MakeFeatures(&sample, pGlyph) ? *((WORD *) CraneFindAnswer(&sample, pqnode)) : 0; FreeFeatures(&sample); // Did CART give us back a meaningful result? if (!wFinal) return FALSE; // Short cut processing if we are top 1 already. if (wFinal == pAlt->awchList[0]) { pAlt->aeScore[0] += (FLOAT)eCARTWeight; return TRUE; } // Did CART give us a code point that is enabled by the current recmask { if (!IsAllowedChar(pLocRunInfo, pCS, wFinal)) { return FALSE; // Can't stick it in. } } // Now, walk through the previous ALT_LIST and see if this character was already proposed index = 0; while (((UINT) index) < pAlt->cAlt) { if (pAlt->awchList[index] == wFinal) break; index++; } if ((UINT) index >= pAlt->cAlt) { if (pAlt->cAlt < (UINT) cAlt) { pAlt->cAlt++; } else { index--; } } while (index) { pAlt->awchList[index] = pAlt->awchList[index - 1]; pAlt->aeScore[index] = pAlt->aeScore[index - 1]; index--; } pAlt->awchList[0] = wFinal; pAlt->aeScore[0] = pAlt->aeScore[1] + (FLOAT)eCARTWeight; return TRUE; }