(二) 算法的初始化

MoveBase中加载局部路径规划器

1
tc_->initialize(blp_loader_.getName(local_planner), &tf_, controller_costmap_ros_);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
void TebLocalPlannerROS::initialize(std::string name, tf::TransformListener* tf, costmap_2d::Costmap2DROS* costmap_ros)
{
if(!initialized_)
{
// create Node Handle with name of plugin (as used in move_base for loading)
ros::NodeHandle nh("~/" + name);

// get parameters of TebConfig via the nodehandle and override the default config
cfg_.loadRosParamFromNodeHandle(nh); // TebConfig cfg_;
// nh.param("trapped_threshold", trapped_threshold_, 20);
nh.param("filter_back_velocity", filter_back_velocity_, true);
// nh.param("setting_acc", setting_acc_, 1.5);

obstacles_.reserve(500); // ObstContainer obstacles_;

// 在TebVisualization构造函数里,注册话题 global_plan, local_plan,teb_poses, teb_markers, teb_feedback
visualization_ = TebVisualizationPtr(new TebVisualization(nh, cfg_));

//创建robot footprint/contour model for optimization,对5种类型分别处理
// 点类型最简单,返回新建的PointRobotFootprint共享指针
// 对于polygon类型,只对 Point2dContainer vertices_;赋值
RobotFootprintModelPtr robot_model = getRobotFootprintFromParamServer(nh);

// 根据参数,选择创建哪种 planner
if (cfg_.hcp.enable_homotopy_class_planning)
{
planner_ = PlannerInterfacePtr(new HomotopyClassPlanner(cfg_, &obstacles_, robot_model, visualization_, &via_points_));
ROS_INFO("Parallel planning in distinctive topologies enabled.");
}
else
{
planner_ = PlannerInterfacePtr(new TebOptimalPlanner(cfg_, &obstacles_, robot_model, visualization_, &via_points_));
ROS_INFO("Parallel planning in distinctive topologies disabled.");
}
// init other variables
tf_ = tf; // 在 move_base_node.cpp中最初定义
// 这个就是 controller_costmap_ros_ = new costmap_2d::Costmap2DROS("local_costmap", tf_);
costmap_ros_ = costmap_ros;
// locking should be done in MoveBase
costmap_ = costmap_ros_->getCostmap();

// 创建 CostmapModel 共享指针
costmap_model_ = boost::make_shared<base_local_planner::CostmapModel>(*costmap_);

// 就是return global_frame, 实际是 "map"
global_frame_ = costmap_ros_->getGlobalFrameID();
cfg_.map_frame = global_frame_; // 更新cf_的成员

// 代价地图的local frame, 实际是 "base_footprint"
robot_base_frame_ = costmap_ros_->getBaseFrameID();

//Initialize a costmap to polygon converter
if (!cfg_.obstacles.costmap_converter_plugin.empty())
{
try
{
costmap_converter_ = costmap_converter_loader_.createInstance(cfg_.obstacles.costmap_converter_plugin);
std::string converter_name = costmap_converter_loader_.getName(cfg_.obstacles.costmap_converter_plugin);
// replace '::' by '/' to convert the c++ namespace to a NodeHandle namespace
boost::replace_all(converter_name, "::", "/");
costmap_converter_->setOdomTopic(cfg_.odom_topic);
costmap_converter_->initialize(ros::NodeHandle(nh, "costmap_converter/" + converter_name));
costmap_converter_->setCostmap2D(costmap_);

costmap_converter_->startWorker(ros::Rate(cfg_.obstacles.costmap_converter_rate), costmap_, cfg_.obstacles.costmap_converter_spin_thread);
ROS_INFO_STREAM("Costmap conversion plugin " << cfg_.obstacles.costmap_converter_plugin << " loaded.");
}
catch(pluginlib::PluginlibException& ex)
{
ROS_WARN("The specified costmap converter plugin cannot be loaded. All occupied costmap cells are treated as point obstacles. Error message: %s", ex.what());
costmap_converter_.reset();
}
}
else
ROS_INFO("No costmap conversion plugin specified. All occupied costmap cells are treaten as point obstacles.");

// 类型是 vector<geometry_msgs::Point>
footprint_spec_ = costmap_ros_->getRobotFootprint();
costmap_2d::calculateMinAndMaxDistances(footprint_spec_, robot_inscribed_radius_, robot_circumscribed_radius);
// 更新cf_的成员
cfg_.robot.robot_inscribed_radius = robot_inscribed_radius_;
cfg_.robot.robot_circumscribed_radius = robot_circumscribed_radius;

// init the odom helper to receive the robot's velocity from odom messages
odom_helper_.setOdomTopic(cfg_.odom_topic);

// setup dynamic reconfigure
dynamic_recfg_ = boost::make_shared< dynamic_reconfigure::Server<TebLocalPlannerReconfigureConfig> >(nh);
dynamic_reconfigure::Server<TebLocalPlannerReconfigureConfig>::CallbackType cb = boost::bind(&TebLocalPlannerROS::reconfigureCB, this, _1, _2);
dynamic_recfg_->setCallback(cb);

// validate optimization footprint and costmap footprint
// 对于point类型,第1个参数是0,要求最好第1项+第3项 > 第2项
// 否则 Infeasible optimziation results might occur frequently
validateFootprints(robot_model->getInscribedRadius(), robot_inscribed_radius_,
cfg_.obstacles.min_obstacle_dist);

// custom obstacles的回调函数,话题 /move_base/TebLocalPlannerROS/obstacles
custom_obst_sub_ = nh.subscribe("obstacles", 1, &TebLocalPlannerROS::customObstacleCB, this);

// custom via-points的回调,话题 /move_base/TebLocalPlannerROS/via_points
via_points_sub_ = nh.subscribe("via_points", 1, &TebLocalPlannerROS::customViaPointsCB, this);
// queue_follow 所用,话题 /rectangle_distance
front_dist_sub_ = nh.subscribe("/rectangle_distance", 1, &TebLocalPlannerROS::frontDistanceCB, this);
// 新添加
footprint_sub_ = nh.subscribe("/footprint", 10, &TebLocalPlannerROS::footprintCB, this);


ros::NodeHandle nh_move_base("~");
double controller_frequency = 5;
// 实际配置是 30
nh_move_base.param("controller_frequency", controller_frequency, controller_frequency);
// initialize failure detector,Detect if the robot got stucked
failure_detector_.setBufferLength(std::round(cfg_.recovery.oscillation_filter_duration*controller_frequency));
// 初始化,返回一个很大的数
min_distance_custom_obstacle = std::numeric_limits<double>::max();

// set initialized flag
initialized_ = true;
ROS_DEBUG("teb_local_planner plugin initialized.");
}
else
ROS_WARN("teb_local_planner has already been initialized, doing nothing.");
}

