在图像处理和与计算机视觉领域,兴趣点(interest points),或称作关键点(keypoints)、特征点(feature points) 被大量用于解决物体识别,图像识别、图像匹配、视觉跟踪、三维重建等一系列的问题。我们不再观察整幅图,而是选择某些特殊的点,然后对他们进行局部有的放矢的分析。如果能检测到足够多的这种点,同时他们的区分度很高,并且可以精确定位稳定的特征,那么这个方法就有使用价值。
图像特征类型可以被分为如下三种:
- <1>边缘
- <2>角点 (感兴趣关键点)
- <3>斑点(Blobs)(感兴趣区域)
其中,角点是个很特殊的存在。他们在图像中可以轻易地定位,同时,他们在人造物体场景,比如门、窗、桌等出随处可见。因为角点位于两条边缘的交点处,代表了两个边缘变化的方向上的点,,所以他们是可以精确定位的二维特征,甚至可以达到亚像素的精度。且其图像梯度有很高的变化,这种变化是可以用来帮助检测角点的。需要注意的是,角点与位于相同强度区域上的点不同,与物体轮廓上的点也不同,因为轮廓点难以在相同的其他物体上精确定位。
为什么角点是特殊的?
- 因为角点是两个边缘的连接点,它代表了两个边缘变化的方向上的点。图像梯度有很高的变化。这种变化是可以用来帮助检测角点的。
在当前的图像处理领域,角点检测算法可归纳为三类:
<1>基于灰度图像的角点检测
<2>基于二值图像的角点检测
<3>基于轮廓曲线的角点检测
而基于灰度图像的角点检测又可分为基于梯度、基于模板和基于模板梯度组合三类方法,其中基于模板的方法主要考虑像素领域点的灰度变化,即图像亮度的变化,将与邻点亮度对比足够大的点定义为角点。常见的基于模板的角点检测算法有Kitchen-Rosenfeld角点检测算法,Harris角点检测算法、KLT角点检测算法及SUSAN角点检测算法。和其他角点检测算法相比,SUSAN角点检测算法具有算法简单、位置准确、抗噪声能力强等特点。
关于角点的具体描述可以有几种:
一阶导数(即灰度的梯度)的局部最大所对应的像素点;
两条及两条以上边缘的交点;
图像中梯度值和梯度方向的变化速率都很高的点;
角点处的一阶导数最大,二阶导数为零,指示物体边缘变化不连续的方向。
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace cv;
using namespace std;
/// Global variables
Mat src, src_gray,dst;
int thresh = 200;
int max_thresh = 255;
char* source_window = "Source image";
char* corners_window = "Corners detected";
/// Function header
void cornerHarris_demo(int, void*);
void cvtColor_src(Mat &src, Mat &dst);
/** @function main */
int main(int argc, char** argv)
{
/// Load source image and convert it to gray
src = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg");
cvtColor(src, src_gray, CV_BGR2GRAY);
// 转换单通道
/// Create a window and a trackbar
namedWindow(source_window, CV_WINDOW_AUTOSIZE);
createTrackbar("Threshold: ", source_window, &thresh, max_thresh, cornerHarris_demo);
imshow(source_window , src);
cornerHarris_demo(0, 0);
waitKey(0);
return(0);
}
/** @function cornerHarris_demo */
void cornerHarris_demo(int, void*)
{
Mat dst, dst_norm, dst_norm_scaled;
dst = Mat::zeros(src.size(), CV_32FC1);
/// Detector parameters
int blockSize = 2;
int apertureSize = 3;
double k = 0.04;
/// Detecting corners
cornerHarris(src_gray, dst, blockSize, apertureSize, k, BORDER_DEFAULT);
/// Normalizing
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
convertScaleAbs(dst_norm, dst_norm_scaled);
/// Drawing a circle around corners
for (int j = 0; j < dst_norm.rows; j++)
{
for (int i = 0; i < dst_norm.cols; i++)
{
if ((int)dst_norm.at<float>(j, i) > thresh)
{
circle(dst_norm_scaled, Point(i, j), 5, Scalar(0), 2, 8, 0);
}
}
}
/// Showing the result
namedWindow(corners_window, CV_WINDOW_AUTOSIZE);
imshow(corners_window, dst_norm_scaled);
}
现在讲解一下其中原理:
w(x,y)是窗口函数;w(x,y)也可以说是权值;在x方向和 v 方向也就是我们说的水平,竖直方向上移动;
上图中红色的窗口可以往任意一个方向上移动;来检测是否是角;
红色的窗口开始有图像的像素,移动后窗口内的图像像素变化;两者相减在平方;
前面说过角在各个方向上梯度都很大,因此我们就用(红色的窗口开始有图像的像素,移动后窗口内的图像像素变化;两者相减在平方,无论窗口移动大哪个方向)原理来找最大变化;下面是数学推导;
由于角点代表了图像像素梯度变化,我们将寻找这个”变化”。
考虑到一个灰度图像 I. 划动窗口 w(x,y) (with displacements u 在x方向和 v 方向) I 计算像素灰度变化。
其中: w(x,y) is the window at position (x,y) I(x,y) is the intensity at (x,y) I(x+u,y+v) is the intensity at the moved window (x+u,y+v)
为了寻找带角点的窗口,我们搜索像素灰度变化较大的窗口。于是, 我们期望最大化以下式子:
使用 泰勒(Taylor)展开式:
具体的可以看数学推导(如果还不懂,可以去学学泰勒展开,治理主要用的是一阶泰勒展开)
式子可以展开为:
将上式表达为矩阵的形式,表达式可以写为:
表示为:
因此我们有等式:
每个窗口中计算得到一个值。这个值决定了这个窗口中是否包含了角点:
其中:
一个窗口,它的分数 R 大于一个特定值,这个窗口就可以被认为是”角点”
上面E(u,v)可以看出,无论u,v取何值,我们需要E尽可能的大;
线代理论中实对称矩阵可以正交相似对角化,如下面理论;可以看出E(u,v)是一个椭圆,若E尽可能的大,则λi要尽可能大;
所以下图中的λi是椭圆的半长轴(及开根号,椭圆公式);所以当λi两个都很大时,是角及corner,一个大一个小是边;具体看下图中英文标识。
讲到这里,大家基本原理懂了,那如何判别两个特征根都很大呢?
首先矩阵行列式的求法:,只有两个都大时,det(M)才会大;k一般去为0.05;
也是两个都大时,其平方也会大;
最重要的是求每个像素的M矩阵,如下面步骤:
最后最重要的一点;M的求法是不会变得,不同的角点检测方法最后区别就在于R的求法,也就是如何判断特征根都很大的。
void cornerHarris_demo(int, void*)
{
Mat dst, dst_norm, dst_norm_scaled;
dst = Mat::zeros(src.size(), CV_32FC1);
/// Detector parameters
int blockSize = 2;
int apertureSize = 3;
double k = 0.04;
/// Detecting corners
cornerHarris(src_gray, dst, blockSize, apertureSize, k, BORDER_DEFAULT);
/// Normalizing
normalize(dst, dst_norm, 0, 255, NORM_MINMAX, CV_32FC1, Mat());
convertScaleAbs(dst_norm, dst_norm_scaled);
/// Drawing a circle around corners
for (int j = 0; j < dst_norm.rows; j++)
{
for (int i = 0; i < dst_norm.cols; i++)
{
if ((int)dst_norm.at<float>(j, i) > thresh)
{
circle(dst_norm_scaled, Point(i, j), 5, Scalar(0), 2, 8, 0);
}
}
}
Shi-Tomasi 算法是Harris 算法的改进。Harris 算法最原始的定义是将黑塞矩阵
M的行列式值与 M 的迹相减,再将差值同预先给定的阈值进行比较。后来Shi 和Tomasi 提出改进的方法,若两个特征值中较小的一个大于最小阈值,则会得到强角点。二者原理一模一样,只是计算角点量的方式不一样。
#include "opencv2/opencv.hpp"
#include <iostream>
using namespace cv;
using namespace std;
#define WINDOW_NAME "【Shi-Tomasi角点检测】"
Mat g_srcImage, g_grayImage;
int g_maxCornerNumber = 33;
int g_maxTrackbarNumber = 500;
RNG g_rng(12345);//初始化随机数生成器
//-----------------------------【on_GoodFeaturesToTrack( )函数】----------------------------
// 描述:响应滑动条移动消息的回调函数
//----------------------------------------------------------------------------------------------
void on_GoodFeaturesToTrack(int, void*)
{
//【1】对变量小于等于1时的处理
if (g_maxCornerNumber <= 1) { g_maxCornerNumber = 1; }
//【2】Shi-Tomasi算法(goodFeaturesToTrack函数)的参数准备
vector<Point2f> corners;
double qualityLevel = 0.01;//角点检测可接受的最小特征值
double minDistance = 10;//角点之间的最小距离
int blockSize = 3;//计算导数自相关矩阵时指定的邻域范围
double k = 0.04;//权重系数
Mat copy = g_srcImage.clone(); //复制源图像到一个临时变量中,作为感兴趣区域
//【3】进行Shi-Tomasi角点检测
goodFeaturesToTrack(g_grayImage,//输入图像
corners,//检测到的角点的输出向量
g_maxCornerNumber,//角点的最大数量
qualityLevel,//角点检测可接受的最小特征值
minDistance,//角点之间的最小距离
Mat(),//感兴趣区域
blockSize,//计算导数自相关矩阵时指定的邻域范围
false,//不使用Harris角点检测
k);//权重系数
//【4】输出文字信息
cout << "\t>此次检测到的角点数量为:" << corners.size() << endl;
//【5】绘制检测到的角点
int r = 4;
for (int i = 0; i < corners.size(); i++)
{
//以随机的颜色绘制出角点
circle(copy, corners[i], r, Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255),
g_rng.uniform(0, 255)), -1, 8, 0);
}
//【6】显示(更新)窗口
imshow(WINDOW_NAME, copy);
}
static void ShowHelpText()
{
//输出欢迎信息和OpenCV版本
printf("\n\n\t\t\t非常感谢购买《OpenCV3编程入门》一书!\n");
printf("\n\n\t\t\t此为本书OpenCV2版的第87个配套示例程序\n");
printf("\n\n\t\t\t 当前使用的OpenCV版本为:" CV_VERSION);
printf("\n\n ----------------------------------------------------------------------------\n");
//输出一些帮助信息
printf("\n\n\n\t欢迎来到【Shi-Tomasi角点检测】示例程序\n");
printf("\n\t请调整滑动条观察图像效果\n\n");
}
void main()
{
system("color 2F");
ShowHelpText();
//【1】载入源图像并将其转换为灰度图
g_srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg", 1);
cvtColor(g_srcImage, g_grayImage, CV_BGR2GRAY);
//【2】创建窗口和滑动条,并进行显示和回调函数初始化
namedWindow(WINDOW_NAME, CV_WINDOW_AUTOSIZE);
createTrackbar("最大角点数", WINDOW_NAME, &g_maxCornerNumber, g_maxTrackbarNumber, on_GoodFeaturesToTrack);
imshow(WINDOW_NAME, g_srcImage);
on_GoodFeaturesToTrack(0, 0);
waitKey(0);
}
自定义角点检测器简介:
-
基于Harris与Shi-Tomasi角点检测
-
首先通过计算矩阵M得到lamda1和lamda2两个特征值根据他们得到角点响应值
-
然后自己设置阈值实现计算出阈值得到有效响应值的角点设置
cornerEigenValsAndVecs()函数在角点检测中计算扫描图像块的特征向量与特征值,其函数原型如下:
C++: void cornerEigenValsAndVecs(
InputArray src, --单通道输入8位或浮点图像
OutputArray dst, --输出图像,同源图像或CV_32FC(6)
int blockSize, --邻域大小值
int apertureSize, --Sobel算子的参数
int borderType=BORDER_DEFAULT --像素外插方法
)//对应于Harris
borderType:像素扩展的方法,因为在滤波处理的过程中会扩展图像边缘,每扩张一个边界像素,都需要计算出该像素点在原图中的位置,这个功能被提炼出来就变成了borderInterpolate()函数。该函数输入一个点坐标,返回他在原图中的坐标;可参考:borderInterpolate()函数。
下面我们采用cornerEigenValsAndVecs()函数和minMaxLoc()函数,根据其原理来编写Harris角点检测的实现代码,示例如下:
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
// 定义全局变量
const string harris_winName = "自定义角点检测";
Mat src_img, gray_img; // src_img表示原图, gray_img表示灰度图
Mat harris_dst_img, harris_response_img; // harris_dst_img存储自相关矩阵M的特征值和特征向量,harris_response_img存储响应函数的结果
double min_respense_value; // 响应函数的结果矩阵中的最小值
double max_respense_value; // 响应函数的结果矩阵中的最大值
int qualityValue = 30;
int max_qualityValue = 100; // 通过qualityValue/max_qualityValue的结果作为qualitylevel来计算阈值
RNG random_number_generator; // 定义一个随机数发生器
void self_defining_Harris_Demo(int, void*); //TrackBar回调函数声明
// 主函数
int main()
{
src_img = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg");
if (src_img.empty())
{
printf("could not load the image...\n");
return -1;
}
namedWindow("原图", CV_WINDOW_AUTOSIZE);
imshow("原图", src_img);
cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将彩色图转化为灰度图
// 计算特征值
int blockSize = 3;
int ksize = 3;
double k = 0.04;
harris_dst_img = Mat::zeros(src_img.size(), CV_32FC(6));
// 目标图像harris_dst_img存储自相关矩阵M的特征值和特征向量,
// 并将它们以(λ1, λ2, x1, y1, x2, y2)的形式存储。其中λ1, λ2是M未经过排序的特征值;
// x1, y1是对应于λ1的特征向量;x2, y2是对应于λ2的特征向量。
// 因此目标矩阵为6通道,即 CV_32FC(6)的矩阵。
harris_response_img = Mat::zeros(src_img.size(), CV_32FC1);
// harris_response_img用来存储通过每个像素值所对应的自相关矩阵所计算得到的响应值
cornerEigenValsAndVecs(gray_img, harris_dst_img, blockSize, ksize, 4);
// 该函数用来计算每个像素值对应的自相关矩阵的特征值和特征向量
// 计算响应函数值
for (int row = 0; row < harris_dst_img.rows; ++row)
{
for (int col = 0; col < harris_dst_img.cols; ++col)
{
double eigenvalue1 = harris_dst_img.at<Vec6f>(row, col)[0]; // 获取特征值1
double eigenvalue2 = harris_dst_img.at<Vec6f>(row, col)[1]; // 获取特征值2
harris_response_img.at<float>(row, col) = eigenvalue1* eigenvalue2 - k*pow((eigenvalue1 + eigenvalue2), 2);
// 通过响应公式R=λ1*λ2 - k*(λ1+λ2)*(λ1+λ2)来计算每个像素对应的响应值
}
}
minMaxLoc(harris_response_img, &min_respense_value, &max_respense_value, 0, 0, Mat()); // 寻找响应矩阵中的最小值和最大值
namedWindow(harris_winName, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality Value:", harris_winName, &qualityValue, max_qualityValue, self_defining_Harris_Demo); //创建TrackBar
self_defining_Harris_Demo(0, 0);
waitKey(0);
return 0;
}
// 回调函数实现
void self_defining_Harris_Demo(int, void*)
{
if (qualityValue < 10)
{
qualityValue = 10; // 控制qualitylevel的下限值
}
Mat result_img = src_img.clone(); // 输出图像
float threshold_value = min_respense_value + (((double)qualityValue) / max_qualityValue)*(max_respense_value - min_respense_value);
for (int row = 0; row <result_img.rows; row++)
{
for (int col = 0; col < result_img.cols; col++)
{
float resp_value = harris_response_img.at<float>(row, col);
if (resp_value > threshold_value)
{
circle(result_img, Point(col, row), 2, Scalar(random_number_generator.uniform(0, 255),
random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255)), 2, 8, 0);
}
}
}
imshow(harris_winName, result_img);
}
erMinEigenVal()函数在角点检测中计算梯度矩阵的最小特征值,其函数原型如下
C++: void cornerMinEigenVal(
InputArray src, --单通道输入8位或浮点图像
OutputArray dst, --图像存储的最小特征值。类型为CV_32FC1
int blockSize, --邻域大小值
int apertureSize=3, --Sobel算子的参数
int borderType=BORDER_DEFAULT --像素外插方法
}//对应Shi-Tomasi
函数参数
函数参数说明中除了dst必须为CV_32FC1类型以外,其它与cornerEigenValsAndVecs()函数的一致。
函数功能
功能与cornerEigenValsAndVecs()函数相似,但是它只计算导数协方差矩阵的最小特征值,
按照cornerEigenValsAndVecs()函数给定的特征值λ1, λ2来说就是min(λ1, λ2)。
采用cornerMinEigenVal()函数和minMaxLoc()函数结合来模拟Shi-Tomasi角点检测的代码示例如下;(参考:定制化创建角点检测子)
#include <opencv2/opencv.hpp>
#include <iostream>
#include <math.h>
using namespace cv;
using namespace std;
// 定义全局变量
const string ShiTomasi_winName = "Custom Shi-Tomasi Corners Detector";
Mat src_img, gray_img; // src_img表示原图, gray_img表示灰度图
Mat ShiTomasi_dst_img; // ShiTomasi_dst_img用来存储每个像素对应的自相关矩阵的最小特征值
double min_ShiTomasi_value; // 最小特征矩阵中的最小值
double max_ShiTomasi_value; // 最小特征矩阵中的最大值
int ShiTomasi_qualityValue = 30;
int max_qualityValue = 100;
RNG random_number_generator; // 定义一个随机数发生器
void self_defining_ShiTomasi_Demo(int, void*); //TrackBar回调函数声明
// 主函数
int main()
{
src_img = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg");
if (src_img.empty())
{
printf("could not load the image...\n");
return -1;
}
namedWindow("原图", CV_WINDOW_AUTOSIZE);
imshow("原图", src_img);
cvtColor(src_img, gray_img, COLOR_BGR2GRAY); //将彩色图转化为灰度图
// 计算特征值
int blockSize = 3;
int ksize = 3;
// 计算最小特征值
ShiTomasi_dst_img = Mat::zeros(src_img.size(), CV_32FC1);
cornerMinEigenVal(gray_img, ShiTomasi_dst_img, blockSize, ksize, 4); // 计算每个像素对应的自相关矩阵的最小特征值
minMaxLoc(ShiTomasi_dst_img, &min_ShiTomasi_value, &max_ShiTomasi_value, 0, 0, Mat()); //计算最小最大值
namedWindow(ShiTomasi_winName, CV_WINDOW_AUTOSIZE);
createTrackbar("Quality:", ShiTomasi_winName, &ShiTomasi_qualityValue, max_qualityValue, self_defining_ShiTomasi_Demo);
self_defining_ShiTomasi_Demo(0, 0);
waitKey(0);
return 0;
}
// 回调函数实现
void self_defining_ShiTomasi_Demo(int, void*)
{
if (ShiTomasi_qualityValue < 10)
{
ShiTomasi_qualityValue = 10; // 控制qualitylevel的下限值
}
Mat result_img = src_img.clone(); // 输出图像
float threshold_value = min_ShiTomasi_value + (((double)ShiTomasi_qualityValue) / max_qualityValue)*(max_ShiTomasi_value - min_ShiTomasi_value);
for (int row = 0; row <result_img.rows; row++)
{
for (int col = 0; col < result_img.cols; col++)
{
float resp_value = ShiTomasi_dst_img.at<float>(row, col);
if (resp_value > threshold_value)
{
circle(result_img, Point(col, row), 2, Scalar(random_number_generator.uniform(0, 255),
random_number_generator.uniform(0, 255), random_number_generator.uniform(0, 255)), 2, 8, 0);
}
}
}
imshow(ShiTomasi_winName, result_img);
}
亚像素角点检测需要先运行常规的角点检测,得到整数表示的角点坐标。然后算法对每个角点做细化,得到实数表示的角点坐标
第一步:goodFeaturesToTrack() //检测角点
第二步:TermCriteria() //设置迭代算法的终止条件
TermCriteria(int type,int max_iter, double epsilon);
参数
type:终止条件类型;
CV_TERMCRIT_ITER--max_iter达到最大值后停止算法;
CV_TERMCRIT_EPS--当算法依赖的精确度低于epsilon后,停止算法;
CV_TERMCRIT_ITER+CV_TERMCRIT_EPS--当max_iter达到最大值或算法依赖的精确度低于epsilon任一个满足时,停止算法;
max_iter:最大迭代次数;
epsilon:要求精度;
第三步:cornerSubPix() //细化角点位置;
void cornerSubPix(InputArray image, InputOutputArray corners, Size winSize, Size zeroZone, TermCriteria criteria);
参数:
image:输入图像;
corners:初始化输入角点的坐标,为输出提供细化的坐标;
winSize:搜索窗口的边长的一半;
zeroZone:搜索区域中间的死区的一半大小,对它在下边的求和公式不计算,有时候它用来避免可能的自相关矩阵的奇异性,(-1,-1)用来表明这里没有这样的规模;
criteria:角点细化的迭代过程的终止条件;
可参考:亚像素角点检测
相关代码和结果
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>
using namespace cv;
using namespace std;
//描述:定义一些辅助宏
#define WINDOW_NAME "【亚像素角点检测】"
//全局变量声明
Mat g_srcImage;
Mat g_grayImage;
int g_maxCornerNumber = 33;
int g_maxTrackbarNumber = 500;
RNG g_rng(12345);
//回调函数
void on_GoodFeaturesToTrack(int, void *)
{
if (g_maxCornerNumber <= 1)
{
g_maxCornerNumber = 1;
}
//shi-tomasi算法
vector<Point2f> corner;
double qualityLevel = 0.01; //角点检测可接收的最小特征
double minDistance = 10; //角点之间的最小的距离
int blockSize = 3; //计算导数自相关矩阵时指定的邻域范围
double k = 0.04; //权重系数
Mat copy = g_srcImage.clone();
//进行角点检测
goodFeaturesToTrack(g_grayImage, corner, g_maxCornerNumber, qualityLevel, minDistance, Mat(), blockSize, false, k);
//输出文字信息
cout << ">此次检测到的角点数量为:" << corner.size() << endl;
//绘制检测到的角点
Size winSize = Size(5, 5);
Size zeroZone = Size(-1, -1);
TermCriteria criteria = TermCriteria(TermCriteria::EPS + TermCriteria::MAX_ITER, 40, 0.001);
cornerSubPix(g_grayImage, corner, winSize, zeroZone, criteria);
for (int i = 0; i < corner.size(); i++)
{
cout << "\t>>精度角点坐标[" << i << "](" << corner[i].x << "," << corner[i].y << ")" << endl;
}
int r = 4;
for (unsigned int i = 0; i < corner.size(); i++)
{
circle(copy, corner[i], r, Scalar(g_rng.uniform(0, 255), g_rng.uniform(0, 255), g_rng.uniform(0, 255)), -1, 8, 0);
}
//显示更新窗口
imshow(WINDOW_NAME, copy);
}
int main()
{
//载入原始图像
g_srcImage = imread("E:\\VS2015Opencv\\vs2015\\project\\picture\\06.jpg", 1);
cvtColor(g_srcImage, g_grayImage, COLOR_BGR2GRAY);
//创建窗口和滑动条
namedWindow(WINDOW_NAME, WINDOW_AUTOSIZE);
createTrackbar("最大角点数:", WINDOW_NAME, &g_maxCornerNumber, g_maxTrackbarNumber, on_GoodFeaturesToTrack);
imshow(WINDOW_NAME, g_srcImage);
on_GoodFeaturesToTrack(0, 0);
waitKey(0);
return 0;
}
原文链接:https://blog.csdn.net/poem_qianmo/article/details/29356187
代码参考:https://blog.csdn.net/poem_qianmo/article/category/1923021和http://www.opencv.org.cn/opencvdoc/2.3.2/html/doc/tutorials/features2d/trackingmotion/good_features_to_track/good_features_to_track.html#good-features-to-track