命名空间

命名空间的三级参数如何设置

参数 /robot_pose 的设置

1
2
ros::NodeHandle  nh;
nh.getParam("robot_pose", robot_pose_ );

/move_base/controller_frequency的设置

1
2
3
4
// 在 move_base 节点内
ros::NodeHandle n("~/");
double control_frequency;
n.param("controller_frequency", control_frequency, 15.0);

参数 /move_base/Planner/linear_vel 的设置

1
2
3
// 在 move_base 节点内
ros::NodeHandle nh("~/" + string("Planner") );
nh.param("linear_vel", linear_vel_, 0.3);


1
2
3
4
5
ros::init(argc, argv, "my_node_name");
ros::NodeHandle nh1("~");
ros::NodeHandle nh2("~foo");
ros::Subscriber sub1 = nh1.subscribe("topic1", ...);
ros::Subscriber sub2 = nh2.subscribe("topic2", ...);

平时对nh,是用无参的构造函数,此时对应的话题是topic1。 加上~后,sub1订阅的话题是my_node_name/topic1。如果不是~,而是word,话题是/word/topic1

对于sub2,命名空间是~foo,相当于上面两种的组合,订阅的话题是my_node_name/foo/topic2

当然可以在launch文件修改命名空间的第一级,也就是 <node pkg="package", name="my_node", type="my_bin" />


cmake教程(六)add_dependencies规定编译依赖项

ROS的工作空间用的时间一长,就会创建很多package,有些package的编译又用到了其他的package,这时单纯使用catkin_make就会出现问题。比如package A用到了B,B里又用到了C。以往使用catkin_make --pkg编译,但是可能会需要把所有的package都处理一遍,此时处理到A时就会报错,也就无法编译B和C。比较笨的方法就是在A的CMakeLists里先把B和C注释掉,同样在B的CMakeLists里把A注释掉,先用catkin_make --pkg C编译C,再依次编译B和A。

经过研究发现使用add_dependencies就可以解决这个问题,但是不能解决find_package的问题

在A的CMakeLists里这样写:

1
2
3
4
5
find_package(catkin REQUIRED COMPONENTS B C)

add_executable(A_program src/A_program.cpp)
add_dependencies(A_program B C ) # B C是package名称,不是编译结果的名称
target_link_libraries(A_program ${catkin_LIBRARIES} B_program C_program)

add_dependencies必须在add_executable之后,可以在target_link_libraries之前或之后。 这样在编译A的时候,会出现一句scanning dependencies,这时编译器就会注意到A的依赖项,等依赖项完成后再编译A。同样在B的CMakeLists里添加C作为依赖项。



有些package需要依赖自定义的消息或服务文件,也就是一个特殊的package,这就更简单了,有专门的命令: add_dependencies(your_program ${catkin_EXPORTED_TARGETS}),不必用专门的package名称,用message函数可以看这个宏的内容,它是一大堆消息和服务名称。这样在编译的时候会出现这样的信息:

现在编译就快捷多了,直接用catkin_make就可以编译整个工作空间,不必自己改编译顺序了。


激光SLAM综述

机器人依据自由度的可控性(controllable)划分为nonholonomic或者holonomic robot

  • holonomic robot: 在所有的自由度上都可控的,比如基于万向轮的机器人(robot built on Omni-wheels)

  • nonholonomic robot: 差速车或阿克曼车。可控的自由度维数小于机器人自身的自由度,比如car自身有3个自由度(x,y,theta),即平面坐标和车身朝向,但car可控的自由度是2(加减速度,转动方向盘,不能随意的漂移运动)

从另一个直观通俗的角度可以理解为:如果机器人可以在N维空间中朝任意方向移动,则对于N维空间是完整性的。机器人在N维空间中不能朝任意方向移动,则认为是非完整性约束。

2D激光SLAM所有流派

传感器

IMU:

  • 直接测量角速度和线加速度
  • 角速度的测量精度高

轮式里程计:

  • 直接测量机器人的位移和角度
  • 较高的局部角度和位置测量精度
  • 更新速度高 (100Hz~200Hz)

传感器辅助(odom+IMU)

  • 极高的位姿更新频率,可以比较准确反应运动情况
  • 高精度的局部位姿估计
  • 跟状态估计解耦,所有信息都来自传感器,不需要匹配


数据预处理:

  • 轮式里程计的标定:轮式里程计的机械标称值并不代表真实值,实际误差可能较大
  • 激光雷达运动畸变去除:每一帧激光数据的采集需要时间,在采集期间如果机器人运动会使测量值产生畸变。运动产生的畸变会让数据严重失真,影响匹配精度。
  • 不同系统之间时间同步(不同CPU)

