本章包括以下內(nèi)容:
- 檢測(cè)圖像中的角點(diǎn);
- 快速檢測(cè)特征;
- 尺度不變特征的檢測(cè);
- 多尺度FAST 特征的檢測(cè)。
8.2 檢測(cè)圖像中的角點(diǎn)
Harris 特征檢測(cè)是檢測(cè)角點(diǎn)的經(jīng)典方法,本節(jié)將詳細(xì)探討這個(gè)方法。
OpenCV 中檢測(cè)Harris 角點(diǎn)的基本函數(shù)是cv::cornerHarris,它的使用方法非常簡(jiǎn)單。調(diào)用該函數(shù)時(shí)輸入一幅圖像,返回的結(jié)果是一個(gè)浮點(diǎn)數(shù)型圖像,其中每個(gè)像素表示角點(diǎn)強(qiáng)度。然后對(duì)輸出圖像閾值化,以獲得檢測(cè)角點(diǎn)的集合。代碼如下所示:
// 檢測(cè)Harris 角點(diǎn)
cv::Mat cornerStrength;
cv::cornerHarris(image, // 輸入圖像
cornerStrength, // 角點(diǎn)強(qiáng)度的圖像
3, // 鄰域尺寸
3, // 口徑尺寸
0.01); // Harris 參數(shù)
// 對(duì)角點(diǎn)強(qiáng)度閾值化
cv::Mat harrisCorners;
double threshold = 0.0001;
cv::threshold(cornerStrength, harrisCorners,
threshold, 255, cv::THRESH_BINARY_INV);
這是原始圖像。

結(jié)果是一個(gè)二值分布圖像,如下圖所示。為了能更直觀地觀察圖像,此處進(jìn)行了反轉(zhuǎn)處理(即用cv::THRESH_BINARY_INV 代替cv::THRESH_BINARY,用黑色表示被檢測(cè)的角點(diǎn))。

