视觉SLAM前端——直接法

2023-10-29

目录
  1. 直接法介绍
  2. 单层直接法
  3. 多层直接法
直接法介绍

  这里所说的直接法不同于上一篇所说的LK光流法,LK光流是首先要选取特征点,如何利用特征点附近的光流进行路标的追踪与位姿的求解的,其本质还是先得到匹配好的点对然后利用点对进行位姿估计。而直接法不同,其核心思想是直接优化位姿 R , t R,t R,t
直接法也需要两个假设:

1.灰度不变假设,即 I 1 ( u , v ) = I 2 ( u + Δ x , v + Δ y ) I_1(u,v)=I_2(u+\Delta x,v+\Delta y) I1(u,v)=I2(u+Δx,v+Δy)
2.小范围灰度不变假设,即存在 W × W W×W W×W的窗口,使 I 1 ( u + w , v + w ) = I 2 ( u + Δ x + w , v + Δ y + w ) I_1(u+w,v+w)=I_2(u+\Delta x+w,v+\Delta y+w) I1(u+w,v+w)=I2(u+Δx+w,v+Δy+w)

  同LK光流法一样,假设1是基础,假设2则是为了构建最小二乘法。
基本理论如下

  同LK相比,直接法舍弃了 Δ x , Δ y \Delta x,\Delta y Δx,Δy作为优化变量,而是直接利用变换矩阵(位姿) T T T作为优化变量,因为相邻图像上的关键点坐标的移动本质是因为相机在运动。这里理论不会太过详细,后期会单独写SLAM的理论。

  设 P P P点的像素坐标为 ( u , v ) (u,v) (u,v),相机坐标为 ( X , Y , Z ) (X,Y,Z) (X,Y,Z)。在已知路标的深度,相机内参的情况下,利用简单的缩放与平移可得到相机坐标。
在这里插入图片描述
  如上图所示,图1中的 p ( u , v ) p(u,v) p(u,v)变换到了图2后,坐标为 p ′ = K T P p^\prime=KTP p=KTP,其中 K K K是相机内参矩阵,是已知的,基于前面的假设,可以得到以下等式:

e r r o r = I 1 ( u , v ) − I 2 ( K T P ) error=I_1(u,v)-I_2(KTP) error=I1(u,v)I2(KTP),这个形式不规范,能理解就行。

C o s t = ∑ j = 1 w ∑ i = 1 w I 1 ( u + w , v + w ) − I 2 ( K T P + W ) Cost=\sum_{j=1}^{w}\sum_{i=1}^{w}I_1(u+w,v+w)-I_2(KTP+W) Cost=j=1wi=1wI1(u+w,v+w)I2(KTP+W)

   同理,等式1基于假设1,等式2基于假设2。至于求导过程可参考《视觉SLAM十四讲》,这里也不做过多介绍。

单层直接法

直接上代码,里面有注解与注意事项。

