命名空间

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

参数 /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就可以编译整个工作空间,不必自己改编译顺序了。


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


sed和awk

删除文本中包含某字符串的行

1
sed -i '/committed/d' your_file`

删除包含committed的行

删除文本第四行

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

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的几种赋值方法的速度比较


使用chrono进行高精度计时

time_t在Linux中的类型其实是long int

1
2
3
struct timeval tv;
gettimeofday(&tv,nullptr);
time_t curTime = tv.tv_sec; // 获取当前UTC时间戳,单位为秒

1
2
3
4
5
6
7
8
9
10
#include <chrono>
using namespace chrono;

auto start = system_clock::now();
/* do something */
auto end = system_clock::now();
auto duration = duration_cast<microseconds>(end - start);
cout << "花费了"
<< double(duration.count()) * microseconds::period::num / microseconds::period::den
<< "秒" << endl;

有时候可能报错:error: ‘chrono’ in namespace ‘std’ does not name a type

解决方法: 改为using namespace std::chrono; in addition to #include <chrono>

或者用steady_clock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <chrono>

using std::chrono::steady_clock;
// 这个类型是 steady_clock::time_point
auto t1 = std::chrono::steady_clock::now();

// your code

auto t2 = std::chrono::steady_clock::now();

//秒
double dr_s=std::chrono::duration<double>(t2-t1).count();
//毫秒
double dr_ms=std::chrono::duration<double,std::milli>(t2-t1).count();
//微秒
double dr_us=std::chrono::duration<double,std::micro>(t2-t1).count();
//纳秒
double dr_us=std::chrono::duration<double,std::nano>(t2-t1).count();

ALOAM中的TicToc类其实就是std::chrono::steady_clock::now()

拥有从毫秒到小时的计时

1
2
3
4
std::chrono::milliseconds 
std::chrono::seconds
std::chrono::minutes
std::chrono::hours

可以用于休眠函数

1
2
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::this_thread::sleep_for(std::chrono::seconds(1));


完全退出ROS的方法

执行rosnode kill -a,这会杀死rosout之外的所有节点,但是这样还差很远。如果有的节点是以python文件启动,它对于的进程并没有杀死。另外rosout是可再生的,必须连roscore也杀掉.

现在执行ps aux | grep ros,会看到剩余的4个进程:

按照它们的pid, 使用kill pid的方式将它们杀死,这样才彻底退出所有ROS进程

但是注意如果是在远程机启动roscore,在本地机是不能终止它的。


OccupancyGrid地图详解

栅格地图是将环境栅格化, 然后根据测量值对每个栅格的占有率进行计算, 通过概率计算判断给各栅格可能被障碍物占的几率, 并赋值一个0到1之间的值。通过激光雷达获取的大量数据帮助对每个栅格点进行准确判断。栅格地图中栅格大小决定了地图的精度, 也将影响机器人的定位精度,因此追求更小更多的栅格,但是栅格变小后,其数据量将会显著增加, 因此栅格地图不适合用于大型地图的构建, 适用于室内定位这种小型环境地图构建

地图在ROS中是以Topic的形式维护和呈现的,这个Topic名称就叫做/map,它的消息类型是nav_msgs/OccupancyGrid。由于/map中实际上存储的是一张图片,为了减少不必要的开销,这个Topic往往采用锁存(latched)的方式来发布。

锁存器的作用就是,将发布者最后一次发布的消息保存下来,然后把它自动发送给后来的订阅者。这种方式非常适合变动较慢、相对固定的数据(例如地图),然后只发布一次,相比于同样的消息不定的发布,锁存的方式既可以减少通信中对带宽的占用,也可以减少消息资源维护的开销。

map话题的消息类型是OccupancyGrid,通过rosmsg show nav_msgs/OccupancyGrid来查看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std_msgs/Header header       #消息的报头
uint32 seq
time stamp
string frame_id #地图消息绑定在TF的哪个frame上,一般为map

nav_msgs/MapMetaData info #地图相关信息
time map_load_time #加载时间
float32 resolution #分辨率 单位:m/pixel
uint32 width #宽 单位:pixel
uint32 height #高 单位:pixel
geometry_msgs/Pose origin #原点
geometry_msgs/Point position
float64 x
float64 y
float64 z
geometry_msgs/Quaternion orientation
float64 x
float64 y
float64 z
float64 w
int8[] data #地图具体信息

info是地图的配置信息,它反映了地图的属性。设置地图原点
1
2
3
// 将栅格地图原点设置在整张图的(-20, -20)位置
grid_map.info.origin.x = -20;
grid_map.info.origin.y = -20;

整张地图的障碍物信息存放在data数据成员中,data是一个int8类型的vector,它存储的内容有width * height个int8型的数据,也就是这张地图上每个像素。map中data存储的格式如下

1
2
3
4
0      空白区域
100 障碍物
-1 未知
1-99 根据像素的灰度值转换后的值,代表有障碍物的概率、

左下角像素点在世界坐标系下的位置为(origin_x,origin_y), 单位米。那么世界坐标系下的点(x,y),转换到地图坐标系,那么该点对应的data中的索引index为:
1
index = (int)( (x - origin_x) / resolution) + ((int)((y - origin_y) / resolution)) * width;

该点在地图中的信息即为data[index]

一个宽和高的栅格地图,索引保存方式是这样的:

1
2
3
4
5
6
7
4        8        12     16

3 7 11 15

2 6 10 14

1 5 9 13

1相当于地图的原点,6就对应坐标(1, 1)。那么点坐标(2, 3),对应的栅格索引是x * map.info.width + y = 12

pgm和yaml

运行rosrun map_server map_saver -f fileName可以保存地图,保存成功后,会有文件名对应的pgm和yaml文件。pgm文件就是地图的文件,标准的地图是用inkscape打开时看到的模样。

地图就是一张普通的灰度图像,通常为pgm格式。在地图中,黑色的网格是障碍区域,白色的网格是无障碍区域,灰色的网格是未知区域。map_server从图片中读取信息然后将每个网格都赋值[-1, 100],灰色是-1,代表未知区域;白色是0,空闲区域;黑色是100,障碍区域。

yaml描述地图元数据,内容如下:

1
2
3
4
5
6
7
8
9
image: Software_Museum.pgm      #指定地图文件,可以是相对路径
resolution: 0.020000 #地图的分辨率 单位为 m/pixel
# 地图的原点,即地图左下像素的2D姿态(x,y,yaw),偏航为逆时针旋转,忽略偏航时为0
# 这个坐标体系用于让ros在图片里找到原点而已
origin: [-15.720000, -12.520000, 0.000000]
negate: 0 # 0代表白色空闲,黑色占据。一般不用修改
occupied_thresh: 0.65 # 当占据的概率大于0.65认为被占据
free_thresh: 0.196 # 当占据的概率小于0.196认为无障碍
# 介于 occupied_thresh 和 free_thresh 的栅格unknown

其中占据的概率occ = (255-color_avg) / 255.0, color_avg为RGB三个通道的平均值。

  • occupied_thresh:大于这个阀值的占用率的像素被认为occupied
  • free_thresh:小于这个阀值的占用率的像素被认为是free

如果occupied_thresh设置的比较大,有些障碍物会没有障碍层和膨胀层。
occupied_thresh=0.85.png
如果建图时,障碍的边缘不够明显,也可能出现此问题,此时可以降低occupied_thresh。 其实还可以降低参数cost_scaling_factor,不过对膨胀层的影响较大。

  • 栅格地图的分辨率不必太好,常用0.05
栅格地图的坐标系原点,是在栅格地图左下角,这里的origin是左下角在map坐标系的坐标,不是(0,0,0)

实际举例,先获得机器人的当前姿态:

然后在rviz上测量出机器人与左下角两个方向的距离

用机器人的坐标减去origin的坐标:

  • x = -2.394 - (-15.72) = 13.326
  • y = 2.37 - (-12.52) = 14.89

显然和测量结果大致相同,计算都是在map坐标系下。

map_server和map_saver

map_server是一个和地图相关的功能包,它可以将已知地图发布出来,供导航和其他功能使用,也可以保存SLAM建立的地图。它还提供了map_saver命令行实用程序,它允许动态生成的映射保存到文件。

要让map_server发布/map,需要yaml和pgm两个文件,然后可以通过指令来加载这张地图,map_server相关命令如下:

1
2
rosrun map_server map_server    test.yaml 	加载自定义的地图,通常使用roslaunch
rosrun map_server map_saver -f mymap 保存当前地图为mymap.pgm和mymap.yaml

或者使用roslaunch加载地图:
1
2
<arg name="map_file" default="/home/hlhp/robot_ws/workspace/maps/test.yaml"/>
<node name="map_server" type="map_server" args="$(arg map_file)" pkg="map_server"/>

map_server节点提供的服务:

  • static_map (nav_msgs/GetMap):提供检索地图服务

map_server节点读取pgm文件,通过服务static_map将地图数据提供给其他节点。比如move_base节点从中获取地图数据,用来进行路径规划;amcl节点用地图来进行定位,代码在AmclNode::requestMap()

map_server发布的话题包括:

1
2
/map_metadata    (nav_msgs/MapMetaData): 发布地图的描述信息,通过此锁存话题接收地图元数据。
/map (nav_msgs/OccupancyGrid): 通过此锁存话题接收地图

加载yaml文件后,出现下面的结果:

运行rostopic echo map_metadata的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
map_load_time: 
secs: 1569549222
nsecs: 440982758
resolution: 0.019999999553
width: 1504
height: 2112
origin:
position:
x: -15.72
y: -12.52
z: 0.0
orientation:
x: 0.0
y: 0.0
z: 0.0
w: 1.0

echo一下map话题,结果是一大堆-1和0,这就是上面所说的occupancy grid,-1代表未知,0代表无障碍,100代表障碍。

参数:

  • ~frame_id (string, default: “map”):地图坐标系的名称,要在已发布地图的标题中设置的框架, 绑定发布的地图与tf中的哪个frame,通常就是map

问题 1

有时运行rosrun map_server map_saver,结果显示 Wating for the map, 无法保存成功,进程阻塞了

问题 2

有几次,map_server打开pgm时一直阻塞,只能重启ROS

1
2
3
4
5
map_server::loadMapFromFile(&map_resp_,mapfname.c_str(),res,negate,occ_th,free_th, origin, mode);
ROS_INFO("map_server after loadMapFromFile");

// To make sure get a consistent time in simulation
ros::Time::waitForValid();

经过加日志,发现是 ros::Time::waitForValid();没有执行完,问题出在这里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bool Time::waitForValid(const ros::WallDuration& timeout)
{
ros::WallTime start = ros::WallTime::now();
while (!isValid() && !g_stopped)
{
ros::WallDuration(0.01).sleep();

if (timeout > ros::WallDuration(0, 0) && (ros::WallTime::now() - start > timeout))
{
return false;
}
}
if (g_stopped)
{
return false;
}
return true;
}