第 8 章 检测兴趣点


本章包括以下内容:

  • 检测图像中的角点;
  • 快速检测特征;
  • 尺度不变特征的检测;
  • 多尺度FAST 特征的检测。


8.2 检测图像中的角点

Harris 特征检测是检测角点的经典方法,本节将详细探讨这个方法。

OpenCV 中检测Harris 角点的基本函数是cv::cornerHarris,它的使用方法非常简单。调用该函数时输入一幅图像,返回的结果是一个浮点数型图像,其中每个像素表示角点强度。然后对输出图像阈值化,以获得检测角点的集合。代码如下所示:

    // 检测Harris 角点
    cv::Mat cornerStrength;
    cv::cornerHarris(image, // 输入图像
        cornerStrength, // 角点强度的图像
        3, // 邻域尺寸
        3, // 口径尺寸
        0.01); // Harris 参数
        // 对角点强度阈值化
    cv::Mat harrisCorners;
    double threshold = 0.0001;
    cv::threshold(cornerStrength, harrisCorners,
        threshold, 255, cv::THRESH_BINARY_INV);

这是原始图像。

church01.jpg

结果是一个二值分布图像,如下图所示。为了能更直观地观察图像,此处进行了反转处理(即用cv::THRESH_BINARY_INV 代替cv::THRESH_BINARY,用黑色表示被检测的角点)。

result.jpg

在前面的函数中,我们发现兴趣点检测法需要使用几个参数,这可能会导致该方法很难调节。此外,得到的角点分布图中包含很多聚集的角点像素,而不是我们想要检测的具有明确定位的角点。因此,我们来定义一个检测Harris 角点的类,以改进角点检测方法。

class HarrisDetector {

private:

    // 32 位浮点数型的角点强度图像
    cv::Mat cornerStrength;
    // 32 位浮点数型的阈值化角点图像
    cv::Mat cornerTh;
    // 局部最大值图像(内部)
    cv::Mat localMax;
    // 平滑导数的邻域尺寸
    int neighborhood;
    // 梯度计算的口径
    int aperture;
    // Harris 参数
    double k;
    // 阈值计算的最大强度
    double maxStrength;
    // 计算得到的阈值(内部)
    double threshold;
    // 非最大值抑制的邻域尺寸
    int nonMaxSize;
    // 非最大值抑制的内核
    cv::Mat kernel;

public:

    HarrisDetector() : neighborhood(3), aperture(3),
        k(0.01), maxStrength(0.0),
        threshold(0.01), nonMaxSize(3) {
        // 创建用于非最大值抑制的内核
        setLocalMaxWindowSize(nonMaxSize);
    }

检测Harris 角点需要两个步骤。首先是计算每个像素的Harris 值:

// 计算Harris 角点
void detect(const cv::Mat& image) {
    // 计算Harris
    cv::cornerHarris(image, cornerStrength,
        neighbourhood,// 邻域尺寸
        aperture, // 口径尺寸
        k); // Harris 参数

    // 计算内部阈值
    cv::minMaxLoc(cornerStrength, 0, &maxStrength);

    // 检测局部最大值
    cv::Mat dilated; // 临时图像
    cv::dilate(cornerStrength, dilated, cv::Mat());
    cv::compare(cornerStrength, dilated, localMax, cv::CMP_EQ);

然后,用指定的阈值获得特征点。因为Harris 值的可选范围取决于选择的参数,所以阈值被作为质量等级,用最大Harris 值的一个比例值表示:

// 用Harris 值得到角点分布图
cv::Mat getCornerMap(double qualityLevel) {

    cv::Mat cornerMap;

    // 对角点强度阈值化
    threshold = qualityLevel * maxStrength;
    cv::threshold(cornerStrength, cornerTh, threshold, 255,
        cv::THRESH_BINARY);

    // 转换成8 位图像
    cornerTh.convertTo(cornerMap, CV_8U);

    // 非最大值抑制
    cv::bitwise_and(cornerMap, localMax, cornerMap);

    return cornerMap;
}

这个方法将返回一个被检测特征的二值角点分布图。因为Harris 特征的检测过程分为两个方法,所以我们可以用不同的阈值来测试检测结果(直到获得适当数量的特征点),而不必重复进行耗时的计算过程。当然,你也可以从以std::vector 形式表示的cv::Point 实例中得到Harris特征:

// 用Harris 值得到特征点
void getCorners(std::vector<cv::Point>& points, double qualityLevel) {

    // 获得角点分布图
    cv::Mat cornerMap = getCornerMap(qualityLevel);
    // 获得角点
    getCorners(points, cornerMap);
}

// 用角点分布图得到特征点
void getCorners(std::vector<cv::Point>& points,
    const cv::Mat& cornerMap) {
    // 迭代遍历像素,得到所有特征
    for (int y = 0; y < cornerMap.rows; y++) {

        const uchar* rowPtr = cornerMap.ptr<uchar>(y);

        for (int x = 0; x < cornerMap.cols; x++) {

            // 如果它是一个特征点
            if (rowPtr[x]) {

                points.push_back(cv::Point(x, y));
            }
        }
    }
}

这个类通过增加非最大值抑制步骤,也改进了Harris 角点的检测过程,下一节会详细解释这个步骤。现在可以用cv::circle 函数画出检测到的特征点,方法如下所示:

// 在特征点的位置画圆形
void drawOnImage(cv::Mat& image,
    const std::vector<cv::Point>& points,
    cv::Scalar color = cv::Scalar(255, 255, 255),
    int radius = 3, int thickness = 1) {

    std::vector<cv::Point>::const_iterator it = points.begin();
    // 针对所有角点
    while (it != points.end()) {
        // 在每个角点位置画一个圆
        cv::circle(image, *it, radius, color, thickness);
        ++it;
    }
}

使用这个类检测Harris 特征点的方法如下所示:

// 创建Harris 检测器实例
HarrisDetector harris;
// 计算Harris 值
harris.detect(image);
// 检测Harris 角点
std::vector<cv::Point> pts;
harris.getCorners(pts,0.02);
// 画出Harris 角点
harris.drawOnImage(image,pts);

结果如下图所示。

result.jpg


8.3 快速检测特征

本节将介绍另一种特征点算子,叫作FAST(Features from Accelerated Segment Test,加速分割测试获得特征)。这种算子专门用来快速检测兴趣点——只需对比几个像素,就可以判断它是否为关键点。

OpenCV 有检测特征点的公共接口。

    // 关键点的向量
    std::vector<cv::KeyPoint> keypoints;
    // FAST 特征检测器,阈值为40
    cv::Ptr<cv::FastFeatureDetector> ptrFAST =
        cv::FastFeatureDetector::create(40);
    // 检测关键点
    ptrFAST->detect(image, keypoints);

OpenCV 也提供了在图像上画关键点的通用函数:

    cv::drawKeypoints(image, // 原始图像
        keypoints, // 关键点的向量
        image, // 输出图像
        cv::Scalar(255, 255, 255), // 关键点的颜色
        cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); // 画图标志

选择这个画图标志后,输入图像上会画出关键点,输出结果如下所示。

result.jpg

有一种比较有趣的做法,就是用一个负数作为关键点颜色。这样一来,画每个圆时会随机选用不同的颜色。

FAST 对角点的定义基于候选特征点周围的图像强度值。以某个点为中心做一个圆,根据圆上的像素值判断该
点是否为关键点。如果存在这样一段圆弧,它的连续长度超过周长的3/4,并且它上面所有像素的强度值都与圆心的强度值明显不同(全部更暗或更亮),那么就认定这是一个关键点。

这种测试方法非常简单,计算速度也很快。而且在它的原始公式中,算法还用了一个技巧来进一步提高处理速度。如果我们测试圆周上相隔90 度的四个点(例如取上、下、左、右四个位置),就很容易证明:为了满足前面的条件,其中必须有三个点都比圆心更亮或都比圆心更暗。

如果不满足该条件,就可以立即排除这个点,不需要检查圆周上的其他点。这种方法非常高效,因为在实际应用中,图像中大部分像素都可以用这种“四点比较法”排除。

从概念上讲,用于检查像素的圆的半径应该作为方法的一个参数。但是根据经验,半径为3时可以得到好的结果和较高的计算效率。因此需要在圆周上检查16 个像素。

image.png

这里用来预测试的像素是1、5、9 和13,至少需要9 个比圆心更暗(或更亮)的连续像素。这种设置通常称为FAST-9 角点检测器,也是OpenCV 默认采用的方法。

一个点与圆心强度值的差距必须达到一个指定的值,才能被认为是明显更暗或更亮;这个值就是创建检测器实例时指定的阈值参数。这个阈值越大,检测到的角点数量就越少。

至于Harris 特征,通常最好在发现的角点上执行非最大值抑制。因此,需要定义一个角点强度的衡量方法。有多种衡量方法可供选择,下面介绍的是实际选用的方法——计算中心点像素与认定的连续圆弧上的像素的差值,然后将这些差值的绝对值累加,就能得到角点强度。可以从cv::KeyPoint 实例的response 属性获取角点强度。

在事先明确兴趣点数量的情况下,可以对检测过程进行动态适配。简单的做法是采用范围较大的阈值检测出很多兴趣点,然后从中提取出n 个强度最大的。

if (numberOfPoints < keypoints.size())
    std::nth_element(keypoints.begin(),
        keypoints.begin() + numberOfPoints,
        keypoints.end(),
        [](cv::KeyPoint& a, cv::KeyPoint& b) {
            return a.response > b.response; });

函数中keypoints 的类型是std::vector,表示检测到的兴趣点,numberOfPoints 是需要的兴趣点数量。最后一个参数是lambda 比较器,用于提取最佳的兴趣点。请注意,如果检测到的兴趣点太少(少于需要的数量),那就要采用更小的阈值,但是阈值太宽松又会加大计算量,所以需要权衡利弊,选取最佳的阈值。

检测图像特征点时还会遇到一种情况,就是兴趣点的分布很不均匀。keypoints 通常会聚集在纹理较多的区域。有一种常用的处理方法,就是把图像分割成网格状,对每个小图像进行单独检测。以下代码就是网格适配特征检测:

    // 关键点的向量
    std::vector<cv::KeyPoint> keypoints;
    int total(100); // requested number of keypoints
    int hstep(5), vstep(3); // a grid of 4 columns by 3 rows
    // hstep= vstep= 1; // try without grid
    int hsize(image.cols / hstep), vsize(image.rows / vstep);
    int subtotal(total / (hstep * vstep)); // number of keypoints per grid
    cv::Mat imageROI;
    std::vector<cv::KeyPoint> gridpoints;

    cv::Ptr<cv::FastFeatureDetector> ptrFAST = cv::FastFeatureDetector::create(40);
    // detection with low threshold
    ptrFAST->setThreshold(20);
    // non-max suppression
    ptrFAST->setNonmaxSuppression(true);

    // 检测每个网格
    for (int i = 0; i < vstep; i++)
        for (int j = 0; j < hstep; j++) {
            // 在当前网格创建ROI
            imageROI = image(cv::Rect(j * hsize, i * vsize, hsize, vsize));
            // 在网格中检测关键点
            gridpoints.clear();
            ptrFAST->detect(imageROI, gridpoints);
            // 获取强度最大的FAST 特征
            auto itEnd(gridpoints.end());
            if (gridpoints.size() > subtotal) {
                // 选取最强的特征
                std::nth_element(gridpoints.begin(),
                    gridpoints.begin() + subtotal,
                    gridpoints.end(),
                    [](cv::KeyPoint& a,
                        cv::KeyPoint& b) {
                            return a.response > b.response; });
                itEnd = gridpoints.begin() + subtotal;
            }
            // 加入全局特征容器
            for (auto it = gridpoints.begin(); it != itEnd; ++it) {
                // 转换成图像上的坐标
                it->pt += cv::Point2f(j * hsize, i * vsize);
                keypoints.push_back(*it);
            }
        }

    cv::drawKeypoints(image, keypoints, image, cv::Scalar(255, 255, 255), cv::DrawMatchesFlags::DRAW_OVER_OUTIMG);

这里的关键在于,利用ROI 对每个网格的小图像进行关键点检测,这样得到的关键点分布较为均匀,如下图所示。

result.jpg


8.4 尺度不变特征的检测

计算机视觉界引入了尺度不变特征的概念。它的理念是,不仅在任何尺度下拍摄的物体都能检测到一致的关键点,而且每个被检测的特征点都对应一个尺度因子。理想情况下,对于两幅图像中不同尺度的同一个物体点,计算得到的两个尺度因子之间的比率应该等于图像尺度的比率。

本节将介绍SURF 特征,它的全称为加速稳健特征(Speeded Up Robust Feature)。我们将会看到,它不仅是尺度不变特征,而且是具有较高计算效率的特征。

SURF 特征检测属于opencv_contrib 库,这里将重点讨论cv::xfeatures2d 模块和它的cv::xfeatures2d::SurfFeatureDetector 类。和其他检测器一样,检测兴趣点之前要先创建检测器实例,然后调用它的检测方法:

    // 创建SURF 特征检测器对象
    cv::Ptr<cv::xfeatures2d::SurfFeatureDetector> ptrSURF =
        cv::xfeatures2d::SurfFeatureDetector::create(2000.0);
    // 检测关键点
    ptrSURF->detect(image, keypoints);

为了画出这些特征,再次使用OpenCV的cv::drawKeypoints 函数,但是要采用cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS 标志以显示相关的尺度因子:

// 画出关键点,包括尺度和方向信息
cv::drawKeypoints(image, // 原始图像
    keypoints, // 关键点的向量
    featureImage, // 结果图像
    cv::Scalar(255, 255, 255), // 点的颜色
    cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);

这里使用cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS 标志得到了关键点的圆,并且圆的尺寸与每个特征计算得到的尺度成正比。为了使特征具有旋转不变性,SURF 还让每个特征关联了一个方向,由每个圆内的辐射线表示。

SURF 算法是SIFT 算法的加速版,而SIFT(Scale-Invariant Feature Transform,尺度不变特征转换)是另一种著名的尺度不变特征检测法。

SIFT 特征的检测过程与SURF 非常相似:

// 构建SIFT 特征检测器实例
cv::Ptr<cv::xfeatures2d::SiftFeatureDetector> ptrSIFT =
cv::xfeatures2d::SiftFeatureDetector::create();
// 检测关键点
ptrSIFT->detect(image, keypoints);

由于SIFT 基于浮点内核计算特征点,因此通常认为SIFT 算法检测在空间和尺度上能取得更加精确的定位。基于同样的原因,它的计算效率也更低,尽管相对效率取决于具体的实现方法。


8.5 多尺度FAST 特征的检测

本节将介
绍BRISK(Binary Robust Invariant Scalable Keypoints,二元稳健恒定可扩展关键点)检测法,它基于上一节介绍的FAST 特征检测法。本节还将讨论另一种检测方法ORB(Oriented FAST andRotated BRIEF,定向FAST 和旋转BRIEF)。

根据上一节介绍的方法,首先创建检测器实例,然后对一幅图像调用detect 方法:

    // 构造BRISK 特征检测器对象
    cv::Ptr<cv::BRISK> ptrBRISK = cv::BRISK::create(60, 5);
    // 检测关键点
    ptrBRISK->detect(image, keypoints);

下图显示了在多个尺度下检测到的BRISK 关键点。

result.jpg

ORB 特征的检测方法如下所示:

    // 构造ORB 特征检测器对象
    cv::Ptr<cv::ORB> ptrORB =
        cv::ORB::create(75, // 关键点的总数
            1.2, // 图层之间的缩放因子
            8); // 金字塔的图层数量
            // 检测关键点
    ptrORB->detect(image, keypoints);

调用的结果如下所示。

result.jpg
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容