在前面的函數(shù)中,我們發(fā)現(xiàn)興趣點(diǎn)檢測(cè)法需要使用幾個(gè)參數(shù),這可能會(huì)導(dǎo)致該方法很難調(diào)節(jié)。此外,得到的角點(diǎn)分布圖中包含很多聚集的角點(diǎn)像素,而不是我們想要檢測(cè)的具有明確定位的角點(diǎn)。因此,我們來定義一個(gè)檢測(cè)Harris 角點(diǎn)的類,以改進(jìn)角點(diǎn)檢測(cè)方法。
class HarrisDetector {
private:
// 32 位浮點(diǎn)數(shù)型的角點(diǎn)強(qiáng)度圖像
cv::Mat cornerStrength;
// 32 位浮點(diǎn)數(shù)型的閾值化角點(diǎn)圖像
cv::Mat cornerTh;
// 局部最大值圖像(內(nèi)部)
cv::Mat localMax;
// 平滑導(dǎo)數(shù)的鄰域尺寸
int neighborhood;
// 梯度計(jì)算的口徑
int aperture;
// Harris 參數(shù)
double k;
// 閾值計(jì)算的最大強(qiáng)度
double maxStrength;
// 計(jì)算得到的閾值(內(nèi)部)
double threshold;
// 非最大值抑制的鄰域尺寸
int nonMaxSize;
// 非最大值抑制的內(nèi)核
cv::Mat kernel;
public:
HarrisDetector() : neighborhood(3), aperture(3),
k(0.01), maxStrength(0.0),
threshold(0.01), nonMaxSize(3) {
// 創(chuàng)建用于非最大值抑制的內(nèi)核
setLocalMaxWindowSize(nonMaxSize);
}
檢測(cè)Harris 角點(diǎn)需要兩個(gè)步驟。首先是計(jì)算每個(gè)像素的Harris 值:
// 計(jì)算Harris 角點(diǎn)
void detect(const cv::Mat& image) {
// 計(jì)算Harris
cv::cornerHarris(image, cornerStrength,
neighbourhood,// 鄰域尺寸
aperture, // 口徑尺寸
k); // Harris 參數(shù)
// 計(jì)算內(nèi)部閾值
cv::minMaxLoc(cornerStrength, 0, &maxStrength);
// 檢測(cè)局部最大值
cv::Mat dilated; // 臨時(shí)圖像
cv::dilate(cornerStrength, dilated, cv::Mat());
cv::compare(cornerStrength, dilated, localMax, cv::CMP_EQ);
然后,用指定的閾值獲得特征點(diǎn)。因?yàn)镠arris 值的可選范圍取決于選擇的參數(shù),所以閾值被作為質(zhì)量等級(jí),用最大Harris 值的一個(gè)比例值表示:
// 用Harris 值得到角點(diǎn)分布圖
cv::Mat getCornerMap(double qualityLevel) {
cv::Mat cornerMap;
// 對(duì)角點(diǎn)強(qiáng)度閾值化
threshold = qualityLevel * maxStrength;
cv::threshold(cornerStrength, cornerTh, threshold, 255,
cv::THRESH_BINARY);
// 轉(zhuǎn)換成8 位圖像
cornerTh.convertTo(cornerMap, CV_8U);
// 非最大值抑制
cv::bitwise_and(cornerMap, localMax, cornerMap);
return cornerMap;
}
這個(gè)方法將返回一個(gè)被檢測(cè)特征的二值角點(diǎn)分布圖。因?yàn)镠arris 特征的檢測(cè)過程分為兩個(gè)方法,所以我們可以用不同的閾值來測(cè)試檢測(cè)結(jié)果(直到獲得適當(dāng)數(shù)量的特征點(diǎn)),而不必重復(fù)進(jìn)行耗時(shí)的計(jì)算過程。當(dāng)然,你也可以從以std::vector 形式表示的cv::Point 實(shí)例中得到Harris特征:
// 用Harris 值得到特征點(diǎn)
void getCorners(std::vector<cv::Point>& points, double qualityLevel) {
// 獲得角點(diǎn)分布圖
cv::Mat cornerMap = getCornerMap(qualityLevel);
// 獲得角點(diǎn)
getCorners(points, cornerMap);
}
// 用角點(diǎn)分布圖得到特征點(diǎn)
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++) {
// 如果它是一個(gè)特征點(diǎn)
if (rowPtr[x]) {
points.push_back(cv::Point(x, y));
}
}
}
}
這個(gè)類通過增加非最大值抑制步驟,也改進(jìn)了Harris 角點(diǎn)的檢測(cè)過程,下一節(jié)會(huì)詳細(xì)解釋這個(gè)步驟?,F(xiàn)在可以用cv::circle 函數(shù)畫出檢測(cè)到的特征點(diǎn),方法如下所示:
// 在特征點(diǎn)的位置畫圓形
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();
// 針對(duì)所有角點(diǎn)
while (it != points.end()) {
// 在每個(gè)角點(diǎn)位置畫一個(gè)圓
cv::circle(image, *it, radius, color, thickness);
++it;
}
}
使用這個(gè)類檢測(cè)Harris 特征點(diǎn)的方法如下所示:
// 創(chuàng)建Harris 檢測(cè)器實(shí)例
HarrisDetector harris;
// 計(jì)算Harris 值
harris.detect(image);
// 檢測(cè)Harris 角點(diǎn)
std::vector<cv::Point> pts;
harris.getCorners(pts,0.02);
// 畫出Harris 角點(diǎn)
harris.drawOnImage(image,pts);
結(jié)果如下圖所示。