实际环境中的问题:

  • 大尺度的动态环境,即有高动态(行走的人和移动前后的物体)
  • 环境变化

  • 几何结构相似的环境,比如长走廊

  • 全局定位:信息量小

  • 地面材质的变化

  • 地面凹凸不平
  • 机器人载重改变(与前两条一起影响里程计)

后面几条为传感器融合可以解决的问题

激光slam的问题:

  • 退化环境: 对于2D激光雷达来说是hallway走廊; 对于3D激光雷达来说,很空旷的环境
  • 地图的动态更新
  • 全局地位
  • 动态环境定位:动态物体检测与跟踪解决

实际问题

  • 环境中的透明玻璃和网状结构,怎么解决
    玻璃也有一定的反射率,大约10%。建图之后,可以直接把玻璃漏光部分直接编辑为黑线。

  • 多线激光常见方案:NDT-mapping和LOAM。3D激光SLAM一般与视觉融合,这是常用方法。LOAM不适合2D激光

  • 主流的激光SLAM可以在普通ARM CPU上实时运行。激光雷达主动发射,在较多机器人时可能产生干扰。尤其是固态激光雷达的大量使用,可能使得场景中充满了信号污染,从而影响激光SLAM的效果。

  • 非结构化环境:不存在特定的平面,比如马路(特定的平面和标志)。

  • 开发农作物网面的识别和采集地图,需要用激光雷达好还是用3D摄像头来开发(采集环境信息然后写识别算法去识别需要找的农作物目的)好?

因为农作物的环境比较复杂,准确度要求也比较高,所以使用Realsense,16线雷达的数据不够,32线又太贵了

机器人绑架问题

机器人定位分为三大问题:全局定位位姿跟踪绑架劫持

  • 全局定位:初始位置未知,机器人靠自身运动确定自己在地图中的位姿。

  • 位姿跟踪:已知自身位姿或者已经通过全局定位得到了一个较好的位姿估计初值,在后续运动时补偿精度较差的运动控制误差

  • 绑架劫持:机器人在已知自身位姿的情况下,得到了一个错误的位姿或者外界将其放到另外一个位姿,而里程计信息给出了错误的信息甚至没有给出控制信息。

最典型的粒子就是人把机器人搬走,机器人就找不到自己位置。这时用激光和里程计都不起作用了,可以用视觉辅助定位。用摄像头建一个特征地图,视觉SLAM和激光SLAM并行使用,激光SLAM失效时换视觉。比如使用二维码全局定位。

另外在绑架发生之前,保存当前状态,则可以使用除视觉传感器之外的传感器,比如反光板


move_base分析(四) planThread线程

MoveBase::planThread()

三个函数唤醒了planThread线程: MoveBase::wakePlanner MoveBase::executeCb MoveBase::executeCycle

MoveBase::planThread()

第一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ros::NodeHandle n;
ros::Timer timer;
bool wait_for_wake = false; // 先不唤醒线程
boost::unique_lock<boost::recursive_mutex> lock(planner_mutex_);
while(n.ok() )
{
// check if we should run the planner (the mutex is locked)
while(wait_for_wake || !runPlanner_){
//if we should not be running the planner then suspend this thread
log.debug("[move_base_plan_thread] %s","Planner thread is suspending");
planner_cond_.wait(lock);
wait_for_wake = false;
}
// 开启线程
ros::Time start_time = ros::Time::now();
//time to plan! get a copy of the goal and unlock the mutex
geometry_msgs::PoseStamped temp_goal = planner_goal_;
lock.unlock();
ROS_DEBUG_NAMED("move_base_plan_thread", "Planning...");
// run planner
planner_plan_->clear();
bool gotPlan = n.ok() && makePlan(temp_goal, *planner_plan_);
......
}

前面就是定义一些变量和unique_lock,然后进入一个大while循环,嵌套一个while循环,由于runPlanner_在构造函数里声明为false,所以条件为真,直接锁住了这个线程,wait_for_wake仍为false

move_base还未完全启动时,线程planThread就开始运行了,然后才会从地图数据加载代价地图。所以在启动时,不要在线程里访问一些未初始化的变量。

线程一直运行在while循环

1
2
3
while(wait_for_wake || !runPlanner_ )
{
}

我在move_base里定义了一个发布者pub,如果在这个while循环里执行pub.publish(msg)move_base就会出错退出。可以使用智能指针,其实Publisher该有个函数,比如isValid来验证是否可用。我发现用getTopic()函数也可以,如果结果不是空,说明发布者和roscore的通信成功了,此时可以调用publish函数了。

在上面的三个函数唤醒线程后,就往下运行,获得临时目标点,clear planner后,进入makePlan

