从程序中学习UKF-SLAM(一)

然间从GitHub上发现一个小项目,做的是一个基于UKF的状态估计,使用C++版本的mathplot做显示,编译之后却发现显示一直有问题,于是萌生了一个想法:能不能把它移植到ROS中?然后就有了接下来的学习过程,以此记录。如有问题,欢迎提出。

这是需要复写的代码:在GitHub上(project的核心代码是来自源码的,如有侵权,请联系我更改,谢谢。)
源码最终实现的效果如下:
在这里插入图片描述

同样也可参考UKF最初代码:GitHub_Self-Driving Car
同样也有相关博客对该代码进行了解析,仅供参考:UKF算法解析

复写完成的代码请看最后一篇博客。
在ROS中实现的效果如下:百度网盘视频或者YouTube视频
在这里插入图片描述

一、前提说明

首先解释一下,路标数据,里程计数据,激光数据各自的组成部分,以及project涉及的运动模型。

1.landmark路标

在程序中(在ukf_console.h的51行),用一个vector表示:

std::vector<Eigen::Vector3f> Landmarks;	

各表示点的id号,点的x坐标以及y坐标。此处说明路标具有id标识的,也就决定了后续过程中数据关联的方式。
在param/world_data.txt中,写明了各个landmark的详细信息:

1 2 1
2 0 4
3 2 7
4 9 2
5 10 5
6 9 8
7 5 5
8 5 3
9 5 9

在程序中直接用C++的文件读取方法,将landmark信息读入vector数组中:

bool ukf_console::getLandmarks(const std::string& path)
{
	std::ifstream in_file(path, std::ifstream::in);
	if (!in_file.is_open()) 
	{
		std::cerr << "Cannot open input file: " << (path) << std::endl;
		exit(EXIT_FAILURE);
	}

	std::string line;
	while(std::getline(in_file, line)) 
	{
		std::istringstream ss(line);
		Eigen::Vector3f mp;
		ss>>mp(0);
		ss>>mp(1);
		ss>>mp(2);
		Landmarks.push_back(mp);
		if (Debug)
			std::cout << (Landmarks.back())(0) << ": " << (Landmarks.back())(1)  << ": " << (Landmarks.back())(2)  << std::endl;
	}

	if (in_file.is_open()) 
		in_file.close();
	
}
2.观测信息

这里的观测信息包括里程计信息和激光信息,在tool.h中定义了一组观测信息的格式:

struct Record {
		Eigen::Vector3f odom;					//里程计数据: 起点夹角, 行走距离, 终点夹角
		std::vector<Eigen::Vector3f> radar;		//激光数据:路标id号, 射线距离, 射线角度
	};

在实际project中,会有多组观测信息,所以这里用一个vector提前保存了所有数据:

std::vector<Record> Measurements;