关于RobotFootprintModelPtr,看TEB中的footprint模型

calculateMinAndMaxDistances函数和RobotFootprintModelPtr说明同时使用了两种footprint模型


(五) 局部子目标的朝向覆盖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 局部的子目标 (transformed plan最后一个元素)
tf::Stamped<tf::Pose> goal_point;
tf::poseStampedMsgToTF(transformed_plan.back(), goal_point);
robot_goal_.x() = goal_point.getOrigin().getX();
robot_goal_.y() = goal_point.getOrigin().getY();

if (cfg_.trajectory.global_plan_overwrite_orientation)
{
robot_goal_.theta() = estimateLocalGoalOrientation(global_plan_, goal_point,
goal_idx, tf_plan_to_global);
// overwrite/update goal orientation of the transformed plan with the actual goal
// (enable using the plan as initialization)
transformed_plan.back().pose.orientation = tf::createQuaternionMsgFromYaw(robot_goal_.theta());
}
else
robot_goal_.theta() = tf::getYaw(goal_point.getRotation());

// plan only contains the goal, 一般在导航的最后出现size是1的情况
// 用实际的机器人位姿更新 transformed plan 的起点(allows using the plan as initial trajectory)
if (transformed_plan.size()==1)
{ // 插入起点,但不初始化
transformed_plan.insert(transformed_plan.begin(), geometry_msgs::PoseStamped() );
}
tf::poseTFToMsg(robot_pose, transformed_plan.front().pose); // 把起点更新为 机器人当前位姿

