在图像处理和计算机视觉领域,兴趣点(inter points),也被称作关键点(key points)、特征点(feature points)。它被大量用于解决物体识别、图像识别、图像匹配、视觉跟踪、三维重建等一系列的问题。我们不再观察整幅图,而是选择某些特殊的点,然后对它们进行局部有的放矢地分析。如果能检测到足够多的这种点,同时它们的区分度很高,并且可以精确定位稳定的特征,那么这个方法就具有实用价值。
图像特征类型可以被分为如下三种:
对于边缘检测之前已经讲过,见 边缘检测1 边缘检测2 。其中角点是个很特殊的存在。如果某一点在任意方向的一个微小变动都会引起灰度很大的变化,那么我们就把它称之为角点。角点作为图像上的特征点,包含有重要的信息,在图像融合和目标跟踪及三维重建中有重要的应用价值。他们在图像中可以轻易地定位,同时,在人造物体场景,比如门、窗、桌等处也随处可见。因为角点位于两条边缘的角点处,代表了两个边缘变化的方向上的点,所以它们是可以精确定位的二维特征,甚至可以达到亚像素的精度。又由于其图像梯度有很高的变化,这种变化是可以用来帮助检测角点的。需要注意的是,角点与位于相同强度区域上的点不同,与物体轮廓上的点也不同,因为轮廓点难以在相同的其他物体上精确定位。
另外,关于角点的具体描述可以有如下几种:
现有的角点检测算法并不是都十分健壮。很多方法都要求有大量的训练集和冗余数据来防止减少错误特征的出现。另外,角点检测方法的一个很重要的评价标准是其对多幅图像种相同或相似的检测能力,并且能够应对光照变换、图像旋转等图像变化。
在当前的图像处理领域,角点检测算法可归纳为以下三类:
而基于灰度图像的角点检测又可分为基于梯度、基于模板和基于模板梯度组合三类方法。其中基于模板的方法主要考虑像素领域点的灰度变化,即图像亮度的变化,将与邻点亮度对比足够大的点定义为角点。常见的基于模板的角点检测算法有 Kitchen-Rosenfeld 角点检测算法、Harris 角点检测算法、KLT角点检测算法及SUSAN角点检测算法。
Harris 角点检测是一种直接基于灰度图像的角点提取算法,稳定性高,尤其对L型角点检测精度高。但由于采用了高斯滤波,运算速度相对较慢,角点信息有丢失和位置偏移的现象,而且角点提取有聚簇现象。
为了定义图像中角点的概念,Harris 特征检测方法在假定的兴趣点周围放置了一个小窗口,并观察窗口内某个方向上强度值的平均变化。如果位移向量为 ( u , v ) (u, v) (u,v) ,那么可以用均方差之和表示强度的变化:
R = ∑ ( I ( x + u , y + v ) − I ( x , y ) ) 2 R = \sum (I(x+u,y+v) - I(x, y))^2 R=∑(I(x+u,y+v)−I(x,y))2
累加的范围是该像素周围一个预先定义的邻域(邻域的尺寸取决于 cv::cornerHarris
函数的第三个参数)。在所有方向上计算平均强度变化值,如果不止一个方向的变化值很高,就认为这个点是角点。根据这个定义,Harris测试的步骤应为:首先获得平均强度值变化最大的方向,然后检查垂直方向上的平均强度变化值,看它是否也很大;如果是,就说明这是一个角点。
从数学的角度看,可以用泰勒展开式近似地计算上述公式,验证这个判断:
R ≈ ∑ ( ( I ( x , y ) + ∂ I ∂ x u + ∂ I ∂ y v − I ( x , y ) ) 2 = ∑ ( ( ∂ I ∂ x u ) 2 + ( ∂ I ∂ y v ) 2 + 2 ∂ I ∂ x ∂ I ∂ y u v ) ) R \approx \sum \left((I(x,y) + \frac{\partial I}{\partial x}u + \frac{\partial I}{\partial y}v - I(x,y)\right)^2 = \sum \left( (\frac{\partial I}{\partial x}u)^2 + (\frac{\partial I}{\partial y}v)^2 + 2 \frac{\partial I}{\partial x} \frac{\partial I}{\partial y}uv) \right) R≈∑((I(x,y)+∂x∂Iu+∂y∂Iv−I(x,y))2=∑((∂x∂Iu)2+(∂y∂Iv)2+2∂x∂I∂y∂Iuv))
写成矩阵形式就是:
R ≈ [ u , v ] [ ∑ ( δ I δ x ) 2 ∑ ( δ I δ x δ I δ y ) ∑ ( δ I δ x − δ I δ y ) ∑ ( δ I δ y ) 2 ] [ u v ] R \approx [u, v] \begin{bmatrix} \sum(\frac{\delta I}{\delta x})^2 & \sum(\frac{\delta I}{\delta x}\frac{\delta I}{\delta y}) \\ \sum(\frac{\delta I}{\delta x}-\frac{\delta I}{\delta y})&\sum(\frac{\delta I}{\delta y})^2 \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} R≈[u,v][∑(δxδI)2∑(δxδI−δyδI)∑(δxδIδyδI)∑(δyδI)2][uv]
这是一个协方差矩阵,表示在所有方向上强度值变化的速率。这个定义包括了图像的一阶导数,通常用 Sobel 计算。这个协方差矩阵的两个特征值分别表示最大平均强度值变化和垂直方向的平均强度值变化。如果这两个特征值都很小,就说明是在相对同质的区域;如果一个特征值很大,另一个很小,那肯定是在边缘上;如果两者都很大,那么就是在角点上。因此判断一个点为角点的条件是它的协方差矩阵的最小特征值要大于指定的阈值。
Harris角点算法的原始定义用到了特征分解理论的一些属性,从而避免显式地计算特征值带来的开销。这些属性是:
通过计算下面的评分,可以验证矩阵的特征值高不高:
D s t ( x , y ) = D e t ( C ( x , y ) ) − k ⋅ ( t r C ( x , y ) ) 2 Dst(x,y) = Det(C^{(x,y)}) - k \cdot (tr C^{(x,y)})^2 Dst(x,y)=Det(C(x,y))−k⋅(trC(x,y))2
只要两个特征值都高,就很容易证明这个评分肯定也高。这个评分在每个像素的位置计算得到。数值 k k k 是参数,确定这个参数的最佳值是比较困难的。但是根据经验,0.05~0.5 通常是比较好的选择。
cornerHarris
void cv::cornerHarris(InputArray src,
OutputArray dst,
int blockSize,
int ksize,
double k,
int borderType = BORDER_DEFAULT
)
//Python:
dst = cv.cornerHarris(src, blockSize, ksize, k[, dst[, borderType]])
参数解释
参数 | 解释 |
---|---|
src | 输入图像,8 位或浮点型图像 |
dst | 输出图像,存储函数调用后的运算结果,即 harris 角点检测的输出结果。类型为CV_32FC1,大小与src相同 |
blockSize | 邻域的大小 |
ksize | Sobel 算子的孔径大小 |
k | Harris 参数 |
borderType | 图像的边界模式,默认 BORDER_DEFAULT |
C++ 示例
void myHarris(Mat& image){
Mat cornerStrenth, harrisTh;
// 检测 Harris 角点
cornerHarris(image, cornerStrenth, 2, 3, 0.01);
// 对角点强度阈值化
threshold(cornerStrenth, harrisTh, 0.0001, 255, CV_THRESH_BINARY);
imwrite("harrisTh.jpg", harrisTh);
}
int main(){
Mat image = imread("test.jpg");
Mat imageGray;
cvtColor(image, imageGray, COLOR_BGR2GRAY);
myHarris(image);
return 0;
}
效果如下:
由此得到的角点分布图中包含很多聚集的角点像素,而不是我们想要检测的具有明确定位的角点。接下来通过定义一个检测 Harris 角点的类,改进角点检测方法。
为了提升检测效果,使用了一个额外的非最大值抑制步骤,作用是排除掉紧邻的 Harris 角点。因此,Harris 角点不仅要有高于指定阈值的评分,还必须是局部范围内的最大值。为了检查这个条件,对 Harris 使用了膨胀运算,膨胀运算会在邻域中把每个像素值替换成最大值,因此只有局部最大值的像素是不变的。
文件 harrisDetector.h
#include
using namespace std;
#include
#include
using namespace cv;
class HarrisDetector{
private:
// 32 位浮点数型的角点强度图像
Mat cornerStrength;
// 32 位浮点数型的阈值化角点图像
Mat cornerTh;
// 局部最大值
Mat localMax;
// 平滑导数的邻域尺寸
int neighborhood;
// 梯度计算的口径
int aperture;
// Harris 参数
double k;
// 阈值计算的最大强度
double maxStrength;
// 计算得到的阈值(内部)
double threshold;
// 非最大值抑制的邻域尺寸
int nonMaxSize;
// 非最大值抑制的内核
Mat kernel;
public:
Mat getCornerTh(){return cornerTh;}
HarrisDetector() : neighborhood(3), aperture(3), k(0.01), maxStrength(0.0), threshold(0.01), nonMaxSize(3){
// 创建用于非最大值一直的内核
setLocalMaxWindowSize(nonMaxSize);
}
// 计算 Harris 角点
void detect(const Mat& image);
// 用 Harris 值得到角点分布图
Mat getCornerMap(double qualityLevel);
// 用 Harris 得到特征点
void getCorners(vector<Point>& points, double qualityLevel);
// 用角点分布图得到特征点
void getCorners(vector<Point>& points, const Mat& cornerMap);
// 在特征点的位置画圆形
void drawOnImage(Mat& image, const vector<Point>& points, Scalar color=Scalar(0, 0, 255), int radius=3, int thickness=1);
void setLocalMaxWindowSize(int nonMaxSize);
};
文件 harrisDetector.cpp
#include "harrisDetector.h"
/**
* 计算 Harris 角点
* */
void HarrisDetector::detect(const Mat& image){
// 计算 Harris
cornerHarris(image, cornerStrength, neighborhood, aperture, k);
// 计算内部阈值
minMaxLoc(cornerStrength, 0, &maxStrength);
// 检测局部最大值
Mat dilated; // 临时图像
dilate(cornerStrength, dilated, Mat());
compare(cornerStrength, dilated, localMax, CMP_EQ);
}
/**
* 用指定的阈值获得特征点
* */
Mat HarrisDetector::getCornerMap(double qualityLevel){
Mat cornerMap;
// 对角点强度阈值化
threshold = qualityLevel * maxStrength;
cv::threshold(cornerStrength, cornerTh, threshold, 255, THRESH_BINARY);
// 转换成 8 位图像
cornerTh.convertTo(cornerMap, CV_8U);
// 非最大值抑制
bitwise_and(cornerMap, localMax, cornerMap);
return cornerMap;
}
void HarrisDetector::getCorners(vector<Point>& points, double qualityLevel){
// 获得角点分布
Mat cornerMap = getCornerMap(qualityLevel);
// 获得角点
getCorners(points, cornerMap);
}
void HarrisDetector::getCorners(vector<Point>& points, const 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(Point(x, y));
}
}
}
}
void HarrisDetector::drawOnImage(Mat& image, const vector<Point>& points, Scalar color, int radius, int thickness){
for(int i = 0; i < points.size(); i++){
circle(image, points[i], 3, color, thickness);
}
}
void HarrisDetector::setLocalMaxWindowSize(int nonMaxSize){
kernel = getStructuringElement(MORPH_RECT, Size(nonMaxSize, nonMaxSize));
}
文件 main.cpp
#include "harrisDetector.h"
int main(){
Mat image = imread("test.jpg");
Mat imageGray;
cvtColor(image, imageGray, COLOR_BGR2GRAY);
// 创建 Harris 检测器实例
HarrisDetector harris;
// 计算 Harris 值
harris.detect(imageGray);
// 检测 Harris 角点
vector<Point> pts;
harris.getCorners(pts, 0.02);
// 画出 Harris 角点
harris.drawOnImage(image, pts);
// 保存画出角点的图像
imwrite("harris.jpg", image);
return 0;
效果如下:
尽管引入了局部最大值这个条件,兴趣点仍不会在图像中均匀分布,而是聚集在高度纹理化的位置。接下来介绍 OpenCV 的一个角点检测优化,可以让兴趣点均匀分布在图像中。
通过限制两个兴趣点之间的最短距离解决特征点聚集的问题,使角点在图像中的分布更加均匀。从 Harris 值最强的点开始(即具有最大的最低特征值),只允许一定距离之外的点成为兴趣点。在 OpenCV 中用 good-features-to-track(GFTT)实现这个算法。这个算法得名于它检测的特征非常适合作为视觉跟踪程序的起始集合,可以使用类 从cv::GFTTDetector
,也可以使用方法 cv::goodFeaturesToTrack
做角点检测。
在内部,cv::goofFeaturesToTrack()
和 cv::GFTTDetector()
有一定特定的阶段:自相关矩阵 C 的计算、该矩阵的分析以及应用的某种阈值。关键步骤使用函数 cv::cornerharris()
和 cv::cornerMinEigenVal()
完成,这两个函数的参数完全类似于函数 cv::goodFeatureTotrack()
,首先使用 Harris 使用的特征值填充 dst,第二次使用 Shi 和 Tomasi 使用的特征值填充 dst,即自相关矩阵 C 的最小特征值。
类 GFTTDetector
使用方法如下:
// 计算适合跟踪的特征
vector<KeyPoint> keypoints;
// GTFF 检测器
Ptr<GFTTDetector> ptrGFTT = GFTTDetector::create(500, 0.01, 10);
// 检测 GFTT
ptrGFTT->detect(image, keypoints);
首先使用特定的静态函数(cv::GFTTDetector::create
)创建特征检测器,并初始化参数。除了质量等级阈值和兴趣点间的最小距离,该函数还需要提供允许返回的最大点数(这些点是按照强度排序的)。函数返回一个执行检测器实例的智能指针。构建完这个实例后,就可以调用检测方法了。其中 cv::KeyPoint
类,封装了每个检测到的特征点的属性,可以使用 drawKeypoints
画出关键点。对于 Harris 角点来说,只与关键点位置和它的反馈强度有关。
方法goodFeaturesToTrack
见以下 OpenCV 函数:
cv::GFTTDetector::create
static Ptr<GFTTDetector> cv::GFTTDetector::create (
int maxCorners = 1000,
double qualityLevel = 0.01,
double minDistance = 1,
int blockSize = 3,
bool useHarrisDetector = false,
double k = 0.04
)
static Ptr<GFTTDetector> cv::GFTTDetector::create (
int maxCorners,
double qualityLevel,
double minDistance,
int blockSize,
int gradiantSize,
bool useHarrisDetector = false,
double k = 0.04
)
//Python:
retval = cv.GFTTDetector_create([,maxCorners[,qualityLevel[,minDistance[, blockSize[,useHarrisDetector[, k]]]]]])
retval = cv.GFTTDetector_create(maxCorners, qualityLevel, minDistance, blockSize, gradiantSize[, useHarrisDetector[, k]])
OpenCV 特征检测公共接口定义了一个虚拟类 cv::Feature2D
,GFTTDetector
继承于这个虚拟类,它可以确保其他类包含以下格式的 detect
方法:
detect
void detect(InputArray image,
CV_OUT std::vector<KeyPoint>& keypoints,
InputArray mask=noArray() );
void detect(InputArrayOfArrays images,
CV_OUT std::vector<std::vector<KeyPoint> >& keypoints,
InputArrayOfArrays masks=noArray() );
第二个方法,可以检测多个图像的关键点。
参数 | 解释 |
---|---|
image | 要检测的图像 |
keypoints | 检测到的关键点,在方法的第二个变体中,keypoints[i] 是在 images[i] 中检测到的一组关键点 |
mask | 指定在哪里寻找关键点的掩码(可选)。 它必须是一个8位整数矩阵,在感兴趣的区域中具有非零值。 |
drawKeypoints
void cv::drawKeypoints( InputArray image,
const std::vector< KeyPoint > & keypoints,
InputOutputArray outImage,
const Scalar & color = Scalar::all(-1),
int flags = DrawMatchesFlags::DEFAULT
)
//Python:
outImage = cv.drawKeypoints(image, keypoints, outImage[, color[, flags]])
参数 | 解释 |
---|---|
image | 源图像 |
keypoints | 源图像的关键点 |
outImage | 输出图像,它的内容取决于 flags,flags 定义了在输出图像中绘制的内容。 |
color | 关键点的颜色 |
flags | 设置绘制方式。 |
使用负数作为关键点的颜色,画每个圆时会随机选取不同的颜色。
flags:
Note : For Python API, flags are modified as cv2.DRAW_MATCHES_FLAGS_DEFAULT, cv2.DRAW_MATCHES_FLAGS_DRAW_RICH_KEYPOINTS, cv2.DRAW_MATCHES_FLAGS_DRAW_OVER_OUTIMG, cv2.DRAW_MATCHES_FLAGS_NOT_DRAW_SINGLE_POINTS
goodFeaturesToTrack
void cv::goodFeaturesToTrack( InputArray image,
OutputArray corners,
int maxCorners,
double qualityLevel,
double minDistance,
InputArray mask = noArray(),
int blockSize = 3,
bool useHarrisDetector = false,
double k = 0.04
)
// Python:
corners =cv.goodFeaturesToTrack(image, maxCorners, qualityLevel, minDistance[, corners[, mask[, blockSize[, useHarrisDetector[, k]]]]])
corners = cv.goodFeaturesToTrack( image, maxCorners, qualityLevel, minDistance, mask, blockSize, gradientSize[, corners[, useHarrisDetector[, k]]])
参数 | 解释 |
---|---|
image | 输入图像,8位或32位单通道图像 |
corners | 检测到的角点的输出向量 |
maxCorners | 角点的最大数量 |
qualityLevel | 角点检测可接受的最小特征值,其实实际用于过滤角点的最小特征值是 qualityLevel 与图像中最大特征值的乘积。所以 qualityLevel 通常不会超过1(常用的值为0.10 或 0.01)。而检测完所有的角点后,还要进一步剔除掉一些距离较近的角点 |
minDistance | 角点之间的最小距离,此参数用于保证返回的角点之间的距离不小于 minDistance 个像素 |
mask | 可选参数,表示感兴趣区域,有默认值 noArray()。若此参数非空(需为 CV_8UC1 类型,且和第一个参数 image 有相同的尺寸),便用于指定角点检测区域。 |
blockSize | 默认值3,计算导数自相关矩阵时指定的邻域范围 |
useHarrisDetector | 默认值 false,指示是否使用 Harris 角点检测 |
k | 默认值 0.04,为用于设置 Hessian 自相关矩阵行列式的相对权重的权重系数 |
C++示例
#include
using namespace std;
#include
#include
using namespace cv;
void myHarris(Mat& image){
// 计算适合跟踪的特征
vector<KeyPoint> keypoints;
// GTFF 检测器
Ptr<GFTTDetector> ptrGFTT = GFTTDetector::create(500, 0.01, 10);
// 检测 GFTT
ptrGFTT->detect(image, keypoints);
// 画出关键点
drawKeypoints(image, keypoints, image, Scalar::all(-1), DrawMatchesFlags::DRAW_OVER_OUTIMG);
imwrite("harrisThGFTT.jpg", image);
}
void myHarris2(Mat& image){
vector<Point> corner;
goodFeaturesToTrack(image, corner, 500, 0.01, 10);
for (int i = 0; i < corner.size(); i++){
circle(image, corner[i], 3, Scalar::all(-1), 1);
}
}
int main(){
Mat image = imread("test.jpeg");
Mat imageGray;
cvtColor(image, imageGray, COLOR_BGR2GRAY);
myHarris(imageGray);
return 0;
}
效果如下:
由于需要让兴趣点按照 Harris 评分排序,因此该检测方法的复杂度有所提高,但是它也明显改进了兴趣点在整幅图像中的分布情况。此函数还有一个可选的标志,该标志要求在检测 Harris 角点时,采用经典的角点评分定义(使用协方差矩阵的行列式值和迹)。
Harris 算子对角点(或者更通用的兴趣点)做出了规范的数学定义,该定义基于强度值在两个互相垂直的方向上的变化率。但是它需要计算图像的导数,而计算导数是非常耗时的。尤其要注意的是,检测兴趣点通常只是更复杂的算法中的第一步。
本篇文章将介绍另一种特征点算子,叫做 FAST(Features from Accelerated Segment Test,加速分割测试获得特征)。这种算子专门用来快速检测兴趣点——只需对比几个像素,就可以判断它是否为关键点。
跟 Harris 检测器的情况一样,FAST 特征算法源于“什么构成了角点”的定义。FAST 对角点的定义基于候选特征点周围的图像强度值。以某个点为中心做一个圆,根据原上的像素值判断该点是否为关键点。如果存在这样一段圆弧,它的连续长度超过周长的 3/4,并且它上面所有像素的强度值都与圆心的强度值明显不同(全部更暗或更亮),那么就认定这是一个关键点。
这种测试方法非常简单,计算速度也很快。而且在它的原始公式中,算法还用了一个技巧来进一步提高处理速度。如果我们测试圆周上相隔 90 度的四个点(例如取上、下、左、右四个位置),就很容易证明:为了满足前面的条件,其中必须有三个点都比圆心更亮或都比圆心更暗。
如果不满足该条件,就可以立即排除这个点,不需要检查圆周上的其他点。这种方法非常高效,因为在实际应用中,图像中大部分像素都可以用这种“四点比较法”排除。
从概念上讲,用于检查像素的圆的半径作为方法的一个参数。但是根据经验,半径为 3 时可以得到好的结果和较高的计算效率。因此需要在圆周上检查 16 个像素,如下图所示。
这里用来预测试的像素时1、5、9 和 13,至少需要 9 个比圆心更暗(或更亮)的连续像素。这种设置通常称为 FAST-9 角点检测器,也是 OpenCV 默认采用的方法。你可以在构建检测器实例时指定 FAST 检测器的类型。
一个点与圆心强度值的差距必须达到一个指定的值,才能被认为是明显更暗或更亮;这个值就是创建检测器实例时指定的阈值参数。这个阈值越大,检测到的角点数量就越少。
至于 Harris 特征,通常最好在发现的角点上执行非最大值抑制。因此,需要定义一个角点强度的衡量方法。有多种衡量方法可供选择,可以通过以下方法获取角点强度——计算中心点像素与认定的连续圆弧上的像素的差值,然后将这些差值的绝对值累加,就能得到角点强度。可以从 cv::KeyPoint
实例的 response
属性获取角点强度。
用这个算法检测兴趣点的速度非常快,因此十分适合需要优先考虑速度的应用,包括实时视觉跟踪、目标识别等,它们需要在实时视频流中跟踪或匹配多个点。
FastFeatureDetector
static Ptr<FastFeatureDetector> cv::FastFeatureDetector::create(
int threshold = 10,
bool nonmaxSuppression = true,
int type = FastFeatureDetector::TYPE_9_16
)
// Python:
retval = cv.FastFeatureDetector_create([, threshold[, nonmaxSuppression[, type]]])
参数解释
参数 | 解释 |
---|---|
threshold | 与圆心强度值的阈值 |
nonmaxSuppression | 非最大值抑制 |
type | 圆的大小 |
每种类型都指定圆的周长和该圆的中心被认为是关键点所需的临近点的数量,type:
C++示例
void myFAST(Mat& image){
// 关键点的向量
vector<KeyPoint> keypoints;
// FAST 特征检测器,阈值为 40
Ptr<FastFeatureDetector> ptrFAST = FastFeatureDetector::create(40);
// 检测关键点
ptrFAST->detect(image, keypoints);
// 画出关键点
drawKeypoints(image, keypoints, image, Scalar::all(-1), DrawMatchesFlags::DRAW_OVER_OUTIMG);
}
int main()
{
string outDir = "./";
Mat image = imread("img7.jpg");
if (image.empty()) {
cout << "could not load image..." << endl;
return -1;
}
myFAST(image);
imwrite(outDir + "FAST.jpg", image);
return 0;
}
效果图
应用程序不同,检测特征点时采用的策略也不同。
例如在事先明确兴趣点数量的情况下,可以对检测过程进行动态适配。简单的做法就是采用范围较大的阈值检测出很多兴趣点,然后从中提取出 n 个强度最大的。为此可以使用这个标准 C++ 函数 std::nth_element()
。
if(numberOfPoint < 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 比较器,用于提取最佳的兴趣点。如果检测到的兴趣点太少(少于需要的数量),那就要采用更小的阈值,但是阈值太宽松又会加大计算量,所以需要权衡利弊,选取最佳的阈值。
检测图像特征点时还会遇到一种情况,就是兴趣点分布很不均匀。keypoint 通常会聚集在纹理较多的区域。对此有一种常用的处理方法,就是把图像分割成网格状,对每个小图像进行单独检测。
C++示例
void myFAST2(Mat& image){
int totalPoints = 100; // 总共 100 个关键点
int cols = image.cols;
int rows = image.rows;
int vstep = 10; // 垂直分 10 份
int hstep = 10; // 水平分 10 份
int hsize = cols / hstep;
int vsize = rows / vstep;
int subtotal = totalPoints / (vstep * hstep); // 每个子块中的关键点
Mat imageROI;
// 关键点
vector<KeyPoint> keypoints;
vector<KeyPoint> gridpoints;
// FAST 特征检测器
Ptr<FastFeatureDetector> ptrFAST = FastFeatureDetector::create(40);
// 检测每个网格
for(int i = 0; i < vstep; i++){
for(int j = 0; j < hstep; j++){
// 在当前网格创建ROI
imageROI = image(Rect(j*hsize, i*vsize, hsize, vsize));
// imageROI = image(Range(), Range());
// 在网格中检测关键点
gridpoints.clear();
ptrFAST->detect(imageROI, gridpoints);
// 获取强度最大的 FAST 特征
auto itEnd(gridpoints.end());
if(gridpoints.size() > subtotal){
// 选取最强的特征
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);
}
}
}
// 画出关键点
drawKeypoints(image, keypoints, image, Scalar::all(-1), DrawMatchesFlags::DRAW_OVER_OUTIMG);
}
int main()
{
string outDir = "./";
Mat image = imread("img7.jpg");
if (image.empty()) {
cout << "could not load image..." << endl;
return -1;
}
Mat imageGray;
cvtColor(image, imageGray, COLOR_BGR2GRAY);
myFAST2(image);
imwrite(outDir + "FAST2.jpg", image);
return 0;
}
利用 ROI 对每个网格的小图像进行关键点检测,这样的到的关键点分布较为均匀。
这一篇文章介绍的特征检测器可以比较好的解决方向不变性问题,即图像旋转后仍能检测到相同的特征点。下一篇文章介绍可以解决尺度不变性问题的方法,不仅在任何尺度下拍摄的物体都能检测到一致的关键点,而且每个被检测的特征点都对应一个尺度因子。