前言:手势识别在Android手势中是最重要的部分,基本上算是手势的精髓;手势识别的算法有点类似人脸识别;手势识别的利用很普遍,涉及到用户安全操作的领域也比较多;比如可以通过手势识别来实现手机的解锁,安全启动用户设置的用户模式应用等;
一. 基于第三方开发的手势识别
一般情况下,如果需要将用户当前绘制手势和已保存的手势进行匹配。那么,在用户绘制完当前手势时就可以进行匹配处理。这个过程可以在手势监听器GesturePerformedListener的onGesturePerformed方法中处理。
如下示例代码片段:
...
@Override
public void onGesturePerformed(GestureOverlayView overlay, Gesture gesture)
{
// TODO Auto-generated method stub
final GestureLibrary store = MainActivity.getStore();
store.setOrientationStyle(4);
//识别用户刚刚所绘制的手势
ArrayList<Prediction> predictions = store.recognize(gesture);
//遍历所有找到的Prediction对象
for(Prediction pred : predictions)
{
//只有相似度大于2.0的手势才会被输出
if (pred.score > 2.0)
{
//testPActivityName();
Log.d("RecogniseGesture", "name-->" + pred.name);
startApp(pred.name);
return;
}
else
{
//Log.d("FxRecogniseGesture", "无匹配手势");
new AlertDialog.Builder(RecogniseGesture.this).setMessage("不存在该手势").
setPositiveButton("确定", null).show();
}
}
}
...
通过上面的代码片段可知,调用GestureLibrary的recognize对当前用户绘制的手势进行匹配操作,recognize的参数gesture对应的是当前用户绘制的手势。recognize方法返回类型为ArrayList<Prediction>集合,该集合中的元素Prediction类的源代码如下:
package android.gesture;
public class Prediction {
public final String name;
public double score;
Prediction(String label, double predictionScore) {
name = label;
score = predictionScore;
}
@Override
public String toString() {
return name;
}
}
Prediction的属性name为被匹配的手势名字,score为手势的匹配分数(匹配分数越高,说明对手势的相似度匹配要求就越高)。所以,通过调用GestureLibrary的recognize方法返回的Prediction,就可以知道当前手势和已保存的手势匹配的相似度。
二. 手势匹配源码实现
在分析手势匹配源码实现之前,先总体来看看有关涉及到手势匹配相关的源码类之间的关系,如下图:
上图中的相关类简介:
GestureLibrary:手势库类,对外提供recognize函数,是实现手势匹配的关键入口。
GestureStore:GestureLibrary的recognize函数真正的源码内部实现类。
Instance:封装手势通过时间采样或空间采样后的离散点。该类的vector属性描述的就是当前被采样后的手势对应的多个离散点(这些点是有方向的,所以又可以称为向量)。
Learner:抽象类,内部提供了对Instance进行添加、获取、移除操作的方法。同时提供了抽象方法classify。
InstanceLearner:继承Learner,覆盖实现Learner中的抽象方法classify。
GestureUtils:手势源码中的工具类。手势匹配源码实现,基本上最终都会通过调用该工具类中提供的方法来实现。
GestureUtils类中相关的重点方法实现介绍:
1). spatialSampling:对连续点进行空间采样(gesture由多个连续点组成)
2). temporalSampling: 对连续点进行时间采样(gesture由多个连续点组成)
3). computeCentroid:计算一组点的质心
4). computeCoVariance: 计算一组点的方差-协方差矩阵
5). computeTotalLength: 计算一组点的总长度
6). computeStraightness: 计算一组点的直线度
7). squaredEuclideanDistance:计算两个向量之间的平方欧式距离
8). cosineDistance: 计算两个向量之间的余弦值,返回的是0到π之间的值
9). minimumCosineDistance: 计算两个向量之前最小的余弦距离,参数vector1为之前保存的向量,vector2为当前输入的向量。
10). computeOrientedBoundingBox:计算一个点集的最小边界框
11). rotate: 旋转一组点
12). translate: 移动一组点
13). scale: 缩放一组点
在前篇文章《手势的保存和加载》中,可以知道,用户绘制的手势是通过调用GestureLibrary的addGesture添加到手势库中,而该函数最终是通过调用GestureStore对象的addGesture来实现的。
回顾一下GestureStore类中的addGesture方法实现代码,如下:
public class GestureStore {
...
private final HashMap<String, ArrayList<Gesture>> mNamedGestures =
new HashMap<String, ArrayList<Gesture>>();
private Learner mClassifier;
private boolean mChanged = false;
public GestureStore() {
mClassifier = new InstanceLearner();
}
//手势保存在一个ArrayList集合里,ArrayList又以entryName为key值保存在HashMap集合里
public void addGesture(String entryName, Gesture gesture) {
if (entryName == null || entryName.length() == 0) {
return;
}
ArrayList<Gesture> gestures = mNamedGestures.get(entryName);
if (gestures == null) {
gestures = new ArrayList<Gesture>();
mNamedGestures.put(entryName, gestures);
}
gestures.add(gesture);
//通过gesture得到的Instance对象,存放到mClassifier对象(Learner类型)的成员mInstances集合中
mClassifier.addInstance(
Instance.createInstance(mSequenceType, mOrientationStyle, gesture, entryName));
mChanged = true;
}
...
}
在addGesture方法中:
Step1. 根据保存的手势及手势名字通过调用Instance的静态方法createInstance,创建对应的Instance对象(参数),然后将创建的Instance对象通过调用Learner的addInstance方法保存到Learner的成员变量mInstance集合中(ArrayList<Instance>类型)。
Step2. 因此,Learner中的mInstance集合将保存着各手势对应的Instance对象。这样的话,在进行手势匹配时,就可以通过执行Learner的getInstances方法取出保存手势对应的Instance对象,然后将保存的Instance对象和当前绘制的手势创建的Instance对象进行匹配;
对当前手势进行匹配是通过调用GestureLibrary的recognize方法实现的,该法返回描述匹配相似度的ArrayList<Prediction>集合。接下来对该方法的源码实现进行分析;
--->GestureLibrary的recognize方法实现代码如下:
public abstract class GestureLibrary {
protected final GestureStore mStore;
protected GestureLibrary() {
mStore = new GestureStore();
}
...
public ArrayList<Prediction> recognize(Gesture gesture) {
return mStore.recognize(gesture);
}
...
}
通过上面代码可知,GestureLibrary的recognize方法通过调用GestureStore对象的recognize方法来实现的。
--->GestureStore的recognize方法实现代码如下:
public class GestureStore {
...
public static final int SEQUENCE_SENSITIVE = 2;
...
public static final int ORIENTATION_SENSITIVE = 2;
...
private int mSequenceType = SEQUENCE_SENSITIVE;
private int mOrientationStyle = ORIENTATION_SENSITIVE;
...
private Learner mClassifier;
...
public GestureStore() {
mClassifier = new InstanceLearner();
}
...
public ArrayList<Prediction> recognize(Gesture gesture) {
//根据gesture创建Instance对象
Instance instance = Instance.createInstance(mSequenceType,
mOrientationStyle, gesture, null);
//此处的instance.vector已经过时间采样或空间采样处理
return mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector);
}
...
}
在GestureStore的recognize方法中:
Step1.根据得到的mSequenceType值(默认值为SEQUENCE_SENSITIVE)、mOrientationStyle值(默认值为ORIENTATION_SENSITIVE)、gesture对象(当前手势),通过调用Instance的静态方法createInstance创建相应的Instance对象instance。
Step2. 根据得到的mSequenceType值值、mOrientationStyle值、instance对象的属性vector,通过调用InstanceLearner对象的classify方法,方法返回的是ArrayList<Prediction>类型。
--->Instance的静态方法createInstance代码实现如下:
class Instance {
...
/**
* create a learning instance for a single stroke gesture
*
* @param gesture
* @param label
* @return the instance
*/
static Instance createInstance(int sequenceType, int orientationType, Gesture gesture, String label) {
float[] pts;
Instance instance;
if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {
//通过时间采样得到对应手势的样品pts
pts = temporalSampler(orientationType, gesture);
instance = new Instance(gesture.getID(), pts, label);
//对pts手势样品进行正常化
instance.normalize();
} else {
//通过空间采样得到对应手势的样品
pts = spatialSampler(gesture);
instance = new Instance(gesture.getID(), pts, label);
}
return instance;
}
...
}
在Instance的静态方法createInstance中:
Step1.根据得到的gesture中的第一个GestureStroke对象(一个Gesture对象由单个或多个GestureStroke组成)、SEQUENCE_SAMPLE_SIZE值(默认值为16),通过调用GestureUtils的静态方法temporalSampling将GestureStroke对象中封装的多个连续点进行时间采样成多个离散点。然后将得到的多个离散点赋值给浮点型数组pts。
Step2.对通过时间采样得到的多个离散点进行一些处理操作(计算质心、移动、旋转等),然后将这些通过处理的离散点返回给Instance的createInstance静态方法中的局部变量pts。最终,通过执行temporalSampler(orientationType, gesture)后,局部变量pts存放的是,当前用户绘制的手势中的第一个GestureStroke(单笔画识别)对象经过时间采样生成的多个离散点。
Step3.执行完temporalSampler(orientationType, gesture)得到pts后,接着,根据得到的当前手势的ID、pts、label(为空)创建Instance对象instance。在Instance的构造函数中,会将ID、pts、label分别赋值给Instance的属性id、vector、label。所以instance. vector即为当前用户绘制的手势经过时间采样后的离散点。
Step4.在执行创建Instance对象后,通过调用Instance对象的normalize对离散点进行正常化。
到此,Instance的temporalSampler方法就执行完了,接着会返回创建得到的Instance对象instance。--->回到GestureStore的recognize方法中:
在执行完Instance的静态方法createInstance返回得到的instance对象后,接着继续执行mClassifier.classify(mSequenceType, mOrientationStyle, instance.vector),instance.vector就是已经经过时间采样或空间采样后得到的当前手势对应的多个离散点。mClassifier为InstanceLearner对象,InstanceLearner的classify方法代码实现如下:
class InstanceLearner extends Learner {
...
//分类识别手势
@Override
ArrayList<Prediction> classify(int sequenceType, int orientationType, float[] vector) {
ArrayList<Prediction> predictions = new ArrayList<Prediction>();
ArrayList<Instance> instances = getInstances();
int count = instances.size();
TreeMap<String, Double> label2score = new TreeMap<String, Double>();
for (int i = 0; i < count; i++) {
//取出之前保存的Instance和当前的Instance进行比较识别
Instance sample = instances.get(i);
if (sample.vector.length != vector.length) {
continue;
}
double distance;
if (sequenceType == GestureStore.SEQUENCE_SENSITIVE) {
//最小的余弦值,此处的vector是经过时间采样的时间序列点
distance = GestureUtils.minimumCosineDistance(sample.vector, vector, orientationType);
} else {
/*平方欧氏距离,欧式距离就是两点之间的距离:如a(x1,y1),b(x2,y2),则欧式距离为d = sqrt((x1-x2)^ + (x2-y2)^)
* vector是经过空间采样的点序列
*/
distance = GestureUtils.squaredEuclideanDistance(sample.vector, vector);
}
double weight;
if (distance == 0) {
weight = Double.MAX_VALUE;
} else {
weight = 1 / distance;
}
Double score = label2score.get(sample.label);
if (score == null || weight > score) {
label2score.put(sample.label, weight);
}
}
// double sum = 0;
for (String name : label2score.keySet()) {
double score = label2score.get(name);
// sum += score;
predictions.add(new Prediction(name, score));
}
// normalize
// for (Prediction prediction : predictions) {
// prediction.score /= sum;
// }
Collections.sort(predictions, sComparator);
return predictions;
}
...
}
InstanceLearner的classify中做了如下处理:
Step1.创建ArrayList<Prediction>集合对象predictions。获取保存在手势库中的所有Instance对象instances(ArrayList<Instance>集合)。创建TreeMap<String, Double>类型映射表label2score,以保存手势的名字为键值,对应保存当前手势与保存手势之间的匹配分数score。
Step2. 遍历手势库中已保存的instances集合中的每个Instance对象,将遍历出的每个Instance对象中封装的vector和当前Instance对象的vector(classify方法传进来的实参instance.vector)进行处理。
Step3. 在遍历instances的处理中,当classify方法传进来的sequenceType为GestureStore.SEQUENCE_SENSITIVE(默认值)时,则根据遍历出的每个Instance对象中封装的vector和当前Instance对象的vector,调用GestureUtils的minimumCosineDistance方法,计算这两个vector的最小的余弦值。minimumCosineDistance返回distance(double类型)。
Step4. 在Step3中,当sequenceType不为GestureStore.SEQUENCE_SENSITIVE时,则调用GestureUtils的squaredEuclideanDistance方法,计算这两个vector平方欧氏距离。squaredEuclideanDistance返回distance(double类型)。
Step4. 通过Step3或Step4得到distance转化为手势匹配分数score(权重weight = 1/distance即为score), 然后将其以对应被遍历的Instance对象的名字(即已保存的某一手势对应的名字)为键值,保存到集合label2score中。
Step5. 遍历完instances,得到label2score后,接着将label2score保存的信息进行遍历,将根据遍历得到的每个key值name和value值score,创建对应的Prediction对象,然后将创建得到的Prediction对象添加到集合predictions中。
Step6. 对predictions集合中的内容进行排序,然后返回predictions。
所以,调用GestureLibrary的recognize方法进行手势匹配操作,最终返回的是ArrayList<Prediction>集合predictions。遍历predictions中的Prediction对象,通过Prediction对象的score值就可以知道当前手势和已保存手势之间的匹配相似度(即手势匹配分数)。