参数global_plan_overwrite_orientation为True时,执行estimateLocalGoalOrientation() 将未来一段的规划的姿态角的平均值返回为估计的局部目标姿态

transformed_plan.back()就是所谓的 local goal ,但是遇到需要终点转弯时,local goal的朝向不是最终的goal的朝向,仍然是规划的


swap内存

解决编译时内存不足c++: internal compiler error: Killed (program cc1plus)

1
2
3
4
5
6
7
8
9
10
11
# 创建分区文件, 大小 3G
sudo dd if=/dev/zero of=/swapfile bs=1M count=3000

sudo mkswap /swapfile

sudo swapon /swapfile

# After compiling, you may wish to

sudo swapoff /swapfile
sudo rm /swapfile

其实swap空间就是把硬盘上的空间用来当内存使用,是一种折中的处理方法。


(一) 理论和安装

理论

time elastic band(时间橡皮筋)从elastic band算法(1993年提出)优化而来。TEB的初始轨迹是全局路径传来的transformed_plan,中间插入N个控制橡皮筋形状的路径点(机器人位姿),在点与点之间定义时间差,转换为明确依赖于时间的轨迹,从而实现对机器人的实时控制。由于其模块化的形式,该方法可以扩展目标函数和约束。

TEB的创新在于考虑了动态约束和增加时间信息,如有限的机器人速度和加速度,将规划问题用加权多目标优化框架表示,也是一个bundle adjustment问题。在实时应用中,在线对全局路径进行大规模的计算往往是不可行的,TEB大多数目标函数是局部(local)的,因为它们依赖于一些邻近的中间 configurations。TEB的这种局部性会生成一个稀疏系统矩阵,才可以实现快速有效的大规模优化。作者写第一篇论文时(2012),把约束表示为一个分段连续、可微的代价函数,到了下一篇论文(2013),改成了使用g2o的图优化方法(利文伯格法)。

路径(Path)与轨迹(Trajectory)区别在于,轨迹包含了时间信息。所以TEB算法里老用Trajectory这个词。

各类目标函数

目标函数的梯度可以理解为作用在弹性带上的外力。

TEB的目标函数分为两类:

  • 约束:例如 速度、加速度限制
  • 目标:例如 最快、最短路径或者到障碍的距离

目标函数依赖的参数只取决于邻近位姿的子集。比如:

  • 速度(加速度)约束取决于两个(三个)连续的位姿和一个(两个)时间差。
  • 障碍物的清除和在中间路线上进行导航,只影响单个位姿和它的k个最近的邻居(实际上大约3-5)。
  • 遵守机器人的非完整运动学约束涉及到了两个相邻的位姿,这两个位姿位于一个常曲率的普通圆弧上

但是最快和最短路径是例外,显然是因为它们需要全局地依赖于所有的参数。

TEB同时考虑原始路径的via-point约束和避开障碍。这两个目标函数相似,不同之处在于via-point吸引路径,而障碍物排斥路径。

约束用到了几个顶点(待优化变量),对应的就是几元边。 比如速度约束用到了两个位姿和一个时间差,是三元边。 加速度约束就是五元边。 障碍约束和via-point就是一元边。非完整运动学约束是二元边。
图的构建过程

为什么可以用非线性优化的框架?

平时所说的图优化的最小二乘形式是这样的:
图优化的最小二乘形式

TEB所优化的目标函数是这样的:
TEB的目标函数
我们先把上面的F(x)看成 , 其中

图优化中,顶点是待估计的参数,观测数据构成了边。 TEB的待估计参数就是 ,也就是位姿和时间差的组合。 边表示目标函数 ,并连接多个顶点。

