这是常见的一种情况
- 起初经过观察,发现在一些地方容易出现,而且是都在雷达的左侧出现,于是怀疑是雷达自身问题,最后把
angle_min
的绝对值改小,不再出现这个问题。
雷达扫描到不存在的障碍
后来又出现这种现象,而且不分左右,有时看不出在哪边
angle_min
的绝对值改小,不再出现这个问题。后来又出现这种现象,而且不分左右,有时看不出在哪边
1 | planner_->visualize(); |
1 | void TebOptimalPlanner::visualize() |
最后部分是发布feedback消息 (single trajectory overload),话题teb_feedback
. 需要设置publish_feedback
为true,默认为false
feedback message
包括规划的轨迹,速度和时间信息(temporal information),也包括 active obstacles
publishLocalPlanAndPoses
很简单,需要注意的是1
2
3
4// 不明白z为什么这样赋值 ???
pose.pose.position.z = cfg_->hcp.visualize_with_time_as_z_axis_scale *
teb.getSumOfTimeDiffsUpToIdx(i);
// getSumOfTimeDiffsUpToIdx 就是从起点到当前位姿i的所有deltaT的总和
publishRobotFootprintModel
也很简单,机器人在局部路径第一个点的时候,可视化轮廓,就是一些对visualization_msgs::Marker
的操作。不过对不同的轮廓类型,调用的函数不同
1 | void TebVisualization::publishFeedbackMessage(const TebOptimalPlanner& teb_planner, const ObstContainer& obstacles) |
这里需要注意的是点障碍的可视化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
41visualization_msgs::Marker marker;
marker.header.frame_id = cfg_->map_frame;
marker.header.stamp = ros::Time::now();
marker.ns = "PointObstacles"; marker.id = 0;
marker.type = visualization_msgs::Marker::POINTS;
marker.action = visualization_msgs::Marker::ADD;
marker.lifetime = ros::Duration(2.0);
for (ObstContainer::const_iterator obst = obstacles.begin(); obst != obstacles.end(); ++obst)
{
boost::shared_ptr<PointObstacle> pobst = boost::dynamic_pointer_cast<PointObstacle>(*obst);
if (!pobst) continue;
if (cfg_->hcp.visualize_with_time_as_z_axis_scale < 0.001)
{
geometry_msgs::Point point;
point.x = pobst->x();
point.y = pobst->y();
point.z = 0;
marker.points.push_back(point);
}
else // Spatiotemporally point obstacles become a line
{
marker.type = visualization_msgs::Marker::LINE_LIST;
geometry_msgs::Point start;
start.x = pobst->x(); start.y = pobst->y();
start.z = 0;
marker.points.push_back(start);
geometry_msgs::Point end;
double t = 20;
Eigen::Vector2d pred;
pobst->predictCentroidConstantVelocity(t, pred);
end.x = pred[0];
end.y = pred[1];
end.z = cfg_->hcp.visualize_with_time_as_z_axis_scale * t;
marker.points.push_back(end);
}
}
// 省略 marker的着色
teb_marker_pub_.publish( marker );
visualization_
的赋值在HomotopyClassPlanner::initialize
最后的 setVisualization(visual);
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
32if (visualization_)
{
// Visualize graph
if (cfg_->hcp.visualize_hc_graph && graph_search_)
visualization_->publishGraph(graph_search_->graph_);
// Visualize active tebs as marker
visualization_->publishTebContainer(tebs_);
// Visualize best teb and feedback message if desired
TebOptimalPlannerConstPtr best_teb = bestTeb();
if (best_teb)
{
// 只发布best_teb的局部路径,类型是nav_msgs::Path 和 geometry_msgs::PoseArray
// 对应话题 local_plan 和 teb_poses
// 位姿点的z = cfg_->hcp.visualize_with_time_as_z_axis_scale *
// teb.getSumOfTimeDiffsUpToIdx(i)
visualization_->publishLocalPlanAndPoses(best_teb->teb() );
if (best_teb->teb().sizePoses() > 0) // 路径的位姿点个数
visualization_->publishRobotFootprintModel(best_teb->teb().Pose(0), *robot_model_);
if (cfg_->trajectory.publish_feedback) // feedback message
{
int best_idx = bestTebIdx();
if (best_idx>=0)
visualization_->publishFeedbackMessage(tebs_,
(unsigned int) best_idx, *obstacles_ );
}
}
}
else ROS_DEBUG("Ignoring HomotopyClassPlanner::visualize() call, since no visualization class was instantiated before.");
sensor_msgs/PointCloud2
类型如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20# N维的点云,点的数据以二进制blob形式保存,其layout用fields数组描述
# The point cloud data may be organized 2d (image-like) or 1d
# (unordered). Point clouds organized as 2d images may be produced by
# camera depth sensors such as stereo or time-of-flight.
Header header
点云的2D结构,如果点云无序,height是1,width就是点云的length
uint32 height
uint32 width
# 用二进制数据blob的形式,描述channels 及其layout
PointField[] fields
bool is_bigendian
uint32 point_step # Length of a point in bytes
uint32 row_step # Length of a row in bytes
uint8[] data # 点的数据,size = row_step * height
bool is_dense # True if there are no invalid points
PointCloud2的data是序列化后的数据,直接看不到物理意义。只能转为点云1类型
sensor_msgs/PointCloud
类型如下:1
2
3
4
5
6
7
8
9Header header
# 在header坐标系中的3D点云
geometry_msgs/Point32[] points
# Each channel should have the same number of elements as points array,
# and the data in each channel should correspond 1:1 with each point.
# Channel names in common practice are listed in ChannelFloat32.msg.
ChannelFloat32[] channels
坐标系和点云的位置关系如下,我们感兴趣的是z,也就是相机坐标系到点云的距离
从sensor_msgs::PointCloud2
类型转为sensor_msgs::PointCloud
,而且获知坐标1
2
3
4
5
6
7
8
9
10
11
12
sensor_msgs::PointCloud2 in_pcl;
sensor_msgs::PointCloud out_pcl;
sensor_msgs::convertPointCloud2ToPointCloud(in_pcl, out_pcl);
for (int i=0; i < out_pcl.points.size(); i++)
{
ROS_INFO("out_pcl x: %f, y: %f, z: %f",
out_pcl.points[i].x, out_pcl.points[i].y, out_pcl.points[i].z);
}
意外发现ROS可以直接发布pcl的类型,不必转为ROS的消息类型1
2typedef pcl::PointCloud<PointT> PointCloudT;
PCL2_publisher_ = nh.advertise<PointCloudT>("pointcloud2", 1, this);
ros在pcl_ros
中做了Publisher的多态。使得ros的标准Publisher可以接收pcl::PointCloud<T>
类型的消息,并自动转换成sensor_msgs::PointCloud2
的消息类型,再发布出去。
点云格式主要有四种,sensor_msgs::PointCloud
,也就是点云1已经弃用,主要用点云2,但是点云1有时还有用。比如点云2转为点云1消息,可看到包含多少个点,rostopic echo --noarr pcl1/points
,结果可以是<array type: geometry_msgs/Point32, length: 256000>
,可看到每个点的坐标值。
sensor_msgs::PointCloud
sensor_msgs::PointCloud2
pcl::PCLPointCloud2 — PCL data structure mostly for compatibility with ROS (I think)
pcl::PointCloud
find_package
里添加sensor_msgs
, pcl_ros
, pcl_conversions
1 | sensor_msgs::PointCloud out_pcl; |
反过来就用sensor_msgs::convertPointCloudToPointCloud2
可以使用point_cloud_converter在点云1和点云2 之间转换1
2PointCloudConverter initialized to transform from PointCloud (/points_in) to PointCloud2 (/points2_out).
PointCloudConverter initialized to transform from PointCloud2 (/camera_/depth/points) to PointCloud (/pcl)
比如 rosrun point_cloud_converter point_cloud_converter_node points2_in:=/camera/depth/points points_out:=/pcl1
1 | sensor_msgs::PointCloud2 cloud; |
1 | pcl::PCLPointCloud2 pcl_pc2; |
1 |
|
反过来就是fromROSMsg(const sensor_msgs::PointCloud2 &, pcl::PointCloud<T>&)
参考: pcl_conversions
InflationLayer的构造及初始化方法和StaticLayer类似。该层是基于已有地图进行膨胀,因此不需要有缺省值。由Layer类单独派生出InflationLayer类. 用于执行每个cell的障碍物膨胀,由于它的父类中不含Costmap2D类,所以其实膨胀层自身没有栅格地图要维护,这一点和另外两层有区别。
膨胀层所用的参数:inflation params
可以修改膨胀层代码 inflation_layer.h
, 添加函数double getInflationRadius() const
和 double getInscribedRadius() const
每个周期传感器数据进来后,都要在代价地图底层占用结构上执行标记和清除障碍操作,并且这种结构会被投影到代价地图附上相应代价值。 这完成之后,对代价赋值为costmap_2d::LETHAL_OBSTACLE的每个cell执行障碍物的膨胀操作,即从每个代价cell向外传播代价值,直到用户定义的膨胀半径为止。这里确实只需要对状态为LETHAL_OBSTACLE的cell进行膨胀操作即可。
命令的作用是打印共享库的依赖关系
ldd不是一个可执行程序,而只是一个shell脚本。ldd能够显示可执行模块的 dependency,其原理是通过设置一系列的环境变量,如下:LD_TRACE_LOADED_OBJECTS、LD_WARN、LD_BIND_NOW、LD_LIBRARY_VERSION、 LD_VERBOSE等。
ldd的工作原理,其实质是通过ld-linux.so(elf动态库的装载器)来实现的。ld-linux.so
模块会先于executable
模块程序工作,并获得控制权,因此当上述的那些环境变量被设置时,ld-linux.so选择了显示可执行模块的dependency。
实际上可以直接执行ld-linux.so模块,如:/lib/ld-linux.so.2 --list program
(这相当于ldd program)
1 | /usr/bin/ld: skipping incompatible /home/user/lib/libtinyalsa.so when searching for -ltinyalsa |
so库的架构不匹配,可以通过file name.so
查看so库的架构,也可以使用readelf
命令查看so文件架构信息的示例: readelf -h libexample.so
。输出so文件的头部信息,其中包括Machine
字段,即架构信息。Machine字段的值对应于不同的架构,比如x86、aarch64、MIPS等。
编译时常常会出错: undefine reference XXX
,显然是不识别函数,比如 _ZN9fast_gicp7NDTCudaIN3pcl9PointXYZIES2_EC1Ev
,但是这个名称看得不清晰,看不出原来的函数原型。
先执行nm -D libhdl_localization_nodelet.so | grep NDTCuda | grep EC1Ev
,输出1
U _ZN9fast_gicp7NDTCudaIN3pcl9PointXYZIES2_EC1Ev
再执行命令 c++filt _ZN9fast_gicp7NDTCudaIN3pcl9PointXYZIES2_EC1Ev
(去掉前面的 U), 输出函数原型1
fast_gicp::NDTCuda<pcl::PointXYZI, pcl::PointXYZI>::NDTCuda()
c++filt
命令确实太好用了。
有时装完某个库后,需要使用命令sudo ldconfig -v
,否则在程序运行后会出现找不到动态库的问题: error while loading shared libraries: liblog4cpp.so.5: cannot open shared object file: No such file or directory
ldconfig是一个动态链接库管理命令,为了让动态链接库为系统所共享,还需运行动态链接库的管理命令
ldconfig
通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令.主要是在默认搜寻目录(/lib
和/usr/lib
)以及动态库配置文件/etc/ld.so.conf
内所列的目录下,搜索出可共享的动态链接库(格式如lib*.so*
),进而创建出动态装入程序(ld.so
)所需的连接和缓存文件.
ldconfig与运行程序时有关,跟编译时一点关系都没有。 不管做了什么动态库的变动后,最好都ldconfig一下
这个错误应该是从别处拷贝so文件过来时出现了软链接错误。删除,重新创建这个软连接即可。
比如A,B,C的链接关系: A --> B
, B --> C
。先把C重命名,删掉A,B。重新建立软链接1
2ln -s C B
ln -s B A
base_global_planner: 指定全局规划器
base_local_planner: 指定局部规划器
recovery_behaviors: true 是否允许恢复行为,恢复行为为原地旋转。 有时候不规则的机器人做恢复行为是比较危险的,容易发生碰撞,圆形机器人不会有这个问题
shutdown_costmaps: false 当 move_base进入inactive状态时候,决定是否停用节点的costmap (bool, default: false)
controller_frequency: 20.0 控制器频率,即向基座发送速度命令的频率
planner_patience: 5.0 在空间清理操作执行前,路径规划器等待多长时间(秒)用来找出一个有效规划 (double, default: 5.0)
controller_patience: 20.0 在空间清理操作执行前,控制器会等待多长时间(秒)用来找出一个有效控制 (double, default: 15.0)
planner_frequency: 5.0 全局路径规划的更新速率,可以是0. 当机器在避障表现较好时,此参数较小即可。 如果local planner问题很大,再频繁更新全局路径,可能会离原先的最优路径越来越远,直到陷入局部障碍物
oscillation_timeout: 10.0 执行修复操作之前,允许的震荡时间是几秒
oscillation_distance: 0.5 机器人需要移动多少距离才算作没有震荡
conservative_reset_dist: 3.0 当在地图中清理出空间时候,距离机器人几米远的障碍将会从costmap清除。 实验中发现局部地图经常会有移动障碍物遗留,并且不会去除,只有在移动后才能清除,部分只能在恢复行为后清除,所以有一个插件是专门用来做local障碍物清除的
剩余参数用于无法行动之后的恢复行为,震荡表示机器人在很短的距离反复运动,可以设置距离、时间来判断是否为震荡以及多久后执行恢复操作
move_base
有很多unknown type的订阅1
2
3
4
5* /footprint [unknown type]
* /footprint_radius [unknown type]
* /move_base/cancel [unknown type]
* /move_base/goal [unknown type]
* /move_base_simple/goal [unknown type]
但是用rostopic info topic_name
查看,没有出现 unknown type
,怀疑是ROS的bug
move_base源码没有获取机器人当前位姿的地方,这属于局部路径算法的范畴。只能获得全局目标的位姿
此函数只在一个地方被调用,就是planThread()
线程被唤醒之后开始规划路径1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22boost::unique_lock<costmap_2d::Costmap2D::mutex_t> lock(*(planner_costmap_ros_->getCostmap()->getMutex() ));
plan.clear();
if(planner_costmap_ros_ == NULL) {
log.error("Planner costmap ROS is NULL, unable to create global plan");
return false;
}
//get the starting pose of the robot
tf::Stamped<tf::Pose> global_pose;
if(!planner_costmap_ros_->getRobotPose(global_pose)) {
log.warn("Unable to get starting pose of robot, unable to create global plan");
return false;
}
geometry_msgs::PoseStamped start;
tf::poseStampedTFToMsg(global_pose, start);
// 这里就是 GlobalPlanner::makePlan 了
if(!planner_->makePlan(start, goal, plan) || plan.empty()){
ROS_WARN("[move_base] Failed to find a plan to point (%.2f, %.2f)",
goal.pose.position.x, goal.pose.position.y);
return false;
}
return true;
代码并不复杂, 最后实际就进入bool GlobalPlanner::makePlan(const geometry_msgs::PoseStamped& start, const geometry_msgs::PoseStamped& goal, double tolerance, std::vector<geometry_msgs::PoseStamped>& plan)
这里要参考全局路径规划(二) makePlan
req参数包含了起点和目标信息,这个服务回调函数的的核心是planner_->makePlan
move_base server必须在inactive状态,否则不向下执行
判断global planner
的costmap是否存在;若req给定机器人初始位姿则使用,否则使用getRobotPose
获得机器人所在位置做初始位姿
调用clearCostmapWindows
完成对机器人区域的clear, clear_radius由参数设置
调用 if(!planner_->makePlan(start, req.goal, global_plan) || global_plan.empty())
这是完成plan计算的核心部分。判断这个调用是否成功,如果失败,则在目标区域附近搜索,多次更改req.goal的值,并重新调用makePlan;新的goal如下图:
各个可能位置之间的水平和竖直间距和req.tolerance
正相关
如果还是失败,则此次路径规划无解。如果成功,直接break循环,把原来无法达到的目标点插入global_plan
容器的最后,local planner
一般会从新的目标点导航到原目标点.
global_plan
拷贝到resp.planplanService·
用到了clearCostmapWindows
函数,它只被调用了这一次,过程如下:
planner_costmap_ros_->getRobotPose(global_pose);
获取在全局地图的global_pose以这个点为中心,找到以size_x和size_y为边长的矩形的四个顶点
调用planner_costmap_ros_->getCostmap()->setConvexPolygonCost(clear_poly, costmap_2d::FREE_SPACE);
完成对机器人所在区域的clear工作
以上同样的操作在controller_costmap_ros
上也操作一遍,这样globa costmap 和local costmap都已经在机器人所在的区域完成clear工作
障碍层地图通过订阅传感器话题,将传感器输出的障碍物信息存进buffer(剔除过高、过远的点),在本层地图上将观测到的点云标记为障碍物,将传感器到点云的连线上的点标记为FREE_SPACE
。最后在bound范围内,将本层地图合并到主地图上。
通用代价地图中的障碍层常常是这样设置的: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
26obstacles:
enabled: true
obstacle_range: 2.0
raytrace_range: 5.0
track_unknown_space: true
combination_method: 1
observation_sources: laser_scan_sensor pointcloud2_sensor
laser_scan_sensor:
data_type: LaserScan
topic: /scan
marking: true
clearing: true
max_obstacle_height: 1.0
min_obstacle_height: -1.0
pointcloud2_sensor:
topic: /people_cloud
data_type: PointCloud2
sensor_frame: "/base_link"
obstacle_range: 5.0
raytrace_range: 5.0
observation_persistence: 5.0
marking: true
clearing: true
min_obstacle_height: 0.0
max_obstacle_height: 2.0
observation_persistence: 2.0. 如果设置为0,代价地图只考虑最近的观测数据
min_obstacle_height: 传感器读数的最小高度(以米为单位)视为有效。通常设置为地面高度。决定是否将地面的部分加入到代价地图,默认0
车周围实际没有障碍物,不同参数的情况如下:
这种情况下,避障太保守了。
max_obstacle_height: 默认2,单位米。插入代价地图的障碍物的最大高度,应当比机器人高度稍微高一点,应该是用于有机械臂的情况。设置为大于全局max_obstacle_height
参数的值将会失效,设置为小于全局max_obstacle_height
的值将过滤掉传感器上大于该高度以的点。
obstacle_range: 设置机器人检测障碍物的最大范围,意思是说超过该范围的障碍物,并不进行检测,只有靠近到该范围内才把该障碍物当作影响路径规划和移动的障碍物。对能否及早发现障碍物至关重要 ,跟车速也有关系。 如果车速0.6,obstacle_range
设置为1.5,连正常避障都实现不了,至少也得3
raytrace_range: 在机器人移动过程中,实时清除代价地图上的障碍物的最大范围,更新可自由移动的空间数据。假如设置该值为3米,那么就意味着在3米内的障碍物,本来开始时是有的,但是本次检测却没有了,那么就需要在代价地图上来更新,将旧障碍物的空间标记为可以自由移动的空间
增大raytrace_range
, obstacle_range
会增大数据量,影响代价地图刷新
inf_is_valid
: 默认是false,但是对点云障碍无效,仅对雷达scan有效:1 | if (inf_is_valid) |
障碍层所用的参数:obstacles params
调来调去,发现min_obstacle_height
还是要设置为-0.3
比较合适,这时因为地面的原因,导致局部代价地图的四角都有了障碍,因此再降低obstacle_range
避免这一现象
costmap raytraces to clear out obstacles. 障碍物要想从地图上清除,它所占据的空间必须被新的观测看到。但是参数observation_persistence
常常无效。
last_updated_
is updated on every new cloud that needs to be added to the buffer. purgeStaleObservations
函数 checks the cloud’s time stamp with last_updated_
. When we stop publishing a point cloud last_updated_
will never be updated. Hence the observations will not time-out and remain in the observation_list_
. This may result in the following warning, when the robot drives away from the clouds in observation_list_
: Sensor origin at (x, y) is out of map bounds. The costmap cannot raytrace for it
将文件observation_buffer.cpp
中的函数purgeStaleObservations
中的last_updated_
改为nh_->now()
,会影响共享库liblayers.so
和 libcostmap_2d.so
1
2nav2_util::LifecycleNode::SharedPtr nh
last_updated_(nh->now())