void JacobianAccumulator::accumulate_jacobian(const cv::Range &range) {
	int max_patch_size = 1;  //w的大小
	int cnt_good = 0; 

	Matrix6d hession = Matrix6d::Zero(); //H矩阵
	Vector6d bias = Vector6d::Zero();  //b矩阵

	//double lastcost = 0;
	double cost_tmp = 0;
	//前提是进行坐标变换,图1的像素坐标系到归一化坐标系
	
	for (int i = range.start; i < range.end; i++)
	{
		
		Eigen::Vector3d point_ref = depth_ref[i]*  Eigen::Vector3d((pix_ref[i][0] - cx) / fx, (pix_ref[i][1] - cy) / fy, 1);
		//变换到P(X,Y,Z)
		Eigen::Vector3d point_cur = T * point_ref;
		//T*P

		//检查d是否存在
		if (point_cur[2]<0)
			continue;

		double X = point_cur[0]; 
		double Y = point_cur[1];
		double Z = point_cur[2]; 
		//以上得到了位姿变换后相机坐标

		double Z2 = Z * Z, Z_inv = 1.0 / Z, Z2_inv = Z_inv * Z_inv;

		float u = fx * X / Z + cx;
		float v = fy * Y / Z + cy;
		//得到了位姿变换后的像素坐标(u,v)

		//检查变换后的坐标是否越界
		if (u<max_patch_size || u>img_estimate.cols - max_patch_size ||
			v<max_patch_size || v>img_estimate.rows - max_patch_size)
			continue;
		projection[i] = Eigen::Vector2d(u, v);

		cnt_good++;			
	
		for (int x = -max_patch_size; x <= max_patch_size; x++)
		
			for  (int y = -max_patch_size;  y <= max_patch_size;  y++)
			{
				//开始计算error和j矩阵
				float error = GetPixelValue(img, pix_ref[i][0] + x, pix_ref[i][1] + y) - GetPixelValue(img_estimate, u + x, v + y);
				
                 //开始计算导数
				matrix26d J_pixel_xi;
				Eigen::Vector2d J_img_pixel;
				J_pixel_xi(0, 0) = fx * Z_inv;
				J_pixel_xi(0, 1) = 0;
				J_pixel_xi(0, 2) = -fx * X * Z2_inv;
				J_pixel_xi(0, 3) = -fx * X * Y * Z2_inv;
				J_pixel_xi(0, 4) = fx + fx * X * X * Z2_inv;
				J_pixel_xi(0, 5) = -fx * Y * Z_inv;

				J_pixel_xi(1, 0) = 0;
				J_pixel_xi(1, 1) = fy * Z_inv;
				J_pixel_xi(1, 2) = -fy * Y * Z2_inv;
				J_pixel_xi(1, 3) = -fy - fy * Y * Y * Z2_inv;
				J_pixel_xi(1, 4) = fy * X * Y * Z2_inv;
				J_pixel_xi(1, 5) = fy * X * Z_inv;

				J_img_pixel = Eigen::Vector2d(
					0.5*(GetPixelValue(img_estimate, u + x + 1, v + y) - GetPixelValue(img_estimate, u + x - 1, v + y)),
					0.5*(GetPixelValue(img_estimate, u + x, v + y + 1) - GetPixelValue(img_estimate, u + x, v + y - 1))
				);
				Vector6d J = -1.0*(J_img_pixel.transpose()*J_pixel_xi).transpose();


				hession += J * J.transpose();
				bias += -error * J;
				cost_tmp += error * error;

			}	

	}

	if (cnt_good) {
		// set hessian, bias and cost
		unique_lock<mutex> lck(hession_mutex);
		H += hession;
		b += bias;
		cost += (cost_tmp / cnt_good);
	}	
}

接下来是Gauss-Newton的主体部分

for (int iter = 0; iter < iteration; iter++)
	{
		jacpbian.reset();  //初始化

		//求解方程
		cv::parallel_for_(cv::Range(0, pixels_ref.size()),
			std::bind(&JacobianAccumulator::accumulate_jacobian, &jacpbian, std::placeholders::_1));  //并行求解

		Matrix6d H = jacpbian.hession();
		Vector6d b = jacpbian.bias();
		double lastcost = 0, cost = 0;

		Vector6d update = H.ldlt().solve(b);
		T = Sophus::SE3d::exp(update)*T;
		cost = jacpbian.cost_function();
		//接下来考虑三个方面:更新量是否为无穷,增量方向,收敛误差
		
		if (std::isnan(update[0]))
		{
			cout << "更新量为无穷" << endl;
			break;
		}

		if (lastcost>cost)
		{
			cout << "增量方向错误" << endl;
			break;
		}

		if (update.norm()<1e-3)
		{
			break;
		}
		lastcost = cost;
		cout << "iteration: " << iter << ", cost: " << cost << endl;
	}

注意
  要创建一个accumulate_jacobian类,这个类主要是用于计算导数,而高斯牛顿法在外部的函数中调用该类完成,体现了面向对象的编程思维。

多层直接法

  多层直接法的核心是金字塔,分为三部分:

  • 定义金字塔
  • 创建金字塔
  • 求解

代码如下:

void multy_derect_method(
	const Mat & img1,
	const Mat & imag_estimate,
	const Vecvector2d & px_ref,
	const vector<double>& depth_ref,
	Sophus::SE3d & T) {

	//金字塔方法的步骤,1.定义金字塔参数,2.创建金字塔(图像+参数) 3.调用单层算法

	//1.定义基本参数:层数,尺度,尺度数组
	int pyramid_size = 4;
	double scal = 0.5;
	double pyramid_scals[] = { 1,0.5,0.25,0.125 };


	//2.创建金字塔
	vector<Mat> pyr_img, pyr_imgestimate;

	for (int i = 0; i < pyramid_size; i++)
	{
		if (i == 0)
		{
			pyr_img.push_back(img1);
			pyr_imgestimate.push_back(imag_estimate);
		}
		else
		{
			Mat img_tmp, imgestimate_tmp;

			cv::resize(pyr_img[i - 1], img_tmp,
				cv::Size(pyr_img[i - 1].cols*scal, pyr_img[i - 1].rows*scal));
			cv::resize(pyr_imgestimate[i - 1], imgestimate_tmp,
				cv::Size(pyr_imgestimate[i - 1].cols*scal, pyr_imgestimate[i - 1].rows*scal));
			pyr_img.push_back(img_tmp);
			pyr_imgestimate.push_back(imgestimate_tmp);

		}//以上为图像金字塔
	}

	double fxG = fx, fyG = fy, cxG = cx, cyG = cy;
	for (int level = pyramid_size - 1; level >= 0; level--)
	{
		fx = fxG * pyramid_scals[level];
		fy = fyG * pyramid_scals[level];
		cx = cxG * pyramid_scals[level];
		cy = cyG * pyramid_scals[level];
		Vecvector2d px_pyr_ref;
		for (auto &kp : px_ref)
		{		
			px_pyr_ref.push_back(pyramid_scals[level] * kp);	
		}
		single_derect_method(pyr_img[level], pyr_imgestimate[level], px_pyr_ref, depth_ref, T);
	}
}

注意:
1.金字塔的层数是从第0层开始的,这样比较符合编程的思维。
2.金字塔不仅仅指图像,只要是需要缩放的各类参数都要建立金字塔。
3.层与层直接的参数传递主要是位姿矩阵T,T传入的是引用,不需要初始化,定义时默认初始化为单位矩阵,每层求解后T均被保存下来。

效果如下

单层法
在这里插入图片描述
多层法
在这里插入图片描述
  实际上,理论与代码的实现有着巨大的鸿沟,代码中还有很多细节部分没有理解清楚。加油!

参考文献:
[1]《视觉SLAM十四讲》 高翔

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

视觉SLAM前端——直接法 的相关文章

  • Django(9)-表单处理

    django支持使用类创建表单实例 polls forms py from django import forms class NameForm forms Form your name forms CharField label Your

