障碍层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,又使机器人表现卡顿。


障碍层5 raytraceFreespace从代价地图清除障碍

先提一下栅格地图的占用值更新的方式。当雷达扫到一个障碍时,会对击中点的栅格更新一次占用,这时在ROS地图下这个点将被表示为障碍物.

如果ROS中设置障碍物的occupied_thresh是0.65,free_thresh是0.25。第一次更新地图时,这个格子的占用值为 ,就会将ROS地图中这个格子设置为100,表示障碍物.如果这个障碍离开了这个位置,在之后的过程中,如果有4次雷达扫描这个栅格都是空闲状态,也就是没有障碍物,则这个格子的占用值将变为 ,所以就会将ROS地图中对应的栅格设置为0,变成可通过区域了.

这就导致了不能通过一次两次的更新之后的值,来代表这个栅格是占用还是空闲的,因为这个状态十分有可能是错的.只有通过长时间的累计之后,最后通过这个值是否大于零,来近似地将这个栅格设置为是否是障碍物,这时候依然不能百分百地确定这个栅格是否就是障碍物.

在上一篇的UpdateBounds函数最后到了

1
2
3
4
for (unsigned int i = 0; i < clearing_observations.size(); ++i)
{
raytraceFreespace(clearing_observations[i], min_x, min_y, max_x, max_y);
}

流程

清理传感器到障碍物间的cell,会首先处理测量值越界的问题,然后调用

1
2
3
4
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);
updateRaytraceBounds(ox, oy, wx, wy, clearing_observation.raytrace_range_, min_x, min_y, max_x, max_y);

最终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 :
1
2
3
4
5
6
7
double px = cloud.points[i].x, py = cloud.points[i].y, pz = cloud.points[i].z;

......省略

unsigned int index = getIndex(mx, my);
costmap_[index] = LETHAL_OBSTACLE;
touch(px, py, min_x, min_y, max_x, max_y);