这个图的顶点vertexs是橡皮筋的状态(机器人姿态+时间),起点和终点当然是固定的,图的边是我们自己定义的优化目标函数。默认使用利文伯格法,优化器是g2o::CSparse

g2o框架在批处理模式下优化了TEB,因此每次迭代都会生成一个新的图。在一个机器人控制周期内对轨迹修改步骤进行多次迭代(四个循环包括5次利文伯格的迭代)。 g2o框架提供了两种基于Cholesky decomposition 的求解器: CHOLMODCSparse。默认是CSParse

对优化后的TEB进行验证,检查是否违反了硬约束,如果违反,在这种情况下机器人要么停止,要么重新调用运动规划器。

TEB每次迭代的平均运行时间依赖于TEB的维数,TEB的维数随位姿数(configurations)线性增长。对于10000多个states,对应大约2500种configurations。

Homotopy Planner

TEB中实现了两种规划器,一个就是普通的TebOptimalPlanner,另一个是HomotopyClassPlanner,HomotopyClassPlanner是一种同时优化多个轨迹的方法,由于目标函数的非凸性(非凸函数)会生成一系列最优的候选轨迹,最终在备选局部解的集合中寻求总体最佳候选轨迹,对应论文:《Integrated online trajectory planning and optimization in distinctive topologies》

optimizer本身只能找到局部最优轨迹,有时找到的路径invalid,所以一般都是用HomotopyClassPlanner。HomotopyClassPlanner类像是多个TebOptimalPlanner类实例的组合。HomotopyClassPlanner类中也会实例化一个由全局规划器生成的路径作为参考的对象。除此之外,它还会使用probabilistic roadmap (PRM) methods在障碍物周边采样一些keypoints,将这些keypoints连接起来,去除方向没有朝向目标点的连接和与障碍物重叠的连接。这样就形成了一个网络,然后将起始点和终点接入到这个网络。如下图所示:

使用Depth First Search(深度优先方法)搜索所有可行的路径。将这些路径作为参考,实例化多个TebOptimalPlanner类的实例。采用多线程并行优化,得到多条优化后的路径。将这些路径进行可行性分析,选出代价值最小的最优路径。不得不说HomotopyClassPlanner类里的方法是一个鲁棒性和可靠性更高的方法。


安装

编译teb时报错,或者在没有编译TEB的计算机上通过.so运行TEB时会报错
加载TEB算法时报警.png
libteb_local_planner.so 缺链接库.png
实际的链接情况.png

因为没有安装g2o和ceres,不过不需要自己安装g2o。 先运行sudo apt-get install ros-melodic-teb-local-planner,这样会在/opt/ros里安装g2o,然后把g2o的so文件复制到/usr/local/lib

1
2
sudo cp /opt/ros/melodic/lib/libg2o* /usr/local/lib
sudo cp -r /opt/ros/melodic/include/g2o /usr/local/include

如果自己之前安装的g2o的相关共享库没有清除干净,解决方法为:

  • 删除/usr/local/include/g2o,指令为 sudo rm -rf /usr/local/include/g2o
  • 删除/usr/local/lib下有关libg2o*.so的库文件,先进入目录/usr/local/lib,然后挨个(可多个同时)删除: `sudo rm -rf libg2o.so libg2o_.so libg2o_*.so`

参考:
对ROS局部运动规划器Teb的理解
Trajectory modification considering dynamic constraints of autonomous robots
Efficient Trajectory Optimization using a Sparse Model—使用稀疏模型对有效轨迹进行优化


AMCL的自动全局定位
Welcome to my blog, enter password to read.
Read more
点积和范数

L2范数 squareNorm(),等价于计算vector的自身点积,norm()返回squareNorm的开方根。

这些操作应用于matrix,norm() 会返回Frobenius或Hilbert-Schmidt范数。

