Merge change 2219 into donut

* changes:
  Updated LetterRecognizer & related gesture recognition code - added personalization for letter recognizer
This commit is contained in:
Android (Google) Code Review
2009-05-21 15:03:57 -07:00
7 changed files with 109 additions and 47 deletions

View File

@@ -49,11 +49,11 @@ public class GestureLibrary {
private static final String NAMESPACE = ""; private static final String NAMESPACE = "";
public static final int SEQUENCE_INVARIANT = 1; public static final int SEQUENCE_INVARIANT = 1;
// when SEQUENCE_SENSITIVE is used, only single stroke gestures are allowed // when SEQUENCE_SENSITIVE is used, only single stroke gestures are currently allowed
public static final int SEQUENCE_SENSITIVE = 2; public static final int SEQUENCE_SENSITIVE = 2;
// ORIENTATION_SENSITIVE and ORIENTATION_INVARIANT are only for SEQUENCE_SENSITIVE gestures
public static final int ORIENTATION_INVARIANT = 1; public static final int ORIENTATION_INVARIANT = 1;
// ORIENTATION_SENSITIVE is only available for single stroke gestures
public static final int ORIENTATION_SENSITIVE = 2; public static final int ORIENTATION_SENSITIVE = 2;
private int mSequenceType = SEQUENCE_SENSITIVE; private int mSequenceType = SEQUENCE_SENSITIVE;
@@ -77,8 +77,8 @@ public class GestureLibrary {
} }
/** /**
* Specify whether the gesture library will handle orientation sensitive * Specify how the gesture library will handle orientation.
* gestures. Use ORIENTATION_INVARIANT or ORIENTATION_SENSITIVE * Use ORIENTATION_INVARIANT or ORIENTATION_SENSITIVE
* *
* @param style * @param style
*/ */
@@ -114,8 +114,8 @@ public class GestureLibrary {
* @return a list of predictions of possible entries for a given gesture * @return a list of predictions of possible entries for a given gesture
*/ */
public ArrayList<Prediction> recognize(Gesture gesture) { public ArrayList<Prediction> recognize(Gesture gesture) {
Instance instance = Instance.createInstance(this, gesture, null); Instance instance = Instance.createInstance(mSequenceType, gesture, null);
return mClassifier.classify(this, instance); return mClassifier.classify(mSequenceType, instance.vector);
} }
/** /**
@@ -134,7 +134,7 @@ public class GestureLibrary {
mEntryName2gestures.put(entryName, gestures); mEntryName2gestures.put(entryName, gestures);
} }
gestures.add(gesture); gestures.add(gesture);
mClassifier.addInstance(Instance.createInstance(this, gesture, entryName)); mClassifier.addInstance(Instance.createInstance(mSequenceType, gesture, entryName));
mChanged = true; mChanged = true;
} }
@@ -300,7 +300,7 @@ public class GestureLibrary {
mGestures = null; mGestures = null;
} else if (localName.equals(GestureConstants.XML_TAG_GESTURE)) { } else if (localName.equals(GestureConstants.XML_TAG_GESTURE)) {
mGestures.add(mCurrentGesture); mGestures.add(mCurrentGesture);
mClassifier.addInstance(Instance.createInstance(GestureLibrary.this, mClassifier.addInstance(Instance.createInstance(mSequenceType,
mCurrentGesture, mEntryName)); mCurrentGesture, mEntryName));
mCurrentGesture = null; mCurrentGesture = null;
} else if (localName.equals(GestureConstants.XML_TAG_STROKE)) { } else if (localName.equals(GestureConstants.XML_TAG_STROKE)) {

View File

@@ -244,4 +244,12 @@ public class GestureStroke {
public void invalidate() { public void invalidate() {
mCachedPath = null; mCachedPath = null;
} }
/**
* Compute an oriented bounding box of the stroke
* @return OrientedBoundingBox
*/
public OrientedBoundingBox computeOrientedBoundingBox() {
return GestureUtilities.computeOrientedBoundingBox(points);
}
} }

View File

