OpenCV4 目标跟踪(二)《详解稀疏光流、稠密光流》
目录
光流定义
光流的应用领域
光流法基本原理
基本假设条件
(1)亮度恒定
(2)小运动
基本约束方程
按照理论基础与数学方法的区分得几种光流估计算法的简介
基于梯度的方法
基于匹配的方法
基于能量的方法
基于相位的方法
根据所形成的光流场中二维矢量疏密程度区分的稠密、稀疏光流
稀疏光流
关于稀疏光流的描述
稀疏光流的相关函数calcOpticalFloPyrLK()
官方测试代码
稠密光流
关于稠密光流的描述
相关函数calcOpticalFloFarneback()
官方测试代码
Lucas-Kanade(LK)光流法
最初的LK:
增加空间一致假设的初衷
约束方程
改进后的金字塔LK光流法
为什么要用金字塔
Lucas-Kanade改进算法处理的步骤
meanshift和camshift对于视频跟踪还是有一定局限性,现在尝试用光流估计法试试。
光流定义
光流(optical flo)是空间运动物体在观察成像平面上的像素运动的瞬时速度。
光流法是利用图像序列中像素在时间域上的变化以及相邻帧之间的相关性来找到上一帧跟当前帧之间存在的对应关系,从而计算出相邻帧之间物体的运动信息的一种方法。
通常将二维图像平面特定坐标点上的灰度瞬时变化率定义为光流矢量。
说白了,所谓光流就是瞬时速率,在时间间隔很小(比如视频的连续前后两帧之间)时,也等同于目标点的位移
它显示了一个球在5个连续帧里的移动。箭头显示了它的位移矢量。
光流的应用领域
- 移动构建
- 视频压缩
- 视频稳定
光流法基本原理
基本假设条件
(1)亮度恒定
基本假设条件
(1)亮度恒定
就是同一点随着时间的变化,其亮度不会发生改变。这是基本光流法的假定(所有光流法变种都必须满足),用于得到光流法基本方程;
(2)小运动
这个也必须满足,就是时间的变化不会引起位置的剧烈变化,这样灰度才能对位置求偏导(换句话说,小运动情况下我们才能用前后帧之间单位位置变化引起的灰度变化去近似灰度对位置的偏导数),这也是光流法不可或缺的假定;
基本约束方程
考虑一个像素I(x,y,t)在第一帧的光强度(其中t代表其所在的时间维度)。它移动了 (dx,dy)的距离到下一帧,用了dt时间。因为是同一个像素点,依据上文提到的第一个假设我们认为该像素在运动前后的光强度是不变的,即
右侧泰勒展开得
其中ε代表二阶无穷小项,可忽略不计。从这个方程中我们可以得到
设u,v分别为光流分别为沿X轴与Y轴的速度矢量,得
令
分别表示图像中像素点的灰度沿X,Y,T方向的偏导数。
综上,式(3)可以写为
其中,Ix,Iy,It均可由图像数据求得,而(u,v)即为所求光流矢量。
约束方程只有一个,而方程的未知量有两个,这种情况下无法求得u和v的确切值。此时需要引入的约束条件,从不同的角度引入约束条件,导致了不同光流场计算方法。按照理论基础与数学方法的区别把它们分成四种
- 基于梯度(微分)的方法
- 基于匹配的方法
- 基于能量(频率)的方法
- 基于相位的方法和神经动力学方法。
按照理论基础与数学方法的区分得几种光流估计算法的简介
基于梯度的方法
基于梯度的方法又称为微分法,它是利用时变图像灰度(或其滤波形式)的时空微分(即时空梯度函数)来计算像素的速度矢量。
由于计算简单和较好的结果,该方法得到了广泛应用和研究。典型的代表是Horn-Schunck算法与Lucas-Kanade(LK)算法。
Horn-Schunck算法在光流基本约束方程的基础上附加了全局平滑假设,假设在整个图像上光流的变化是光滑的,即物体运动矢量是平滑的或只是缓慢变化的。
基于此思想,大量的改进算法不断提出。Nagel采用有条件的平滑约束,即通过加权矩阵的控制对梯度进行不同平滑处理;Black和Anandan针对多运动的估计问题,提出了分段平滑的方法。
基于匹配的方法
基于匹配的光流计算方法包括基于特征和区域的两种。
基于特征的方法不断地对目标主要特征进行定位和跟踪,对目标大的运动和亮度变化具有鲁棒性。存在的问题是光流通常很稀疏,而且特征提取和精确匹配也十分困难。
基于区域的方法先对类似的区域进行定位,然后通过相似区域的位移计算光流。这种方法在视频编码中得到了广泛的应用。,它计算的光流仍不稠密。,这两种方法估计亚像素精度的光流也有困难,计算量很大。
基于能量的方法
基于能量的方法又称为基于频率的方法,在使用该类方法的过程中,要获得均匀流场的准确的速度估计,就必须对输入的图像进行时空滤波处理,即对时间和空间的整合,这样会降低光流的时间和空间分辨率。基于频率的方法往往会涉及大量的计算,,要进行可靠性评价也比较困难。
基于相位的方法
基于相位的方法是由Fleet和Jepson提出的,Fleet和Jepson最先提出将相位信息用于光流计算的思想。当我们计算光流的时候,相比亮度信息,图像的相位信息更加可靠,所以利用相位信息获得的光流场具有更好的鲁棒性。基于相位的光流算法的优点是对图像序列的适用范围较宽,而且速度估计比较精确,但也存在着一些问题第一,基于相位的模型有一定的合理性,有较高的时间复杂性;第二,基于相位的方法通过两帧图像就可以计算出光流,但如果要提高估计精度,就需要花费一定的时间;第三,基于相位的光流计算法对图像序列的时间混叠是比较敏感的。
根据所形成的光流场中二维矢量疏密程度区分的稠密、稀疏光流
稀疏光流
关于稀疏光流的描述
关于稀疏光流的描述
相对于稠密光流,它通常需要指定一组点进行跟踪,这组点最好具有某种明显的特性,例如Harris角点等,那么跟踪就会相对稳定和可靠。稀疏跟踪的计算开销比稠密跟踪小得多。
(绿色为跟踪点效果)
上文提到的基于特征的匹配方法是典型的属于稀疏光流的算法。
稀疏光流的相关函数calcOpticalFloPyrLK() void cv::calcOpticalFloPyrLK(InputArrayprevImg,InputArraynextImg,InputArrayprevPts,InputOutputArraynextPts,OutputArraystatus,OutputArrayerr,SizeinSize = Size(21, 21),int maxLevel = 3,TermCriteriacriteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),int flags = 0,double minEigThreshold = 1e-4 )
参数解释
prevImg--> 上一帧图片;
nextImg--> 当前帧图片;
prevPts--> 上一帧找到的特征点向量;
nextPts--> 与返回值中的nextPtrs相同;
status--> 与返回的status相同;
err--> 与返回的err相同;
inSize--> 在计算局部连续运动的窗口尺寸(在图像金字塔中),default=Size(21, 21);
maxLevel--> 图像金字塔层数,0表示不使用金字塔, default=3;
criteria--> 寻找光流迭代终止的条件;
flags--> 有两个宏,表示两种计算方法,分别是OPTFLOW_USE_INITIAL_FLOW表示使用估计值作为寻找到的初始光流,OPTFLOW_LK_GET_MIN_EIGENVALS表示使用最小特征值作为误差测量,default=0;
minEigThreshold--> 该算法计算光流方程的2×2规范化矩阵的最小特征值,除以窗口中的像素数; 如果此值小于minEigThreshold,则会过滤掉相应的功能并且不会处理该光流,它允许删除坏点并获得性能提升, default=1e-4.
返回值
nextPtrs--> 输出一个二维点的向量,这个向量可以是用来作为光流算法的输入特征点,也是光流算法在当前帧找到特征点的新位置(浮点数);
status--> 标志,在当前帧当中发现的特征点标志status==1,否则为0;
err--> 向量中的每个特征对应的错误率.
实现原理 在第一帧图像中检测Shi-Tomasi角点,使用LK算法来迭代的跟踪这些特征点。迭代的方式就是不断向CV2.calcOpticalFloPyrLK()中传入上一帧图片的特征点以及当前帧的图片。函数会返回当前帧的点,这些点带有状态1或者0,如果在当前帧找到了上一帧中的点,那么这个点的状态就是1,否则就是0。
实现流程
· 加载视频。
· 调用CV2.GoodFeaturesToTrack 函数寻找兴趣点(关键点)。
· 调用CV2.CalcOpticalFloPyrLK 函数计算出两帧图像中兴趣点的移动情况。
· 删除未移动的兴趣点。
· 在两次移动的点之间绘制一条线段。
官方测试代码
#include "opencv2/video/tracking.hpp"
#include "opencv2/imgproc.hpp"
#include "opencv2/videoio.hpp"
#include "opencv2/highgui.hpp"
#include
#include
using namespace cv;
using namespace std;
static void help()
{
// print a ele message, and the OpenCV version
cout << "nThis is a demo of Lukas-Kanade optical flo lkdemo(),n"
"Using OpenCV version " << CV_VERSION << endl;
cout << "nIt uses camera by default, but you can provide a path to video as an argument.n";
cout << "nHot keys: n"
"tESC - quit the programn"
"tr - auto-initialize trackingn"
"tc - delete all the pointsn"
"tn - sitch the "night" mode on/offn"
"To add/remove a feature point click itn" << endl;
}
Point2f point;
bool addRemovePt = false;
static void onMouse(int event, int x, int y, int , void )
{
if (event == EVENT_LBUTTONDOWN)
{
point = Point2f((float)x, (float)y);
addRemovePt = true;
}
}
int main(int argc, char argv)
{
VideoCapture cap;
TermCriteria termcrit(TermCriteria::COUNT | TermCriteria::EPS, 20, 0.03);
Size subPixWinSize(10, 10), inSize(31, 31);
const int MAX_COUNT = 500;
bool needToInit = false;
bool nightMode = false;
help();
cv::CommandLineParser parser(argc, argv, "{@input|0|}");
string input = parser.get("@input");
if (input.size() == 1 && isdigit(input[0]))
cap.open(input[0] - '0');
else
cap.open(input);
if (!cap.isOpened())
{
cout << "Could not initialize capturing...n";
return 0;
}
namedWindo("LK Demo", 1);
setMouseCallback("LK Demo", onMouse, 0);
Mat gray, prevGray, image, frame;
vector points[2];
for (;;)
{
cap >> frame;
if (frame.empty())
break;
frame.copyTo(image);
cvtColor(image, gray, COLOR_BGR2GRAY);
if (nightMode)
image = Scalar::all(0);
if (needToInit)
{
// automatic initialization
goodFeaturesToTrack(gray, points[1], MAX_COUNT, 0.01, 10, Mat(), 3, 3, 0, 0.04);
cornerSubPix(gray, points[1], subPixWinSize, Size(-1, -1), termcrit);
addRemovePt = false;
}
else if (!points[0].empty())
{
vector status;
vector err;
if (prevGray.empty())
gray.copyTo(prevGray);
calcOpticalFloPyrLK(prevGray, gray, points[0], points[1], status, err, inSize,
3, termcrit, 0, 0.001);
size_t i, k;
for (i = k = 0; i < points[1].size(); i++)
{
if (addRemovePt)
{
if (norm(point - points[1][i]) <= 5)
{
addRemovePt = false;
continue;
}
}
if (!status[i])
continue;
points[1][k++] = points[1][i];
circle(image, points[1][i], 3, Scalar(0, 255, 0), -1, 8);
}
points[1].resize(k);
}
if (addRemovePt && points[1].size() < (size_t)MAX_COUNT)
{
vector tmp;
tmp.push_back(point);
cornerSubPix(gray, tmp, inSize, Size(-1, -1), termcrit);
points[1].push_back(tmp[0]);
addRemovePt = false;
}
needToInit = false;
imsho("LK Demo", image);
char c = (char)aitKey(10);
if (c == 27)
break;
sitch (c)
{
case 'r':
needToInit = true;
break;
case 'c':
points[0].clear();
points[1].clear();
break;
case 'n':
nightMode = !nightMode;
break;
}
std::sap(points[1], points[0]);
cv::sap(prevGray, gray);
}
return 0;
}
鼠标在视频运动目标中点几个关注点,即可查看跟踪效果
稠密光流
关于稠密光流的描述
针对图像或指定的某一片区域进行逐点匹配的图像配准方法,它计算图像上所有的点的偏移量,从而形成一个稠密的光流场。通过这个稠密的光流场,可以进行像素级别的图像配准。
(红色为逐点匹配区域)
Horn-Schunck算法以及基于区域匹配的大多数光流法都属于稠密光流的范畴。 由于光流矢量稠密,所以其配准后的效果也明显优于稀疏光流配准的效果。其副作用也是明显的,由于要计算每个点的偏移量,其计算量也明显较大,时效性较差。
相关函数calcOpticalFloFarneback() void cv::calcOpticalFloFarneback(InputArrayprev,InputArraynext,InputOutputArrayflo,double pyr_scale,int levels,int insize,int iterations,int poly_n,double poly_sigma,int flags )
参数解释
prev 输入前一帧图像(8位单通道);
next 输入后一帧图像(与prev大小和类型相同);
flo 计算的流量图像具有与prev相同的大小并为CV_32FC2类型;
pyr_scale 指定图像比例( <1)为每个图像构建金字塔; pyr_scale = 0.5意味着一个古典金字塔,其中每个下一层比前一层小两倍。
levels 金字塔层数包括初始图像; levels = 1意味着不会创建额外的图层,只会使用原始图像。
insize 平均窗口大小;较大的值会增加算法对图像噪声的鲁棒性,并可以检测更快速的运动,但会产生更模糊的运动场。
iterations 每个金字塔等级上执行迭代算法的迭代次数。用于在每个像素中查找多项式展开的像素邻域;
poly_n大小;较大的值意味着图像将近似于更光滑的表面,产生更稳健的算法和更模糊的运动场,一般取poly_n = 5或7。
poly_sigma用于平滑导数的高斯的标准偏差,用作多项式展开的基础;对于poly_n = 5,可以设置poly_sigma = 1.1,对于poly_n = 7,可以设置poly_sigma = 1.5;
flags 操作标志,可取计算方法有
OPTFLOW_USE_INITIAL_FLOW 使用输入流作为初始流近似。
OPTFLOW_FARNEBACK_GAUSSIAN 使用Gaussian insize×insiz过滤器代替光流估计的相同大小的盒子过滤器;通常情况下,这个选项可以比使用箱式过滤器提供更精确的流量,代价是速度更低;通常,应将高斯窗口的胜利设置为更大的值以实现相同的稳健性水平。
#include#include using namespace cv; using namespace std; static void draOptFloMap(const Mat& flo, Mat& cflomap, int step, double, const Scalar& color) { for (int y = 0; y < cflomap.ros; y += step) for (int x = 0; x < cflomap.cols; x += step) { const Point2f& fxy = flo.at (y, x); line(cflomap, Point(x, y), Point(cvRound(x + fxy.x), cvRound(y + fxy.y)), color); circle(cflomap, Point(x, y), 2, color, -1); } } int main(int argc, char argv) { //VideoCapture cap(0); VideoCapture cap; cap.open("768x576.avi"); if (!cap.isOpened()) return -1; Mat flo, cflo, frame, preframe; //Mat gray, prevgray, uflo; UMat gray, prevgray, uflo; namedWindo("flo", 1); for (;;) { //cap >> frame; bool ret = cap.read(frame); cvtColor(frame, gray, COLOR_BGR2GRAY); if (!prevgray.empty()) { calcOpticalFloFarneback(prevgray, gray, uflo, 0.5, 3, 15, 3, 5, 1.2, 0); //cvtColor(prevgray, cflo, COLOR_GRAY2BGR); uflo.copyTo(flo); draOptFloMap(flo, preframe, 16, 1.5, Scalar(0, 255, 0)); imsho("flo", preframe); } if (aitKey(30) >= 0) break; std::sap(prevgray, gray); std::sap(preframe, frame); } return 0; }
测试效果显示全图每个像素点的光流
Lucas-Kanade(LK)光流法
最初的LK:
增加空间一致假设的初衷
增加空间一致假设的初衷
LK光流法于1981年提出,最初是用于求稠密光流的,由于算法易于应用在输入图像的一组点上,而成为求稀疏光流的一种重要方法。
LK光流法在原先的光流法两个基本假设的基础上,增加了一个“空间一致”的假设,即所有的相邻像素有相似的行动。也即在目标像素周围m×m的区域内,每个像素均拥有相同的光流矢量。以此假设解决式无法求解的问题。
约束方程
在一个小邻域内,LK光流法通过对下式的加权平方和最小化来估计光流矢量
上述的W2(x)是一个窗口权重函数,该函数使得邻域中心的加权比周围的大。对于Ω内的n个点X1⋯Xn,设
由最小二乘法得到
得
通过结合几个邻近像素点的信息,LK光流法通常能够消除光流方程里的多义性。而且,与逐点计算的方法相比,LK方法对图像噪声不敏感。
改进后的金字塔LK光流法
Jean-Yves Bouguet提出一种基于金字塔分层,针对仿射变换的改进Lucas-Kanade算法。
为什么要用金字塔
因为LK算法的约束条件即小速度,亮度不变以及区域一致性都是较强的假设,并不很容易得到满足。如当物体运动速度较快时,假设不成立,那么后续的假设就会有较大的偏差,使得最终求出的光流值有较大的误差。构建图像金字塔可以解决大运动目标跟踪,也可以一定程度上解决孔径问题(相同大小的窗口能覆盖大尺度图片上尽量多的角点,而这些角点无法在原始图片上被覆盖)。
考虑物体的运动速度较大时,算法会出现较大的误差。那么就电脑维修网希望能减少图像中物体的运动速度。一个直观的方法就是,缩小图像的尺寸。假设当图像为400×400时,物体速度为[16 16],那么图像缩小为200×200时,速度变为[8,8]。缩小为100100时,速度减少到[4,4]。所以光流可以通过生成 原图像的金字塔图像,逐层求解,不断精确来求得。简单来说上层金字塔(低分辨率)中的一个像素可以代表下层的两个。
Lucas-Kanade改进算法处理的步骤
假设I和J是两幅2D的灰度图像,每个像素点的灰度值定义为I(x)=I(x,y) 和 J(x)=j(x,y) 在实际场景中图像I和图像J可以代表前后两帧图像。对于图像特征点金字塔跟踪来说的目的是对于前一帧的图像I上一点u(ux, uy),要在后一帧图像J上找到一点v(ux + dx, uy + dy)与之相匹配,即灰度值最接近。那么向量d = [dx, dy]就是图像在点u处的运动速度,也就是所说像素点u的光流。
对于Lucas-Kanade改进算法来说,主要的步骤有三步
- 建立金字塔
- 基于金字塔跟踪
- 迭代过程。
参考计算机视觉--光流法(optical flo)简介_T-Jhon的博客-CSDN博客_光流法
光流--LK光流--基于金字塔分层的LK光流--中值流_mini猿要成长QAQ-CSDN博客