本章包括以下内容:
- 检测图像中的角点;
- 快速检测特征;
- 尺度不变特征的检测;
- 多尺度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);
这是原始图像。
结果是一个二值分布图像,如下图所示。为了能更直观地观察图像,此处进行了反转处理(即用cv::THRESH_BINARY_INV 代替cv::THRESH_BINARY,用黑色表示被检测的角点)。
在前面的函数中,我们发现兴趣点检测法需要使用几个参数,这可能会导致该方法很难调节。此外,得到的角点分布图中包含很多聚集的角点像素,而不是我们想要检测的具有明确定位的角点。因此,我们来定义一个检测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);
结果如下图所示。
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); // 画图标志
选择这个画图标志后,输入图像上会画出关键点,输出结果如下所示。
有一种比较有趣的做法,就是用一个负数作为关键点颜色。这样一来,画每个圆时会随机选用不同的颜色。
FAST 对角点的定义基于候选特征点周围的图像强度值。以某个点为中心做一个圆,根据圆上的像素值判断该
点是否为关键点。如果存在这样一段圆弧,它的连续长度超过周长的3/4,并且它上面所有像素的强度值都与圆心的强度值明显不同(全部更暗或更亮),那么就认定这是一个关键点。
这种测试方法非常简单,计算速度也很快。而且在它的原始公式中,算法还用了一个技巧来进一步提高处理速度。如果我们测试圆周上相隔90 度的四个点(例如取上、下、左、右四个位置),就很容易证明:为了满足前面的条件,其中必须有三个点都比圆心更亮或都比圆心更暗。
如果不满足该条件,就可以立即排除这个点,不需要检查圆周上的其他点。这种方法非常高效,因为在实际应用中,图像中大部分像素都可以用这种“四点比较法”排除。
从概念上讲,用于检查像素的圆的半径应该作为方法的一个参数。但是根据经验,半径为3时可以得到好的结果和较高的计算效率。因此需要在圆周上检查16 个像素。
这里用来预测试的像素是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 对每个网格的小图像进行关键点检测,这样得到的关键点分布较为均匀,如下图所示。
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 关键点。
ORB 特征的检测方法如下所示:
// 构造ORB 特征检测器对象
cv::Ptr<cv::ORB> ptrORB =
cv::ORB::create(75, // 关键点的总数
1.2, // 图层之间的缩放因子
8); // 金字塔的图层数量
// 检测关键点
ptrORB->detect(image, keypoints);
调用的结果如下所示。