光流跟踪算法,通常应用于连续时间序列图像的点追踪。当图像序列之间存在运动时,相同部位的点在不同图像上将处于不同的坐标位置,使用光流跟踪算法可以追踪相同部位的点在不同图像上分别运动到什么位置。光流算法可以分为稠密光流算法和稀疏光流算法,顾名思义,前者追踪图像中所有点的运动,后者仅追踪部分特征点的运动。

LK金字塔光流算法是一种经典的稀疏光流算法,该算法有三个假设条件:亮度恒定、小运动、邻域空间一致。图像越满足这三个条件,算法的跟踪效果就越好。本文在原算法的基础上尝试作一点改进,使得跟踪效果更准确更稳定。下面详细讲解该算法的原理,接着尝试进行改进,并使用C++与Opencv来实现算法。

LK光流金字塔算法首先计算图像金字塔,如下图所示,原图像是第0层图像,通过对第0层图像进行高斯卷积(也即高斯滤波)和向下采样(删除所有的偶数行与列)得到第1层图像,再对第1层图像进行高斯卷积和向下采样得到第2层图像……以此类推,一直计算得到最顶层图像,即尺寸分辨率最小的图像。

光流即待跟踪点与跟踪点的坐标偏移,每一层金字塔图像都有一个初始光流g和一个剩余光流d,从最顶层往下开始逐层计算,假设有n层金字塔(最顶层为第n-1层),则每层金字塔的初始光流计算如下所示。

初始光流g决定初始跟踪点,剩余光流d在初始跟踪点的基础上进行迭代优化,获取精确跟踪点。剩余光流d的计算思路为:取参考图像I上待跟踪点和浮动图像J上跟踪点的邻域窗口作平方差,当邻域窗口的平方差最小,则认为得到了精确的剩余光流,也即跟踪到了对应位置的点,如下图所示。

假设待跟踪点坐标为(ux,uy),邻域窗口大小为wx*wy,光流为(dx,dy),则待跟踪点与跟踪点的邻域窗口平方差可如下式表示:

对上式的d求偏导:

因为是小运动,dx和dy都很小,故对J(x+dx,y+dy)进行泰勒展开并略去高阶项得到:

于是有:

记:

上式中,由于dx和dy都很小,把浮动图像J对x和y的梯度近似替换为参考图像I对x和y的梯度。因为后者在迭代优化过程中是固定不变的,这样就可以避免在每一轮迭代中都计算梯度,可减少很多计算时间。

于是有:

对以上等式两边取倒置得到:

再记:

当满足下式时,邻域窗口的平方差最小:

计算出d之后,根据d移动浮动图像上的跟踪点,在此基础上重复上述计算过程进行迭代优化,直到满足给定精度或达到最大迭代次数为止。迭代过程中,[Ix Iy]是不变的,故G保持不变,但因为δI与上一轮得到的光流有关,b在每轮迭代中是变化的。假设第k轮迭代得到的光流为dk=(dxk,dyk),第k-1轮迭代得到的光流为dk-1=(dxk-1,dyk-1),那么有:

以上计算过程中,δI实际上是参考图像I上待跟踪点与浮动图像J上相同坐标点的像素差,且▽I实际上是浮动图像J上点的梯度。但原算法为了减小计算时间,在迭代计算过程中,使用参考图像的梯度来代替浮动图像的梯度,并且δI改为取参考图像上待跟踪点与浮动图像上跟踪点的像素差。当参考图像与浮动图像之间的运动偏移很大时,就不再适合使用浮动图像的梯度来近似替代参考图像的梯度了,所以这种情况下跟踪效果会受很大影响。本文为解决此问题,将δI与▽I的计算还原回去,如下图所示,此时δI取领域块A与邻域块B的像素差值,▽I取邻域块C的梯度。

由此,在迭代过程中δI取相同坐标点的像素差并保持不变,▽I取浮动图像中跟踪点的梯度,此时G和b都会随着▽I的改变而变化,第k轮的迭代计算如下式所示:

下面上代码,基于C++与Opencv实现。

计算图像金字塔代码:

typedef float DTYPE_FD;
typedef unsigned char DTYPE;//获取图像高斯金字塔
void build_gauss_Pyramid_cuda(Mat src, DTYPE *gauss_Pyramid, int num)
{memcpy(gauss_Pyramid, (DTYPE *)src.data, src.rows*src.cols*sizeof(DTYPE));   //先把第0层的原图拷贝进去Mat currentMat = src.clone();int len_x = src.cols;int len_y = src.rows;int img_size = src.rows*src.cols;int offset = 0;for (int i = 0; i < num; i++) {Mat nextLayer;offset += img_size;img_size >>= 2;len_x >>= 1;len_y >>= 1;pyrDown(currentMat, nextLayer, Size(len_x, len_y));memcpy(gauss_Pyramid+offset, (DTYPE *)nextLayer.data, img_size*sizeof(DTYPE));currentMat = nextLayer.clone();}
}

计算图像高斯金字塔的梯度代码:

//分别计算单帧图像的x,y方向的梯度
void cal_gradient(DTYPE *src, int row, int col, DTYPE_FD *Fx, DTYPE_FD *Fy)
{int index;//计算x方向的首列和尾列梯度, i = 0~row-1, j = 0, col-1for(int i = 0; i < row; i++){index = i*col;Fx[index] = (DTYPE_FD)(src[index+1] - src[index]);   //首列Fx[index+col-1] = (DTYPE_FD)(src[index+col-1] - src[index+col-2]);    //尾列}//计算y方向的首行和尾行梯度, i = 0, row-1, j = 0~col-1for(int j = 0; j < col; j++){Fy[j] = (DTYPE_FD)(src[col+j] - src[j]);   //首行index = (row-1)*col;Fy[index+j] = (DTYPE_FD)(src[index+j] - src[index-col+j]);    //尾行}for(int i = 1; i < row-1; i++){for(int j = 1; j < col-1; j++){index = i*col+j;Fx[index] = (src[index+1] - src[index-1])/2.0;Fy[index] = (src[index+col] - src[index-col])/2.0;}}
}//获取图像高斯金字塔的梯度,row和col为原图尺寸
void cal_gauss_Pyramid_gradient_cuda(DTYPE *gauss_Pyramid, int row, int col, DTYPE_FD *Fx, DTYPE_FD *Fy, int num)
{cal_gradient(gauss_Pyramid, row, col, Fx, Fy);   //计算第0层的梯度,Fx和Fy也是金字塔,是多层图像的梯度int len_x = col;int len_y = row;int img_size = len_x*len_y;int offset = 0;for (int i = 0; i < num; i++) {offset += img_size;img_size >>= 2;len_x >>= 1;len_y >>= 1;cal_gradient(gauss_Pyramid+offset, len_y, len_x, Fx+offset, Fy+offset);     //Fx和Fy也是金字塔,是多层图像的梯度}
}

计算参考图像与浮动图像的高斯金字塔图像的像素差值图代码:

//计算前后帧的高斯金字塔的差值
void cal_diff(DTYPE *pre_gauss_Pyramid, DTYPE *cur_gauss_Pyramid, int length, DTYPE_FD *diff)
{for(int i = 0; i < length; i++){diff[i] = (DTYPE_FD)(pre_gauss_Pyramid[i] - cur_gauss_Pyramid[i]);}
}

改进的LK金字塔光流算法实现代码:

vector<Point2f> lk_track_ori1(Mat preFrame, Mat curFrame, vector<Point2f> keyPoints, vector<uchar> &status, int maxPyramidLayer, int maxIteration, int winsize, DTYPE_FD opticalflowResidual)
{status.clear();int gaussian_len = get_gauss_Pyramid_memory_len(preFrame.rows, preFrame.cols, maxPyramidLayer);   //总共maxPyramidLayer+1层DTYPE *pre_gauss_Pyramid_memory = (DTYPE *)malloc(gaussian_len);DTYPE *cur_gauss_Pyramid_memory = (DTYPE *)malloc(gaussian_len);DTYPE_FD *cur_Fx = (DTYPE_FD *)malloc(gaussian_len / sizeof(DTYPE) * sizeof(DTYPE_FD));DTYPE_FD *cur_Fy = (DTYPE_FD *)malloc(gaussian_len / sizeof(DTYPE) * sizeof(DTYPE_FD));DTYPE_FD *diff = (DTYPE_FD *)malloc(gaussian_len / sizeof(DTYPE) * sizeof(DTYPE_FD));build_gauss_Pyramid_cuda(preFrame, pre_gauss_Pyramid_memory, maxPyramidLayer);   //计算图像金字塔build_gauss_Pyramid_cuda(curFrame, cur_gauss_Pyramid_memory, maxPyramidLayer);cal_gauss_Pyramid_gradient_cuda(cur_gauss_Pyramid_memory, curFrame.rows, curFrame.cols, cur_Fx, cur_Fy, maxPyramidLayer);cal_diff(pre_gauss_Pyramid_memory, cur_gauss_Pyramid_memory, gaussian_len / sizeof(DTYPE), diff);vector<Point2f> tPoints;int width, height;DTYPE_FD delta[2];const int winsize_2_1 = 2 * winsize + 1;const int img_size = preFrame.rows*preFrame.cols;DTYPE_FD *dd = (DTYPE_FD *)malloc(winsize_2_1*winsize_2_1 * sizeof(DTYPE_FD));float coeff[16];const DTYPE_FD maxPyramidLayer_coeff = (float)(1. / (1 << maxPyramidLayer));for (unsigned int i = 0; i < keyPoints.size(); i++){DTYPE_FD g[2] = { 0 };bool isValid = true;float prePoint_x;   //最顶层的初始跟踪点float prePoint_y;prePoint_x = keyPoints[i].x*maxPyramidLayer_coeff;prePoint_y = keyPoints[i].y*maxPyramidLayer_coeff;for (int j = maxPyramidLayer; j >= 0; j--){//int offset = get_gauss_Pyramid_memory_offset_size(preFrame.rows, preFrame.cols, j); //, height, width);int offset = (j > 0) ? ((int)(img_size*(4 - 1.0 / (1 << (2 * j - 2))) / 3.0)) : 0;height = preFrame.rows >> j;  //row/pow(2.0, index);width = preFrame.cols >> j;   //col/pow(2.0, index);DTYPE_FD *fx = &cur_Fx[offset];DTYPE_FD *fy = &cur_Fy[offset];DTYPE_FD *d = &diff[offset];DTYPE_FD x, y, curX, curY, ix, iy;for (int t1 = -winsize; t1 <= winsize; t1++)    //row{for (int t2 = -winsize; t2 <= winsize; t2++)    //col{x = prePoint_x + t2;y = prePoint_y + t1;int floorRow = floor(y);   //δI=A(x,y)-B(x,y),而不是δI=A(x,y)-B(x+Δx,y+Δy),所以这样做会更准确一点int floorCol = floor(x);//双线性插值int ceilRow = floorRow + 1;int ceilCol = floorCol + 1;if (floorCol >= 0 && floorCol < width - 1 && floorRow >= 0 && floorRow < height - 1){DTYPE_FD fracRow = y - floorRow;DTYPE_FD fracCol = x - floorCol;float k0 = 1.0 - fracRow;float k1 = 1.0 - fracCol;dd[(t1 + winsize)*(2 * winsize + 1) + t2 + winsize] = (k0*k1*d[floorRow*width + floorCol] + fracRow*k1*d[ceilRow*width + floorCol]+ k0*fracCol*d[floorRow*width + ceilCol] + fracRow*fracCol*d[ceilRow*width + ceilCol]);}else{dd[(t1 + winsize)*(2 * winsize + 1) + t2 + winsize] = 0;}}}DTYPE_FD v[2] = { 0 };  //[x y]int iterCount = 0;DTYPE_FD residual;while (iterCount < maxIteration)    //迭代计算剩余光流{iterCount++;DTYPE_FD G[4] = { 0 };    //[Ix*Ix, Ix*Iy, Ix*Iy, Iy*Iy]DTYPE_FD b[2] = { 0 };  //[x y]for (int t1 = -winsize; t1 <= winsize; t1++)    //row{for (int t2 = -winsize; t2 <= winsize; t2++)    //col{x = prePoint_x + t2;y = prePoint_y + t1;curX = x + g[0] + v[0];   //这里需注意:后一帧图像的点坐标是变化的curY = y + g[1] + v[1];/插值//int floorRow = floor(curY);int floorCol = floor(curX);if (floorCol >= 0 && floorCol < width - 1 && floorRow >= 0 && floorRow < height - 1){int ceilRow = floorRow + 1;int ceilCol = floorCol + 1;DTYPE_FD fracRow = curY - floorRow;DTYPE_FD fracCol = curX - floorCol;DTYPE_FD k0 = 1.0 - fracRow;DTYPE_FD k1 = 1.0 - fracCol;DTYPE_FD a0 = k0*k1;DTYPE_FD a1 = fracRow*k1;DTYPE_FD a2 = k0*fracCol;DTYPE_FD a3 = fracRow*fracCol;int idx0 = floorRow*width + floorCol;int idx1 = ceilRow*width + floorCol;int idx2 = floorRow*width + ceilCol;int idx3 = ceilRow*width + ceilCol;ix = a0 * fx[idx0] + a1 * fx[idx1] + a2 * fx[idx2] + a3 * fx[idx3];iy = a0 * fy[idx0] + a1 * fy[idx1] + a2 * fy[idx2] + a3 * fy[idx3];}else{ix = 0;iy = 0;}G[0] += ix * ix;    //计算GG[1] += ix * iy;G[3] += iy * iy;int idx = (t1 + winsize)*winsize_2_1 + t2 + winsize;b[0] += dd[idx] * ix;    //计算bb[1] += dd[idx] * iy;}}G[2] = G[1];DTYPE_FD abs_G = 1.f / (G[0] * G[3] - G[1] * G[2]);    //求矩阵的行列式的倒数delta[0] = (G[3] * b[0] - G[2] * b[1])*abs_G;delta[1] = (-G[1] * b[0] + G[0] * b[1])*abs_G;v[0] += delta[0];v[1] += delta[1];residual = delta[0] * delta[0] + delta[1] * delta[1];if (residual <= opticalflowResidual*opticalflowResidual)break;}if (iterCount >= maxIteration){isValid = false;break;}if (j == 0){g[0] += v[0];  //最终光流g[1] += v[1];}else{g[0] = 2 * (g[0] + v[0]);  //金字塔中间层光流g[1] = 2 * (g[1] + v[1]);}prePoint_x = prePoint_x*2.f;    //下一层的初始跟踪点,迭代的时候已经加上了猜测光流,所以此处不需要再加了prePoint_y = prePoint_y*2.f;}Point2f dstPoint(keyPoints[i].x + g[0], keyPoints[i].y + g[1]);  //得到跟踪点tPoints.push_back(dstPoint);if (isValid && dstPoint.x >= 0 && dstPoint.x < curFrame.cols && dstPoint.y >= 0 && dstPoint.y < curFrame.rows){status.push_back(1);}else{status.push_back(0);}}free(pre_gauss_Pyramid_memory);free(cur_gauss_Pyramid_memory);free(cur_Fx);free(cur_Fy);free(diff);free(dd);return tPoints;
}