第二部分

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
if(gotPlan)
{
ROS_DEBUG_NAMED("move_base_plan_thread","Got Plan with %zu points!", planner_plan_->size());
//pointer swap the plans under mutex (the controller will pull from latest_plan_)
std::vector<geometry_msgs::PoseStamped>* temp_plan = planner_plan_;

lock.lock();
// 交换变量,赋值给 latest_plan_
planner_plan_ = latest_plan_;
latest_plan_ = temp_plan;
last_valid_plan_ = ros::Time::now();
planning_retries_ = 0;
new_global_plan_ = true;

//make sure we only start the controller if we still haven't reached the goal
if(runPlanner_)
state_ = CONTROLLING;
if(planner_frequency_ <= 0)
runPlanner_ = false;
lock.unlock();
}
// 如果没找到路径,而且还在 PLANNING 状态(机器人不动)
else if(state_==PLANNING)
{
ros::Time attempt_end = last_valid_plan_ + ros::Duration(planner_patience_);
//check if make a plan for over time limit or maximum number of retries
lock.lock();
// lock是用于这个非原子操作
planning_retries_++;
// 如果参数 max_planning_retries还是默认值 -1,那转为uint32_t就是一个很大的数,就是不考虑重试次数
if(runPlanner_ &&
(ros::Time::now() > attempt_end || planning_retries_ > uint32_t(max_planning_retries_)) )
{
// 进入 CLEARING 状态
state_ = CLEARING;
runPlanner_ = false; // proper solution for issue #523
publishZeroVelocity(); // 车不准动
recovery_trigger_ = PLANNING_R;
}
lock.unlock();
}
//if can't get new plan, robot will move with last global plan
// 其实跟上面的情况一样
else if(state_ == CONTROLLING)
{
movebase_log.warn("Robot in controlling, but can't get new global plan ");
publishZeroVelocity();
state_ = CLEARING;
recovery_trigger_ = PLANNING_R;
}
//take the mutex for the next iteration
lock.lock();
if(planner_frequency_ > 0)
{
ros::Duration sleep_time = (start_time + ros::Duration(1.0/planner_frequency_)) - ros::Time::now();
if (sleep_time > ros::Duration(0.0) )
{
wait_for_wake = true;
// wakePlanner的函数只有一句: planner_cond_.notify_one();
timer = n.createTimer(sleep_time, &MoveBase::wakePlanner, this);
}
}

最后的部分就是根据全局路径规划频率,调用makePlan的地方。


move_base分析(三)源码和调用流程

move_base_node.cpp

先来看move_base_node.cpp的源码,我们实际需要的程序是这个:

1
2
3
4
5
6
7
8
ros::init(argc, argv, "move_base_node");
ros::console::set_logger_level(ROSCONSOLE_DEFAULT_NAME,ros::console::levels::Debug);

tf::TransformListener tf(ros::Duration(10));
move_base::MoveBase move_base( tf );
ros::spin();

return(0);

MoveBase类

MoveBase构造函数

MoveBase构造函数开始对一些成员进行了列表初始化,没有值得说明的

第一部分

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
//  枚举变量RecoveryTrigger,可取值有PLANNING_R, CONTROLLING_R, OSCILLATION_R
recovery_trigger_ = PLANNING_R;

// as_维护movebase的actionServer状态机,并且新建了一个executeCb线程
as_ = new MoveBaseActionServer(ros::NodeHandle(), "move_base", boost::bind(&MoveBase::executeCb, this, _1), false);

ros::NodeHandle private_nh("~");
ros::NodeHandle nh;
// 给一系列参数设置默认值
std::string global_planner, local_planner;
private_nh.param("base_global_planner", global_planner, std::string("navfn/NavfnROS"));
private_nh.param("base_local_planner", local_planner, std::string("base_local_planner/TrajectoryPlannerROS"));
private_nh.param("global_costmap/robot_base_frame", robot_base_frame_, std::string("base_link"));
private_nh.param("global_costmap/global_frame", global_frame_, std::string("/map"));
private_nh.param("planner_frequency", planner_frequency_, 0.0);
private_nh.param("controller_frequency", controller_frequency_, 20.0);
private_nh.param("planner_patience", planner_patience_, 5.0);
private_nh.param("controller_patience", controller_patience_, 15.0);
private_nh.param("max_planning_retries", max_planning_retries_, -1);
private_nh.param("oscillation_timeout", oscillation_timeout_, 0.0);
private_nh.param("oscillation_distance", oscillation_distance_, 0.5);

// set up plan triple buffer, 声明3个vector指针
// 三个plan都是global_plan,最终由controller_plan_ 传给local planner: if(!tc_->setPlan(*controller_plan_))
planner_plan_ = new std::vector<geometry_msgs::PoseStamped>();
latest_plan_ = new std::vector<geometry_msgs::PoseStamped>();
controller_plan_ = new std::vector<geometry_msgs::PoseStamped>();

