视觉SLAM前端——LK光流法
目录:
- LK光流介绍
- 单层LK光流
- 多层LK光流
LK光流
LK光流是一种描述图像运动的方法,利用LK光流可以实现对图像的追踪,从而求解图像运动的位姿。其基本思想如下:
img1,img2分别为两张已知的图像,相机在运动过程中,img1中的特征点P(u,v)P(u,v)P(u,v)经过变换后得到了img2中的P′(u+Δx,v+Δy)P^\prime(u+\Delta x,v+\Delta y)P′(u+Δx,v+Δy)。
LK光流法求解相机位姿的基本问题可以描述为:已知PPP点,求解最佳的Δx,Δy\Delta x,\Delta yΔx,Δy,从而得到img2中的P′P^\primeP′坐标。因此便求得一对匹配点P−P′P-P^\primeP−P′。在求得多对匹配点的坐标后,整个位姿估计问题可以转换为ICPICPICP或PnPPnPPnP问题。
为了求解最佳的Δx,Δy\Delta x,\Delta yΔx,Δy,通常采用最小二乘法。在LK光流中有两条非常重要的假设:
1.灰度不变假设,即I(u,v)=I(u+Δx,v+Δy)I(u,v)=I(u+\Delta x,v+\Delta y)I(u,v)=I(u+Δx,v+Δy)
2.小范围灰度不变假设,即存在W×WW×WW×W的窗口,使I(u+w,v+w)=I(u+Δx+w,v+Δy+w)I(u+w,v+w)=I(u+\Delta x+w,v+\Delta y+w)I(u+w,v+w)=I(u+Δx+w,v+Δy+w)
假设1是基础,假设2则是为了构建最小二乘解。
单层光流法
求解的基本流程如下:
- 特征点的选取
- Gauss-Newton法求解Δx,Δy\Delta x,\Delta yΔx,Δy
1.特征点的选取
关于特征点的选取,笔者前面写了一篇文章进行了详细介绍,这里不在赘述。详情可点击此处.
vector<KeyPoint> kp1;vector<KeyPoint> kp2_single;Ptr<GFTTDetector> detector = cv::GFTTDetector::create(500, 0.01, 20);detector->detect(img1, kp1);
这里使用的是GFFT特征。
2.Gauss-Newton求解Δx,Δy\Delta x,\Delta yΔx,Δy
根据假设1有:error=I1(u,v)−I2(u+Δx,v+Δy)error=I_1(u,v)-I_2(u+\Delta x,v+\Delta y)error=I1(u,v)−I2(u+Δx,v+Δy),这是基于假设1所得到了,理论上error应该为0,由于噪声的存在,实际上不可能为0,因此只能求解最小二乘解。
根据假设2有Cost=∑j=1w∑i=1wI1(u+w,v+w)−I2(u+Δx+w,v+Δy+w)Cost=\sum_{j=1}^{w}\sum_{i=1}^{w}I_1(u+w_,v+w)-I_2(u+\Delta x+w,v+\Delta y+w)Cost=∑j=1w∑i=1wI1(u+w,v+w)−I2(u+Δx+w,v+Δy+w)。
导数JJJ是img2的像素梯度,也可以用img1的像素梯度代替,此时为反向光流法。具体形式可参考《视觉SLAM十四讲》。
代码如下:
void OpticalFlowTracker::calculateOpticalFlow(const Range &range) { //参数是一个cv::range的引用// parameters range是一个区间,应该看作一个窗口//第一步:初始化工作:定义全局变量int iteration = 15; //迭代次数int max_patch_size = 4; //w的大小for (int i = range.start; i < range.end; i++) //初始化变量工作{auto kp = kp1[i];double dx = 0, dy = 0;Eigen::Matrix2d H = Eigen::Matrix2d::Zero();Eigen::Vector2d b = Eigen::Vector2d::Zero();Eigen::Vector2d J;double cost = 0;double lastcost = 0;bool succ = true;if (has_initial) //金字塔的核心{dx = kp2[i].pt.x - kp.pt.x;dy = kp2[i].pt.y - kp.pt.y; //这个值的加减顺序不能变,} //初始化或者不初始化都可以//第二部:计算e,cost,h,b,jfor (int iter = 0; iter < iteration; iter++){if (inverse == false) {H = Eigen::Matrix2d::Zero();b = Eigen::Vector2d::Zero();}else {// only reset bb = Eigen::Vector2d::Zero();}cost = 0;// 两个for循环为w窗口的迭代for (int x = -max_patch_size; x < max_patch_size; x++){for (int y = -max_patch_size; y < max_patch_size; y++){double error = GetPixelValue(img1, kp.pt.x + x, kp.pt.y + y) -GetPixelValue(img2, kp.pt.x + x + dx, kp.pt.y + y + dy);;if (inverse == false) //inverse是反向的意思{J = -1.0 * Eigen::Vector2d( //正向梯度,img20.5*(GetPixelValue(img2, kp.pt.x + x + dx + 1, kp.pt.y + y + dy) -GetPixelValue(img2, kp.pt.x + x + dx - 1, kp.pt.y + y + dy)),0.5*(GetPixelValue(img2, kp.pt.x + x + dx, kp.pt.y + y + dy + 1) -GetPixelValue(img2, kp.pt.x + dx + x, kp.pt.y + dy + y - 1)));}else if (iter == 0){J = -1.0 * Eigen::Vector2d( //反向梯度,img10.5*(GetPixelValue(img1, kp.pt.x + x + dx + 1, kp.pt.y + y + dy) -GetPixelValue(img1, kp.pt.x + x + dx - 1, kp.pt.y + y + dy)),0.5*(GetPixelValue(img1, kp.pt.x + x + dx, kp.pt.y + y + dy + 1) -GetPixelValue(img1, kp.pt.x + dx + x, kp.pt.y + dy + y - 1)));}cost += error * error;b += -J * error;if (iter == 0 || inverse == false){H += J * J.transpose();}}}//第三部分:第i次迭代完毕,开始求解/Eigen::Vector2d update = H.ldlt().solve(b);//求解后不要急于迭代,先看看结果是否正确if (std::isnan(update[0])) {// sometimes occurred when we have a black or white patch and H is irreversiblecout << "update is nan" << endl;//判断是不是为无穷大,succ = false;break;}if (iter > 0 && cost > lastcost) { //判断损失函数的增长方向break;}lastcost = cost;dx += update(0, 0);dy += update(1, 0);succ = true;//这一句放在这可以保证每次迭代都不出问题if (update.norm() < 1e-2) {// convergebreak;}}success[i] = succ;//把特征点放到kp2里面 注意:keypoint.pt返回的是Point2f而不是2d;kp2[i].pt = kp.pt + Point2f(dx, dy); //把结果返回}
}
多层光流法
多层光流法的基本思想是在单层的基础上,多次运用单层光流法,同时保存每层的求解结果并作为下一层的初始值。这样可以逐层求解,得到比较实用的光流法。其中,最关键的工具应该是金字塔。
图像金字塔其实是在原图像的基础上,对图像进行缩放,当然,各参数也要进行相应的缩放。一般顶层图像最粗糙,底层为原图,求解过程由上至下,求解结果逐步精确。多层LK光流法的流程如下:
- 定义金字塔:一般是定义容器
- 创建金字塔:一般是利用循环以及resize函数,给容器赋值
- 求解:逐层调用单层的求解算法,但要注意各层之间的结果是如何传递的。
代码如下:
void OpticalFlowMultiLevel(const Mat &img1,const Mat &img2,const vector<KeyPoint> &kp1,vector<KeyPoint> &kp2,vector<bool> &success,bool inverse) {//定义全局参数int pyramid_size = 4;double pyramid_scal[] = { 1.0,0.5,0.25,0.125 };double scal = 0.5;//创建金字塔:第0层为底层,第3层为顶层vector<Mat> img1_pyramid, img2_pyramid;for (int i = 0; i < pyramid_size; i++){if (i == 0) //第0层为原图{img1_pyramid.push_back(img1);img2_pyramid.push_back(img2);}else{Mat pyramid1, pyramid2; //利用好resize函数resize(img1_pyramid[i - 1], pyramid1,cv::Size(img1_pyramid[i - 1].cols*scal, img1_pyramid[i - 1].rows*scal));img1_pyramid.push_back(pyramid1);resize(img2_pyramid[i - 1], pyramid2,cv::Size(img2_pyramid[i - 1].cols*scal, img2_pyramid[i - 1].rows*scal));img2_pyramid.push_back(pyramid2); //图像金字塔仅仅作为导数的计算用}}//同理,坐标也要创建为金字塔vector<KeyPoint> kp1_pyramid, kp2_pyramid;for (auto kp_top : kp1){kp_top.pt *= pyramid_scal[pyramid_size - 1];kp1_pyramid.push_back(kp_top);kp2_pyramid.push_back(kp_top);}//开始迭代了for (int i = pyramid_size - 1; i >= 0; i--){success.clear();OpticalFlowSingleLevel(img1_pyramid[i], img2_pyramid[i], kp1_pyramid, kp2_pyramid, success, inverse, true);if (i > 0){for (auto &kp : kp1_pyramid) {kp.pt /= scal;}for (auto &kp : kp2_pyramid) {kp.pt /= scal;}}}//迭代完毕后的kp2是最需要的for (auto &kp_end : kp2_pyramid) { kp2.push_back(kp_end); };
}
注意:
1.坐标的修正
if (i > 0)
{
for (auto &kp : kp1_pyramid) {
kp.pt /= scal;
}
for (auto &kp : kp2_pyramid) {
kp.pt /= scal;
}
这里是利用参数的引用间接地修改了关键点的坐标,其实kp1_pyramid是没有变化的,只是对原坐标缩放,关键是kp2_pyramid,kp2_pyramid是求解得到的特征点。
从顶层到底层,与kp2_pyramidkp1_pyramid的匹配程度越来越高,在每层求解后均需要进行缩放,以便于下一层使用。
2.求解结果的传递
OpticalFlowSingleLevel(img1_pyramid[i], img2_pyramid[i], kp1_pyramid, kp2_pyramid, success, inverse, true);
认真理解 true 这个参数,我们翻到单层求解的算法中,true对应的参数是has_initial,相应的功能是改变dx,dy的初始化方式,前面说过,LK光流法的关键是求解dx,dy。而多层LK光流是步骤得到好的dx,dy初始值。
当has_initial=true时:
dx = kp2[i].pt.x - kp.pt.x;
dy = kp2[i].pt.y - kp.pt.y;
kp2是上一层求解的结果,经过坐标缩放后,用于初始化dx,dy,这也说明了上层求解得到的kp2是如何在下一层被利用的。
当has_initial=false时:
dx=0,dy=0。此时为单层求解
因此,金字塔的核心是关于被优化变量的初始化方式。思想是:每层初始化越接近真实值越好。
参考文献:
[1]《视觉SLAM十四讲》高翔
视觉SLAM前端——LK光流法相关推荐
- SLAM - 视觉里程计 - LK光流法
CMakeLists.txt: cmake_minimum_required( VERSION 2.8 ) project( 项目名 )set( CMAKE_BUILD_TYPE Release ) ...
- OpenCV Using Python——基于SURF特征提取和金字塔LK光流法的单目视觉三维重建 (光流、场景流)...
https://blog.csdn.net/shadow_guo/article/details/44312691 基于SURF特征提取和金字塔LK光流法的单目视觉三维重建 1. 单目视觉三维重建问题 ...
- OpenCV3学习(11.2)LK光流法原理及opencv实现
光流的概念:(Optical flow or optic flow) 它是一种运动模式,这种运动模式指的是一个物体.表面.边缘在一个视角下由一个观察者(比如眼睛.摄像头等)和背景之间形成的明显移动.光 ...
- LK光流法与反向LK光流法
文章目录 一.基本概念 二.2D中的LK光流法 1.空间点在图像中的灰度表示 2.2D中的LK光流法推导 3.将2D光流法抽象成超定方程问题 4.超定线性方程的最小二乘最优解定理证明 5.将2D光流法 ...
- 图像金字塔LK光流法原理分析
图像金字塔LK光流法原理分析 1.LK光流法原理分析 2.基于图像金字塔的LK光流法原理分析 本篇博客只讲述原理,c++代码实现请参考博客< 基于金字塔LK的光流法实现-根据论文自己实现的c++ ...
- OpenCV:金字塔LK光流法
金字塔LK光流法的三个假设 亮度恒定,即图像场景中目标的像素在帧间运动时外观上保持不变: 时间连续或者运动是"小运动",即图像的运动随时间的变化比较缓慢: 空间一致,即一个场景中同 ...
- 详解LK光流法(含金字塔多层光流),反向光流法(附代码)
LK光流法可用来跟踪特征点的位置. 比如在img1中的特征点,由于相机或物体的运动,在img2中来到了不同的位置.后面会称img1为Template(T),img2为I. 光流法有个假设: 灰度不变假 ...
- 【老生谈算法】matlab实现金字塔LK光流法源码——金字塔LK光流法
基于金字塔LK光流法的MATLAB代码 1.原文下载: 本算法原文如下,有需要的朋友可以点击进行下载 序号 原文(点击下载) 本项目原文 [老生谈算法]基于金字塔LK光流法的MATLAB代码.docx ...
- 视觉SLAM前端特征检测与跟踪的思考
点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 就目前视觉SLAM的引用来区分,分为基于特征法的和直接法的视觉SL ...
最新文章
- 「Python」一文读懂装饰器
- 反射型 DDoS 攻击的原理和防范措施
- Python爬虫应用实战-网站数据爬取及数据分析
- 获取程序所有加载的dll名称
- 【X264系列】之命令参数解析
- 转!面向对象设计原则
- php rewrite重写,yaf 自定义重写路由rewrite
- java 回文素数_java实现回文质数
- 数学建模matlab视频教程,matlab编程教程_求matlab视频教程,主要用于数学建模方面的...
- devexpress,dotnetbar控件
- \u5168\u56fd\u7f8e\u5bb9\u5927\u592b数据采集数据(\u82b1\u5bb9\u7f51 huaroo 公开数据),爬虫120例之26例
- 这是啥SQL,室友看了人傻了
- 'rm' 不是内部或外部命令,也不是可运行的程序 或批处理文件。
- 鲸探APP处罚60余位转售数字藏品用户 | 产业区块链发展周报
- Aspose.Cells使用教程:使用 .NET 在 Linux 上创建或编辑 Excel 文件
- 【探花交友】用户登录总结
- 大数据未来发展趋势,主要取决于这八个要素
- 清除电脑各种使用记录不留痕迹,保护你的隐私!
- Python数据分析案例14——文本计算TF-IDF值和LDA主题模型
- Linux Shell 通配符、元字符、转义符使用实例介绍--Learning the korn shell
热门文章
- 商圈热点事件:极智嘉拟科创板上市、小鹅通D轮融资……
- Python数据分析-笔记01
- JVM内存模型(摘抄至五月的仓颉的博客)
- 计算机-IEEE ACCESS-论文投稿上岸经验分享
- 我的世界模组-minecraft fpv
- 安全帽识别与火焰识别系统功能应用
- 家庭媒体中心解决方案(四、	群晖系列nas基本功能使用指南篇2)
- 法雷奥:进军L4市场,带来比航空标准更加安全的自动驾驶车 | CES 2019
- 计算机专业退休有退休金,那些专业有退休金 哪个行业的退休金最多?
- p50, p90, p99 (pct 50, pct 90, pct 99)指什么?