@@ -26,7 +26,7 @@ import java.io.IOException;
import static com.android.gesture.GestureConstants.*; import static com.android.gesture.GestureConstants.*;
public final class GestureUtilities { final class GestureUtilities {
private static final int TEMPORAL_SAMPLING_RATE = 16; private static final int TEMPORAL_SAMPLING_RATE = 16;
private GestureUtilities() { private GestureUtilities() {
@@ -348,33 +348,31 @@ public final class GestureUtilities {
/** /**
* Calculate the cosine distance between two instances * Calculate the cosine distance between two instances
* *
* @param in1 * @param vector1
* @param in2 * @param vector2
* @return the distance between 0 and Math.PI * @return the distance between 0 and Math.PI
*/ */
static double cosineDistance(Instance in1, Instance in2) { static double cosineDistance(float[] vector1, float[] vector2) {
float sum = 0; float sum = 0;
float[] vector1 = in1.vector;
float[] vector2 = in2.vector;
int len = vector1.length; int len = vector1.length;
for (int i = 0; i < len; i++) { for (int i = 0; i < len; i++) {
sum += vector1[i] * vector2[i]; sum += vector1[i] * vector2[i];
} }
return Math.acos(sum / (in1.magnitude * in2.magnitude)); return Math.acos(sum);
} }
public static OrientedBoundingBox computeOrientedBoundingBox(ArrayList<GesturePoint> pts) { static OrientedBoundingBox computeOrientedBoundingBox(ArrayList<GesturePoint> pts) {
GestureStroke stroke = new GestureStroke(pts); GestureStroke stroke = new GestureStroke(pts);
float[] points = temporalSampling(stroke, TEMPORAL_SAMPLING_RATE); float[] points = temporalSampling(stroke, TEMPORAL_SAMPLING_RATE);
return computeOrientedBoundingBox(points); return computeOrientedBoundingBox(points);
} }
public static OrientedBoundingBox computeOrientedBoundingBox(float[] points) { static OrientedBoundingBox computeOrientedBoundingBox(float[] points) {
float[] meanVector = computeCentroid(points); float[] meanVector = computeCentroid(points);
return computeOrientedBoundingBox(points, meanVector); return computeOrientedBoundingBox(points, meanVector);
} }
public static OrientedBoundingBox computeOrientedBoundingBox(float[] points, float[] centroid) { static OrientedBoundingBox computeOrientedBoundingBox(float[] points, float[] centroid) {
Matrix tr = new Matrix(); Matrix tr = new Matrix();
tr.setTranslate(-centroid[0], -centroid[1]); tr.setTranslate(-centroid[0], -centroid[1]);
tr.mapPoints(points); tr.mapPoints(points);

View File

@@ -23,7 +23,7 @@ package com.android.gesture;
class Instance { class Instance {
private static final int SEQUENCE_SAMPLE_SIZE = 16; private static final int SEQUENCE_SAMPLE_SIZE = 16;
private static final int PATCH_SAMPLE_SIZE = 8; private static final int PATCH_SAMPLE_SIZE = 16;
private final static float[] ORIENTATIONS = { private final static float[] ORIENTATIONS = {
0, 45, 90, 135, 180, -0, -45, -90, -135, -180 0, 45, 90, 135, 180, -0, -45, -90, -135, -180
@@ -35,22 +35,26 @@ class Instance {
// the label can be null // the label can be null
final String label; final String label;
// the length of the vector
final float magnitude;
// the id of the instance // the id of the instance
final long id; final long id;
private Instance(long id, float[] sample, String sampleName) { private Instance(long id, float[] sample, String sampleName) {
this.id = id; this.id = id;
vector = sample; vector = sample;
label = sampleName; label = sampleName;
}
private void normalize() {
float[] sample = vector;
float sum = 0; float sum = 0;
int size = sample.length; int size = sample.length;
for (int i = 0; i < size; i++) { for (int i = 0; i < size; i++) {
sum += sample[i] * sample[i]; sum += sample[i] * sample[i];
} }
magnitude = (float) Math.sqrt(sum); float magnitude = (float) Math.sqrt(sum);
for (int i = 0; i < size; i++) {
sample[i] /= magnitude;
}
} }
/** /**
@@ -60,21 +64,25 @@ class Instance {
* @param label * @param label
* @return the instance * @return the instance
*/ */
static Instance createInstance(GestureLibrary gesturelib, Gesture gesture, String label) { static Instance createInstance(int samplingType, Gesture gesture, String label) {
float[] pts; float[] pts;
if (gesturelib.getGestureType() == GestureLibrary.SEQUENCE_SENSITIVE) { Instance instance;
pts = temporalSampler(gesturelib, gesture); if (samplingType == GestureLibrary.SEQUENCE_SENSITIVE) {
pts = temporalSampler(samplingType, gesture);
instance = new Instance(gesture.getID(), pts, label);
instance.normalize();
} else { } else {
pts = spatialSampler(gesture); pts = spatialSampler(gesture);
instance = new Instance(gesture.getID(), pts, label);
} }
return new Instance(gesture.getID(), pts, label); return instance;
} }
private static float[] spatialSampler(Gesture gesture) { private static float[] spatialSampler(Gesture gesture) {
return GestureUtilities.spatialSampling(gesture, PATCH_SAMPLE_SIZE); return GestureUtilities.spatialSampling(gesture, PATCH_SAMPLE_SIZE);
} }
private static float[] temporalSampler(GestureLibrary gesturelib, Gesture gesture) { private static float[] temporalSampler(int samplingType, Gesture gesture) {
float[] pts = GestureUtilities.temporalSampling(gesture.getStrokes().get(0), float[] pts = GestureUtilities.temporalSampling(gesture.getStrokes().get(0),
SEQUENCE_SAMPLE_SIZE); SEQUENCE_SAMPLE_SIZE);
float[] center = GestureUtilities.computeCentroid(pts); float[] center = GestureUtilities.computeCentroid(pts);
@@ -82,7 +90,7 @@ class Instance {
orientation *= 180 / Math.PI; orientation *= 180 / Math.PI;
float adjustment = -orientation; float adjustment = -orientation;
if (gesturelib.getOrientationStyle() == GestureLibrary.ORIENTATION_SENSITIVE) { if (samplingType == GestureLibrary.ORIENTATION_SENSITIVE) {
int count = ORIENTATIONS.length; int count = ORIENTATIONS.length;
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
float delta = ORIENTATIONS[i] - orientation; float delta = ORIENTATIONS[i] - orientation;

View File

@@ -34,21 +34,21 @@ class InstanceLearner extends Learner {
private static final String LOGTAG = "InstanceLearner"; private static final String LOGTAG = "InstanceLearner";
@Override @Override
ArrayList<Prediction> classify(GestureLibrary lib, Instance instance) { ArrayList<Prediction> classify(int gestureType, float[] vector) {
ArrayList<Prediction> predictions = new ArrayList<Prediction>(); ArrayList<Prediction> predictions = new ArrayList<Prediction>();
ArrayList<Instance> instances = getInstances(); ArrayList<Instance> instances = getInstances();
int count = instances.size(); int count = instances.size();
TreeMap<String, Double> label2score = new TreeMap<String, Double>(); TreeMap<String, Double> label2score = new TreeMap<String, Double>();
for (int i = 0; i < count; i++) { for (int i = 0; i < count; i++) {
Instance sample = instances.get(i); Instance sample = instances.get(i);
if (sample.vector.length != instance.vector.length) { if (sample.vector.length != vector.length) {
continue; continue;
} }
double distance; double distance;
if (lib.getGestureType() == GestureLibrary.SEQUENCE_SENSITIVE) { if (gestureType == GestureLibrary.SEQUENCE_SENSITIVE) {
distance = GestureUtilities.cosineDistance(sample, instance); distance = GestureUtilities.cosineDistance(sample.vector, vector);
} else { } else {
distance = GestureUtilities.squaredEuclideanDistance(sample.vector, instance.vector); distance = GestureUtilities.squaredEuclideanDistance(sample.vector, vector);
} }
double weight; double weight;
if (distance == 0) { if (distance == 0) {

View File

@@ -79,5 +79,5 @@ abstract class Learner {
instances.removeAll(toDelete); instances.removeAll(toDelete);
} }
abstract ArrayList<Prediction> classify(GestureLibrary library, Instance instance); abstract ArrayList<Prediction> classify(int gestureType, float[] vector);
} }

View File

@@ -20,12 +20,14 @@ import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.util.Log; import android.util.Log;
import java.io.IOException;
import java.io.DataInputStream;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashMap;
public class LetterRecognizer { public class LetterRecognizer {
private static final String LOG_TAG = "LetterRecognizer"; private static final String LOG_TAG = "LetterRecognizer";
@@ -37,8 +39,13 @@ public class LetterRecognizer {
private final String[] mClasses; private final String[] mClasses;
private final int mInputCount; private final int mPatchSize;
static final String GESTURE_FILE_NAME = "letters.xml";
private GestureLibrary mGestureLibrary;
private final static int ADJUST_RANGE = 3;
private static class SigmoidUnit { private static class SigmoidUnit {
final float[] mWeights; final float[] mWeights;
@@ -62,11 +69,15 @@ public class LetterRecognizer {
} }
private LetterRecognizer(int numOfInput, int numOfHidden, String[] classes) { private LetterRecognizer(int numOfInput, int numOfHidden, String[] classes) {
mInputCount = (int)Math.sqrt(numOfInput); mPatchSize = (int)Math.sqrt(numOfInput);
mHiddenLayer = new SigmoidUnit[numOfHidden]; mHiddenLayer = new SigmoidUnit[numOfHidden];
mClasses = classes; mClasses = classes;
mOutputLayer = new SigmoidUnit[classes.length]; mOutputLayer = new SigmoidUnit[classes.length];
} }
public void save() {
mGestureLibrary.save();
}
public static LetterRecognizer getLetterRecognizer(Context context, int type) { public static LetterRecognizer getLetterRecognizer(Context context, int type) {
switch (type) { switch (type) {
@@ -78,7 +89,12 @@ public class LetterRecognizer {
} }
public ArrayList<Prediction> recognize(Gesture gesture) { public ArrayList<Prediction> recognize(Gesture gesture) {
return classify(GestureUtilities.spatialSampling(gesture, mInputCount)); float[] query = GestureUtilities.spatialSampling(gesture, mPatchSize);
ArrayList<Prediction> predictions = classify(query);
if (mGestureLibrary != null) {
adjustPrediction(gesture, predictions);
}
return predictions;
} }
private ArrayList<Prediction> classify(float[] vector) { private ArrayList<Prediction> classify(float[] vector) {
@@ -151,16 +167,16 @@ public class LetterRecognizer {
SigmoidUnit[] outputLayer = new SigmoidUnit[oCount]; SigmoidUnit[] outputLayer = new SigmoidUnit[oCount];
for (int i = 0; i < hCount; i++) { for (int i = 0; i < hCount; i++) {
float[] weights = new float[iCount]; float[] weights = new float[iCount + 1];
for (int j = 0; j < iCount; j++) { for (int j = 0; j <= iCount; j++) {
weights[j] = in.readFloat(); weights[j] = in.readFloat();
} }
hiddenLayer[i] = new SigmoidUnit(weights); hiddenLayer[i] = new SigmoidUnit(weights);
} }
for (int i = 0; i < oCount; i++) { for (int i = 0; i < oCount; i++) {
float[] weights = new float[hCount]; float[] weights = new float[hCount + 1];
for (int j = 0; j < hCount; j++) { for (int j = 0; j <= hCount; j++) {
weights[j] = in.readFloat(); weights[j] = in.readFloat();
} }
outputLayer[i] = new SigmoidUnit(weights); outputLayer[i] = new SigmoidUnit(weights);
@@ -170,11 +186,43 @@ public class LetterRecognizer {
classifier.mOutputLayer = outputLayer; classifier.mOutputLayer = outputLayer;
} catch (IOException e) { } catch (IOException e) {
Log.d(LOG_TAG, "Failed to load gestures:", e); Log.d(LOG_TAG, "Failed to load handwriting data:", e);
} finally { } finally {
GestureUtilities.closeStream(in); GestureUtilities.closeStream(in);
} }
return classifier; return classifier;
} }
public void enablePersonalization(boolean enable) {
if (enable) {
mGestureLibrary = new GestureLibrary(GESTURE_FILE_NAME);
mGestureLibrary.setGestureType(GestureLibrary.SEQUENCE_INVARIANT);
mGestureLibrary.load();
} else {
mGestureLibrary = null;
}
}
public void addExample(String letter, Gesture example) {
mGestureLibrary.addGesture(letter, example);
}
private void adjustPrediction(Gesture query, ArrayList<Prediction> predictions) {
ArrayList<Prediction> results = mGestureLibrary.recognize(query);
HashMap<String, Prediction> topNList = new HashMap<String, Prediction>();
for (int j = 0; j < ADJUST_RANGE; j++) {
Prediction prediction = predictions.remove(0);
topNList.put(prediction.name, prediction);
}
int count = results.size();
for (int j = count - 1; j >= 0 && !topNList.isEmpty(); j--) {
Prediction item = results.get(j);
Prediction original = topNList.get(item.name);
if (original != null) {
predictions.add(0, original);
topNList.remove(item.name);
}
}
}
} }