为什么说move_base框架不适合3D导航

检测高度难以调整

多线雷达安装在一定高度,地面上有个比较矮的障碍物,要在代价地图里添加。这显然涉及参数min_obstacle_height,要将其降低
-0.5 未检测到低处障碍
-0.55 检测到障碍,但不合理
只稍微降低了min_obstacle_height,虽然是检测到了,但是个环形,这跟多线雷达数据的形状一样,所以认为其实是检测到了地面。

这里发现代价地图能做的修改其实很少了,主要在LOAM和octomap_server

检测距离不能太大

这说的是obstacle_range,如果太大,局部代价地图的窗口的四角都会出现障碍,不知是怎么添加进来的。

代价地图添加了太多不需要的点云数据

对于多线雷达,点云数据量比单线大了很多,在检测近处障碍的时候,计算sq_dist会得到很多点(虽然delta_z不是0)。单线雷达扫描的是一条线,多线其实是很多条线,但是代价地图是2D的,投影会有很多重复的,需要针对xy坐标去重,显著降低了效率,这又是costmap_2d不适合多线雷达的一个例子。

这种情况下,增大的网络占用已经不能忽略了,不能开Rviz,否则会大大影响到导航。


Gazebo的使用配置
Kinetic 和 Melodic的 xacro 文件语法不同

Gazebo对电脑显卡有一定要求,对Nvida显卡支持较好,对AMD的显卡支持较差,如果是AMD的显卡,复杂的世界模型一般加载不出来

利用spawn_model脚本向gazebo_ros节点(在主题中,空间名为 gazebo)发出服务请求,进而添加 URDF 到 Gazebo 中

1
rosrun gazebo_ros spawn_model -file `rospack find MYROBOT_description`/urdf/MYROBOT.urdf -urdf -x 0 -y 0 -z 1 -model MYROBOT

如果是.xacro, 可以转化为 urdf 文件:

xacro转urdf

如果是kinetic,执行

1
rosrun xacro xacro.py robot1.xacro > robot1_processed.urdf

会得到:
1
2
3
xacro: Traditional processing is deprecated. Switch to --inorder processing!
To check for compatibility of your document, use option --check-order.
For more infos, see http://wiki.ros.org/xacro#Processing_Order xacro.py is deprecated; please use xacro instead

说明xacro.py命令已经过期,使用xacro替换: rosrun xacro xacro robot1.xacro > robot1_processed.urdf


如果是在Melodic,执行rosrun xacro xacro --inorder $(locate robot.urdf.xacro) > robot_inorder.urdf,结果得到

1
xacro: in-order processing became default in ROS Melodic. You can drop the option.

查看关节图

先执行sudo apt install -y liburdfdom-tools

1
2
3
check_urdf pr2.urdf
# 再打开转换的pdf
urdf_to_graphiz pr2.urdf

查看spawn_model的所有参数,可以运行:

1
rosrun gazebo_ros spawn_model -h

在 launch文件中,对于urdf可以这样编写:

1
2
<!-- Spawn a robot into Gazebo -->
<node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-file $(find baxter_description)/urdf/baxter.urdf -urdf -z 1 -model baxter" />

对于xacro,这样编写:

1
2
3
4
5
<!-- Convert an xacro and put on parameter server -->
<param name="robot_description" command="$(find xacro)/xacro.py $(find pr2_description)/robots/pr2.urdf.xacro" />

<!-- Spawn a robot into Gazebo -->
<node name="spawn_urdf" pkg="gazebo_ros" type="spawn_model" args="-param robot_description -urdf -model pr2" />

URDF的报错原因:加了等标签,但没加具体内容。

rosrun gazebo_ros spawn_model -file robot.urdf -urdf -model robot

sdf: standard definition of world for Gazebo

1
2
3
4
5
6
7
8
9
10
<launch>
<include file="$(find gazebo_ros)/launch/empty_world.launch" >
</include>