1
2
3
4
5
6
7
8
9
10
/*
* @brief Given distance in the world, convert it to cells
* @param world_dist: The world distance
* @return The equivalent cell distance
*/
unsigned int Costmap2D::cellDistance(double world_dist)
{
double cells_dist = max(0.0, ceil(world_dist / resolution_));
return (unsigned int)cells_dist;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MarkCell
{
public:
MarkCell(unsigned char* costmap, unsigned char value) :
costmap_(costmap), value_(value)
{
}
inline void operator()(unsigned int offset)
{
costmap_[offset] = value_;
}
private:
unsigned char* costmap_;
unsigned char value_;
};

updateCosts

代价地图中每个cell可用255个不同值中任何一个值,可是下层数据结构仅需要3个值。具体来说,每个cell仅需要3个值来表示cell的3种状态:free,occupied,unknown。 当投影到代价地图时候,每种状态被赋一个特定的代价值,也就是说 每个cell的cost值是由这个cell对应的各层中对应的cell的状态进行加权得到的。 如果列有一定量的占用就被赋代价值。 如果存储的障碍物信息是3D的,需要将每一列的障碍物信息投影成2D后才能放入到代价地图。

更新障碍地图代价,将机器人足迹范围内设置为 FREE_SPACE,并且在 bound 范围内将本层障碍地图的内容合并到主地图上。

障碍物层是将传感器检测到的点云投影到地图中,不同的传感器来源会分别存到Observation类中。只要遍历存储Observation的容器,将检测到的点云的每个点分别投影到地图中,将对应的网格的代价值设为LETHAL_OBSTACLE,实现函数在ObstacleLayer::updateBounds中:

清除代价地图的方式也很好理解,以单线激光雷达的激光线为例子,当激光线发射出去之后,在某处检测到一个障碍物,那说明:从发射的地方至某处之间是free的,那么这之间的旧的障碍物应当被删除,这之间网格的代价值应当被修改为free

可能是Bresenham2D的round off error(舍入误差)造成的. It marks a particular cell as obstacle but the next time when the sensor reading changes instantaneously, it no longer traces it through the same path and hence, the blob seem remains in the costmap.

added 2 lines of code to the Bresenham2D algorithm. This basically clears the cell to the left and right of the grid through which the Line segment constructed by the algorithm passes. This results in loosing some resolution of the map, but the solution works pretty well in real life application

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// A 2D implementation of Bresenham's raytracing algorithm, applies an action 
// at each step template<class ActionType>
inline void bresenham2D( ActionType at, unsigned int abs_da, unsigned int abs_db,
int error_b, int offset_a, int offset_b, unsigned int offset,
unsigned int max_length)
{
unsigned int end = std::min(max_length, abs_da);
for (unsigned int i = 0; i < end; ++i)
{
at(offset);
at(offset+1); // **ADDED THIS LINE**
at(offset-1); // **ADDED THIS LINE**
offset += offset_a;
error_b += abs_db;
if ((unsigned int)error_b >= abs_da)
{
offset += offset_b;
error_b -= abs_da;
}
}
at(offset);
}

map_server加载不同大小地图的影响
abstract Welcome to my blog, enter password to read.
Read more
xsens IMU的使用

roslaunch xsens_driver xsens_driver.launch

节点xsens_driver发布话题:

  • /diagnostics [diagnostic_msgs/DiagnosticArray]
  • /imu/data [sensor_msgs/Imu], 被节点data_pretreat_node订阅
  • /imu_data_str [std_msgs/String]
  • /time_reference [sensor_msgs/TimeReference]
  • /velocity [geometry_msgs/TwistStamped]

xsens.bag对应车先向前行驶,然后原地转约180°,然后再往回走。

imu数据占用的带宽.png

参考:
基于ros melodic 及 MTI-G-710测试
Xsens MTi -1 姿态传感器恢复与MTI的通信的方法


python中使用ROS的注意点

python2.7和python3的冲突问题

之前在ROS安装和编译等常见问题.md提到了python3.8和python2.7冲突的问题,那次是不同文件可以分别用不同版本的python运行,但是如果同一个文件里用到了python的两个版本,那就不好解决了。

比如一个文件需要用到python3的函数,然后发现import tf报错,没法用tf了,因为tf2_ros是针对python2编译的,为了适应python3 (melodic),进行如下步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sudo apt update
sudo apt install python3-catkin-pkg-modules python3-rospkg-modules python3-empy
Prepare catkin workspace

mkdir -p ~/catkin_ws/src; cd ~/catkin_ws
catkin_make
source devel/setup.bash
wstool init
wstool set -y src/geometry2 --git https://github.com/ros/geometry2 -v 0.6.5
wstool up
rosdep install --from-paths src --ignore-src -y -r
Finally compile for Python 3

catkin_make --cmake-args \
-DCMAKE_BUILD_TYPE=Release \
-DPYTHON_EXECUTABLE=/usr/bin/python3 \
-DPYTHON_INCLUDE_DIR=/usr/include/python3.6m \
-DPYTHON_LIBRARY=/usr/lib/x86_64-linux-gnu/libpython3.6m.so
source devel/setup.bash

让python支持中文注释

在源文件的初始部分,而且必须放在第一行,添加#coding=utf-8 或者 #coding=gbk# -- coding: gb2312 --

参考:
ImportError: dynamic module does not define module export function (PyInit__tf2)


全局路径不正常的规划失败

所用全局路径的参数

1
2
3
4
5
6
planner_window_x: 0.0
planner_window_y: 0.0
default_tolerance: 0.0

publish_scale: 100
planner_costmap_publish_frequency: 0.0

全局路径很奇怪.png
全局路径失败时的报错.png
全局路径规划失败

看代码GlobalPlanner::makePlan中的if (found_legal)部分,再看GlobalPlanner::getPlanFromPotential函数,继续定位到path_maker_->getPath。这里就需要看path_maker是什么东西,在文件中发现

1
2
3
4
5
6
if (use_grid_path)   // 参数
//栅格路径,从终点开始找上下或左右4个中最小的栅格直到起点
path_maker_ = new GridPath(p_calc_);
else
//梯度路径,从周围八个栅格中找到下降梯度最大的点
path_maker_ = new GradientPath(p_calc_);

use_grid_path默认是false,所以一般用GradientPath,最终看GradientPath::getPath为什么返回false

if (fabs(nx - start_x) < .5 && fabs(ny - start_y) < .5) 中的0.5改为1

查来查去,终于发现其实就是全局代价地图里,本来就处于障碍的附近,所以规划如此奇怪。只是换到了室外环境,不习惯看全局代价地图了


message_filters::Synchronizer进行时间同步

多传感器数据融合的时候,由于各个传感器采集数据的频率的不同,例如odom 50Hz、Imu 100Hz、camera 25Hz,需要将传感器数据进行时间同步后才能进行融合。

分别订阅不同的需要融合的传感器的主题,通过TimeSynchronizer统一接收多个主题,只有在所有的topic都有相同的时间戳时,才会产生一个同步结果的回调函数,在回调函数里处理同步时间后的数据。

注意: 只有多个主题都有数据的时候才可以触发回调函数。如果其中一个主题的发布节点崩溃了,则整个回调函数永远无法触发回调。 频率一般趋于和最低的频率一样。 当多个主题频率一致的时候,回调函数的频率(融合后的频率)可能会小于订阅主题的频率。

message_filters::Synchronizer省去了不同线程(回调函数)之间的同步问题,两线程的数据直接放到一个回调里处理。 但是它对传感器数据进行时间同步后,回调函数里的两个话题的时间戳会完全一样,纳秒级的相同。这是经过了ROS的调整,但有时候可能不是我们想要的。

对齐传感信息时间戳有两种方式, 一种是时间戳完全对齐 ExactTime Policy, 另一种是时间戳相近 ApproximateTime Policy, 前者更为严格, 但有时会没有结果而无法进行回调函数。所以一般还是用后者

message_filters
message_filters::Synchronizer的使用
传感器数据之间的时间同步问题


octomap_server

octomap_server除了将点云地图转化为基于Octree的OctoMap,还能将点云地图转化为二维地图。可以增量建3D的 OctoMaps,还提供了能保存地图的节点octomap_saver. octomap_server节点很消耗内存,增量建图的过程中内存持续上升,因为当中不存在内存释放,没有静态储存八叉树图信息

1
2
3
4
5
6
7
8
9
10
11
12
<node pkg="octomap_server" type="octomap_server_node" name="octomap_server">
<!-- resolution in meters per pixel -->
<param name="resolution" value="0.05"/>
<!-- name of the fixed frame, needs to be "/map" for SLAM -->
<param name="frame_id" type="string" value="$(arg frame_id)"/>
<param name="sensor_model/max_range" value="150.0"/>
<param name="latch" value="true"/>
<!-- 需要和 publish_pointcloud 的frame_id相同,否则会导致Octomap没有发布数据 -->
<!-- topic from where pointcloud2 messages are subscribed,改为自己的点云话题 -->
<remap from="/cloud_in" to="$(arg cloud_topic)"/>
<remap from="/projected_map" to="$(arg map_topic)"/>
</node>

也就是将点云数据发布到一个指定的 topic 上,然后调用 Octomap 在ROS下的srv组件进行实时转换,并发布到另外一个 Octomap topic 上去,最后通过rviz 进行显示Octomap。注意octomap的输入话题(topic)和数据的坐标系(frame_id)两个参数的设置, 通常octomap 没有数据输出都是由于这两个参数设置错误导致的。

Rviz配置

安装octomap-rviz-plugins后,打开rviz,这时候点开Add选项,会多一个octomap_rviz_plugins模组。 其中的OccupancyGrid是显示三维概率地图,也就是octomap地图。OccupancyMap是显示二维占据栅格地图。

打开rviz,在里面添加OccupancyGrid,OccupancyMap,Map显示选项,显示话题选择octomap_full或者octomap_binary

octomap topics are 3D occupancy maps, which would be used by some other 3D planning approach (e.g., move_it). They are not the same as /grid_map or /proj_map from rtabmap_ros. For 3D trajectories (having to move in Z, e.g., quadcopter), then using OctoMap with OMPL/move_it can be another approach, though not sure if there is one most popular approach for the 3D case.

参考:
Octomap在ROS环境下实时显示,但点云来源是ORB稠密点云