shithub: purgatorio

ref: 75c92428225428c8fde2d015f010e608a0b12f1d
dir: purgatorio/appl/lib/strokes/strokes.b

View raw version
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);
	}
}