8.3 快速檢測(cè)特征
本節(jié)將介紹另一種特征點(diǎn)算子,叫作FAST(Features from Accelerated Segment Test,加速分割測(cè)試獲得特征)。這種算子專門用來快速檢測(cè)興趣點(diǎn)——只需對(duì)比幾個(gè)像素,就可以判斷它是否為關(guān)鍵點(diǎn)。
OpenCV 有檢測(cè)特征點(diǎn)的公共接口。
// 關(guān)鍵點(diǎn)的向量
std::vector<cv::KeyPoint> keypoints;
// FAST 特征檢測(cè)器,閾值為40
cv::Ptr<cv::FastFeatureDetector> ptrFAST =
cv::FastFeatureDetector::create(40);
// 檢測(cè)關(guān)鍵點(diǎn)
ptrFAST->detect(image, keypoints);
OpenCV 也提供了在圖像上畫關(guān)鍵點(diǎn)的通用函數(shù):
cv::drawKeypoints(image, // 原始圖像
keypoints, // 關(guān)鍵點(diǎn)的向量
image, // 輸出圖像
cv::Scalar(255, 255, 255), // 關(guān)鍵點(diǎn)的顏色
cv::DrawMatchesFlags::DRAW_OVER_OUTIMG); // 畫圖標(biāo)志
選擇這個(gè)畫圖標(biāo)志后,輸入圖像上會(huì)畫出關(guān)鍵點(diǎn),輸出結(jié)果如下所示。

有一種比較有趣的做法,就是用一個(gè)負(fù)數(shù)作為關(guān)鍵點(diǎn)顏色。這樣一來,畫每個(gè)圓時(shí)會(huì)隨機(jī)選用不同的顏色。
FAST 對(duì)角點(diǎn)的定義基于候選特征點(diǎn)周圍的圖像強(qiáng)度值。以某個(gè)點(diǎn)為中心做一個(gè)圓,根據(jù)圓上的像素值判斷該
點(diǎn)是否為關(guān)鍵點(diǎn)。如果存在這樣一段圓弧,它的連續(xù)長(zhǎng)度超過周長(zhǎng)的3/4,并且它上面所有像素的強(qiáng)度值都與圓心的強(qiáng)度值明顯不同(全部更暗或更亮),那么就認(rèn)定這是一個(gè)關(guān)鍵點(diǎn)。
這種測(cè)試方法非常簡(jiǎn)單,計(jì)算速度也很快。而且在它的原始公式中,算法還用了一個(gè)技巧來進(jìn)一步提高處理速度。如果我們測(cè)試圓周上相隔90 度的四個(gè)點(diǎn)(例如取上、下、左、右四個(gè)位置),就很容易證明:為了滿足前面的條件,其中必須有三個(gè)點(diǎn)都比圓心更亮或都比圓心更暗。
如果不滿足該條件,就可以立即排除這個(gè)點(diǎn),不需要檢查圓周上的其他點(diǎn)。這種方法非常高效,因?yàn)樵趯?shí)際應(yīng)用中,圖像中大部分像素都可以用這種“四點(diǎn)比較法”排除。
從概念上講,用于檢查像素的圓的半徑應(yīng)該作為方法的一個(gè)參數(shù)。但是根據(jù)經(jīng)驗(yàn),半徑為3時(shí)可以得到好的結(jié)果和較高的計(jì)算效率。因此需要在圓周上檢查16 個(gè)像素。

