Opencv 关键点和描述符(一)—— 关键点及跟踪基础
corners:包含大量本地信息的像素块,并能够在另一张图中被快速识别
keypoints:作为 corners 的扩展,它将像素块的信息进行编码从而使得更易辨识,至少在原则上唯一
descriptors:它是对 keypoints 进一步处理的结果。通常它具有更低的维度,从而使得图像块能够在另一幅不同的图像中被更快地识别。
角
为了实现上述图像的区分和辨识的功能,有很多特性可以使用。对于毫无变化的白墙上的点就不是好的特性,因此像素存在较大变化的点才可能成为特性点。不过由于光流的存在通常一条边通常也不足够构成一个特性。因此,最简单的特性就是角,角中主要的信息来自于两条不同方向边的交叉点中。
Opencv 提供了一个 cv::goodFeaturesToTrack() 函数来计算微分,并分析返回一系列的特征点,从而实现较好地跟踪。
void cv::goodFeaturesToTrack(
cv::InputArray image, // Input, CV_8UC1 or CV_32FC1
cv::OutputArray corners, // Output vector of corners
int maxCorners, // Keep this many corners
double qualityLevel, // (fraction) rel to best
double minDistance, // Discard corner this close
cv::InputArray mask = noArray(), // Ignore corners where mask=0
int blockSize = 3, // Neighborhood used
bool useHarrisDetector = false, // false='Shi Tomasi metric'
double k = 0.04 // Used for Harris metric
);
参数说明:
- image:8U 或 32F 的单通道图像
- corners:返回值,找到的所有角。其可能为 vector<Point2f> 或者 Mat(其中 Mat 只有一行两列,列分别给出了点的 x 和 y 坐标)
- maxCorners:设置希望找到最多多少个点
- qualityLevel:设置返回点的质量,通常 0.01~0.1 之间,不要超过 1.0
- minDistance:找到的特征点的最小间隔
- mask:与 image 同维,同时只检测 mask 不为 0 的点为特征点
- blockSize:多大的区域用于计算角。通常设置为 3,不过对于高分辨率图像可以适当加大
- useHarrisDetector:true 使用 Harris 的原始方法;false 使用 Shi Tomasi 方法
- k:Harris 方法参数,通常不要改变
亚像素角
角的位置不再以像素的形式给出,而是通过分析估计一个更精确的位置,通常将不再为整数。
void cv::cornerSubPix(
cv::InputArray image, // Input image
cv::InputOutputArray corners, // Guesses in, and results out
cv::Size winSize, // Area is NXN; N=(winSize*2+1)
cv::Size zeroZone, // Size(-1,-1) to ignore
cv::TermCriteria criteria // When to stop refinement
);
参数说明:
- image:输入图像
- corners:整数像素点的位置
- winSize:计算区域,如果为 4,那么实际的区域宽度为 2 * 4 + 1 = 9
- zeroZone:不参与计算的隔离区域
- criteria:迭代终止条件,cv::TermCriteria::MAX_ITER(最大次数)和 cv::TermCriteria::EPS(终止误差)。如果终止误差定为 0.1,那么最终的结果精度将为原始像素点的十分之一
其基本原理是亚像素角点到附近像素点的向量与选取的附近像素点梯度的内积(当选取的附近像素点在平缓区域或者处于边界出)都为零,即
光流
光流问题就是去找出一系列图片(视频流)中多个像素或者全部像素的关联关系。光流可以被用于物体运动估计或者相机位姿估计中。而如果需要估计图像中每个像素点的速度和位置,被称为密集光流;而只估计其中某一些点的速度,则被称为稀疏光流。稀疏光流比稠密光流更快也更可靠,因为它限制跟踪对象为一些容易跟踪的特征点,比如上面介绍的角点。在现实中,我们基本不会使用稠密光流去估计每一个像素。
稀疏光流的 Lucas-Kanade 方法
Lucas-Kanade 方法最开始提出被用来解决稠密光流问题,但由于算法只依赖小范围内的信息,因此能够很方便地被迁移来做稀疏光流问题。同时图像金字塔LK方法也很好地解决了目标运动过快超过局部区域的问题。
Lucas-Kanade 方法原理
LK 方法基于以下假设:
- 亮度恒定:无论是黑白还是彩色图像,图像中一个物体的像素值并不随图像帧的改变而改变
- 时间一致性:视频的采样率是足够的,因此图像帧之间的物体改变不能是跳跃性的
- 空间一致性:场景中的相近点应该有相似的运动方式,同时投影到相近的像素点
首先对于一维图像,由于时间和空间的微分都很小,因此下式成立。
同时考虑图像的亮度和采样率即使不能完全满足假设,在第一次得到近似值之后也可以在之后的处理中进一步迭代从而更加满足图像亮度和采样率的一致性,得到更精确的结果。同时,由于亮度一致性假设,图像像素值对 x 的导数将保持不变,因此在迭代过程中只需要求解一下,这可以节省大量的计算成本。最后已经证明,如果起始速度足够接近真值,迭代将在五次左右收敛;而如果起始值不够准确,算法将会发散。
而对于二维图像,由于有两个速度需要估计,而只有一个等式,因此只能求得垂直于直线方向的速度大小。此时,根据空间一致性假设,我们可以选取中心像素点周围多个像素点分别建立相应的等式从而求解中心像素点的速度,比如 5 * 5 的窗
最后得到计算公式
当 是可逆矩阵,即存在两个较大的特征值,物体(纹理)至少存在两个方向的运动,就可以对两个方向的速度进行求解。这也是为什么上面的 cv::goodFeaturesToTrack() 必须选取角作为特性点的原因。
同时为了解决速度过快,采样率不足等问题,通常采用图像金字塔 LK 方法。它从最顶端开始对速度进行估计,并随着不断向下,速度的精度将不断提高。
cv::calcOpticalFlowPyrLK() 函数。基本的逻辑是提供图像和需跟踪的点,然后调用函数,检查状态位 status 判断哪些点被成功地跟踪,从而在 nextPts 中获取跟踪点新的位置。
void cv::calcOpticalFlowPyrLK(
cv::InputArray prevImg, // Prior image (t-1), CV_8UC1
cv::InputArray nextImg, // Next image (t), CV_8UC1
cv::InputArray prevPts, // Vector of 2d start points (CV_32F)
cv::InputOutputArray nextPts, // Results: 2d end points (CV_32F)
cv::OutputArray status, // For each point, found=1, else=0
cv::OutputArray err, // Error measure for found points
cv::Size winSize = Size(15, 15), // size of search window
int maxLevel = 3, // Pyramid layers to add
cv::TermCriteria criteria = TermCriteria( // How to end search
cv::TermCriteria::COUNT | cv::TermCriteria::EPS,
30,
0.01
),
int flags = 0, // use guesses, and/or eigenvalues
double minEigThreshold = 1e-4 // for spatial gradient matrix
);
参数说明:
- prevImg,nextImg:起始和最后的图像
- prevPts,nextPts:起始和最后图像的特性列表
- status:标示了 prevPts 是否在 nextImg 中被找到
- err:如果被找到,对应元素表示了误差
- winSize:计算窗口的大小
- maxLevel:层级化 LK 方法的层级数,如果为零,将不使用层级
- criteria:终止条件,通常不需要修改
cv::TermCriteria 介绍
struct cv::TermCriteria(
public:
enum {
COUNT = 1,
MAX_ITER = COUNT,
EPS = 2
};
TermCriteria();
TermCriteria(int _type, int_maxCount, double _epsilon);
int type, // one of the enum types above
int max_iter,
double epsilon
);
- flags:cv::OPTFLOW_LK_GET_MIN_EIGENVALS 使用角的 Harris 矩阵的最小特征值代替原始的每个像素点的平均强度改变;而 cv::OPTFLOW_USE_INITIAL_FLOW 设置将起始估计值将设置在 nextPts 中。
- minEigThreshold:去除掉一些不好的跟踪点。一般使用默认值 即可。
生产实例
// Example 16-1. Pyramid L-K optical flow
//
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
static const int MAX_CORNERS = 1000;
using std::cout;
using std::endl;
using std::vector;
void help( char** argv ) {
cout << "\nExample 16-1: Pyramid L-K optical flow example.\n" << endl;
cout << "Call: " <<argv[0] <<" [image1] [image2]\n" << endl;
cout << "\nExample:\n" << argv[0] << " ../example_16-01-imgA.png ../example_16-01-imgB.png\n" << endl;
cout << "Demonstrates Pyramid Lucas-Kanade optical flow.\n" << endl;
}
int main(int argc, char** argv) {
if (argc != 3) {
help(argv);
exit(-1);
}
// Initialize, load two images from the file system, and
// allocate the images and other structures we will need for
// results.
//
cv::Mat imgA = cv::imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
cv::Mat imgB = cv::imread(argv[2], CV_LOAD_IMAGE_GRAYSCALE);
cv::Size img_sz = imgA.size();
int win_size = 10;
cv::Mat imgC = cv::imread(argv[2], CV_LOAD_IMAGE_UNCHANGED);
// The first thing we need to do is get the features
// we want to track.
//
vector< cv::Point2f > cornersA, cornersB;
const int MAX_CORNERS = 500;
cv::goodFeaturesToTrack(
imgA, // Image to track
cornersA, // Vector of detected corners (output)
MAX_CORNERS, // Keep up to this many corners
0.01, // Quality level (percent of maximum)
5, // Min distance between corners
cv::noArray(), // Mask
3, // Block size
false, // true: Harris, false: Shi-Tomasi
0.04 // method specific parameter
);
cv::cornerSubPix(
imgA, // Input image
cornersA, // Vector of corners (input and output)
cv::Size(win_size, win_size), // Half side length of search window
cv::Size(-1, -1), // Half side length of dead zone (-1=none)
cv::TermCriteria(
cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS,
20, // Maximum number of iterations
0.03 // Minimum change per iteration
)
);
// Call the Lucas Kanade algorithm
//
vector<uchar> features_found;
cv::calcOpticalFlowPyrLK(
imgA, // Previous image
imgB, // Next image
cornersA, // Previous set of corners (from imgA)
cornersB, // Next set of corners (from imgB)
features_found, // Output vector, each is 1 for tracked
cv::noArray(), // Output vector, lists errors (optional)
cv::Size(win_size * 2 + 1, win_size * 2 + 1), // Search window size
5, // Maximum pyramid level to construct
cv::TermCriteria(
cv::TermCriteria::MAX_ITER | cv::TermCriteria::EPS,
20, // Maximum number of iterations
0.3 // Minimum change per iteration
)
);
// Now make some image of what we are looking at:
// Note that if you want to track cornersB further, i.e.
// pass them as input to the next calcOpticalFlowPyrLK,
// you would need to "compress" the vector, i.e., exclude points for which
// features_found[i] == false.
for (int i = 0; i < static_cast<int>(cornersA.size()); ++i) {
if (!features_found[i]) {
continue;
}
line(
imgC, // Draw onto this image
cornersA[i], // Starting here
cornersB[i], // Ending here
cv::Scalar(0, 255, 0), // This color
1, // This many pixels wide
cv::LINE_AA // Draw line in this style
);
}
cv::imshow("ImageA", imgA);
cv::imshow("ImageB", imgB);
cv::imshow("LK Optical Flow Example", imgC);
cv::waitKey(0);
return 0;
}