<arg name='robot_urdf' default="$(find xacro)/xacro '$(find vehicle_simulator)/urdf/robot.urdf.xacro'" />
<param name="robot_description" command="$(arg robot_urdf)" />
<node pkg="gazebo_ros" type="spawn_model" name="spawn_robot" args="-urdf -param robot_description -model robot -x 0 -y 0 -z 0.5"/>

<node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="teleop" />
</launch>

正常的输出信息.png

Gazebo的gui参数经常是false,这是因为显示界面会显著加大资源占用

world文件可以直接改名称,不影响使用

兼容问题

2022-05-24_51.png
2022-05-24_52.png

加入camera仿真

Gazebo只给出了水平FOV, 竖直FOV会根据图片的宽和高自动计算。 Gazebo有函数double VerticalFOV() const,解释: The vertical FOV is calculated from width, height, hfov。有bool SetHorizontalFOV (double _hfov)函数,但是没有函数bool SetVerticalFOV (double _hfov)

差速模块

出现下面日志,说明差速控制模块正常加载。

1
2
3
4
5
6
[ INFO] [1720590402.172629168, 68.803000000]: Starting plugin DiffDrive(ns = //)
[ INFO] [1720590402.172729086, 68.803000000]: DiffDrive(ns = //): <rosDebugLevel> = na
[ INFO] [1720590402.173268546, 68.803000000]: DiffDrive(ns = //): <tf_prefix> =
[ INFO] [1720590402.173831755, 68.803000000]: DiffDrive(ns = //): Try to subscribe to cmd_vel
[ INFO] [1720590402.175204165, 68.803000000]: DiffDrive(ns = //): Subscribe to cmd_vel
[ INFO] [1720590402.175652313, 68.803000000]: DiffDrive(ns = //): Advertise odom on odom

libgazebo_ros_gps_sensor.so

libgazebo_ros_bumper.so

libgazebo_ros_ray_sensor.so


move_base的CPU占用和两个常见报警
abstract Welcome to my blog, enter password to read.
Read more
(一) 概述

张楫的CMU团队提出了几个上层规划算法,包括在未知环境中的探索算法和全局路径规划算法,以及全套算法的系统集成和扩展应用。目前开源的是autonomous_exploration, dsv_planner, tare_planner, far_plannerAutonomous Exploration Development Environment里自带的local planner只是用来做局部路径规划小范围避障的,目标点太远时容易卡在死胡同里。如果需要长距离的路径规划建议使用far planner

TARE算法的论文获得了Best Paper Award and Best System Paper Award of RSS 2021。 CMU-OSU参加DARPA SubT 2021挑战赛用的是M-TARE的初级版本(没有开源)。地面机器人同样用的LOAM。
过窄门
下楼梯

我目前只详细研究了autonomous_exploration的代码。

CMU提供的避障算法优势在于能实时高效地应对复杂环境,同时最大化到达目标点的概率。算法的主要思想在于尽可能地让计算在线下完成。具体来说,一个避障算法最耗时的计算一般在于检测机器人在未来时间点与环境发生碰撞的可能。我们的算法先通过离线生成一个庞大的轨迹库(trajectory library)来模拟机器人在未来一段时间内可能走过的轨迹。接下来对于所有轨迹覆盖的空间,我们计算其内部所有点(在一定解析度下)与所有轨迹发生碰撞的可能性。经过这样的离线计算,我们可以得到一个空间内3D点到轨迹的对应关系。在实时运行的过程中,一旦空间内的某个点上有障碍物,我们可以马上知道哪些轨迹将会受到影响。我们的算法会降低选择这些轨迹作为最终路径的可能性。因为大量的计算都在线下进行,线上运行的时候只需要实时选择无碰撞的轨迹,我们的算法可以在几毫秒之内规划出一条无碰撞而最接近目标点的路径。

ROS里经常出现一种情况:规划出的全局路径没有考虑车是否能过窄通道,结果车到了窄通道附近要么又重新规划到其他方向,要么强行过窄通道。本算法会优先规划路径到开阔的地方。 另外也就避免了全局路径的无限规划问题

测试平台

测试平台是室外机器人,车上用了velodyne-16激光雷达,IMU是xsens mti-200。速度只能达到2m/s,所以高速机器人尚需验证。工控机CPU为4.1GHz i7。 定位使用vLOAM的商业版,参考论文。 运行系统要求是18.04或者20.04,其实是因为仿真环境使用Gazebo7,如果不用Gazebo,在16.04上也可以使用。

算法应用框架
使用registered scans的好处:

  • 使scan数据和state estimation之间的processing对时间同步不太敏感
  • 多个registered scans可以 stacked 到一起,从中提取丰富的几何环境信息
这套框架不能用于汽车型机器人

目前我实现的效果

由于LOAM的问题,z坐标不可靠,所以我在代码里都把里程计返回的z按0处理,so far只在没有上下坡的情况下测试过。目前基本可以正常导航,环境中可以有动态障碍,可以调整线速度和角速度,可以倒退,不会有路径混乱、花费时间长的情况。

成功的导航,提取码 2k50

点云障碍的生成和清除

目前在线速度0.5m/s的情况下,还是有一定误差的,亟待优化
导航目标和实际位置对比


全局路径无法走出障碍

视频
这个问题的本质是ROS的全局路径算法不考虑车体的轮廓,跟轮廓有关的避障交给了局部路径,DWA和TEB算法里都有footprintCost之类的函数,可以判断是否撞了障碍,但全局路径算法没有。 全局路径是把车当成了点,从所在栅格到目标栅格进行规划。这里开始规划出的路线就是这样,在走了一段时间被TEB认为不可行后,全局路径换成另一条,但是马上被认为不如之前的全局路径更优,又换回去了。如此循环,所以走不出去了。

但是要让全局路径考虑轮廓,会改动很大(即使不考虑动态障碍),成本太高了。所以有必要考虑其他导航框架了。

暂时的解决方法只有设置中间点或者加虚拟墙


ROS中的多线程 MultiThreadedSpinner和AsyncSpinner

在ROS当中,原作者是不推荐用多线程的,他建议用多进程,变成一个个节点的形式进行通信。多线程分为两种模式:同步和异步。

  • 同步:MultiThreadSpinner s(4),一共5个线程。包括了主线程。

  • 异步:AsyncSpinner s(4), 一共5个线程。包括了主线程。

回调方法 阻塞 线程
ros::spin() 阻塞 单线程
ros::spinOnce 非阻塞 单线程
ros::MultiThreadedSpinner 阻塞 多线程
ros::AsyncSpinner 非阻塞 多线程

这里讨论的是多线程形式的回调函数,而不是多线程之间如何同步的问题


对于多话题的订阅,我们先看传统的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void cb1(const geometry_msgs::PoseStamped::ConstPtr& msg) 
{
ROS_INFO("uwb_pose x: %f", msg->pose.position.x);
}

void cb2(const geometry_msgs::PoseStamped::ConstPtr& msg)
{
sleep(2);
ROS_INFO("yolo_pose x: %f", msg->pose.position.x);
}

int main(int argc, char **argv)
{
ros::init(argc, argv, "node");
ros::NodeHandle nh;
setlocale(LC_ALL, "");

ros::Subscriber uwbSub = nh.subscribe<geometry_msgs::PoseStamped>("uwb_pose", 1, cb1);
ros::Subscriber yoloSub = nh.subscribe<geometry_msgs::PoseStamped>("yolo_pose", 1, cb2);
ros::spin();
return 1;
}

在回调函数cb2里,可能先执行一大堆耗时的命令,这里用sleep(2)代替,这样cb1ROS_INFO获得的消息就会缺失,这明显就是多线程的问题了。

把代码加上ros::MultiThreadedSpinner s(2); ( 无需加入头文件 ), ros::spin();改为ros::spin(s);, 再运行会发现cb1里没有缺少一个消息。

这里多线程的目的是保证线程cb1不丢失消息,而不是cb2,它丢失消息是必然的。

对于ros::AsyncSpinner,代码在ros::Subscriber定义之后这样写:

1
2
3
ros::AsyncSpinner spinner(2);
spinner.start();
ros::waitForShutdown();

当程序当中有数据处理线程的时候,建议开辟异步多线程订阅,算法写在订阅函数里面。 当然,目前的处理当中,我更倾向于重新开辟一个线程,然后通过循环数组来进行数据交互。

参考:ROS多线程订阅消息


扫到太低的障碍和扫不到障碍
abstract Welcome to my blog, enter password to read.
Read more
障碍层7 添加障碍

后面的observations是下面这样获得的

1
2
3
std::vector<Observation>  observations;
// get the marking observations
getMarkingObservations(observations);

Observation保存的是 observation in terms of a point cloud and the origin of the source,有4个成员变量:

  • geometry_msgs::Point origin_; The origin point of the observation
  • pcl::PointCloud<pcl::PointXYZ>* cloud_; point cloud of the observation
  • double obstacle_range_; The range out to which an observation should be able to insert obstacles
  • double raytrace_range_; The range out to which an observation should be able to clear via raytracing

获得的observations实际上是从bufferCloud里的observation_list_而来。

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
// place the new obstacles into a priority queue
// each with a priority of zero to begin with
for (std::vector<Observation>::const_iterator it = observations.begin();
it != observations.end(); ++it)
{
const Observation& obs = *it;
const pcl::PointCloud<pcl::PointXYZ>& cloud = *(obs.cloud_);
// obstacle_range_ 其实就是参数 obstacle_range
double sq_obstacle_range = obs.obstacle_range_ * obs.obstacle_range_;
// 遍历点云中的每一个点
for (unsigned int i = 0; i < cloud.points.size(); ++i)
{
double px = cloud.points[i].x, py = cloud.points[i].y, pz = cloud.points[i].z;
// if the obstacle is too high or too far away from the robot we won't add it
if (pz > max_obstacle_height_)
{
ROS_DEBUG("The point is too high");
continue;
}
// compute the squared distance from the hitpoint to the pointcloud's origin
double sq_dist = (px - obs.origin_.x) * (px - obs.origin_.x) + (py - obs.origin_.y) * (py - obs.origin_.y)
+ (pz - obs.origin_.z) * (pz - obs.origin_.z);

// if the point is far enough away... we won't consider it
if (sq_dist >= sq_obstacle_range)
{
ROS_DEBUG("The point is too far away");
continue;
}
// now we need to compute the map coordinates for the observation
unsigned int mx, my;
if (!worldToMap(px, py, mx, my))
{
ROS_DEBUG("Computing map coords failed");
continue;
}
unsigned int index = getIndex(mx, my);
costmap_[index] = LETHAL_OBSTACLE;
touch(px, py, min_x, min_y, max_x, max_y);
}
}
updateFootprint(robot_x, robot_y, robot_yaw, min_x, min_y, max_x, max_y);

对于多线雷达,(pz - obs.origin_.z) * (pz - obs.origin_.z)是0,可以考虑把这段去掉。 另外点云数据量比单线大了很多,在检测近处障碍的时候,计算sq_dist会得到很多点(虽然delta_z不是0)。单线雷达扫描的是一条线,多线其实是很多条线,但是代价地图是2D的,投影会有很多重复的,需要针对xy去重,显著降低了效率,这又是costmap_2d不适合多线雷达的一个例子。 另外这里就是参数obstacle_range出现的地方。


障碍层4 障碍层的updateBounds

代码流程比较复杂: Costmap2DROS::mapUpdateLoop —— Costmap2DROS::updateMap() —— LayeredCostmap::updateMap—— 每一层的 updateBounds

mapUpdateLoop

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
void Costmap2DROS::mapUpdateLoop(double frequency)
{
if (frequency == 0.0) return;
ros::NodeHandle nh;
ros::Rate r(frequency);
while (nh.ok() && !map_update_thread_shutdown_)
{
struct timeval start, end;
double start_t, end_t, t_diff;
gettimeofday(&start, NULL);

updateMap();
// 目前的时间用tv 结构体返回
gettimeofday(&end, NULL);
start_t = start.tv_sec + double(start.tv_usec) / 1e6;
end_t = end.tv_sec + double(end.tv_usec) / 1e6;
t_diff = end_t - start_t;
ROS_DEBUG("Map update time: %.9f", t_diff);
if (publish_cycle.toSec() > 0 && layered_costmap_->isInitialized())
{
unsigned int x0, y0, xn, yn;
layered_costmap_->getBounds(&x0, &xn, &y0, &yn);
// publisher_ 是 Costmap2DPublisher
publisher_->updateBounds(x0, xn, y0, yn);

ros::Time now = ros::Time::now();
if (last_publish_ + publish_cycle < now)
{
publisher_->publishCostmap();
last_publish_ = now;
}
}
r.sleep();
// make sure to sleep for the remainder of our cycle time
if (r.cycleTime() > ros::Duration(1 / frequency))
ROS_WARN("Map update loop missed its desired rate of %.4fHz... the loop actually took %.4f seconds", frequency, r.cycleTime().toSec());
}
}

updateMap

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
void Costmap2DROS::updateMap()
{
if (!stop_updates_)
{
tf::Stamped < tf::Pose > pose;
//得到global_frame_ 与 robot_base_frame_ tf
// 转换信息中的 translation 坐标信息
if (getRobotPose (pose))
{
//获取当前机器人位置,必须要弄清楚这个x,y 代表什么意思
double x = pose.getOrigin().x(),
y = pose.getOrigin().y(),
yaw = tf::getYaw(pose.getRotation());

/*调用LayeredCostmap 类中的updateMap 更新边界和cost值
* 先依据各层的更新情况,判断地图更新过的范围的边界。
* 然后用初始值重置全局地图更新边界范围内的地图信息,
* 并用各层的信息在更新边界内部更新地图信息
*/
layered_costmap_->updateMap(x, y, yaw);

geometry_msgs::PolygonStamped footprint;
footprint.header.frame_id = global_frame_;
footprint.header.stamp = ros::Time::now();
transformFootprint(x, y, yaw, padded_footprint_, footprint);
footprint_pub_.publish(footprint);
initialized_ = true;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bool Costmap2DROS::getRobotPose(tf::Stamped<tf::Pose>& global_pose) const
{
//local_costmap和global_costmap的robot_base_frame_都是/base_footprint
robot_pose.frame_id_ = robot_base_frame_;
try
{
/*
*如果是local_costmap,global_frame_是/map
* 如果是global_costmap 是/odom_combined
* 然后根据tf信息,转换的到global_pose,即机器人当前位置
*得到global_frame_ 与robot_base_frame_ tf转换信息中的 translation 坐标信息
*/
tf_.transformPose(global_frame_, robot_pose, global_pose);
}
return true;
}

这里就只看障碍层的updateBounds函数。这个函数主要完成 clearing, marking以及确定bound。和静态地图类似,同样也是先判断是否是rolling地图,若是则更新地图原点。


点云数据最终传到const pcl::PointCloud<pcl::PointXYZ>& cloud = *(obs.cloud_); 点云z坐标在计算sq_dist之后就不处理了。

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
void ObstacleLayer::updateBounds(double robot_x, double robot_y, 
double robot_yaw, double* min_x, double* min_y,
double* max_x, double* max_y)
{
if (rolling_window_)
updateOrigin(robot_x - getSizeInMetersX() / 2,
robot_y - getSizeInMetersY() / 2);
if (!enabled_) return;
useExtraBounds(min_x, min_y, max_x, max_y);

bool current = true;
std::vector<Observation> observations, clearing_observations;

// get the marking observations
current = current && getMarkingObservations(observations);

// get the clearing observations
current = current && getClearingObservations(clearing_observations);

// update the global current status
current_ = current;

// raytrace freespace
for (unsigned int i = 0; i < clearing_observations.size(); ++i)
{
raytraceFreespace(clearing_observations[i], min_x, min_y, max_x, max_y);
}

清除和添加障碍在后面的文章继续


障碍层6 加速清除代价地图中的障碍

雷达无法及时清除代价地图中的障碍

使用代价地图时,会出现由于激光雷达测距的局限性,代价地图中的障碍会不能及时清除。 比如有行人走过会拖出一个条状障碍或者雷达扫描到障碍仍不能清除。问题根源在于某些激光雷达测的距离超过极限,会没有相对应的数据。

不同的激光雷达的情况不一样。所以costmap的源码不可能处理所有的情况。 在测不到数据时, sick雷达,返回的是inf。国内一些雷达,在未测到数据时,返回的是0.0 或者默认的最大值。

代码在障碍层的ObstacleLayer::laserScanValidInfCallback

1
2
3
4
5
6
7
8
for (size_t i = 0; i < message.ranges.size(); i++)
{
float range = message.ranges[ i ];
if ( (!std::isfinite(range) && range > 0) )
{
message.ranges[ i ] = message.range_max - epsilon;
}
}

上面的代码处理了每一个激光的数据,如果是激光的点是最大的距离(不是 finite),那么将这个点设置为比最大距离小十分之一毫米。看来程序作者也考虑到了这个问题,当激光的距离等于最大的距离的时候会出现障碍物无法清除的现象,但是作者不可能对所有雷达出现数据invalid的情况都做判断,这就需要自己加了。

有两种已知的雷达数据invalid的情况:

  • 雷达在超出量程或没数据时返回0,上面的if判断加入 || range ==0.0

  • 雷达的数据超出量程后设为大于range_max的值,而不是inf,比如量程位30m的HOKUYO激光雷达,超量程后会返回65.33m。这样也不在上面的if判断里,加入range >= range_max

最后还要在通用代价地图中设置inf_is_valid参数为true

但是这只适用于单线雷达,如果是多线雷达或相机的深度点云,回调函数就是 pointCloud2Callback,不能用这个方法了。

地图分辨率高于激光雷达分辨率

costmap_common_params.yaml中有两个配置参数

1
2
obstacle_range: 2.5  //只有障碍物在这个范围内才会被标记
raytrace_range: 3 //这个范围内不存在的障碍物都会被清除

raytrace_range = 3时,假设激光雷达的角分辨率是1,即360度每一度一个激光点时,每个激光点之间的距离大约是0.052(1x3.14/180 x3), 如果此时地图的分辨率是0.01,在靠近激光点附近有一个障碍物,但是始终在激光雷达两条射线之间的话,也就是始终没有扫描到的话,那这个障碍始终无法被清除掉。所以参数raytrace_range不是越大越好

我们知道代价地图实际上是一定分辨率的方格图,两条射线如下图所示,根据bresenham2D算法找到经过的蓝色方格,这些方格的代价值会置为FREE_SPACE。但实际上黄色方格也应该置为FREE_SPACE,但是因为角分辨率不足而没有扫射到,这就是问题的由来。
示意图

解决方法首先是买好雷达,角分辨率一定要好。

清除障碍物是ObstacleLayer::raytraceFreespace
raytraceLine(marker, x0, y0, x1, y1, cell_raytrace_range); 会将所有在(x0,y0)>>(x1,y1)之间的所有cell标记为FREE_SPACE

updateRaytraceBounds会根据测量的距离,更新扩张(min_x, min_y, max_x, max_y)

updateBounds 在根据测量数据完成 clear 操作之后,就开始了mark 操作,对每个测量到的点,标记为obstacle

修改清除代价值的规则,即ObstacleLayer::raytraceFreespace,把一个激光点做十字形扩展:

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
for (unsigned int i = 0; i < cloud.points.size(); ++i)
{
double wx = cloud.points[i].x;
double wy = cloud.points[i].y;

//ROS_INFO("laser scan wx = %.2f, wy = %.2f", wx, wy);
//在检测到的点周围生成6x6的点,
double inflate_dx = 0.01, inflate_dy = 0.01; //在原来点的位置膨胀的尺度
std::vector< std::pair<double,double> > inflate_pts;
inflate_pts.push_back(std::make_pair(wx + 0 , wy + 0 ));
inflate_pts.push_back(std::make_pair(wx - 0 , wy - inflate_dy));
inflate_pts.push_back(std::make_pair(wx - inflate_dx, wy - 0 ));
inflate_pts.push_back(std::make_pair(wx + 0 , wy + inflate_dy));
inflate_pts.push_back(std::make_pair(wx + inflate_dx, wy + 0 ));
inflate_pts.push_back(std::make_pair(wx - 0 , wy - 2*inflate_dy));
inflate_pts.push_back(std::make_pair(wx - 2*inflate_dx, wy - 0 ));
inflate_pts.push_back(std::make_pair(wx + 0 , wy + 2*inflate_dy));
inflate_pts.push_back(std::make_pair(wx + 2*inflate_dx, wy + 0 ));
inflate_pts.push_back(std::make_pair(wx - 0 , wy - 3*inflate_dy));
inflate_pts.push_back(std::make_pair(wx - 3*inflate_dx, wy - 0 ));
inflate_pts.push_back(std::make_pair(wx + 0 , wy + 3*inflate_dy));
inflate_pts.push_back(std::make_pair(wx + 3*inflate_dx, wy + 0 ));

// 实质上增加一个循环
std::vector< std::pair<double,double> >::iterator inflate_iter;
for(inflate_iter = inflate_pts.begin(); inflate_iter != inflate_pts.end(); inflate_iter++)
{
wx = (*inflate_iter).first;
wy = (*inflate_iter).second;
......
MarkCell marker(costmap_, FREE_SPACE);
// and finally... we can execute our trace to clear obstacles along that line
//最终raytraceLine(marker, x0, y0, x1, y1, cell_raytrace_range);
//会将所有在(x0,y0)>>(x1,y1)之间的所有cell标记为FREE_SPACE。
raytraceLine(marker, x0, y0, x1, y1, cell_raytrace_range);
//而updateRaytraceBounds 会根据测量的距离,更新扩张(min_x, min_y, max_x, max_y)。
updateRaytraceBounds(ox, oy, wx, wy, clearing_observation.raytrace_range_, min_x, min_y, max_x, max_y);
}
}

move_base中自动清除代价地图

MoveBase的状态机,在全局规划失败达到上限时,会进入恢复行为,清除代价地图。这样的效率比较低,而且清除的方法不是我们需要的。所以修改MoveBase::makePlan,在多次找不到全局路径时(次数较少),清除代价地图,其实跟clear_costmaps服务端的代码是一样的。一开始我还在move_base里定义了客户端函数,在规划失败太多时发起service请求move_base/clear_costmaps,后来发现服务端的代码就在move_base里,就两行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if(!planner_->makePlan(start, goal, plan) || plan.empty())
{
ROS_DEBUG_NAMED("move_base","Failed to find a plan to point (%.2f, %.2f)",
goal.pose.position.x, goal.pose.position.y);

failed_num++;
if(failed_num > min_clear_ )
{
controller_costmap_ros_->resetLayers();
planner_costmap_ros_->resetLayers();
ROS_INFO("\033[44;37m clear costmaps done ! \033[0m");
}
return false;
}

min_clear_是我自定义的参数min_clear的赋值,默认是4。

failed_num在构造函数中初始化为0,这是当然的。 另外注意failed_numMoveBase::executeCb开头置为0,因为这里是收到新目标的地方。

参考:激光点做圆形扩展

客户端发起清除代价地图的请求

有时全局代价地图还是不能及时清除,但是又不能使用上面自动清除的方法。考虑在客户端(python 程序)发目标点时,先发起move_base/clear_costmaps服务,让move_base清除一次,这样效果好多了。我开始是修改planThreadexecuteCb函数,但是这两个函数在路径规划时并不是只运行一次,多次运行会清除过多,会撞障碍;如果清除后执行sleep,又使机器人表现卡顿。