首先解释一下里程计数据的格式,其中的起点夹角和终点夹角是指 d θ t + 1 ′ d\theta _{t+1}^{&#x27;} dθt+1 d θ t + 1 d\theta _{t+1} dθt+1,行走距离应该是直线距离 R R R。(可能这里的角度关系还是有点理解不对,,,)
在这里插入图片描述

激光数据主要是关于landmark的观测信息,这里出了观测距离ranges和观测角度 φ \varphi φ,还有路标的特殊id代号。激光观测原理如下

在这里插入图片描述
实际的观测数据由param/sensor_data.txt文件读入,某一项数据格式如下:

ODOMETRY 0.100692392654 0.100072845247 0.000171392857486
SENSOR 1 1.89645381418 0.374031885671
SENSOR 2 3.85367751107 1.51951017943

同样,用C++的文件读取方法将信息读入vector中:

bool ukf_console::getMeasurements(const std::string& path)
{
	std::ifstream in_file(path, std::ifstream::in);
	if (!in_file.is_open()) {
		std::cerr << "Cannot open input file: " << path << std::endl;
		exit(EXIT_FAILURE);
	}

	std::string line;
	Record record;
	int index = 0;
	while(getline(in_file, line)) 
	{
		std::string sensor_type;
		std::istringstream ss(line);
		ss >> sensor_type;
		//measurement type r1 t r2
		if (sensor_type.compare("ODOMETRY") == 0) 
		{//end the first record;
			if (record.radar.size() != 0) 
			{
				Measurements.push_back(record);
				record.radar.clear();
				if (Debug && index < 50)
					std::cout << index << "-----------" << std::endl;
				index++;
			}
			auto& odo = record.odom;
			ss >> odo(0);
			ss >> odo(1);
			ss >> odo(2);
			if (Debug && index < 50)
				std::cout << (record.odom)(0) << ": " << (record.odom)(1) << ": " << (record.odom)(2) << std::endl;
		} else if (sensor_type.compare("SENSOR") == 0) 
		{
			auto& radars = record.radar;
			Eigen::Vector3f radarR;
			ss >> radarR(0);
			ss >> radarR(1);
			ss >> radarR(2);
			radars.push_back(radarR);
			if (Debug && index < 50)
				std::cout << (radars.back())(0) << ": " << (radars.back())(1) << ": " << (radars.back())(2) << std::endl;
		}
	}
	if (record.radar.size() != 0) 
		Measurements.push_back(record);
	
	if (in_file.is_open()) 
		in_file.close();			
}
3.运动模型

项目中的小车可以认为是两轮差动运动模型,如上图所示,即很容易就能得出下面的运行模型:

{ x t + 1 = x t + R ⋅ cos ⁡ ( θ t + Δ θ ) y t + 1 = y t + R ⋅ sin ⁡ ( θ t + Δ θ ) θ t + 1 = θ t + Δ θ + Δ θ ˊ \left\{ \begin{array}{c} x_{t+1}=x_t+R\cdot \cos \left( \theta _t+\varDelta \theta \right)\\ y_{t+1}=y_t+R\cdot \sin \left( \theta _t+\varDelta \theta \right)\\ {\theta _{t+1}=\theta _t+\varDelta \theta +\varDelta \acute{\theta}}\\ \end{array} \right. xt+1=xt+Rcos(θt+Δθ)yt+1=yt+Rsin(θt+Δθ)θt+1=θt+Δθ+Δθˊ

在程序(unscented_kf.cpp的90-93)中,这样实现:

double angle = Xsig(2,i);
Xsig(0, i) = Xsig(0, i) + t * cos(angle + r1);
Xsig(1, i) = Xsig(1, i) + t * sin(angle + r1);
Xsig(2, i) = Xsig(2, i) + r1 + r2;

二、相关Markers的显示

喜欢使用ROS的一个很重要的原因就是它的可视化界面了,RVIZ里的各种marker可以显示很多丰富的信息,令程序调试十分的方便。

1.landmark以及pose的显示

这里采用圆柱体进行显示,直接看landmark的代码,estimatePose的不看了:

void ukf_console::LandmarksPublish()
{
	visualization_msgs::MarkerArray ma;
	visualization_msgs::Marker marker;
	// 设置该标记的命名空间和ID,ID应该是独一无二的
	// 具有相同命名空间和ID的标记将会覆盖前一个
	marker.header.frame_id = "map";
	marker.ns = "2Dlandmarks";
	// 设置标记行为:ADD(添 加),DELETE(删 除)
	marker.action = visualization_msgs::Marker::ADD;
	// 设置标记类型,初始值为球体。在立方体、球体、箭头和 圆柱体之间循环
	marker.type = visualization_msgs::Marker::CUBE;// 设置初始形状为圆柱体
	marker.id = 0;
	
	marker.pose.position.z = 0;
	marker.pose.orientation.x = 0.0;
	marker.pose.orientation.y = 0.0;
	marker.pose.orientation.z = 0.0;
	marker.pose.orientation.w = 1.0;
	// 设置标记的比例,所有方向上尺度1表示1米
	marker.scale.x = marker_scale;
	marker.scale.y = marker_scale;
	marker.scale.z = marker_scale;
	//设置标记颜色,确保alpha(不透明度)值不为0, 紫色
	marker.color.r = 160.0/255.0;
	marker.color.g = 32.0/255.0;
	marker.color.b = 240.0/255.0;
	marker.color.a = 1.0;
	marker.lifetime = ros::Duration(0);
	
	int markers_size = Landmarks.size();
	for(int i=0 ; i < markers_size ; i++)
	{
		// 设置帧 ID和时间戳
		marker.header.stamp = ros::Time::now();
		marker.id++;
		//设置标记位姿。 
		marker.pose.position.x = (Landmarks[i])(1);
		marker.pose.position.y = (Landmarks[i])(2);
		ma.markers.push_back(marker);
	}
	Landmarks_pub.publish(ma);
}
2.观测信息显示

这里采用的是线段类型LINE_STRIP进行显示,看代码:

void ukf_console::AssociationPublish(const std::vector< Eigen::Vector3f >& radars)
{
	// visualization of the data association
	visualization_msgs::MarkerArray ma;
	visualization_msgs::Marker line_strip;

	line_strip.header.frame_id = "map";
	line_strip.header.stamp = ros::Time::now();

	line_strip.id = 0;
	line_strip.ns = "data_association";
	line_strip.type = visualization_msgs::Marker::LINE_STRIP;
	line_strip.action = visualization_msgs::Marker::ADD;

	line_strip.lifetime = ros::Duration(0.1);
	line_strip.pose.position.x = 0;
	line_strip.pose.position.y = 0;
	line_strip.pose.position.z = 0;
	line_strip.pose.orientation.x = 0.0;
	line_strip.pose.orientation.y = 0.0;
	line_strip.pose.orientation.z = 0.0;
	line_strip.pose.orientation.w = 1.0;
	line_strip.scale.x = marker_scale*0.3;  // line uses only x component
	line_strip.scale.y = marker_scale*0.3;
	line_strip.scale.z = marker_scale*0.3;
	line_strip.color.a = 1.0;
	line_strip.color.r = 255/255;
	line_strip.color.g = 0;
	line_strip.color.b = 0;

	// robot pose		
	geometry_msgs::Point pointRobotPose;
	pointRobotPose.x = (ukf_tool->Mu_x)(0);
	pointRobotPose.y = (ukf_tool->Mu_x)(1);
	pointRobotPose.z = 0;
	
	//draw observation lines
	int visible_num = radars.size();
	for(int i = 0; i < visible_num; i++) 
	{
		int index = 0;
		for ( ; index < ukf_tool->LandmarkINmap.size() ; index++) 
		{
			if(ukf_tool->LandmarkINmap[index] == (int)(radars[i])(0)) 
				break;
		}
	
		line_strip.points.clear();
		line_strip.points.push_back(pointRobotPose);
		
		geometry_msgs::Point pointLm;
		pointLm.x = ukf_tool->Mu_x(2 * index + 3);
		pointLm.y = ukf_tool->Mu_x(2 * index + 4);
		pointLm.z = 0.0;
		line_strip.points.push_back(pointLm);
		ma.markers.push_back(line_strip);
		line_strip.id++;
	}   

	Assoc_pub.publish(ma);
}
3.误差的显示

这里对位姿估计的误差,landmark估计的误差都进行了显示,通常都是采用不确定椭圆进行显示,这里也不例外。首先需要对相应的协方差矩阵求解特征向量和特征值,在tool.cpp文件用下面这个函数实现了这样的功能:

bool tool::computeEllipseOrientationScale2D(Eigen::Matrix2d& eigenvectors, Eigen::Vector2d& eigenvalues, const Eigen::Matrix2d& covariance)
{
	eigenvectors.setZero(2, 2);
	eigenvalues.setIdentity(2, 1);
	
	Eigen::SelfAdjointEigenSolver<Eigen::Matrix2d> eigensolver(covariance);
	if (eigensolver.info() == Eigen::Success)
	{
		eigenvalues = eigensolver.eigenvalues();
		eigenvectors = eigensolver.eigenvectors();
	}else{
		eigenvalues = Eigen::Vector2d::Zero();  // Setting the scale to zero will hide it on the screen
		eigenvectors = Eigen::Matrix2d::Identity();
		return false;
	}
	
	// Be sure we have a right-handed orientation system
	makeRightHanded(eigenvectors, eigenvalues);
	return true;
}

另外,为了确保是右手坐标系(因为ROS系统中的tf坐标系是右手坐标系),这里需要进行一次检查:

void tool::makeRightHanded(Eigen::Matrix2d& eigenvectors, Eigen::Vector2d& eigenvalues)
{
	Eigen::Vector3d c0;
	c0.setZero();
	c0.head(2) = eigenvectors.col(0);
	c0.normalize();
	Eigen::Vector3d c1;
	c1.setZero();
	c1.head(2) = eigenvectors.col(1);
	c1.normalize();
	Eigen::Vector3d cc = c0.cross(c1);
	if (cc(2) < 0)
	{
		eigenvectors << c1.head(2), c0.head(2);
		double e = eigenvalues(0);
		eigenvalues(0) = eigenvalues(1);
		eigenvalues(1) = e;
	}else
		eigenvectors << c0.head(2), c1.head(2);
}

然后,就可以用协方差矩阵进行不确定性椭圆的绘制了,看代码:

void ukf_console::UncertaintyEllipseShow()
{
	visualization_msgs::MarkerArray ma;
	visualization_msgs::Marker marker;
	marker.header.frame_id = "map";
	marker.id = 0;
	marker.ns = "ekf_predict";
	marker.type = visualization_msgs::Marker::SPHERE;
	marker.action = visualization_msgs::Marker::ADD;
	marker.lifetime = ros::Duration(0.5);
	
	marker.pose.position.z = 0.1;
	marker.color.r = 135/255.0;
	marker.color.g = 206/255.0;
	marker.color.b = 235/255.0;
	marker.color.a = 0.8;//设置标记颜色,确保alpha(不透明度)值为0
	marker.scale.x = marker_scale;
	marker.scale.y = marker_scale;
	marker.scale.z = marker_scale;
	
	// covariance ellipses
	tf::Quaternion orientation;
	tf::Matrix3x3 tf3d;
	Eigen::Matrix2d eigenvectors;
	Eigen::Vector2d eigenvalues;
	Eigen::Matrix2d covariance;// get the covariance matrix 2x2 for each ellipsoid including robot pose
	
	// Count the number of landmarks + robot pose
	unsigned int objs_counter = ((ukf_tool->Mu_x.rows())-3 )/2 + 1;
	unsigned int pose_id=0;
	double quantiles=1 ;
	double ellipse_scale_=5;
	for (size_t i = 0; i < objs_counter; i++)
	{
		marker.id++;
		if( i != 0)
			pose_id = i * 2 + 1;
		
		covariance=ukf_tool->Sigma_y.block(pose_id, pose_id, 2, 2);
		if(tool::computeEllipseOrientationScale2D(eigenvectors, eigenvalues, covariance) == false)
			continue;
		
		// Rotation matrix around  z axis
		tf3d.setValue(eigenvectors(0, 0), eigenvectors(0, 1), 0, eigenvectors(1, 0), eigenvectors(1, 1), 0, 0, 0, 1);

		// get orientation from rotation matrix
		tf3d.getRotation(orientation);
		
		marker.pose.position.x = (ukf_tool->Mu_x)(pose_id);
		marker.pose.position.y = (ukf_tool->Mu_x)(pose_id+1);
		marker.pose.position.z = 0;
		marker.pose.orientation.x = orientation.x();
		marker.pose.orientation.y = orientation.y();
		marker.pose.orientation.z = orientation.z();
		marker.pose.orientation.w = orientation.w();
		marker.scale.x = ellipse_scale_ * quantiles * sqrt(eigenvalues(0));
		marker.scale.y = ellipse_scale_ * quantiles * sqrt(eigenvalues(1));
		marker.scale.z = 0.00001;  // Z can't be 0, limitation of ROS
		ma.markers.push_back(marker);
	}
	
	Uncertainty_pub.publish(ma);
}

三、主循环

在结束各项准备工作之后,就可以进入主循环了,主循环其实也就是两个工作,预测和更新,非常简单:

void ukf_console::run()
{
	//read the map data for all landmarks
	getLandmarks(Worlddata_filepath);

	//read the measurements with odometry and radar data
	getMeasurements(Sensordata_filepath);
	
	LandmarksPublish();
	
	ukf_tool = new unscented_kf(Landmarks.size());
	
	ros::Rate rat(10);
	for (const auto& record : Measurements) 
	{ 
		if (!ros::ok())	
			break;
		
		ukf_tool->ProcessMeasurement(record);
		EstimatePosePublish((ukf_tool->Mu_x).head(3));
		AssociationPublish(record.radar);
		UncertaintyEllipseShow();
		
		rat.sleep();
	}

}

大致思路就是,将观测数据逐条送入ukf系统,进行ukf估计,观测结束,也就预示着程序结束了。

下一篇将会重点说说ukf的估计过程,这里就告一段落了。

  • 5
    点赞
  • 47
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值