第二部分

1
2
3
4
5
6
7
8
9
10
11
12
13
//建立planner线程
planner_thread_ = new boost::thread(boost::bind(&MoveBase::planThread, this));

//用于控制机器人底座
vel_pub_ = nh.advertise<geometry_msgs::Twist>("cmd_vel", 1);
current_goal_pub_ = private_nh.advertise<geometry_msgs::PoseStamped>("current_goal", 0 );

ros::NodeHandle action_nh("move_base");
action_goal_pub_ = action_nh.advertise<move_base_msgs::MoveBaseActionGoal>("goal", 1);

// we'll provide a mechanism for some people to send goals as //PoseStamped messages over a topic, they won't get any useful //information back about its status, but this is useful for tools like rviz
ros::NodeHandle simple_nh("move_base_simple");
goal_sub_ = simple_nh.subscribe<geometry_msgs::PoseStamped>("goal", 1, boost::bind(&MoveBase::goalCB, this, _1));

先是运行了一个线程planner_thread_,详细看MoveBase::planThread()。接下来一共涉及了4个ros::NodeHandle对象,注册了3个话题,订阅了话题goal,订阅话题的回调函数是MoveBase::goalCB

第三部分

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
// /机器人几何尺寸有关参数  we'll assume the radius of the robot to be consistent with what's specified for the costmaps
private_nh.param("local_costmap/inscribed_radius", inscribed_radius_, 0.325);
private_nh.param("local_costmap/circumscribed_radius", circumscribed_radius_, 0.46);
private_nh.param("clearing_radius", clearing_radius_, circumscribed_radius_);
private_nh.param("conservative_reset_dist", conservative_reset_dist_, 3.0);
private_nh.param("shutdown_costmaps", shutdown_costmaps_, false);
private_nh.param("clearing_rotation_allowed", clearing_rotation_allowed_, true);
private_nh.param("recovery_behavior_enabled", recovery_behavior_enabled_, true);

//创建planner's costmap的ROS封装类, 初始化一个指针,用于underlying map
planner_costmap_ros_ = new costmap_2d::Costmap2DROS("global_costmap", tf_);
planner_costmap_ros_->pause();

//初始化global planner
try {
// boost::shared_ptr<nav_core::BaseGlobalPlanner> planner_;
// 模板类ClassLoader,实例化 BaseGlobalPlanner
planner_ = bgp_loader_.createInstance(global_planner);
planner_->initialize(bgp_loader_.getName(global_planner), planner_costmap_ros_);
log.info("Created global_planner %s", global_planner.c_str());
} catch (const pluginlib::PluginlibException& ex) {
log.fatal("Failed to create the %s planner, are you sure it is properly registered and that the containing library is built? Exception: %s", global_planner.c_str(), ex.what());
exit(1);
}

//创建controller's costmap的ROS封装类, 初始化一个指针,用于underlying map
controller_costmap_ros_ = new costmap_2d::Costmap2DROS("local_costmap", tf_);
controller_costmap_ros_->pause();

//初始化local planner
try {
tc_ = blp_loader_.createInstance(local_planner);
log.info("Created local_planner %s", local_planner.c_str());
tc_->initialize(blp_loader_.getName(local_planner), &tf_, controller_costmap_ros_);
} catch (const pluginlib::PluginlibException& ex) {
log.fatal("Failed to create the %s planner, are you sure it is properly registered and that the containing library is built? Exception: %s", local_planner.c_str(), ex.what());
exit(1);
}
// 开启costmap基于传感器数据的更新
planner_costmap_ros_->start();
controller_costmap_ros_->start();

//advertise a service for getting a plan
make_plan_srv_ = private_nh.advertiseService("make_plan", &MoveBase::planService, this);

//advertise a service for clearing the costmaps
clear_costmaps_srv_ = private_nh.advertiseService("clear_costmaps", &MoveBase::clearCostmapsService, this);
// if we shutdown our costmaps when we're deactivated... we'll do that now
if(shutdown_costmaps_){
log.debug("[move_base] %s","Stopping costmaps initially");
planner_costmap_ros_->stop();
controller_costmap_ros_->stop();
}

planner_costmap_ros_ 用于全局导航的地图
controller_costmap_ros_ 用于局部导航的地图

GlobalPlanner类,继承nav_core::BaseGlobalPlanner, Provides a ROS wrapper for the global_planner planner which runs a fast, interpolated navigation function on a costmap.

第四部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 载入recovery behavior, 这部分是状态机的设计问题,优先加载用户的设置,如果没有则加载默认设置
if(!loadRecoveryBehaviors(private_nh)){
loadDefaultRecoveryBehaviors();
}
//initially, we'll need to make a plan
state_ = PLANNING;

//we'll start executing recovery behaviors at the beginning of our list
recovery_index_ = 0;