這里用來預(yù)測(cè)試的像素是1、5、9 和13,至少需要9 個(gè)比圓心更暗(或更亮)的連續(xù)像素。這種設(shè)置通常稱為FAST-9 角點(diǎn)檢測(cè)器,也是OpenCV 默認(rèn)采用的方法。
一個(gè)點(diǎn)與圓心強(qiáng)度值的差距必須達(dá)到一個(gè)指定的值,才能被認(rèn)為是明顯更暗或更亮;這個(gè)值就是創(chuàng)建檢測(cè)器實(shí)例時(shí)指定的閾值參數(shù)。這個(gè)閾值越大,檢測(cè)到的角點(diǎn)數(shù)量就越少。
至于Harris 特征,通常最好在發(fā)現(xiàn)的角點(diǎn)上執(zhí)行非最大值抑制。因此,需要定義一個(gè)角點(diǎn)強(qiáng)度的衡量方法。有多種衡量方法可供選擇,下面介紹的是實(shí)際選用的方法——計(jì)算中心點(diǎn)像素與認(rèn)定的連續(xù)圓弧上的像素的差值,然后將這些差值的絕對(duì)值累加,就能得到角點(diǎn)強(qiáng)度??梢詮腸v::KeyPoint 實(shí)例的response 屬性獲取角點(diǎn)強(qiáng)度。
在事先明確興趣點(diǎn)數(shù)量的情況下,可以對(duì)檢測(cè)過程進(jìn)行動(dòng)態(tài)適配。簡(jiǎn)單的做法是采用范圍較大的閾值檢測(cè)出很多興趣點(diǎn),然后從中提取出n 個(gè)強(qiáng)度最大的。
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; });
函數(shù)中keypoints 的類型是std::vector,表示檢測(cè)到的興趣點(diǎn),numberOfPoints 是需要的興趣點(diǎn)數(shù)量。最后一個(gè)參數(shù)是lambda 比較器,用于提取最佳的興趣點(diǎn)。請(qǐng)注意,如果檢測(cè)到的興趣點(diǎn)太少(少于需要的數(shù)量),那就要采用更小的閾值,但是閾值太寬松又會(huì)加大計(jì)算量,所以需要權(quán)衡利弊,選取最佳的閾值。
檢測(cè)圖像特征點(diǎn)時(shí)還會(huì)遇到一種情況,就是興趣點(diǎn)的分布很不均勻。keypoints 通常會(huì)聚集在紋理較多的區(qū)域。有一種常用的處理方法,就是把圖像分割成網(wǎng)格狀,對(duì)每個(gè)小圖像進(jìn)行單獨(dú)檢測(cè)。以下代碼就是網(wǎng)格適配特征檢測(cè):
// 關(guān)鍵點(diǎn)的向量
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);
// 檢測(cè)每個(gè)網(wǎng)格
for (int i = 0; i < vstep; i++)
for (int j = 0; j < hstep; j++) {
// 在當(dāng)前網(wǎng)格創(chuàng)建ROI
imageROI = image(cv::Rect(j * hsize, i * vsize, hsize, vsize));
// 在網(wǎng)格中檢測(cè)關(guān)鍵點(diǎn)
gridpoints.clear();
ptrFAST->detect(imageROI, gridpoints);
// 獲取強(qiáng)度最大的FAST 特征
auto itEnd(gridpoints.end());
if (gridpoints.size() > subtotal) {
// 選取最強(qiáng)的特征
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) {
// 轉(zhuǎn)換成圖像上的坐標(biāo)
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);
這里的關(guān)鍵在于,利用ROI 對(duì)每個(gè)網(wǎng)格的小圖像進(jìn)行關(guān)鍵點(diǎn)檢測(cè),這樣得到的關(guān)鍵點(diǎn)分布較為均勻,如下圖所示。

8.4 尺度不變特征的檢測(cè)
計(jì)算機(jī)視覺界引入了尺度不變特征的概念。它的理念是,不僅在任何尺度下拍攝的物體都能檢測(cè)到一致的關(guān)鍵點(diǎn),而且每個(gè)被檢測(cè)的特征點(diǎn)都對(duì)應(yīng)一個(gè)尺度因子。理想情況下,對(duì)于兩幅圖像中不同尺度的同一個(gè)物體點(diǎn),計(jì)算得到的兩個(gè)尺度因子之間的比率應(yīng)該等于圖像尺度的比率。
本節(jié)將介紹SURF 特征,它的全稱為加速穩(wěn)健特征(Speeded Up Robust Feature)。我們將會(huì)看到,它不僅是尺度不變特征,而且是具有較高計(jì)算效率的特征。
SURF 特征檢測(cè)屬于opencv_contrib 庫,這里將重點(diǎn)討論cv::xfeatures2d 模塊和它的cv::xfeatures2d::SurfFeatureDetector 類。和其他檢測(cè)器一樣,檢測(cè)興趣點(diǎn)之前要先創(chuàng)建檢測(cè)器實(shí)例,然后調(diào)用它的檢測(cè)方法:
// 創(chuàng)建SURF 特征檢測(cè)器對(duì)象
cv::Ptr<cv::xfeatures2d::SurfFeatureDetector> ptrSURF =
cv::xfeatures2d::SurfFeatureDetector::create(2000.0);
// 檢測(cè)關(guān)鍵點(diǎn)
ptrSURF->detect(image, keypoints);
為了畫出這些特征,再次使用OpenCV的cv::drawKeypoints 函數(shù),但是要采用cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS 標(biāo)志以顯示相關(guān)的尺度因子:
// 畫出關(guān)鍵點(diǎn),包括尺度和方向信息
cv::drawKeypoints(image, // 原始圖像
keypoints, // 關(guān)鍵點(diǎn)的向量
featureImage, // 結(jié)果圖像
cv::Scalar(255, 255, 255), // 點(diǎn)的顏色
cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS);
這里使用cv::DrawMatchesFlags::DRAW_RICH_KEYPOINTS 標(biāo)志得到了關(guān)鍵點(diǎn)的圓,并且圓的尺寸與每個(gè)特征計(jì)算得到的尺度成正比。為了使特征具有旋轉(zhuǎn)不變性,SURF 還讓每個(gè)特征關(guān)聯(lián)了一個(gè)方向,由每個(gè)圓內(nèi)的輻射線表示。
SURF 算法是SIFT 算法的加速版,而SIFT(Scale-Invariant Feature Transform,尺度不變特征轉(zhuǎn)換)是另一種著名的尺度不變特征檢測(cè)法。
SIFT 特征的檢測(cè)過程與SURF 非常相似:
// 構(gòu)建SIFT 特征檢測(cè)器實(shí)例
cv::Ptr<cv::xfeatures2d::SiftFeatureDetector> ptrSIFT =
cv::xfeatures2d::SiftFeatureDetector::create();
// 檢測(cè)關(guān)鍵點(diǎn)
ptrSIFT->detect(image, keypoints);
由于SIFT 基于浮點(diǎn)內(nèi)核計(jì)算特征點(diǎn),因此通常認(rèn)為SIFT 算法檢測(cè)在空間和尺度上能取得更加精確的定位?;谕瑯拥脑?,它的計(jì)算效率也更低,盡管相對(duì)效率取決于具體的實(shí)現(xiàn)方法。
8.5 多尺度FAST 特征的檢測(cè)
本節(jié)將介
紹BRISK(Binary Robust Invariant Scalable Keypoints,二元穩(wěn)健恒定可擴(kuò)展關(guān)鍵點(diǎn))檢測(cè)法,它基于上一節(jié)介紹的FAST 特征檢測(cè)法。本節(jié)還將討論另一種檢測(cè)方法ORB(Oriented FAST andRotated BRIEF,定向FAST 和旋轉(zhuǎn)BRIEF)。
根據(jù)上一節(jié)介紹的方法,首先創(chuàng)建檢測(cè)器實(shí)例,然后對(duì)一幅圖像調(diào)用detect 方法:
// 構(gòu)造BRISK 特征檢測(cè)器對(duì)象
cv::Ptr<cv::BRISK> ptrBRISK = cv::BRISK::create(60, 5);
// 檢測(cè)關(guān)鍵點(diǎn)
ptrBRISK->detect(image, keypoints);
下圖顯示了在多個(gè)尺度下檢測(cè)到的BRISK 關(guān)鍵點(diǎn)。

ORB 特征的檢測(cè)方法如下所示:
// 構(gòu)造ORB 特征檢測(cè)器對(duì)象
cv::Ptr<cv::ORB> ptrORB =
cv::ORB::create(75, // 關(guān)鍵點(diǎn)的總數(shù)
1.2, // 圖層之間的縮放因子
8); // 金字塔的圖層數(shù)量
// 檢測(cè)關(guān)鍵點(diǎn)
ptrORB->detect(image, keypoints);
調(diào)用的結(jié)果如下所示。
