iOS图形手势识别框架SGGestureRecognizer

作者: Soulghost | 来源:发表于2016-07-03 17:46 被阅读280次

    简介

    苹果官方为我们提供了简单手势的识别器,但对于图形手势,例如五角星、三角形等的识别,就需要自己实现了。通过识别这些手势,可以去执行特定的操作,或是输入公式、释放魔法等,可以为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反归档即可。

    相关文章

      网友评论

      • AidenRao:The document “LaunchScreen.storyboard” requires Xcode 8.0 or later.
        GitHub 上的 demo 打不开,大部分人还是没有升级 Xcode 8.0的。 :smile: 并不知道实际效果是什么啊
        Soulghost:@饶志臻 不好意思哈,当时用Xcode8不小心点开了一次LaunchScreen,已经更新,现在可以用Xcode7打开了。

      本文标题:iOS图形手势识别框架SGGestureRecognizer

      本文链接:https://www.haomeiwen.com/subject/koqcjttx.html