ref: babf901b4a508c3ec5d1f89655f10377bbdf9637
dir: /appl/lib/strokes/strokes.b/
implement Strokes;
#
# this Limbo code is derived from C code that had the following
# copyright notice, which i reproduce as requested
#
# li_recognizer.c
#
# Copyright 2000 Compaq Computer Corporation.
# Copying or modifying this code for any purpose is permitted,
# provided that this copyright notice is preserved in its entirety
# in all copies or modifications.
# COMPAQ COMPUTER CORPORATION MAKES NO WARRANTIES, EXPRESSED OR
# IMPLIED, AS TO THE USEFULNESS OR CORRECTNESS OF THIS CODE OR
#
#
# Adapted from cmu_recognizer.c by Jay Kistler.
#
# Where is the CMU copyright???? Gotta track it down - Jim Gettys
#
# Credit to Dean Rubine, Jim Kempf, and Ari Rapkin.
#
#
# the copyright notice really did end in the middle of the sentence
#
#
# Limbo version for Inferno by forsyth@vitanuova.com, Vita Nuova, September 2001
#
#
# the code is reasonably close to the algorithms described in
# ``On-line Handwritten Alphanumeric Character Recognition Using Dominant Stroke in Strokes'',
# Xiaolin Li and Dit-Yan Yueng, Department of Computer Science,
# Hong Kong University of Science and Technology, Hong Kong (23 August 1996)
#
include "sys.m";
sys: Sys;
include "strokes.m";
MAXINT: con 16r7FFFFFFF;
# Dynamic programming parameters
DP_BAND: con 3;
SIM_THLD: con 60; # x100
#DIST_THLD: con 3200; # x100
DIST_THLD: con 3300; # x100
# Low-pass filter parameters -- empirically derived
LP_FILTER_WIDTH: con 6;
LP_FILTER_ITERS: con 8;
LP_FILTER_THLD: con 250; # x100
LP_FILTER_MIN: con 5;
# Pseudo-extrema parameters -- empirically derived
PE_AL_THLD: con 1500; # x100
PE_ATCR_THLD: con 135; # x100
# Pre-processing and canonicalization parameters
CANONICAL_X: con 108;
CANONICAL_Y: con 128;
DIST_SQ_THRESHOLD: con 3*3;
# direction-code table; indexed by dx, dy
dctbl := array[] of {array[] of {1, 0, 7}, array[] of {2, MAXINT, 6}, array[] of {3, 4, 5}};
# low-pass filter weights
lpfwts := array[2 * LP_FILTER_WIDTH + 1] of int;
lpfconst := -1;
lidebug: con 0;
stderr: ref Sys->FD;
# x := 0.04 * (i * i);
# wtvals[i] = floor(100.0 * exp(x));
wtvals := array[] of {100, 104, 117, 143, 189, 271, 422};
init()
{
sys = load Sys Sys->PATH;
if(lidebug)
stderr = sys->fildes(2);
for(i := LP_FILTER_WIDTH; i >= 0; i--){
wt := wtvals[i];
lpfwts[LP_FILTER_WIDTH - i] = wt;
lpfwts[LP_FILTER_WIDTH + i] = wt;
}
lpfconst = 0;
for(i = 0; i < (2 * LP_FILTER_WIDTH + 1); i++)
lpfconst += lpfwts[i];
}
Stroke.new(n: int): ref Stroke
{
return ref Stroke(n, array[n] of Penpoint, 0, 0);
}
Stroke.trim(ps: self ref Stroke, n: int)
{
ps.npts = n;
ps.pts = ps.pts[0:n];
}
Stroke.copy(ps: self ref Stroke): ref Stroke
{
n := ps.npts;
a := array[n] of Penpoint;
a[0:] = ps.pts[0:n];
return ref Stroke(n, a, ps.xrange, ps.yrange);
}
#
# return the bounding box of a set of points
# (note: unlike Draw->Rectangle, the region is closed)
#
Stroke.bbox(ps: self ref Stroke): (int, int, int, int)
{
minx := maxx := ps.pts[0].x;
miny := maxy := ps.pts[0].y;
for(i := 1; i < ps.npts; i++){
pt := ps.pts[i];
if(pt.x < minx)
minx = pt.x;
if(pt.x > maxx)
maxx = pt.x;
if(pt.y < miny)
miny = pt.y;
if(pt.y > maxy)
maxy = pt.y;
}
return (minx, miny, maxx, maxy); # warning: closed interval
}
Stroke.center(ps: self ref Stroke)
{
(minx, miny, maxx, maxy) := ps.bbox();
ps.xrange = maxx-minx;
ps.yrange = maxy-miny;
avgxoff := -((CANONICAL_X - ps.xrange + 1) / 2);
avgyoff := -((CANONICAL_Y - ps.yrange + 1) / 2);
ps.translate(avgxoff, avgyoff, 100, 100);
}
Stroke.scaleup(ps: self ref Stroke): int
{
(minx, miny, maxx, maxy) := ps.bbox();
xrange := maxx - minx;
yrange := maxy - miny;
scale: int;
if(((100 * xrange + CANONICAL_X / 2) / CANONICAL_X) >
((100 * yrange + CANONICAL_Y / 2) / CANONICAL_Y))
scale = (100 * CANONICAL_X + xrange / 2) / xrange;
else
scale = (100 * CANONICAL_Y + yrange / 2) / yrange;
ps.translate(minx, miny, scale, scale);
return scale;
}
# scalex and scaley are x 100.
# Note that this does NOT update points.xrange and points.yrange!
Stroke.translate(ps: self ref Stroke, minx: int, miny: int, scalex: int, scaley: int)
{
for(i := 0; i < ps.npts; i++){
ps.pts[i].x = ((ps.pts[i].x - minx) * scalex + 50) / 100;
ps.pts[i].y = ((ps.pts[i].y - miny) * scaley + 50) / 100;
}
}
TAP_PATHLEN: con 10*100; # x100
Classifier.match(rec: self ref Classifier, stroke: ref Stroke): (int, string)
{
if(stroke.npts < 1)
return (-1, nil);
# Check for tap.
# First thing is to filter out ``close points.''
stroke = stroke.filter();
# Unfortunately, we don't have the actual time that each point
# was recorded (i.e., dt is invalid). Hence, we have to use a
# heuristic based on total distance and the number of points.
if(stroke.npts == 1 || stroke.length() < TAP_PATHLEN)
return (-1, "tap");
preprocess_stroke(stroke);
# Compute its dominant points.
dompts := stroke.interpolate().dominant();
best_dist := MAXDIST;
best_i := -1;
best_name: string;
# Score input stroke against every class in classifier.
for(i := 0; i < len rec.cnames; i++){
name := rec.cnames[i];
(sim, dist) := score_stroke(dompts, rec.dompts[i]);
if(dist < MAXDIST)
sys->fprint(stderr, " (%s, %d, %d)", name, sim, dist);
if(dist < DIST_THLD){
if(lidebug)
sys->fprint(stderr, " (%s, %d, %d)", name, sim, dist);
# Is it the best so far?
if(dist < best_dist){
best_dist = dist;
best_i = i;
best_name = name;
}
}
}
if(lidebug)
sys->fprint(stderr, "\n");
return (best_i, best_name);
}
preprocess_stroke(s: ref Stroke)
{
# Filter out points that are too close.
# We did this earlier, when we checked for a tap.
# s = s.filter();
# assert(s.npts > 0);
# Scale up to avoid conversion errors.
s.scaleup();
# Center the stroke.
s.center();
if(lidebug){
(minx, miny, maxx, maxy) := s.bbox();
sys->fprint(stderr, "After pre-processing: [ %d %d %d %d]\n",
minx, miny, maxx, maxy);
printpoints(stderr, s, "\n");
}
}
#
# return the dominant points of Stroke s, assuming s has been through interpolation
#
Stroke.dominant(s: self ref Stroke): ref Stroke
{
regions := s.regions();
# Dominant points are: start, end, extrema of non plain regions, and midpoints of the preceding.
nonplain := 0;
for(r := regions; r != nil; r = r.next)
if(r.rtype != Rplain)
nonplain++;
dom := Stroke.new(1 + 2*nonplain + 2);
# Pick out dominant points.
# start point
dp := 0;
previx := 0;
dom.pts[dp++] = s.pts[previx];
currix: int;
cas := s.contourangles(regions);
for(r = regions; r != nil; r = r.next)
if(r.rtype != Rplain){
max_v := 0;
min_v := MAXINT;
max_ix := -1;
min_ix := -1;
for(i := r.start; i <= r.end; i++){
v := cas[i];
if(v > max_v){
max_v = v; max_ix = i;
}
if(v < min_v){
min_v = v; min_ix = i;
}
if(lidebug > 1)
sys->fprint(stderr, " %d\n", v);
}
if(r.rtype == Rconvex)
currix = max_ix;
else
currix = min_ix;
dom.pts[dp++] = s.pts[(previx+currix)/2]; # midpoint
dom.pts[dp++] = s.pts[currix]; # extreme
previx = currix;
}
# last midpoint, and end point
lastp := s.npts - 1;
dom.pts[dp++] = s.pts[(previx+lastp)/2];
dom.pts[dp++] = s.pts[lastp];
dom.trim(dp);
# Compute chain-code.
compute_chain_code(dom);
return dom;
}
Stroke.contourangles(s: self ref Stroke, regions: ref Region): array of int
{
V := array[s.npts] of int;
V[0] = 18000;
for(r := regions; r != nil; r = r.next){
for(i := r.start; i <= r.end; i++){
if(r.rtype == Rplain){
V[i] = 18000;
}else{
# For now, simply choose the mid-point.
ismidpt := i == (r.start + r.end)/2;
if(ismidpt ^ (r.rtype!=Rconvex))
V[i] = 18000;
else
V[i] = 0;
}
}
}
V[s.npts - 1] = 18000;
return V;
}
Stroke.interpolate(s: self ref Stroke): ref Stroke
{
# Compute an upper-bound on the number of interpolated points
maxpts := s.npts;
for(i := 0; i < s.npts - 1; i++){
a := s.pts[i];
b := s.pts[i+1];
maxpts += abs(a.x - b.x) + abs(a.y - b.y);
}
# Allocate an array of the maximum size
newpts := Stroke.new(maxpts);
# Interpolate each of the segments.
j := 0;
for(i = 0; i < s.npts - 1; i++){
j = bresline(s.pts[i], s.pts[i+1], newpts, j);
j--; # end point gets recorded as start point of next segment!
}
# Add-in last point and trim
newpts.pts[j++] = s.pts[s.npts - 1];
newpts.trim(j);
if(lidebug){
sys->fprint(stderr, "After interpolation:\n");
printpoints(stderr, newpts, "\n");
}
# Compute the chain code for P (the list of points).
compute_unit_chain_code(newpts);
return newpts;
}
# This implementation is due to an anonymous page on the net
bresline(startpt: Penpoint, endpt: Penpoint, newpts: ref Stroke, j: int): int
{
x0 := startpt.x;
x1 := endpt.x;
y0 := startpt.y;
y1 := endpt.y;
stepx := 1;
dx := x1-x0;
if(dx < 0){
dx = -dx;
stepx = -1;
}
dx <<= 1;
stepy := 1;
dy := y1-y0;
if(dy < 0){
dy = -dy;
stepy = -1;
}
dy <<= 1;
newpts.pts[j++] = (x0, y0, 0);
if(dx >= dy){
e := dy - (dx>>1);
while(x0 != x1){
if(e >= 0){
y0 += stepy;
e -= dx;
}
x0 += stepx;
e += dy;
newpts.pts[j++] = (x0, y0, 0);
}
}else{
e := dx - (dy>>1);
while(y0 != y1){
if(e >= 0){
x0 += stepx;
e -= dy;
}
y0 += stepy;
e += dx;
newpts.pts[j++] = (x0, y0, 0);
}
}
return j;
}
compute_chain_code(pts: ref Stroke)
{
for(i := 0; i < pts.npts - 1; i++){
dx := pts.pts[i+1].x - pts.pts[i].x;
dy := pts.pts[i+1].y - pts.pts[i].y;
pts.pts[i].chaincode = (12 - quadr(likeatan(dy, dx))) % 8;
}
}
compute_unit_chain_code(pts: ref Stroke)
{
for(i := 0; i < pts.npts - 1; i++){
dx := pts.pts[i+1].x - pts.pts[i].x;
dy := pts.pts[i+1].y - pts.pts[i].y;
pts.pts[i].chaincode = dctbl[dx+1][dy+1];
}
}
Stroke.regions(pts: self ref Stroke): ref Region
{
# Allocate a 2 x pts.npts array for use in computing the (filtered) Angle set, A_n.
R := array[] of {0 to LP_FILTER_ITERS+1 => array[pts.npts] of int};
curr := R[0];
# Compute the Angle set, A, in the first element of array R.
# Values in R are in degrees, x 100.
curr[0] = 18000; # a_0
for(i := 1; i < pts.npts - 1; i++){
d_i := pts.pts[i].chaincode;
d_iminusone := pts.pts[i-1].chaincode;
if(d_iminusone < d_i)
d_iminusone += 8;
a_i := (d_iminusone - d_i) % 8;
# convert to degrees, x 100
curr[i] = ((12 - a_i) % 8) * 45 * 100;
}
curr[pts.npts-1] = 18000; # a_L-1
# Perform a number of filtering iterations.
next := R[1];
for(j := 0; j < LP_FILTER_ITERS; ){
for(i = 0; i < pts.npts; i++){
next[i] = 0;
for(k := i - LP_FILTER_WIDTH; k <= i + LP_FILTER_WIDTH; k++){
oldval: int;
if(k < 0 || k >= pts.npts)
oldval = 18000;
else
oldval = curr[k];
next[i] += oldval * lpfwts[k - (i - LP_FILTER_WIDTH)]; # overflow?
}
next[i] /= lpfconst;
}
j++;
curr = R[j];
next = R[j+1];
}
# Do final thresholding around PI.
# curr and next are set-up correctly at end of previous loop!
for(i = 0; i < pts.npts; i++)
if(abs(curr[i] - 18000) < LP_FILTER_THLD)
next[i] = 18000;
else
next[i] = curr[i];
curr = next;
# Debugging.
if(lidebug > 1){
for(i = 0; i < pts.npts; i++){
p := pts.pts[i];
sys->fprint(stderr, "%3d: (%d %d) %ud ",
i, p.x, p.y, p.chaincode);
for(j = 0; j < 2 + LP_FILTER_ITERS; j++)
sys->fprint(stderr, "%d ", R[j][i]);
sys->fprint(stderr, "\n");
}
}
# Do the region segmentation.
r := regions := ref Region(regiontype(curr[0]), 0, 0, nil);
for(i = 1; i < pts.npts; i++){
t := regiontype(curr[i]);
if(t != r.rtype){
r.end = i-1;
if(lidebug > 1)
sys->fprint(stderr, " (%d, %d) %d\n", r.start, r.end, r.rtype);
r.next = ref Region(t, i, 0, nil);
r = r.next;
}
}
r.end = i-1;
if(lidebug > 1)
sys->fprint(stderr, " (%d, %d), %d\n", r.start, r.end, r.rtype);
# Filter out convex/concave regions that are too short.
for(r = regions; r != nil; r = r.next)
if(r.rtype == Rplain){
while((nr := r.next) != nil && (nr.end - nr.start) < LP_FILTER_MIN){
# nr must not be plain, and it must be followed by a plain
# assert(nr.rtype != Rplain);
# assert(nr.next != nil && (nr.next).rtype == Rplain);
if(nr.next == nil){
sys->fprint(stderr, "recog: nr.next==nil\n"); # can't happen
break;
}
r.next = nr.next.next;
r.end = nr.next.end;
}
}
# Add-in pseudo-extremes.
for(r = regions; r != nil; r = r.next)
if(r.rtype == Rplain){
arclen := pts.pathlen(r.start, r.end);
dx := pts.pts[r.end].x - pts.pts[r.start].x;
dy := pts.pts[r.end].y - pts.pts[r.start].y;
chordlen := sqrt(100*100 * (dx*dx + dy*dy));
atcr := 0;
if(chordlen)
atcr = (100*arclen + chordlen/2) / chordlen;
if(lidebug)
sys->fprint(stderr, "%d, %d, %d\n", arclen, chordlen, atcr);
# Split region if necessary.
if(arclen >= PE_AL_THLD && atcr >= PE_ATCR_THLD){
mid := (r.start + r.end)/2;
end := r.end;
r.end = mid - 1;
r = r.next = ref Region(Rpseudo, mid, mid,
ref Region(Rplain, mid+1, end, r.next));
}
}
return regions;
}
regiontype(val: int): int
{
if(val == 18000)
return Rplain;
if(val < 18000)
return Rconcave;
return Rconvex;
}
#
# return the similarity of two strokes and,
# if similar, the distance between them;
# if dissimilar, the distance is MAXDIST)
#
score_stroke(a: ref Stroke, b: ref Stroke): (int, int)
{
sim := compute_similarity(a, b);
if(sim < SIM_THLD)
return (sim, MAXDIST);
return (sim, compute_distance(a, b));
}
compute_similarity(A: ref Stroke, B: ref Stroke): int
{
# A is the longer sequence, length N.
# B is the shorter sequence, length M.
if(A.npts < B.npts){
t := A; A = B; B = t;
}
N := A.npts;
M := B.npts;
# Allocate and initialize the Gain matrix, G.
# The size of G is M x (N + 1).
# Note that row 0 is unused.
# Similarities are x 10.
G := array[M] of array of int;
for(i := 1; i < M; i++){
G[i] = array[N+1] of int;
bcode := B.pts[i-1].chaincode;
G[i][0] = 0; # source column
for(j := 1; j < N; j++){
diff := abs(bcode - A.pts[j-1].chaincode);
if(diff > 4)
diff = 8 - diff; # symmetry
v := 0;
if(diff == 0)
v = 10;
else if(diff == 1)
v = 6;
G[i][j] = v;
}
G[i][N] = 0; # sink column
}
# Do the DP algorithm.
# Proceed in column order, from highest column to the lowest.
# Within each column, proceed from the highest row to the lowest.
# Skip the highest column.
for(j := N - 1; j >= 0; j--)
for(i = M - 1; i > 0; i--){
max := G[i][j + 1];
if(i < M-1){
t := G[i + 1][j + 1];
if(t > max)
max = t;
}
G[i][j] += max;
}
return (10*G[1][0] + (N-1)/2) / (N-1);
}
compute_distance(A: ref Stroke, B: ref Stroke): int
{
# A is the longer sequence, length N.
# B is the shorter sequence, length M.
if(A.npts < B.npts){
t := A; A = B; B = t;
}
N := A.npts;
M := B.npts;
# Construct the helper vectors, BE and TE, which say for each column
# what are the ``bottom'' and ``top'' rows of interest.
BE := array[N+1] of int;
TE := array[N+1] of int;
for(j := 1; j <= N; j++){
bot := j + (M - DP_BAND);
if(bot > M) bot = M;
BE[j] = bot;
top := j - (N - DP_BAND);
if(top < 1) top = 1;
TE[j] = top;
}
# Allocate and initialize the Cost matrix, C.
# The size of C is (M + 1) x (N + 1).
# Note that row and column 0 are unused.
# Costs are x 100.
C := array[M+1] of array of int;
for(i := 1; i <= M; i++){
C[i] = array[N+1] of int;
bx := B.pts[i-1].x;
by := B.pts[i-1].y;
for(j = 1; j <= N; j++){
ax := A.pts[j-1].x;
ay := A.pts[j-1].y;
dx := bx - ax;
dy := by - ay;
dist := sqrt(10000 * (dx * dx + dy * dy));
C[i][j] = dist;
}
}
# Do the DP algorithm.
# Proceed in column order, from highest column to the lowest.
# Within each column, proceed from the highest row to the lowest.
for(j = N; j > 0; j--)
for(i = M; i > 0; i--){
min := MAXDIST;
if(i > BE[j] || i < TE[j] || (j == N && i == M))
continue;
if(j < N){
if(i >= TE[j+1]){
tmp := C[i][j+1];
if(tmp < min)
min = tmp;
}
if(i < M){
tmp := C[i+1][j+1];
if(tmp < min)
min = tmp;
}
}
if(i < BE[j]){
tmp := C[i+1][j];
if(tmp < min)
min = tmp;
}
C[i][j] += min;
}
return (C[1][1] + N / 2) / N; # dist
}
# Result is x 100.
Stroke.length(s: self ref Stroke): int
{
return s.pathlen(0, s.npts-1);
}
# Result is x 100.
Stroke.pathlen(s: self ref Stroke, first: int, last: int): int
{
l := 0;
for(i := first + 1; i <= last; i++){
dx := s.pts[i].x - s.pts[i-1].x;
dy := s.pts[i].y - s.pts[i-1].y;
l += sqrt(100*100 * (dx*dx + dy*dy));
}
return l;
}
# Note that this does NOT update points.xrange and points.yrange!
Stroke.filter(s: self ref Stroke): ref Stroke
{
pts := array[s.npts] of Penpoint;
pts[0] = s.pts[0];
npts := 1;
for(i := 1; i < s.npts; i++){
j := npts - 1;
dx := s.pts[i].x - pts[j].x;
dy := s.pts[i].y - pts[j].y;
magsq := dx * dx + dy * dy;
if(magsq >= DIST_SQ_THRESHOLD){
pts[npts] = s.pts[i];
npts++;
}
}
return ref Stroke(npts, pts[0:npts], 0, 0);
}
abs(a: int): int
{
if(a < 0)
return -a;
return a;
}
# Code from Joseph Hall (jnhall@sat.mot.com).
sqrt(n: int): int
{
nn := n;
k0 := 2;
for(i := n; i > 0; i >>= 2)
k0 <<= 1;
nn <<= 2;
k1: int;
for(;;){
k1 = (nn / k0 + k0) >> 1;
if(((k0 ^ k1) & ~1) == 0)
break;
k0 = k1;
}
return (k1 + 1) >> 1;
}
# Helper routines from Mark Hayter.
likeatan(tantop: int, tanbot: int): int
{
# Use tan(theta)=top/bot -. order for t
# t in range 0..16r40000
if(tantop == 0 && tantop == 0)
return 0;
t := (tantop << 16) / (abs(tantop) + abs(tanbot));
if(tanbot < 0)
t = 16r20000 - t;
else if(tantop < 0)
t = 16r40000 + t;
return t;
}
quadr(t: int): int
{
return (8 - (((t + 16r4000) >> 15) & 7)) & 7;
}
printpoints(fd: ref Sys->FD, pts: ref Stroke, sep: string)
{
for(j := 0; j < pts.npts; j++){
p := pts.pts[j];
sys->fprint(fd, " (%d %d) %ud%s", p.x, p.y, pts.pts[j].chaincode, sep);
}
}