随机推荐

  • 面试准备:Mybatis常见面试题汇总

    文章目录 1 和 的区别是什么 2 当实体类中的属性名和表中的字段名不一样 怎么办 3 模糊查询like语句该怎么写 4 Mybatis 一对一 一对多的xml怎么写 5 Dao 接口的工作原理是什么 Dao 接口里的方法 参数不同时 方法
  • Python中函数,类,模块,包,库的区别

    文章目录 关系图 参考文章 关系图 参考文章 借鉴了以下文章 Python中函数 类 模块 包 库的区别 一分钟带你分清Python的模块 包和库的区别 python中的模块 库 包有什么区别
  • c# 保存软件配置

    保存配置方法 一 Settings setting 文件 1 1 配置Settings settings文件 1 2 加载配置信息 1 3 保存配置信息 二 使用文本保存 2 1 引入命名空间 2 2 新增IniConfigHelper 类
  • 装win10提示“在EFI系统上,Windows只能安装到GPT磁盘”

    在安装界面 按 shift F10 键 在命令提示符窗口依次执行如下命令 输入 diskpart 命令后 按enter键 进入到 DISKPART 模式 输入 list disk 命令后 按enter键 查看电脑的硬盘 编号0 表示电脑的第
  • 【python文本分析】——基于股评文本的情绪分析

    目录 一 文本处理 1 精确模式 默认 2 全模式 3 搜索引擎模式 二 词云图 1 wordcloud模块导入 2 词云图实现 三 实例 利用股评进行情绪分析 1 数据来源及snownlp模块导入 2 代码实现 2 1 读取股评文件 2
  • windows 10 安装子系统(WSL2)

    以前在学习docker时 是在自己的虚拟机上进行的 最近刚换了电脑 想在windows中使用子系统来运行docker 现在WSL2要比以前的WSL1运行更快 io操作方面的很大的提升 在这里记录一下我的安装过程吧 希望小白们有些参考 关注微
  • 【使用mindspore复现segmenter语义分割算法时,loss一直在一个范围内附近波动,降不下去】

    使用mindspore复现segmenter语义分割算法 操作步骤 问题现象 1 即使训练了很多个epoch 精度一直下降不了 一直在1 2左右 测试出来的miou也是一个非常低的值 例如0 0198 2 目前尝试过不同的优化器SGD AD
  • 腾讯mini项目-【指标监控服务重构】2023-07-27

    今日已办 SigNoz Log Management SigNoz原生支持 OpenTelemetry 来收集日志 SigNoz 在收集器端进行了优化 为SigNoz中的日志添加了不同的功能 OpenTelemetry 提供了各种接收器和处
  • 合并两个有序数组

    给定两个有序整数数组 nums1 和 nums2 将 nums2 合并到 nums1 中 使得 num1 成为一个有序数组 说明 初始化 nums1 和 nums2 的元素数量分别为 m 和 n 你可以假设 nums1 有足够的空间 空间大
  • es6 class类与class类中constructor

    序言 在es6 中的class关键字用于声明类 在此之前js一直没有类的概念 本文只要讨论class的与es5中对象的关系以及class中constructor的作用 关键字class ES6 的class可以看作只是一个语法糖 而类本身可
  • Android颜色透明度十六进制表示法

    Android开发中经常会用到色值的透明度 比如要70 透明或者30 透明 这时候就有点犯难了 要么Google 要么借助PS等工具 其实都比较麻烦 下面将把0到100的透明度按照5 的梯度列出 方便收藏使用 其实在开发的过程中我们会经常遇
  • php字段验证规则,ThinkPHP 自动验证及验证规则详解

    ThinkPHP 自动验证及验证规则详解 ThinkPHP 自动验证 ThinkPHP 内置了数据对象的自动验证功能来完成模型的业务规则验证 自动验证是基于数据对象的 而大多情况下数据对象是基于 POST表单 不是绝对的 创建的 基本的自动
  • scrapy POST方式抓取走过的坑

    背景 今天老板让核查新上线的app中的中标数据展示情况 一条一条数据点开看实在是太慢了 于是想抓包获取app请求的api接口以及传入的参数 获取返回的数据内容 将数据存储到sqlite3中直接通过执行sql来统计数据质量 先打开fiddle
  • XSS quiz 1~5解题方案

    第1题 第一题很简单 没做过滤 直接可A过 第二题 查询框中写123查看源码 需要先闭合左边的input 所以 gt 即可 第三题 本题有过滤当输入 gt 时发现引号 尖括号都被过滤 lt gt 分别变成了转义符 尝试Unicode编码也未
  • antd Table中显示图片

  • qt 里面使用webengine

    qt使用webengine 条件 qt在windows上使用webengine必须用visual studio 使用mingw无效 webengine可以集成我们得html5页面 这样可以让界面开发人员更加省心 code 1 包含qwebe
  • YouTube上的版权保护

    早在2007年的时候 我曾写过一篇名为 YouTube The Big Copyright Lie YouTube 关于版权的弥天大谎 的文章 表达了我对YouTube又爱又恨的情感纠结 现在回想一下你在YouTube上看过的所有视频 它们
  • TabLayout+ViewPager+Fragment自定义tab添加小红点(kotlin事例)

    首先看哈效果 下面是两个布局 一个主布局 一个tab的布局 主布局很简单tablayout viewpager
  • Java制作JDK8文档搜索引擎项目并部署到阿里云服务器

    背景介绍 对于一个网站来说 搜索引擎需要提前预备好很多很多的静态资源 当用户输入查询的关键词的时候根据这些关键词来模糊查询匹配对应的资源 然后将这些资源展示给用户即可 搜索核心思路 互联网上主要是依赖于爬虫程序 它们可以极大效率的利用互联网
  • 视觉SLAM前端——直接法

    目录 直接法介绍 单层直接法 多层直接法 直接法介绍 这里所说的直接法不同于上一篇所说的LK光流法 LK光流是首先要选取特征点 如何利用特征点附近的光流进行路标的追踪与位姿的求解的 其本质还是先得到匹配好的点对 然后利用点对进行位姿估计 而