设置相同的参数,分别运行以上代码和Opencv实现的LK光流金字塔算法接口calcOpticalFlowPyrLK,对运动图像进行追踪,对比结果如下。可以看到,当参考图像与浮动图像的运动偏移较小时,Opencv实现的原算法与本文C++实现的算法,跟踪效果都很好,但是当运动偏移较大时,Opencv实现原算法的跟踪结果中,误跟踪点明显增多,然而本文C++实现算法的跟踪效果还是比较好的。因此改进之后算法对大运动具有更好的适应性了。

参考图像

偏移浮动图像

偏移浮动图像

Opencv calcOpticalFlowPyrLK函数追踪偏移图像

上述C++实现代码追踪偏移图像

Opencv calcOpticalFlowPyrLK函数追踪偏移图像

上述C++实现代码追踪偏移图像

微信公众号如下,欢迎关注,欢迎留言:

LK光流金字塔算法原理及C++实现相关推荐

  1. 金字塔LK光流法数学原理学习笔记

    参考: 1.总结:光流--LK光流--基于金字塔分层的LK光流--中值流 https://blog.csdn.net/sgfmby1994/article/details/68489944 2.< ...

  2. 浅谈光流跟踪之KLT稀疏光流跟踪算法

    文章目录 0 简介 1 光流的三大假设条件 2 LK光流跟踪算法原理 3 LK-金字塔光流 3.1 金字塔创建 3.2 金字塔跟踪 4 KLT光流跟踪 5 Harris角点检测 6 Shi-Tomas ...

  3. 【视频抖动程度检测】基于LK光流算法的视频图像序列抖动程度计算matlab仿真

    1.软件版本 matlab2021a 2.算法原理概述 根据LK光流提取算法,得到视频前后两帧图像的光流,假设, X(t),Y(t) 表示t时刻光流场的X分量和Y分量: 那么晃动计算公为: 其中R为光 ...

  4. 图像金字塔LK光流法原理分析

    图像金字塔LK光流法原理分析 1.LK光流法原理分析 2.基于图像金字塔的LK光流法原理分析 本篇博客只讲述原理,c++代码实现请参考博客< 基于金字塔LK的光流法实现-根据论文自己实现的c++ ...

  5. 【老生谈算法】matlab实现金字塔LK光流法源码——金字塔LK光流法

    基于金字塔LK光流法的MATLAB代码 1.原文下载: 本算法原文如下,有需要的朋友可以点击进行下载 序号 原文(点击下载) 本项目原文 [老生谈算法]基于金字塔LK光流法的MATLAB代码.docx ...

  6. OpenCV Using Python——基于SURF特征提取和金字塔LK光流法的单目视觉三维重建 (光流、场景流)...

    https://blog.csdn.net/shadow_guo/article/details/44312691 基于SURF特征提取和金字塔LK光流法的单目视觉三维重建 1. 单目视觉三维重建问题 ...

  7. OpenCV:金字塔LK光流法

    金字塔LK光流法的三个假设 亮度恒定,即图像场景中目标的像素在帧间运动时外观上保持不变: 时间连续或者运动是"小运动",即图像的运动随时间的变化比较缓慢: 空间一致,即一个场景中同 ...

  8. 视频光流估计综述:从算法原理到具体应用

    点击上方"3D视觉工坊",选择"星标" 干货第一时间送达 作者:肖泽东 | 来源:知乎 https://zhuanlan.zhihu.com/p/7446034 ...

  9. 图像缩放算法_技术专栏|基于无人机LK光流算法的适用性及其优化方法探究

    点击上方蓝字关注我们 问题描述 ◆ ◆ ◆ 一般的LK光流算法存在一个比较明显的局限性.我们知道,一般的LK光流算法必须包含三个假设: (1)亮度恒定: (2)时间连续或者运动是小运动: (3)空间一 ...