如果你想使用其他Lp范数,可以使用lpNorm< p >()方法。p可以取Infinity,表示L∞范数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int main()
{
VectorXf v(2);
MatrixXf m(2,2), n(2,2);

v << -1,
2;

m << 1,-2,
-3,4;
cout << "v.squaredNorm() = " << v.squaredNorm() << endl;
cout << "v.norm() = " << v.norm() << endl;
cout << "v.lpNorm<1>() = " << v.lpNorm<1>() << endl;
cout << "v.lpNorm<Infinity>() = " << v.lpNorm<Infinity>() << endl;
cout << endl;
cout << "m.squaredNorm() = " << m.squaredNorm() << endl;
cout << "m.norm() = " << m.norm() << endl;
cout << "m.lpNorm<1>() = " << m.lpNorm<1>() << endl;
cout << "m.lpNorm<Infinity>() = " << m.lpNorm<Infinity>() << endl;
}


业务问题和小问题累计

roslaunch move_base 出错

roslaunch 不识别move_base.png

其他launch没问题,使用file命令,原来它真是个shell脚本
xml被识别为shell脚本.png
但是新建一个launch,类型正常了,但是仍然失败。

结果发现是虚拟机关闭时,是直接关标签,不是正常关机
.so文件损坏.png
.so文件损坏 1.png
只好重新编译了

move_base和rviz进程突然终止

很简单,就是CPU占用太大。最好在本地打开rviz

在 mini-httpd 环境下使用ROS

因为与上位机的通信要使用CGI, 但是不能运行ROS程序, 后来发现是要修改mini-httpd的源码,添加需要的三个ROS环境变量, 如果用到了其他自定义的环境变量,也要添加


single orientation layer
abstract Welcome to my blog, enter password to read.
Read more
导航相关的命令

查看导航状态

rostopic echo move_base/status

发导航命令到某点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rostopic pub /move_base_simple/goal geometry_msgs/PoseStamped "header:
seq: 0
stamp:
secs: 0
nsecs: 0
frame_id: 'map'
pose:
position:
x: 60.0
y: -6.0
z: 0.0
orientation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0"

停止导航

一句话:rostopic pub /move_base/cancel actionlib_msgs/GoalID -- {},即向话题/move_base/cancel发空消息

1
2
3
4
ros::Publisher cancle_pub_ = nh_.advertise("move_base/cancel",1);

actionlib_msgs::GoalID first_goal;
cancle_pub_.publish(first_goal);


cmake教程(七) INSTALL

常用的INSTALL配置

1
2
3
install(TARGETS node
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

最终的可执行文件的路径在install/lib/node

另一种情况,test_node是可执行文件,test是动态库,target_link_libraries只是保证了devel/lib/test_node中的可执行文件链接了test,但不保证install/lib/test_node中链接了test,需要如下配置

1
2
3
4
5
6
install(
TARGETS test test_node
ARCHIVE DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
LIBRARY DESTINATION ${CATKIN_PACKAGE_LIB_DESTINATION}
RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

注意,如果修改了test对应的源文件, 需要把编译后的libtest.so也更新到远程机,否则test_node没有变化

CMAKE_INSTALL_PREFIX

CMAKE_INSTALL_PREFIX变量类似于configure脚本的–prefix,常见的使用方法是在build文件夹里执行cmake -DCMAKE_INSTALL_PREFIX=/usr ..

INSTALL指令用于定义安装规则,安装的内容可以包括目标二进制、动态库、静态库以及
文件、目录、脚本等。

检查CMakelist文件发现路径是include_directories("/usr/include/eigen3")

检查eigen3的路径,发现我的是/usr/local/include/eigen3/Eigen,可以链接对应的文件,即进行以下命令:

1
sudo ln -s /usr/local/include/eigen3/Eigen /usr/include/Eigen

sudo ln -s 源文件 目标文件是一个常用的linux命令,功能是为源文件在目标文件的位置建立一个同步的链接,当二者建立联系后即可在源文件中访问目标文件。

链接有两种,一种被称为硬链接(Hard Link),另一种被称为符号链接(Symbolic Link). 建立硬链接时,链接文件和被链接文件必须位于同一个文件系统中,并且不能建立指向目录的硬链接。默认情况下,ln产生硬链接。如果给ln命令加上- s选项,则建立符号链接。