iOS图形手势识别框架SGGestureRecognizer

简介

苹果官方为我们提供了简单手势的识别器,但对于图形手势,例如五角星、三角形等的识别,就需要自己实现了。通过识别这些手势,可以去执行特定的操作,或是输入公式、释放魔法等,可以为App增光添彩。

下载与使用

该框架已经上传到github,点击这里前去下载,欢迎Star!
有关该框架的使用在github上已经写明,这里不再赘述,本文主要介绍的是图形手势识别的实现原理与框架的结构。

框架的结构

一个图形手势是一条曲线,可以用采样点来描述,存储采样点的类为SGGesturePoint,使用它替代CGPoint,更符合面向对象的操作规范。
采样点的集合为SGGestureSet,它用于记录所有采样点以及手势的名称,以及标准化以后的手势向量。
手势向量使用SGGestureVector存储,向量通过将所有采样点的(x,y)坐标依次插入并标准化得来,手势向量用于进行余弦相似度计算。
用于标准化集合、生成向量、保存、加载与识别手势的类为SGGestureManager,它是一个单例对象,正常使用中只需要关心manager和set两个对象,其他对象由manager负责管理。

手势采样的过程

1.采样

Demo中给出的是使用UIPanGestureRecognizer完成的采样,每个采样点都是一个CGPoint,将其包装秤NSValue并且存储在数组中。

2.生成集合

使用SGGestureSet的gestureSetWithName:points:方法,传入手势的名称与采样点(NSValue数组),即可使用采样点初始化一个集合。

3.重新采样

将集合中的所有采样点构成的图形看作折线,根据所需要的采样密度确定采样间距interval,并在原曲线上生成均匀分布的采样点,生成采样点的代码如下,代码后将结合图例进行讲解。

// to resample the curve, calculate the length of the curve
SGGestureSet *tempSet = *set;
double sumLength = 0;
for (int i = 1; i < tempSet.countPoints; i++) {
    SGGesturePoint *pt1 = [tempSet pointAtIndex:i];
    SGGesturePoint *pt2 = [tempSet pointAtIndex:i - 1];
    sumLength += [pt1 distanceTo:pt2];
}
// resample with sample uniform distributed points
SGGestureSet *resampleSet = [SGGestureSet gestureSetWithName:tempSet.name];
double Interval = sumLength / self.samplePointCount;
double D = 0;
SGGesturePoint *p1 = [tempSet pointAtIndex:0];
[resampleSet addGesturePoint:p1];
for (int i = 1; i < tempSet.countPoints;) {
    SGGesturePoint *p2 = [tempSet pointAtIndex:i];
    double d = [p1 distanceTo:p2];
    if ((D + d) >= Interval) {
        double k = (Interval - D ) / d;
        double x = p1.x + k * (p2.x - p1.x);
        double y = p1.y + k * (p2.y - p1.y);
        SGGesturePoint *p = [SGGesturePoint gesturePointWithCGPoint:CGPointMake(x, y)];
        [resampleSet addGesturePoint:p];
        D = 0;
        p1 = p;
    }else{
        D += d;
        p1 = p2;
        i++;
    }
}

其中D用于折现拐点后确定下一采样点的距离,d为原集合中的相邻采样点间距,下图是某图形手势的局部折线图。
第一次进入循环时,P1为原集合的第一个采样点(也是重新采样集合的第一个点),P2为原集合的第二个采样点,他们之间的距离P大于重新采样的采样点间距interval,这时D=0,D+d=d>interval,因此进入if分支。

接下来根据interval与d的比例关系求出x、y的步进值,从而得到下一个重新采样点的坐标点,并且这个点作为新的P1。

以此类推,由于折线段的长度比interval大的多,因此能够分布许多新采样点,直到P1足够接近P2,使得interval>d,如下图所示。