最新文章

  1. 工作笔记---巡检记录
  2. html5 list css,使用HTML5的classList属性操做CSS类
  3. PG数据向Kingbase移植
  4. pytorch安装-Windows(pip install失败)
  5. Python Web框架Tornado的异步处理代码演示样例
  6. 文东工作室开通微信公众号了!欢迎订阅!~
  7. JasperReports学习(1)
  8. 张鹏程:7月24日阿里云上海峰会弹性计算大神
  9. 上网痕迹查询助手Viewurl 2017
  10. my sql实验视图_数据库SQL 视图的创建及使用实验报告(共5篇)
  11. 去除CAJviewer右上侧的广告栏位去除CAJviewer右上侧的广告栏位
  12. GPRS附着,PDP激活失败
  13. CentOS下MySQL安装失败,报socket '/tmp/mysql.sock错误解决方法
  14. 正则表达式隐藏手机号、身份证号、台胞证、护照、回乡证中间几位数字信息
  15. php微信公众号报警,微信报警函数定义与用法汇总
  16. java课程设计之球球大作战
  17. 天勤python_天勤量化策略库:网格交易策略(难度:中级)
  18. linux ps命令 详细介绍
  19. Web的相关概念及BC、CS结构
  20. Ghost硬盘对拷图解教程(双硬盘克隆)

热门文章

  1. matlab psb,基于Matlab_PSB的电路仿真分析
  2. 解决方案:Linux Ubuntu16.04 下无法挂载大容量U盘,优盘打不开
  3. eSpeak与ekho交叉编译
  4. Android Broadcast用法
  5. 联想收购诺基亚?玩笑而已别太当真
  6. 整车模型系列之变速器模型的建立
  7. linux语言是什么意思,linux怎么读(linux什么意思,准确发音是什么)
  8. Buck电路基本介绍
  9. 深入理解文件操作——纯C(2)
  10. unity3d 摄像机近地面清楚,远地面模糊