光流法概述
- 光流可以看成是图像结构光的变化或者图像亮度模式明显的移动
- 分为稀疏光流与稠密光流
- 基于相邻视频帧进行分析
相关API:calcOpticalFlowPyrLK
calcOpticalFlowPyrLK函数属于OpenCV库,用于光流计算。在C++ 中,函数签名大致如下:
void cv::calcOpticalFlowPyrLK(
InputArray prevImg, InputArray nextImg,
InputArray prevPts, InputArray& nextPts,
OutputArray status, OutputArray err,
Size winSize = Size(21,21),
int maxLevel = 3,
TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 0.01),
int flags = 0,
double minEigThreshold = 1e - 4
);
2. 参数解释
• prevImg和nextImg
• 这两个参数是光流计算的基础,它们分别代表前一帧图像和当前帧图像。要求是单通道(灰度图)。
• 例如,在处理视频流时,prevImg是视频中的第n - 1帧的灰度图像,nextImg是第n帧的灰度图像。
• prevPts和nextPts
• prevPts是一个输入参数,用于存储前一帧图像中的特征点坐标。这些特征点通常是通过角点检测等方法预先提取出来的感兴趣的点,例如goodFeaturesToTrack函数提取的角点。
• nextPts是一个输入/输出参数。在输入时,它可以被初始化为和prevPts相同的形状和大小(在一些情况下,也可以提供一个初始猜测值)。在函数执行后,它会被更新为计算得到的当前帧中对应的特征点坐标。
• status
• 这是一个输出参数,以向量形式存在。它用于表示每个特征点的状态。
• 如果特征点成功被跟踪,对应的状态值为1;否则为0。例如,当一个特征点因为被遮挡或者运动到图像边缘等情况而无法被正确跟踪时,其状态就会被标记为0。
• err
• 同样是一个输出参数,也是向量形式。它存储计算每个特征点时的误差。
• 这个误差可以用于评估光流计算的准确性,或者用于筛选出误差较大的特征点,例如,在后续处理中,可以舍弃误差超过一定阈值的特征点。
• winSize
• 它表示光流计算的窗口大小。默认值是Size(21,21)。
• 这个窗口用于在图像上确定一个小区域,在这个小区域内假设像素点的运动是一致的,这是基于Lucas - Kanade算法的基本假设。窗口大小的选择会影响光流计算的精度和速度。较大的窗口可以处理更复杂的运动,但计算速度可能会变慢,并且可能会受到其他物体运动的干扰;较小的窗口计算速度快,但可能无法准确处理较大的位移。
• maxLevel
• 代表金字塔的层数。默认值是3。
• 图像金字塔是通过对原始图像进行多次下采样得到的一组图像序列,最底层是原始图像,上面每一层图像的尺寸是下一层的一半。在计算光流时,先从金字塔的顶层(尺寸最小的图像)开始计算,得到一个大概的光流估计,然后把这个估计作为下一层(尺寸更大的图像)计算的初始值,逐步细化光流估计,这样可以处理较大位移的情况。
• criteria
• 这是一个迭代终止条件。它由三部分组成:TermCriteria::COUNT(迭代次数)、TermCriteria::EPS(精度要求)和两者的组合。
• 例如,默认值TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 0.01)表示当迭代次数达到30次或者误差小于0.01时,停止光流计算。
• flags和minEigThreshold(在某些情况下使用)
• flags用于指定一些特殊的计算模式或选项,不过在一般情况下可以使用默认值0。
• minEigThreshold主要用于在计算过程中根据特征值来筛选点,在一些高级应用或者特定算法变体中可能会用到。
3. 返回值
• 在C++ 中,函数没有返回值,但会修改传入的nextPts、status和err参数,让用户能够获取到当前帧特征点的位置、状态和误差信息。
• 在Python中,函数返回nextPts(当前帧特征点坐标)、status(特征点状态)和err(特征点计算误差)这三个值。
4. 应用场景示例
• 假设我们要跟踪视频中一个移动的物体,首先通过goodFeaturesToTrack函数在视频的第一帧提取特征点(存储在prevPts)。然后,在每一帧之间使用calcOpticalFlowPyrLK函数。根据nextPts更新特征点的位置,根据status判断特征点是否成功跟踪,根据err评估跟踪的质量。通过这些信息,我们可以在视频帧上绘制出物体的运动轨迹,实现目标跟踪。
goodFeaturesToTrack
goodFeaturesToTrack是OpenCV中的一个函数,用于在图像中检测出具有显著特征的点,这些点可以用于后续的处理,比如目标跟踪、图像配准等操作。
1. 函数签名(以C++为例)
• 函数原型如下:
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
);
2. 参数解释
• image
• 这是输入参数,必须是单通道(灰度图)。它是要在其中检测特征点的图像。例如,在一个视频处理场景中,可能是视频帧转换后的灰度图像。
• corners
• 输出参数,用于存储检测到的特征点坐标。坐标形式通常是(x,y)二维点的集合。
• maxCorners
• 它指定了要检测的最大特征点数。例如,如果设置为100,函数会尝试找出100个最佳的特征点(如果图像中有足够多的特征点)。
• qualityLevel
• 这是一个重要的参数,用于定义特征点的质量水平。它是一个介于0和1之间的数。
• 具体来说,函数会计算每个潜在特征点周围的最小特征值(如果不使用哈里斯角点检测)或者哈里斯响应值(如果使用哈里斯角点检测),只有当这个值大于qualityLevel乘以图像中该区域的最大特征值(或哈里斯响应值)时,这个点才会被考虑作为一个特征点。例如,设置qualityLevel = 0.01,表示只有那些特征值(或哈里斯响应值)大于图像中最大特征值(或哈里斯响应值)的1%的点才会被当作特征点。
• minDistance
• 用于指定两个特征点之间的最小距离。以像素为单位。
• 这个参数的目的是避免检测到过于密集的特征点。例如,设置minDistance = 10,则检测到的任意两个特征点之间的距离至少为10像素。
• mask(可选)
• 这是一个可选的输入参数,它是一个和image大小相同的单通道图像(通常是uint8类型),用于指定在图像的哪些区域进行特征点检测。
• 如果某个像素位置的mask值为0,则在这个位置不会检测特征点;如果mask值非0,则会在对应的位置考虑检测特征点。例如,可以使用一个二值图像,白色区域(非0值)表示允许检测特征点的区域,黑色区域(0值)表示禁止检测的区域。
• blockSize(可选)
• 这个参数主要用于计算特征点周围区域的大小。默认值是3。
• 当计算特征点的特征值(或哈里斯响应值)时,会考虑以这个点为中心,边长为blockSize的正方形区域内的像素。例如,blockSize = 3表示考虑一个3x3的像素区域。
• useHarrisDetector(可选)
• 这是一个布尔值,用于指定是否使用哈里斯角点检测算法。默认值是false,即不使用。
• 如果设置为true,则函数会使用哈里斯角点检测来计算特征点的质量,这种方法在某些情况下可能会检测到更鲁棒的角点。
• k(可选)
• 当useHarrisDetector为true时,这个参数会被用到。它是哈里斯角点检测算法中的一个系数,默认值是0.04。
3. 返回值
• 在C++中,函数没有返回值,但会将检测到的特征点存储在corners参数中。
• 在Python中,函数返回corners,即检测到的特征点坐标。
4. 应用场景示例
• 在目标跟踪场景中,首先使用goodFeaturesToTrack函数在视频的第一帧图像中检测出一些特征点。然后,利用光流法(如calcOpticalFlowPyrLK函数)跟踪这些特征点在后续视频帧中的位置,从而实现对目标物体的跟踪。
#include <opencv2/opencv.hpp>
#include <iostream>
using namespace cv;
using namespace std;
RNG rng(12345);
void draw_lines(Mat &frame, vector<Point2f> pts1, vector<Point2f> pts2);
int main(int argc, char** argv) {
VideoCapture capture("D:/images/video/CarsDrivingUnderBridge.mp4");
if (!capture.isOpened()) {
printf("could not open the camera...\n");
}
namedWindow("frame", WINDOW_AUTOSIZE);
Mat old_frame, old_gray;
capture.read(old_frame);
cvtColor(old_frame, old_gray, COLOR_BGR2GRAY);
vector<Point2f> feature_pts;
vector<Point2f> initPoints;
double quality_level = 0.01;
int minDistance = 10;
goodFeaturesToTrack(old_gray, feature_pts, 5000, quality_level, minDistance, Mat(), 3, false);//获取关键点位坐标
Mat frame, gray;
vector<Point2f> pts[2];
pts[0].insert(pts[0].end(), feature_pts.begin(), feature_pts.end());//迭代
initPoints.insert(initPoints.end(), feature_pts.begin(), feature_pts.end());
vector<uchar> status;
vector<float> err;
TermCriteria criteria = TermCriteria(TermCriteria::COUNT + TermCriteria::EPS, 30, 0.01);
while (true) {
bool ret = capture.read(frame);
if (!ret) break;
imshow("frame", frame);
cvtColor(frame, gray, COLOR_BGR2GRAY);
// calculate optical flow
calcOpticalFlowPyrLK(old_gray, gray, pts[0], pts[1], status, err, Size(31, 31), 3, criteria, 0);
size_t i = 0, k = 0;
for (i = 0; i < pts[1].size(); i++) {
// 距离与状态检测
double dist = abs(pts[0][i].x - pts[1][i].x) + abs(pts[0][i].y - pts[1][i].y);
if (status[i] && dist > 2) {
pts[0][k] = pts[0][i];
initPoints[k] = initPoints[i];
pts[1][k++] = pts[1][i];
int b = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int r = rng.uniform(0, 255);
circle(frame, pts[1][i], 2, Scalar(b, g, r), 2, 8, 0);
}
}
以下是对这段代码的详细解释:
1. 变量声明与初始化
size_t i = 0, k = 0;
这里声明了两个无符号整型变量 i 和 k,并都初始化为 0。在后续的循环和相关操作中,i 通常用于遍历数组或者向量中的元素,k 更多地是作为一个新的索引来更新有效元素的存储位置,起到筛选和整理元素的作用。
2. 循环部分
for (i = 0; i < pts[1].size(); i++) {
// 距离与状态检测
double dist = abs(pts[0][i].x - pts[1][i].x) + abs(pts[0][i].y - pts[1][i].y);
if (status[i] && dist > 2) {
// 相关操作
}
}
• 循环目的:
这是一个 for 循环,其目的是遍历 pts[1] 这个 vector<Point2f> 类型的容器(前面代码中应该是存储了经过光流计算后当前帧的特征点坐标等相关信息)中的每一个元素,以便对每个特征点进行一些后续的筛选和处理操作。循环条件 i < pts[1].size() 表示只要索引 i 小于 pts[1] 的元素个数,就会持续循环遍历。
• 距离与状态检测:
在循环内部,首先计算了一个距离变量 dist,它是通过计算当前帧(对应 pts[1] 中的元素)和前一帧(对应 pts[0] 中的元素)在同一索引位置 i 处的特征点坐标差值的绝对值之和来得到的。具体来说,abs(pts[0][i].x - pts[1][i].x) 计算了 x 坐标方向上的差值绝对值,abs(pts[0][i].y - pts[1][i].y) 计算了 y 坐标方向上的差值绝对值,两者相加得到特征点从 pts[0] 到 pts[1] 的整体位置变化距离。
然后有一个 if 条件判断 if (status[i] && dist > 2),这里涉及到两个判断条件:
- status[i]:status 是一个 vector<uchar> 类型的向量(在前面代码中应该是记录了每个特征点的跟踪状态,值为 1 表示跟踪成功,值为 0 表示跟踪失败),通过检查 status[i] 的值来确定当前索引 i 对应的特征点是否被成功跟踪。
- dist > 2:表示只有当特征点的位置变化距离大于 2 (这里的 2 可能是根据实际情况设定的一个阈值,用于筛选出有明显位移、相对更值得关注的特征点)时,才会进入后续的操作。
3. 满足条件后的操作
当满足上述 if 条件(即特征点跟踪成功且位置变化距离大于 2 )时,会执行以下操作:
pts[0][k] = pts[0][i];
initPoints[k] = initPoints[i];
pts[1][k++] = pts[1][i];
• 更新特征点数据:
这里把 pts[0]、initPoints 和 pts[1] 这三个 vector<Point2f> 类型的容器中,索引为 i 的元素(也就是满足条件的那个特征点相关的数据),赋值给它们各自索引为 k 的位置。相当于把这些有效特征点重新整理,按照新的索引 k 来存储,而 k++ 这个操作在完成赋值后会让 k 的值自增 1,为下一次满足条件的特征点数据存储做准备,这样就实现了筛选出有效特征点并更新存储它们的位置,去除了那些不符合条件的特征点(比如跟踪失败或者位移过小的特征点)在对应向量中的原有位置。
int b = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int r = rng.uniform(0, 255);
circle(frame, pts[1][i], 2, Scalar(b, g, r), 2, 8, 0);
• 可视化绘制操作:
首先,通过 rng.uniform(0, 255) 三次调用,利用之前初始化的随机数生成器 rng 分别生成了三个范围在 0 到 255 之间的随机整数 b、g、r,这三个值分别代表蓝色(Blue)、绿色(Green)、红色(Red)通道的颜色值,也就是生成了一个随机颜色。
然后,调用 circle 函数在 frame (从代码上下文来看应该是一个表示图像帧的 Mat 类型变量,可能是视频中的某一帧画面)上进行绘制操作。具体是以 pts[1][i] (也就是当前帧中满足条件的那个特征点坐标位置)为圆心,半径为 2 绘制一个实心圆,颜色使用刚才生成的随机颜色 Scalar(b, g, r),并且设置了圆的线条粗细等绘制参数(这里 2 表示线条粗细,8 和 0 等其他参数也与绘制的具体细节相关,比如 8 可能涉及到绘制的边界类型等情况)。通过这样的绘制操作,在图像帧上就可以直观地看到那些满足条件的特征点被标记出来了,方便后续查看和分析特征点的分布以及运动情况等。
总体来说,这段代码的核心功能是对经过光流计算后的特征点进行筛选,保留跟踪成功且有明显位移的特征点,同时更新相关数据存储并在图像帧上对这些有效特征点进行可视化标记,便于后续的目标跟踪等相关操作进一步利用这些信息。
// update key points
pts[0].resize(k);
pts[1].resize(k);
initPoints.resize(k);
// 绘制跟踪线
draw_lines(frame, initPoints, pts[1]);
// 绘制跟踪
imshow("KLT-demo", frame);
char c = waitKey(50);
if (c == 27) { // ESC
break;
}
// update to old
std::swap(pts[1], pts[0]);
cv::swap(old_gray, gray);//这行代码使用了C++标准库中的std::swap函数,用于交换pts容器中索引为0和索引为1的两个元素。假设pts是一个std::vector或者其他支持随机访问且元素可交换的容器,pts[0]和pts[1]的内容会被交换。
// re-init
if (pts[0].size() < 40) {
goodFeaturesToTrack(old_gray, feature_pts, 5000, quality_level, minDistance, Mat(), 3, false);
pts[0].insert(pts[0].end(), feature_pts.begin(), feature_pts.end());
initPoints.insert(initPoints.end(), feature_pts.begin(), feature_pts.end());
}
}
capture.release();
waitKey(0);
destroyAllWindows();
return 0;
}
void draw_lines(Mat &frame, vector<Point2f> pts1, vector<Point2f> pts2) {
vector<Scalar> lut;
for (size_t t = 0; t < pts1.size(); t++) {
int b = rng.uniform(0, 255);
int g = rng.uniform(0, 255);
int r = rng.uniform(0, 255);
lut.push_back(Scalar(b, g, r));
}
for (size_t t = 0; t < pts1.size(); t++) {
line(frame, pts1[t], pts2[t], lut[t], 2, 8, 0);
}
}