这时下一个采样点应该落在下一个折线段上,并且为了保证均匀分布,下一个采样点距离折线段拐点的距离应该减去当前P1到P2的距离,这就是D的作用了。下图说明了这个计算的目的。

这时候会进入else分支,将P1更新为P2坐标,并且将d累加到D中,需要注意的是,下一个P2的坐标并不是原集合中的点,而是根据D计算出的点,因此应该跳过原集合中的下一个点,这就是i++的作用。如果曲线有足够多的短折线段,则会不断的进入else分支,一直累加d,直到满足新采样点间距,也就是D+d>=interval,才生成一个新的采样点,本图中的情况仅仅计算了一次D就进入了下一条比较长的折线段,这时候清空D,开始在折线段上分布采样点,如下图所示。

经过多次这样的运算,就可以完成均匀分布的重新采样了,之后使用的是重新采样点的集合。

4.曲线位置的标准化

将集合中的x、y分别求平均值,得到曲线的重心,根据重心坐标将曲线移动到坐标原点,得到标准位置的曲线。

5.曲线尺寸的标准化

根据曲线的外接矩形与标准尺寸将曲线上的每个点进行比例运算,即可得到缩放到标准尺寸的曲线。

6.曲线转角的标准化

根据曲线上的第一个采样点与中心的连线的角度对曲线进行标准化,设当前角度为iAngle,目标角度为r,则如下图所示经过坐标变换将曲线旋转到红色位置。

7.生成向量

为了进行后续运算,需要将二元采样点集化为一元集合,也可以看做多维向量,方法是依次将采样点的x、y坐标插入一元集合,并对向量进行标准化,每个向量代表一个手势,可用于后续的比较运算。

手势识别的过程

对于需要识别的手势,先经过上面的运算得到手势向量,然后将这个手势向量与手势库中的向量逐一进行余弦相似度的运算,余弦相似度比较的是向量的夹角,夹角越小则越相似,根据一定的阈值来筛选出符合条件的所有手势,并在遍历结束后取最优(运算结果最小)的作为匹配结果。
余弦相似度的计算代码如下:

- (double)cosDistanceWithVector1:(SGGestureVector *)vec1 vector2:(SGGestureVector *)vec2 {
    double a = 0;
    double b = 0;
    for (int i = 0; i <= vec1.length - 1 && i <= vec2.length - 1; i+=2) {
        a += [vec1 doubleAtIndex:i] * [vec2 doubleAtIndex:i] + [vec1 doubleAtIndex:i + 1] * [vec2 doubleAtIndex:i + 1];
        b += [vec1 doubleAtIndex:i] * [vec2 doubleAtIndex:i + 1] - [vec1 doubleAtIndex:i + 1] * [vec2 doubleAtIndex:i];
    }
    double angle = atan(b / a);
    return acos(a * cos(angle) + b * sin(angle));
}

识别一个手势的代码如下,先标准化手势集合并得到向量,然后在手势库中筛选,最后选择最优结果。

- (NSString *)recognizeGestureSet:(SGGestureSet *)set {
    [self standardizeSet:&set];
    SGGestureVector *vec1 = [set getVector];
    SGGestureSet *bestSet = nil;
    double minD = CGFLOAT_MAX;
    for (int i = 0; i < self.gestureSets.count; i++) {
        SGGestureSet *libSet = self.gestureSets[i];
        SGGestureVector *vec2 = [libSet getVector];
        double D = [self cosDistanceWithVector1:vec1 vector2:vec2];
        if(D <= self.threshold && D < minD){
            minD = D;
            bestSet = libSet;
        }
    }
    return bestSet.name;
}

手势的存取

以上介绍的每一个与存储有关的类都遵循NSCoding协议,将每一个标准化的SGGestureSet存入到数组中,并将数组利用NSKeyedArchiver归档存储到磁盘,需要读取时再通过NSKeyedUnarchiver反归档即可。

posted on 2016-07-03 17:56  张大大123  阅读(249)  评论(0编辑  收藏  举报

导航