【工程应用八】终极的基于形状匹配方案解决(小模型+预生成模型+无效边缘去除+多尺度+各项异性+最小组件尺寸)

   我估摸着这个应该是关于形状匹配或者模版匹配的最后一篇文章了,其实大概是2个多月前这些东西都已经弄完了,只是一直静不下来心整理文章,提醒一点,这篇文章后续可能会有多次修改(但不会重新发文章,而是在后台直接修改或者增加),所以有需要的朋友可以随时重复查看。

  这次带来的更新也是革命性和带有建设性的,使得该算法向工程化更加迈进了一步。不过严格意义上说和halcon还是有较大的差距的。

  能够带来这次的变化和提升,其实也得益于偶尔的一次交流,一个微信好友给我发了一份可以不用配置就可以运行的linemod的代码,因为可以运行,我就有兴趣看其代码的细节,也从中得到了更多的灵感,用于我自己的工程。从这个事情可以得出两个小经验:一个是要多交流,二是一个事情如果当前搞不定,不要一直搞,要有准备的等,说不定哪天就有意外的收获。 就好似我们掉了一个东西,怎么找都找不到,但是有可能突然某一天他就冒出来了。

  好,言归正传,后续的文字也不讲究就逻辑性,想到啥说啥。

  一、还是打击下linemod的速度

    linemod的过程包含了原图的(被查找图,也许很大)梯度角度量化(QuantizedOrientations)、计算响应图(IM_ComputeResponseMaps)、梯度扩散等等过程,这些过程,就是使用特殊的指令集进行优化,对于稍微大一点的图来说,也是相当耗时的,所以对于快速的目标识别来说linemod应该不是很好的方法。

  另外,linemod的代码里虽然有金字塔,但是他只是对特征方面用了金字塔,在旋转角度上他确没有进行下采样,也就是说,如果底层金字塔有100个旋转角度,第二层还是100个,第三个也还是100个,以此类对,而不是每层也减半的策略,这个可以从他的addTemplate含数里能看到细节。

  二、从linemod中学到一些非常有用的东西。

  1、创建模型时,直接旋转0角度时识别的特征作为其他角度的特征,而非旋转图像,然后在识别特征。

  在前面所有的文章中,我们创建模型的时候,都是采用的先旋转模版图像,然后再计算特征,并把这些特征保存起来。这样做,可以解决问题,但是带来了两个大的麻烦:首先是创建模型的速度会有影响,特备是对于稍微大一点的模版图,耗时非常吓人。对于形状匹配来说,是有不少场景的模版图特别大的,甚至占到了被查找图的一半以上的。这种情况,早期的方案基本就不能解决问题。 第二个麻烦是,我们存储这些特征也会占用大量的内存,同样对于大模版图,存在内存不够的风险。

  这个问题其实很多写模版匹配的朋友都有遇到过,而且早期我也没有好的解决方案,曾经尝试过旋转特征,不过不知道为什么当时得到的结果总是有问题。 

  那么在linemod里我们发现这样一段代码:

 int Detector::addTemplate_rotate(const string& class_id, int zero_id, float theta, cv::Point2f center)
    {
        std::vector& template_pyramids = class_templates[class_id];
        int template_id = static_cast(template_pyramids.size());
        const auto& to_rotate_tp = template_pyramids[zero_id];
        TemplatePyramid tp;
        tp.resize(pyramid_levels);
        for (int l = 0; l < pyramid_levels; ++l)
        {
            if (l > 0) center /= 2;
            for (auto& f : to_rotate_tp[l].features) 
            {
                Point2f p;
                p.x = f.x + to_rotate_tp[l].tl_x;
                p.y = f.y + to_rotate_tp[l].tl_y;
                Point2f p_rot = rotatePoint(p, center, -theta / 180 * CV_PI);
                Feature f_new;
                f_new.x = int(p_rot.x + 0.5f);
                f_new.y = int(p_rot.y + 0.5f);
                f_new.theta = f.theta - theta;
                while (f_new.theta > 360) f_new.theta -= 360;
                while (f_new.theta < 0) f_new.theta += 360;
                f_new.label = int(f_new.theta * 16 / 360 + 0.5f);
                f_new.label &= 7;
                tp[l].features.push_back(f_new);
            }
            tp[l].pyramid_level = l;
        }
        cropTemplates(tp);
        template_pyramids.push_back(tp);
        return template_id;
    }

  其中rotatePoint相关函数如下:

    static cv::Point2f rotate2d(const cv::Point2f inPoint, const double angRad)
    {
        cv::Point2f outPoint;
        //CW rotation
        outPoint.x = std::cos(angRad) * inPoint.x - std::sin(angRad) * inPoint.y;
        outPoint.y = std::sin(angRad) * inPoint.x + std::cos(angRad) * inPoint.y;
        return outPoint;
    }
    static cv::Point2f rotatePoint(const cv::Point2f inPoint, const cv::Point2f center, const double angRad)
    {
        return rotate2d(inPoint - center, angRad) + center;
    }

  这个反应了一些问题。首先,linemod现在的结果似乎还比较靠谱,因此,这种直接旋转特征的方法是可行的。其次,这个代码也给我们提供了特征旋转的计算公式,主要到那些sin cos了吧。就是那些公式。注意,这里的旋转获得不仅仅是旋转后特征的坐标位置(可能需要取整),而且特征的本质属性(对于linemod,是量化到0和8之间的角度值,对于我们标准的基于梯度的计算式,则是归一化后的X和Y方向的梯度值)也同步予以获取。

       我们对这个做适当的扩展。

        第一,我们看到linemod的代码里这种旋转前后的特征数量是没有改变的,也就是说,如果0度模版图有1000个特征,旋转后的其他角度也会有1000个特征。这里其实有个隐藏的问题。因为一般而言,这些特征在0角度时很多的坐标是连续的,当旋转取整后,一定概率上存在取整后的两个坐标是相同的,但是可能仅仅是坐标相同,量化角度和归一化的X和Y方向梯度确不一样,这是个矛盾的东西,一个固定的位置只能有一个特征,因此,遇到这种情况,可能就要进行一定的取舍,在linemod的代码里没有见到这样的内容和处理方案。我的做法时全部旋转后,按照坐标进行一次去重的操作,至于重复的留下哪一个,那就看谁排在前面了。 

  第二、我们知道0度的时候的特征是有亚像素这个讲法的,也就是说0度特征的坐标位置可以是亚像素的。所以我们用0度亚像素的位置作为旋转的基础,这样可能带来的旋转特征更为准确,这就为自己实现类似于create_shape_model_xld这样的接口提供了可能性。

       第三、这个旋转的方案是不带缩放的,为了支持缩放,可以在这个基础上加上X和Y方向的缩放因子,当X和Y方向的缩放因子相同时,就是多尺度匹配,当X和Y方向不相同时,即为各项异性匹配。

    第四、还有个问题值得探讨,就是所有金字塔层、所有角度的特征是都由最底层0角度的模版的特征经过缩放旋转生成呢,还是由每层金字塔的0角度特征旋转生成。 个人觉得,还是由每层金字塔的0角度特征旋转生成更为合理。 毕竟这个特征比用基层的特征旋转缩放少了一个缩放,准确度及精度上应该更为靠谱。

      第五、上述方案带来的最大好处时,我们不需要旋转模版图,再求这些特征了,因此,速度提升会非常明显,而且本身旋转这些特征的耗时基本可以忽略不计,甚至都可以实时计算。

      第六、注意到上面的cropTemplates函数,其具体的代码如下:

    static Rect cropTemplates(std::vector