// start the action server
as_->start();

dsrv_ = new dynamic_reconfigure::Server<move_base::MoveBaseConfig>(ros::NodeHandle("~"));
dynamic_reconfigure::Server<move_base::MoveBaseConfig>::CallbackType cb = boost::bind(&MoveBase::reconfigureCB, this, _1, _2);
dsrv_->setCallback(cb);

MoveBase::executeCb

每次有新的goal到来都会执行此回调函数, 如果有新目标到来而被抢占则唤醒planThread线程, 否则为取消目标并挂起处理线程

MoveBase::executeCycle

local planner的核心工作函数, 只在MoveBase::executeCb中调用一次,它是局部路径规划的工作函数,其中会调用computeVelocityCommands来规划出当前机器人的速度


局部路径规划
1
private_nh.param("base_local_planner", local_planner, std::string("base_local_planner/TrajectoryPlannerROS"));

local planner

根据附近的障碍物进行躲避路线规划,只处理局部代价地图中的数据. 是利用base_local_planner包实现的。该包使用Trajectory RolloutDynamic Window approaches算法计算机器人每个周期内应该行驶的速度和角度。

对于全向机器人来说,也就是存在x方向的速度,y方向的速度,和角速度。DWAlocalplanner确实效率高一点。但是如果是非全向机器人,比如说只存在角速度和线速度,trajectorylocalplanner会更适用一点。

base_local_planner这个包通过地图数据,通过算法搜索到达目标的多条路经,利用一些评价标准(是否会撞击障碍物,所需要的时间等等)选取最优的路径,并且计算所需要的实时速度和角度。
其中,Trajectory Rollout 和Dynamic Window approaches算法的主要思路如下:

  1. 采样机器人当前的状态(dx,dy,dtheta);
  2. 针对每个采样的速度,计算机器人以该速度行驶一段时间后的状态,得出一条行驶的路线。
  3. 利用一些评价标准为多条路线打分。
  4. 根据打分,选择最优路径。
  5. 重复上面过程。



发送导航目标点后,首先在rviz上出现一个绿色全局路径,机器人开始行走,同时还有一个蓝色的本地路径,只是很小,不容易观察到,示意图如下:

现在机器人正在按照规划的全局路径行走,地图中突然增加了一个障碍物,它不会进入全局代价地图,但会在局部代价地图中创建.所以全局路径开始没有改变,当它快接近障碍物时,局部路径检测到了它,进而会重新规划一个全局路径,这样避开了障碍物. 局部规划器把全局路径分成几部分,再执行每一部分,生成控制命令。

话题

机器人行走时,在rviz上显示全局路径的相关话题是/move_base/DWAPlannerROS/global_plan,属于Global Map。局部规划器把全局路径的一部分发布到这个话题,代码是在DWAPlannerROS::computeVelocityCommands——DWAPlannerROS::publishGlobalPlan

rviz中显示局部路径的相关话题是/move_base/DWAPlannerROS/local_plan,属于Local Map。一旦局部路径计算出来,它就会发布到这个话题,代码是在DWAPlannerROS::computeVelocityCommands——DWAPlannerROS::publishLocalPlan

常用算法

ROS的局部规划器的接口plugin类为nav_core::BaseLocalPlanner,常用的包有4种:

  • base_local_planner - 提供了 Dynamic Window Approach(DWA)和Trajectory Rollout方法来局部控制;
  • dwa_local_planner - 模块化DWA方法应用,相比base_local_planner具有更多的恢复机制、更便于理解的接口和针对全向运动机器人更灵活的Y轴变量控制;
  • teb_local_planner

参考:
dwa_local_planner


使用机器人建图的技巧

可以参考
如何在环境轮廓建图完成之后,继续完善地图细节
SLAMTEC激光建图传感器使用技巧

建地图时,最好雷达面对墙壁,小车逐渐倒退来完善地图,不要让小车正面对着墙壁和空旷环境前进,在某部分环境建地图后,可以让小车正面前进。

绘制的地图,黑线(墙)周围的黑点越少越好,毛刺越少越好,漏光线的环境会出现很长的白线,蓝色的块是避障保护区。

建图时出现重影


一般有两个方面的原因:

  1. 里程计累计误差
  2. 建图数据的时间戳可能没有对齐。

一般第一种比较常见,比如说编码器是否存在丢失脉冲的情况、换个比较坚硬的地面跑一跑看看建图效果。第二种比较难排查,最好能够录下数据查看。


sed和awk

删除文本第四行

1
sed '4d' input.txt > output.txt

实际上没有修改input.txt,而是修改文件流,然后生成到文件output.txt。前后两个文件名称不能相同,否则不能保存

使用sed处理当前目录(包括子目录)的所有cpp文件,将其中的aaa替换为bbb

1
sed -i "s/aaa/bbb/g" `grep aaa -rl . --include="*.cpp" `

grep:

  • r 表示查找所有子目录
  • l 表示仅列出符合条件的文件名,用来传给sed命令做操作
  • --include="*.cpp" 表示仅查找cpp文件

整个grep命令用`号括起来,表示结果是一系列文件名

sed -i后面是要处理的文件,s/aaa/bbb/g跟vim下的替换命令一样,表示把aaa全替换为bbb,加g表示一行有多个aaa时,要全部替换,

删除空白行: sed ‘/^$/d’ -i delete.txt

对于当前目录(包括子目录)的所有txt文件,删除含有以ros开头字符串的所有行

1
sed -i '/ros/d' `grep -rl "^ros" . --include="*.txt" `

删除前5个字符

1
sed 's/.\{5\}//' test.log > a.log

(二) slam_gmapping节点和所有参数

slam_gmapping节点在gmapping包里面,它的节点信息如下:

它获取sensor_msgs/LaserScan的消息,然后用来建图。/slam_gmapping一边建图一边将地图数据发布到map话题,如果用rviz订阅map话题,我们就能看到建图的过程了.

话题和服务

entropy话题实际是simple_gmapping/entropy,发布机器人姿态分布熵的估计

服务dynamic_map(nav_msgs/GetMap),调用该服务可以获取地图数据,可以用rosservice call试验

需要的tf变换:

  • laser → base_link, 一般是固定的变换,使用static_transform_publisher节点实现
  • base_link → odom, 由里程计系统提供

运行导航程序后,获得tf变换关系:

slam_gmapping的参数

gmapping包生成三个节点,分别为slam_gmappingslam_gmapping_replay及一个测试用的节点。我们主要看slam_gmapping节点,它用到了很多参数,蓝色的是影响CPU性能的

小强目前所用参数只有map_update_interval改为1,粒子数为50,激光评分为140,其他都是默认的

gmapping wrapper参数

  • throttle_scans : [int,默认1] 当接收到n个(默认1个)激光扫描数据,就进行一次更新
  • base_frame: [string, default:”base_link”] 机器人上用的坐标系frame_id
  • map_frame: [string,default:”map”] 机器人地图所用的frame_id
  • odom_frame: [string,default:”odom”] 里程计坐标系的frame_id
  • map_update_interval : [float,default:5.0] 两次更新地图的间隔,降低参数会更新occupancy grid更频繁,但增大CPU占用

Laser Parameters

  • maxRange [double] 雷达扫描的最大有效距离,超过这个数值的扫描会忽略。

  • maxUrange [double,default: maxRange] 用于建图的最大可用扫描距离,因为激光
    雷达的测量距离越远,测量数据的误差越大, 考虑扫地机器人为室内机器人, 因
    maxUrange尽量设置小一些。

  • sigma [float, default: 0.05] The sigma used by the greedy endpoint matching。endpoint匹配标准差,计算score时的方差

  • kernelSize [int,default: 1] The kernel in which to look for a correspondence, search window for the scan matching process. 用于查找对应的kernel size, 主要用在计算score时搜索框的大小

  • lstep [float,default: 0.05] initial search step for scan matching (linear).平移优化步长

  • astep [float,default: 0.05] initial search step for scan matching (angular).旋转优化步长

  • iterations [int, default: 5] 扫描匹配的迭代步长的细分数量,最终距离和角度的匹配精度分别为lstep * 2 ^ (-iterations)astep * 2 ^ (-iterations),这个不是ICP迭代次数,而是用在ScanMatcher::optimize里的搜索步长范围

  • lsigma [float, default: 0.075] The sigma of a beam used for likelihood computation. 用于扫描匹配概率的激光标准差

  • ogain [float, default: 3.0] Gain to be used while evaluating the likelihood, for smoothing the resampling effects. 似然估计为平滑重采样影响使用的gain. 对应函数normalize()中的m_obsSigmaGain,用于粒子权重归一化

  • lskip [int, default: 0]计算似然时,跳过的激光束。0表示所有的激光都处理,如果计算压力过大,可以改成1。源码中的逻辑相当于这样:

1
2
3
4
5
6
7
8
9
10
// 令 lskip 为5, 50为激光束的个数
unsigned int skip=0;
for (unsigned int i=0; i<50; i++)
{
skip++;
skip=skip > 5 ? 0 : skip;
if (skip)
continue;
cout << i << " ";
}

结果为 5 11 17 23 29 35 41 47

  • minimumScore [float, 默认 0.0] 最小匹配得分,一定要自定义,它决定了你对激光的一个置信度,越高说明你对激光匹配算法的要求越高,激光的匹配也越容易失败而转去使用里程计数据,而设的太低又会使地图中出现大量噪声。为获得好的扫描匹配输出结果,用于避免在大空间范围使用有限距离的激光扫描仪(如5m)出现的jumping pose estimates问题。当Scores高达600+,如果出现了该问题可以考虑设定值50。
它取决于激光雷达的测量范围 、角度分辨率、测量精度以及环境特征。

Others:

  • linearUpdate: [float,默认1] 机器人走过多少米,gmapping才处理雷达scan数据

  • angularUpdate: [float,默认0.5] 机器人转过多少弧度,gmapping才处理雷达scan数据

  • temporalUpdate: [float,默认-1] 两次雷达scan数据之前的等待时间,小于0则不启用这个功能

  • resampleThreshold [float,default: 0.5] 粒子重新采样的门槛值,越大表示重新采样越频繁

  • particles : [int,默认30] 决定gmapping算法中的粒子数,每个粒子代表了机器人走过的可能的trajectory,因为gmapping使用的是粒子滤波算法,粒子在不断地迭代更新,所以选取一个合适的粒子数可以让算法在保证比较准确的同时有较高的速度。 看gmapping论文,一般设置为30,高难度地图用80,我一般用50个,大概对应CPU占比25%

运动模型参数 (高斯噪声模型的所有标准偏差)

  • srr [float, default: 0.1] linear noise component (x and y),Odometry error in translation as a function of translation (rho/rho) 线性运动造成的线性误差的方差

  • srt [float, default: 0.2] linear -> angular noise component,Odometry error in translation as a function of rotation (rho/theta). 线性运动造成的角度误差的方差 (rho/theta)

  • str [float, default: 0.1] angular -> linear noise component,Odometry error in rotation as a function of translation (theta/rho). 旋转运动造成的线性误差的方差 (theta/rho)

  • stt [float, default: 0.2] angular noise component (theta),Odometry error in rotation as a function of rotation (theta/theta). 旋转运动造成的角度误差的方差 (theta/theta)

Likelihood sampling (used in scan matching)

  • llsamplerange [float, default: 0.01] Translational sampling range for the likelihood. 用于似然计算的平移采样距离
  • llsamplestep [float, default: 0.005] Translational sampling step for the likelihood. 用于似然计算的平移采样步长
  • lasamplerange [float, default: 0.01] Angular sampling range for the likelihood. 用于似然计算的角度采样距离
  • lasamplestep [float, default: 0.005] Angular sampling step for the likelihood. 用于似然计算的角度采样步长

地图初始维度和分辨率

  • xmin [double] minimum x position in the map [m]
  • ymin [double] minimum y position in the map [m]
  • xmax [double] maximum x position in the map [m]
  • ymax [double] maximum y position in the map [m]
  • delta [double] 地图分辨率,就是话题map_metadata中的resolution
  • occ_thresh (float, default: 0.25),栅格地图栅格值 (i.e., set to 100 in the resultingsensor_msgs/LaserScan).

参考: ROS Wiki-gmapping


vector

vector的底层(存储)机制

vector就是一个动态数组,底层有一个指针指向一片连续的内存空间,当空间不够装下数据时,会自动申请另一片更大的空间(一般是当前容量的2倍),然后把原来的数据拷贝过去,接着释放原来的那片空间;如果元素数量比较大(比如上百个),重新分配内存的耗费会非常大,可能会出错。

当删除里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

vector中begin和end函数返回的是什么?

begin返回的是第一个元素的迭代器,end返回的是最后一个元素后面位置的迭代器。

为什么vector的插入操作可能会导致迭代器失效?

vector动态增加大小时,并不是在原空间后增加新的空间,而是以原大小的两倍在另外配置一片较大的新空间,然后将内容拷贝过来,并释放原来的空间。由于操作改变了空间,所以迭代器失效。

vector怎么实现动态空间分布?

vector容器基于数组实现,其元素在内存中连续存放,vector容器除了容器尾部之外,在其他任意位置插入或删除元素时,都需要移动该元素后面的所有元素

capacity和size

capacity是容器不用重新分配内存就可容纳的元素个数,容器的大小一旦超过capacity的大小,vector会重新配置内部的存储器,导致和vector元素相关的所有reference、pointers、iterator都会失效。内存的重新配置会很耗时间,
size()是容器中现有的元素个数,max_size()返回Vector所能容纳元素数量的最大值(包括可重新分配内存),一般是一个很大的数

reserve

vector频繁的销毁新建内存会导致效率很低,所以正确的做法是新建vector的时候初始化一个合适的大小,就像使用普通数组一样,这就用到了reserve。 如果reserve的值大于当前的capacity,那么会重新分配内存

reserve是容器预留空间,但并不真正创建元素对象,在创建对象之前,不能引用容器内的元素。否则会报错terminate called after throwing an instance of 'std::out_of_range'

保证vector的capacity最少达到它的参数所指定的大小n

resize

resize跟reserve不同了,它是改变容器的大小,并且可以创建对象,调用这个函数之后,就可以引用容器内的对象了.

1
2
void resize(size_type _Newsize)
void resize(size_type _Newsize, _Ty _Val)

对于第1个版本:

  • 若_Newsize小于oldsize,则剩余元素值不变
  • 若_Newsize大于oldsize,则新添加的元素值用元素的默认构造函数初始化(特别的,int型的将被初始化为0)

对于第2个版本:

  • _Newsize小于oldsize,则剩余元素值不变,全部调用erase(begin() + _Newsize,end())擦除掉多余元素
  • _Newsize大于oldsize,则新添加的元素值用提供的第2个参数初始化。
1
2
3
4
5
6
7
8
9
10
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);

cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl; // 5 8
vec.resize(10); // 改变大小
cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl; // 10 10

如果_Newsize小于之前的capacity,那么capacity不变。如果大于,那么capacity也要改变.也就是说 resize也涉及了内存重新分配,同时改变了capacity和size

data

返回指向vector中第一个数据的指针或空vector之后的位置

1
2
vector<int> v{1,2,3,4,5};
qDebug()<< *(v.data() ) ; // 1

assign

1
2
3
4
// 先清除vector之前的内容,再将区间[first, last)的元素赋到当前vector
void assign(const_iterator first, const_iterator last);
// 先清除vector之前的内容,再将Count个Val插入到vector
void assign( size_type _Count, const Type& _Val );

使用assign赋值给容器:

1
2
3
4
vector<int> vec;
vec.assign(5,9);

cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl;

容器的capacitysize都是5

push_back

但是如果使用push_back:

1
2
3
4
5
6
7
vector<int> vec;
vec.push_back(1);
vec.push_back(2);
vec.push_back(3);
vec.push_back(4);
vec.push_back(5);
cout<< vec.size() <<" "<<vec.capacity()<<" "<<endl;

此时容器的capacity为8

每次执行push_back操作,相当于底层的数组实现要重新分配大小,如果capacity不够插入新元素,需要开辟新的内存空间,把原来的元素复制到新内存,释放原来内存。

频繁的vector扩容会使效率很低,所以 不要使用for循环和push_back给vector赋值,这样会多次重新分配内存;如果先reserve分配需要的内存空间,再push_back会显著提高效率。

vector的几种清空容器办法

1
2
3
4
iterator erase(iterator it); // 删除指定元素,并返回删除元素后一个元素的位置(如果无元素,返回end())
iterator erase(iterator first, iterator last); // 注意:删除元素后,删除点之后的元素对应的迭代器不再有效。

void clear() const; // 函数删除当前vector中的所有元素,相当于调用erase( begin(), end())

释放内存

vector的内存空间是只增加不减少的,erase和clear只删除元素,不能释放内存。如果是C++11环境,直接使用std::vector::shrink_to_fit(),它会将capacity缩减到size大小,所以如果要释放内存,可以先clear,再shrink_to_fit,这跟Qt里的qDeleteAll非常相似

元素是指针时,如何释放内存

如果vector中存放的是指针,而这些指针指向的对象没有销毁,那么当vector销毁时,内存不会被释放。

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
int v[6] = {1,2,3,4,5,6};
std::vector<int*> ps;
int* p1 = v;
int* p2 = v+1;
int* p3 = v+2;
int* p4 = v+3;

ps.push_back( p1 );
ps.push_back( p2 );
ps.push_back( p3 );
ps.push_back( p4 );

// delete ps.back(), ps.pop_back();

for (auto it = ps.begin(); it != ps.end(); it++)
{
if (*it != NULL)
{
delete *it;
*it = NULL;
}
}

ps.clear();
ps.shrink_to_fit();

vector赋值速度

vector所有赋值方式的速度排名:

  1. assign
  2. resize以后用copy算法赋值
  3. 赋值操作符,也就是 destinationVector = sourceVector;
  4. 采用插入迭代器再用copy的方式
  5. insert
  6. push_back

记住最快的是assign,最慢的是push_back

vector遍历元素的速度

一般的速度排名: iterator > [] > at

但是根据编译器和平台的不同,iterator和[]速度有快有慢,但基本上at都是最慢的

尽量避免在vector前部插入元素

一般也没人这么干,任何在vetor前部的插入操作其复杂度都是O(n)的,所以十分低效,如果实在需要在前部插入元素,可以用list,它的效率远远超过vector

这样的代码是valid

1
2
3
using Type = std::vector<int>;
int f = 12;
Type vec={f};

参考:
vector/list的几种赋值方法的速度比较