cartographer_occupancy_grid_node获取地图

Rviz中的地图绘制使用了cairo库。注意gflag的参数include_frozen_submapsinclude_unfrozen_submaps不能都为false,否则会报错

cartographerrviz的绘制方式,就是`submap_node控制一个子图的绘制,每个子图有一个子图原点,这个是submap_query中的slice_pose。而每个子图之间是有微弱旋转的,而控制旋转的中心也是这个slice_pose。而每个子图之间的旋转和平移可以通过submap_list`的pose得到。

main函数值得看的就一句::cartographer_ros::Node node(FLAGS_resolution, FLAGS_publish_period_sec);,构造函数如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Node::Node(const double resolution, const double publish_period_sec)
: resolution_(resolution),
// submap_query 服务的客户端
client_(node_handle_.serviceClient<::cartographer_ros_msgs::SubmapQuery>(
kSubmapQueryServiceName)),
// submap_list话题的订阅者,回调函数 HandleSubmapList
submap_list_subscriber_(node_handle_.subscribe(
kSubmapListTopic, kLatestOnlyPublisherQueueSize,
boost::function<void(
const cartographer_ros_msgs::SubmapList::ConstPtr&)>(
[this](const cartographer_ros_msgs::SubmapList::ConstPtr& msg) {
HandleSubmapList(msg);
}))),
// 发布话题 map
occupancy_grid_publisher_(
node_handle_.advertise<::nav_msgs::OccupancyGrid>(
kOccupancyGridTopic, kLatestOnlyPublisherQueueSize,
true /* latched */)),
// timer执行DrawAndPublish函数
occupancy_grid_publisher_timer_(
node_handle_.createWallTimer(::ros::WallDuration(publish_period_sec),
&Node::DrawAndPublish, this)) {}

Node::DrawAndPublish是画图和发布map,不是重点


HandleSubmapList函数大部分都很简单,终点是下面这句:

1
2
auto fetched_textures =
::cartographer_ros::FetchSubmapTextures(id, &client_);

内部其实就是submap_query的客户端发起请求,服务端的注册在Node的构造函数里,回调是Node::HandleSubmapQuery

代码调用顺序是: Node::HandleSubmapQuery —— MapBuilderBridge::HandleSubmapQuery —— MapBuilder::SubmapToProto —— PoseGraph2D::GetSubmapDataSubmap2D::ToResponseProto —— ProbabilityGrid::DrawToSubmapTexture

重点:

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
// 这里比较简单,最终返回的是后端传来的 global坐标系下的位姿
PoseGraphInterface::SubmapData PoseGraph2D::GetSubmapDataUnderLock(
const SubmapId& submap_id) const
{
const auto it = data_.submap_data.find(submap_id);
if (it == data_.submap_data.end())
return {};
auto submap = it->data.submap;
if (data_.global_submap_poses_2d.Contains(submap_id) )
{
// We already have an optimized pose.
return {submap,
transform::Embed3D(
data_.global_submap_poses_2d.at(submap_id).global_pose) };
}
// We have to extrapolate.
return {submap, ComputeLocalToGlobalTransform(data_.global_submap_poses_2d,
submap_id.trajectory_id) *
submap->local_pose() };
}

void Submap2D::ToResponseProto(
const transform::Rigid3d&,
proto::SubmapQuery::Response* const response) const
{
if (!grid_) return;
// 这个 version 是子图插入了多少雷达数据,完成的子图是 2* num_range_data
response->set_submap_version( num_range_data() );
proto::SubmapQuery::Response::SubmapTexture* const texture =
response->add_textures();
grid()->DrawToSubmapTexture(texture, local_pose());
}

再看DrawToSubmapTexture

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
bool ProbabilityGrid::DrawToSubmapTexture(
proto::SubmapQuery::Response::SubmapTexture* const texture,
transform::Rigid3d local_pose) const
{
Eigen::Array2i offset;
CellLimits cell_limits;
ComputeCroppedLimits(&offset, &cell_limits);

std::string cells;
for (const Eigen::Array2i& xy_index : XYIndexRangeIterator(cell_limits))
{
if (!IsKnown(xy_index + offset)) {
cells.push_back(0 /* unknown log odds value */);
cells.push_back(0 /* alpha */);
continue;
}
// We would like to add 'delta' but this is not possible using a value and
// alpha. We use premultiplied alpha, so when 'delta' is positive we can
// add it by setting 'alpha' to zero. If it is negative, we set 'value' to
// zero, and use 'alpha' to subtract. This is only correct when the pixel
// is currently white, so walls will look too gray. This should be hard to
// detect visually for the user, though.

// delta范围[-127, 127]
const int delta =
128 - ProbabilityToLogOddsInteger(GetProbability(xy_index + offset));
const uint8 alpha = delta > 0 ? 0 : -delta;
const uint8 value = delta > 0 ? delta : 0;
// 存数据有2个值,栅格值value和alpha透明度
cells.push_back(value);
cells.push_back((value || alpha) ? alpha : 1);
}
// 保存地图栅格数据时进行压缩
common::FastGzipString(cells, texture->mutable_cells());
// 地图描述信息赋值,省略
......
*texture->mutable_slice_pose() = transform::ToProto(
local_pose.inverse() *
transform::Rigid3d::Translation(Eigen::Vector3d(max_x, max_y, 0.)) );

return true;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 解压后的栅格数据
struct SubmapTexture {
struct Pixels {
std::vector<char> intensity; // 栅格值
std::vector<char> alpha; // 透明度
};
Pixels pixels;
int width;
int height;
double resolution;
::cartographer::transform::Rigid3d slice_pose;
};

// 压缩后的栅格数据
struct SubmapTextures {
int version;
std::vector<SubmapTexture> textures;
};

MIT和软件所的数据集

根据我的经验,先启动cartographer后,等待出现下面的日志,然后再播放bag。如果这个日志没出现,播放bag通常不会成功开始建图

1
2
[/cartographer_node   node.cc:886] Expected topic "scan" (trajectory 0) (resolved topic "/base_scan") but no publisher is currently active.
[/cartographer_node node.cc:895] Currently available topics are: /constraint_list,/submap_list,/scan_matched_points2,/rosout,/tf,/rosout_agg,/map,/trajectory_node_list,/landmark_poses_list,

MIT数据集老出问题,德意志博物馆数据集却从不出问题 最好数据集里不要有tf信息
博物馆的tf
MIT数据集的 tf

博物馆的地图大小 3066平方米

MIT的数据集运行

这个数据集使用雷达北洋UTM-30LX,也就是LOAM论文所用的雷达,有一个问题是它的机器人是会坐电梯在楼层之间跑的,因此需要选择一个只在一个平面上跑的数据集2011-03-28-08-38-59.bag

从TF变换树可以看到,这里的里程计信息是采用经过EKF滤波之后的里程计坐标系/robot_pose_ekf/odom_combined,而不是直接的base_odometry/odom第2个问题来了:cartographer需要的里程计消息是nav_msgs/Odometry,但是/robot_pose_ekf/odom_combined的类型是geometry_msgs/PoseWithCovarianceStamped,在播放rosbag时会报错,无法建图。于是想用原来的里程计/base_odometry/odom,结果发现缺少child_frame_id,一般为base_link。无法给bag消息加坐标系,只能写一个程序mit_to_odom做转换

我们要使用数据集的IMU和odom,否则建图不能进行,日志如下

1
2
3
4
[/cartographer_node] [local_trajectory_builder_2d.cc:136] time_first_point: 634369235390529797
[/cartographer_node] [local_trajectory_builder_2d.cc:137] GetLastPoseTime: 634369235390647384
[/cartographer_node] [local_trajectory_builder_2d.cc:138] Extrapolator is still initializing
[/cartographer_node] [pose_graph_2d.cc:148] Inserted submap (0, 0).

阻塞在
1
2
3
4
5
if (time_first_point < extrapolator_->GetLastPoseTime() )
{
LOG(INFO) << "Extrapolator is still initializing.";
return nullptr;
}

launch如下,不能加<param name="/use_sim_time" value="true" />,否则cartographer一直阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
<launch>
<node name="cartographer_node" pkg="cartographer_ros"
type="cartographer_node" args="
-configuration_directory $(find cartographer_ros)/configuration_files
-configuration_basename mit_stata_odom.lua"
output="screen">
<remap from="scan" to="base_scan" />
<remap from="imu" to="torso_lift_imu/data" />
</node>

<node name="cartographer_occupancy_grid_node" pkg="cartographer_ros"
type="cartographer_occupancy_grid_node" args="-resolution 0.05" />
</launch>

TF_REPEATED_DATA 问题

1
2
Warning: TF_REPEATED_DATA ignoring data with redundant timestamp for frame odom at time 1301326803.532597 according to authority unknown_publisher
at line 278 in /tmp/binarydeb/ros-noetic-tf2-0.7.5/src/buffer_core.cpp

这个问题困扰了我好长时间,后来发现是数据集发布了坐标转换odom_combined —— base_footprint,而cartographer也容易发布这个变换,此时仍然可以建图,但这个报警无穷无尽,没法看其他日志了。

如此解决:首先在播放bag的终端,先运行rosparam set use_sim_time true,lua设置如下

1
2
3
4
5
6
7
8
9
map_frame = "map",
tracking_frame = "imu_link",
published_frame = "odom_combined",
odom_frame = "odom_combined",

provide_odom_frame = false,
use_odometry = true,

TRAJECTORY_BUILDER_2D.use_imu_data = true

但是Rviz终端也出现了报警,按说把Rviz里的Submaps的Tracking Frame改为lua 文件中的 tracking_frame 即可,但是我改了后无效,把base_footprintbase_link都试了一遍也没有解决

1
[ WARN] Could not compute submap fading: Could not find a connection between 'map' and 'base_link' because they are not part of the same tree.Tf has two or more unconnected trees

这样导致无法获得`base_link`在`map`中的坐标

mapbase_link的关系问题

数据集中已经包含了odom_combinedbase_link之间的tf关系,运行carto之后会获得odom_combinedmap之间的关系,但是无法获得mapbase_link之间的tf,运行rosrun tf tf_echo map base_link报警如下

1
Exception thrown:Lookup would require extrapolation 367325588.004024029s into the past.  Requested time 1301326755.953867912 but the earliest data is at time 1668652343.957891941, when looking up transform from frame [base_link] to frame [map]

1668652343是当前时间的,1301326755是数据集中的时间。 目前未解决

软件所的数据集运行


不需要在任何地方设置use_sim_time,如果出现时间戳问题,关掉一切节点,重新开始roscore。launch里不能加<param name="/use_sim_time" value="true" />,否则cartographer一直阻塞。

问题:

  1. scan话题里的坐标系是laser,这是驱动程序决定的。但是tf里的坐标系变成了laser_mount_link,到了tf_bridge_.LookupToTracking那里卡住了,即使手动改了源码也不行。

  2. 坐标系名称带了/,这个其实在cartographer里会被解决,但是习惯上不要加上。

  3. 也无法获得mapbase_link的转换。

  4. tfecho map odom会发现位姿跳变极大, 还没搞清楚, 不是是否和时间戳有关

参考:
TF_OLD_DATA 问题
TF_OLD_DATA的另一种原因


move_base的服务端和客户端

find_package中添加actionlibactionlib_msgs

1
2
3
4
5
add_action_files(
DIRECTORY action
FILES
DoDishes.action
)

package.xml添加actionlib的编译和执行依赖,如下:

1
2
3
4
<build_depend>actionlib</build_depend>
<build_depend>actionlib_msgs</build_depend>
<run_depend>actionlib</run_depend>
<run_depend>actionlib_msgs</run_depend>

服务端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// action定义在 scheduler/action/Scheduler.action
typedef actionlib::SimpleActionServer<scheduler::SchedulerAction> Server;
actionlib::SimpleActionServer<scheduler::SchedulerAction> as_;

// 注册抢占回调函数
as_.registerPreemptCallback(boost::bind(&Explore::preemptCB, this))
as_.registerGoalCallback(boost::bind(&Explore::goalCB, this));


void Explore::preemptCB()
{
ROS_INFO("%s: Preempted", action_name_.c_str());
explore_ = false;
explore_state_ = STOP;
ac_.cancelAllGoals();
if(as_.isActive())
as_.setPreempted();
}

void Explore::goalCB()
{
explore_state_ = FULL_ROTATE;
as_.acceptNewGoal();
}

SimpleActionServer有构造函数可以注册ExecuteCallback回调函数,但没有其他两个回调,原型:
SimpleActionServer (ros::NodeHandle n, std::string name, ExecuteCallback execute_callback, bool auto_start)

move_base的构造函数中是这样使用的:
as_ = new MoveBaseActionServer(ros::NodeHandle(), "move_base", boost::bind(&MoveBase::executeCb, this, _1), false);

服务端as_最后需要start()来启动,再加ros::spin()阻塞。 MoveBase构造函数的最后只有前者,ros::spin()其实是在move_base_node.cpp

客户端

这里只看带回调函数的函数原型:

1
2
3
4
5
void actionlib::SimpleActionClient< ActionSpec >::sendGoal(const Goal& goal,
SimpleDoneCallback done_cb = SimpleDoneCallback(),
SimpleActiveCallback active_cb = SimpleActiveCallback(),
SimpleFeedbackCallback feedback_cb = SimpleFeedbackCallback()
)

ActionServer发送goal, 并注册回调函数。 如果调用本函数时已经有个goal处于active,状态机就会忽略goal并且开始向新的goal导航,不用发取消的请求。

  • done_cb: Callback that gets called on transitions to Done,用于监听服务器任务执行完后的相应消息以及客户端的相应处理
  • active_cb: Callback that gets called on transitions to Active,服务器任务被激活时的消息提示以及客户端的相应处理
  • feedback_cb: Callback that gets called whenever feedback for this goal is received,接收服务器的反馈消息以及客户端的相应处理
    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
    typedef actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction> MoveBaseClient;
    MoveBaseClient ac_;

    // 示例
    ac_.sendGoal(goal,
    boost::bind(&DoDishesActionClient::DoneCb, this, _1, _2),
    boost::bind(&DoDishesActionClient::ActiveCb, this),
    boost::bind(&DoDishesActionClient::FeedbackCb, this, _1));


    void DoneCb(const actionlib::SimpleClientGoalState& state,
    const first_actionlib_sample::DoDishesResultConstPtr& result)
    {
    ROS_INFO("Finished in state [%s]", state.toString().c_str());
    ROS_INFO("Toal dish cleaned: %i", result->toal_dishes_cleaned);
    ros::shutdown();
    }

    // 当目标激活的时候,会调用一次
    void ActiveCb()
    {
    ROS_INFO("Goal just went active");
    }

    // 接收服务器的反馈信息
    void FeedbackCb(const first_actionlib_sample::DoDishesFeedbackConstPtr& feedback) {
    ROS_INFO("Got Feedback Complete Rate: %f", feedback->percent_complete);
    }

Goal的状态

GoalStatus

1
2
3
4
5
6
7
8
9
10
PENDING = 0
ACTIVE = 1
PREEMPTED = 2
SUCCEEDED = 3
ABORTED = 4
REJECTED = 5
PREEMPTING = 6
RECALLING = 7
RECALLED = 8
LOST = 9

1
2
3
4
5
6
7
cout<< ac_.getState().toString() <<endl;

// 获取goal的状态信息
// PENDING, ACTIVE, RECALLED, REJECTED, PREEMPTED, ABORTED, SUCCEEDED, LOST(isn't tracking a goal)
SimpleClientGoalState getState () const
// 输出上面的几个状态,大写字母,对调试很有用
std::string toString() const

如果当前已经在某个状态,再次设置为这个状态就会报警:比如ABORTED状态 To transition to an aborted state, the goal must be in a preempting or active state, it is currently in state: 4

参考:ROS Action动作通讯编程


连续发导航目标时的导航中断

车收到导航命令没有动,查日志如下
move_base接收的目标四元数invalid.png

在一个客户端程序里,在sendGoal之前调用了两次cancelAllGoals,结果得到下面的日志,某次goal的消息全是0

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
63
64
65
66
67
68
[ INFO] [1648276976.479444090]: move_base client send goal: type: 1
target_pose:
header:
seq: 11
stamp: 1648276976.479361050
frame_id: map
pose:
position:
x: -3.9901
y: 0.427604
z: 0
orientation:
x: 0
y: 0
z: 0
w: 1

[ INFO] [1648276976.492017723]: move base task just went active
[ WARN] [1648276979.458537608]: 32 receive cmd: position:
x: -4.01633
y: 0.440816
z: 0
orientation:
x: 0
y: 0
z: 0
w: 1

[ INFO] [1648276979.494004129]: actionlib result state: PREEMPTED
[ INFO] [1648276979.494157917]: actionlib result: result: 0
result_pose:
header:
seq: 0
stamp: 0.000000000
frame_id:
pose:
position:
x: 0
y: 0
z: 0
orientation:
x: 0
y: 0
z: 0
w: 0

[ INFO] [1648276979.494228945]: actionDoneCb state PREEMPTED
[ INFO] [1648276979.494308493]: move_base client send goal: type: 1
target_pose:
header:
seq: 12
stamp: 1648276979.494253319
frame_id: map
pose:
position:
x: 0
y: 0
z: 0
orientation:
x: 0
y: 0
z: 0
w: 0

[ERROR] [1648276979.504876032]: BUG: Got a transition to CommState [PENDING] when our in SimpleGoalState [DONE]
[ERROR] [1648276979.504963821]: BUG: Got a transition to CommState [RECALLING] when our in SimpleGoalState [DONE]
[ERROR] [1648276979.514087412]: BUG: Got a transition to CommState [PREEMPTING] when in SimpleGoalState [DONE]
[ERROR] [1648276979.515084871]: BUG: Got a second transition to DONE

结果导致move_base漏接了一个目标,最后取消两个cancelAllGoals就好了

  • cancelAllGoals()[inline]: Cancel all goals currently running on the action server. This preempts all goals running on the action server at the point that this message is serviced by the ActionServer.

  • cancelGoal()[inline]: Cancel the goal that we are currently pursuing

cancellAllGoals()cancellGoal()经常放在SIGINT信号的回调函数里,用于停止所有actions

参考: elektron_ballcollector


智能指针(三) weak_ptr

weak_ptr本身也是一个模板类,但是不能直接用它来定义一个智能指针的对象,只能配合shared_ptr来使用,可以将shared_ptr的对象赋值给weak_ptr,并且这样并不会改变引用计数的值。查看weak_ptr的代码时发现,它主要有lockswapresetexpiredoperator=use_count几个函数,与shared_ptr相比多了lockexpired函数,但是却少了get函数,甚至连operator*operator->都没有

weak_ptr解决循环引用

weak_ptr必须跟shared_ptr配合使用, 它用于解决shared_ptr的死锁问题,如果两个shared_ptr一直互相引用,那么它们的引用计数永远不是0,资源永远不释放,这样实际造成了内存泄露。weak_ptr并不拥有其指向的对象,让weak_ptr指向shared_ptr所指向对象,对象的引用计数并不会增加

循环引用的情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Son;
class Father
{
public:
~Father() {cout<<" father 析构"<<endl; }
shared_ptr<Son> son_;
};
class Son {
public:
~Son() {cout<<" son 析构"<<endl; }
shared_ptr<Father> father_;
};

auto father = make_shared<Father>();
auto son = make_shared<Son>();
father->son_ = son;
son->father_ = father;

结果一个析构函数也没运行,说明对象资源没有释放。使用weak_ptr解决很简单,让Son的成员变量father改为weak_ptr类型,运行后发现两个析构都有了。

main函数退出前,Son对象的引用计数是2,而Father的引用计数是1。
son指针销毁,Son对象的引用计数变成1。
father指针销毁,Father对象的引用计数变成0,导致Father对象析构,Father对象的析构会导致它包含的son_指针被销毁,这时Son对象的引用计数变成0,所以Son对象也会被析构。

参考:shared_ptr循环引用的例子及解决方法示例


c++11之后的新特性 1

auto 和 for循环

1
2
3
4
5
6
7
std::vector<int> vec {1,2,3,4,5};
for (auto n :vec)
std::cout << n;

int arr[10] = { 1, 2, 3, 4, 5};
for (auto n : arr)
std::cout << n;

C++11的for使用方法简单了很多,但是上述对容器的遍历是只读的,也就是说遍历的值是不可修改的。

如果要修改遍历的值,需要将遍历的变量声明为引用类型:

1
2
3
4
5
6
7
8
9
std::vector<int> vec {1,2,3,4,5};
cout << "修改前" << endl;
for (auto& n :vec)
std::cout << n++;
cout << endl;

cout << "修改后" << endl;
for (auto j : vec)
std::cout << j;

结果:
1
2
3
4
修改前
12345
修改后
23456

maybe_unused (C++17 起)

[[maybe_unused]]抑制针对未使用实体的警告。此属性可出现在下列实体的声明中:

  • class/struct/union
  • typedef
  • 变量
  • 非静态数据成员
  • 函数
  • 枚举
  • 枚举项

可以说几乎能随处使用。若编译器针对未使用实体发布警告,则对于任何声明为 maybe_unused 的实体抑制该警告。

获取当前的程序路径

1
std::filesystem::path current_path = std::filesystem::current_path();

代码经常出现相对路径,有时我们想的路径并不是我们需要的,此时最好获取一下当前的程序路径,再使用相对路径

static_assert

noexcept

查询表达式是否能抛出异常

std::as_const

需要头文件<utility>,将左值转化为const类型

该方法接收一个引用参数,返回它的const引用版本。基本上,std::as_const(obj) 等价于 const_cast<const T&>(obj),其中T是obj的类型。与使用 const_cast 相比,使用 std::as_const() 可以使代码更短更易读。

1
2
3
4
5
string str = { "C++ as const test" };
cout << is_const<decltype(str)>::value << endl; // 0

const string str_const = std::as_const(str);
cout << is_const<decltype(str_const)>::value << endl; // 1

大括号初始化

在C++11中,自动变量和全局变量的初始化方式增加了几种:

1
2
3
int a = {3+4};
int a{6+8};
int a(6+8);

主要是大括号的用法,另外大括号内可以不包含任何内容,此时变量初始化为0
1
2
int roc = {};
int roc{};

STL也可以用大括号的方式初始化,比如 vector

1
vector<int> c{ 1, 2, 3 };

这显然不能从构造函数角度理解了,源于<initializer_list>头文件中initialize_list类模板的支持。只要#include<initializer_list>并声明一个以initialize_List<T>模板类为参数的构造函数,也可以使得自定义类使用列表初始化。

在C++11中,除了初始化列表(在构造函数中初始化)外,允许使用等=或花括号{}进行就地的非静态成员变量初始化,例如:

1
2
3
4
5
class example
{
int a = 1;
double b{ 1.2 };
};

如果在一个类中,既使用了就地初始化来初始化非静态成员变量,又在构造函数中使用了初始化列表,执行顺序是: 先执行就地初始化,然后执行初始化列表。

1
2
3
4
5
6
7
8
enum class SensorType {
RANGE = 0,
IMU,
ODOMETRY,
FIXED_FRAME_POSE,
LANDMARK,
LOCAL_SLAM_RESULT
};

using

1
2
3
4
5
6
7
8
9
class MapBuilderInterface {
public:
using LocalSlamResultCallback =
TrajectoryBuilderInterface::LocalSlamResultCallback;

using SensorId = TrajectoryBuilderInterface::SensorId;

using Callback =
std::function<void(const std::string&, std::unique_ptr<Data>)>;

cartographer新版本的变化

publish_tracked_pose 和 use_pose_extrapolator

新版本增加的参数publish_tracked_pose,默认false

1
2
3
4
5
6
7
8
9
10
11
struct NodeOptions {
::cartographer::mapping::proto::MapBuilderOptions map_builder_options;
std::string map_frame;
double lookup_transform_timeout_sec;
double submap_publish_period_sec;
double pose_publish_period_sec;
double trajectory_publish_period_sec;
bool publish_to_tf = true;
bool publish_tracked_pose = false;
bool use_pose_extrapolator = true;
};

Node构造函数中的部分

1
2
3
4
5
if (node_options_.publish_tracked_pose) {
tracked_pose_publisher_ =
node_handle_.advertise<::geometry_msgs::PoseStamped>(
kTrackedPoseTopic, kLatestOnlyPublisherQueueSize);
}

发布消息的部分在Node::PublishLocalTrajectoryData的最后。

这里用到了新版本的另一个新内容: 参数use_pose_extrapolator

Node里的位姿估计器,作用是融合里程计和IMU,推测出一个位姿。 如果use_pose_extrapolator参数为true,发布出的这个位姿不准,因为是先验的位姿,没有经过雷达校准,除非IMU和里程计特别准。因此这个参数一般都是false

如果参数publish_tracked_pose为false,use_pose_extrapolator其实就无效了

Node::MaybeWarnAboutTopicMismatch

新版本中的Node::AddTrajectory添加

1
2
3
wall_timers_.push_back(node_handle_.createWallTimer(
::ros::WallDuration(kTopicMismatchCheckDelaySec),
&Node::MaybeWarnAboutTopicMismatch, this, /*oneshot=*/true) );

创建一个3s执行一次的定时器,由于oneshot=true,所以只执行一次。检查设置的topic名字在ROS中是否存在,不存在则报错

pose_graph_odometry_motion_filter

CreateGlobalTrajectoryBuilder2D的参数增加 pose_graph_odometry_motion_filter里程计的滤波器,但没有初始化

1
2
3
4
5
6
7
absl::optional<MotionFilter> pose_graph_odometry_motion_filter;
if (trajectory_options.has_pose_graph_odometry_motion_filter())
{
LOG(INFO) << "Using a motion filter for adding odometry to the pose graph.";
pose_graph_odometry_motion_filter.emplace(
MotionFilter(trajectory_options.pose_graph_odometry_motion_filter()));
}

GetTrajectoryStates 改名为 GetLocalTrajectoryData

原因在于后端里也有GetTrajectoryStates,容易混淆

Node构造函数 —— Node::PublishLocalTrajectoryData —— map_builder_bridge_.GetLocalTrajectoryData()

最后用到的local_slam_data_MapBuilderBridge::OnLocalSlamResult中赋值


ROS常用数据类型

geometry_msgs 系列

  • geometry_msgs/Pose

    1
    2
    geometry_msgs/Point position
    geometry_msgs/Quaternion orientation
  • geometry_msgs::PoseStamped

    1
    2
    std_msgs/Header  header
    geometry_msgs/Pose pose
  • geometry_msgs::Point

    1
    2
    3
    float64 x
    float64 y
    float64 z
  • geometry_msgs::Quaternion

    1
    2
    3
    4
    float64 x
    float64 y
    float64 z
    float64 w
  • geometry_msgs/PoseArray

    1
    2
    std_msgs/Header header
    geometry_msgs/Pose[] poses
  • geometry_msgs/Pose2D

    1
    2
    3
    float64 x
    float64 y
    float64 theta
  • geometry_msgs/Transform

    1
    2
    geometry_msgs/Vector3 translation
    geometry_msgs/Quaternion rotation
  • geometry_msgs/PoseWithCovariance

    1
    2
    geometry_msgs/Pose  pose
    float64[36] covariance
  • geometry_msgs::Polygon

    1
    geometry_msgs/Point32[]  points

tf 系列

以下两个类都继承 tf::Transform

tf::Pose

tf::Stamped对数据类型做模板化(除了tf::Transform),并附带元素frameid和stamp、
setData成员函数

1
2
3
4
tf::Stamped<tf::Pose> pose;
getOrigin().getX();
getOrigin().getY();
tf::getYaw(pose.getRotation())

tf::StampedTransform

1
2
3
4
5
double x= transform.getOrigin().getX();
double y= transform.getOrigin().getY();
double z= transform.getOrigin().getZ();
double angle = transform.getRotation().getAngle();
ROS_INFO("x: %f, y: %f, z: %f, angle: %f",x,y,z,angle);
1
2
3
4
5
6
7
8
9
10
11
const geometry_msgs::PoseStamped& pose = ;
tf::StampedTransform transform;
geometry_msgs::PoseStamped new_pose;

tf::Stamped<tf::Pose> tf_pose;
tf::poseStampedMsgToTF(pose, tf_pose);

tf_pose.setData(transform * tf_pose);
tf_pose.stamp_ = transform.stamp_;
tf_pose.frame_id_ = global_frame;
tf::poseStampedTFToMsg(tf_pose, new_pose); //转为 geometry_msgs::PoseStamped类型

tf::poseStampedMsgToTF函数,把geometry_msgs::PoseStamped转化为Stamped<Pose>


PoseSE2

源码在pose_se2.h,有两个private成员

1
2
Eigen::Vector2d  _position; 
double _theta;

支持下列函数:
1
2
3
4
5
PoseSE2(double x, double y, double theta)
PoseSE2(const geometry_msgs::Pose& pose)
PoseSE2(const tf::Pose& pose)

friend std::ostream& operator<< (std::ostream& stream, const PoseSE2& pose)

另外还支持运算符* + - =

Eigen::Vector2d orientationUnitVec() const: 获得当前朝向的单位向量

PoseSE2::average(start, goal);

costmap_converter/ObstacleArrayMsg

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
std_msgs/Header header    # frame_id 为map
costmap_converter/ObstacleMsg[] obstacles

# costmap_converter/ObstacleMsg 的成员如下
std_msgs/Header header # frame_id 为map
# Obstacle footprint (polygon descriptions)
geometry_msgs/Polygon polygon
# radius for circular/point obstacles
float64 radius
# Obstacle ID
# Specify IDs in order to provide (temporal) relationships
# between obstacles among multiple messages.
int64 id
# Individual orientation (centroid)
geometry_msgs/Quaternion orientation
# Individual velocities (centroid)
geometry_msgs/TwistWithCovariance velocities

Obstacle定义在obstacle.h,派生类有PointObstacle, CircularObstacle, LineObstacle,PolygonObstacle

1
2
typedef boost::shared_ptr<Obstacle> ObstaclePtr;
typedef std::vector<ObstaclePtr> ObstContainer;

PoseSequence posevec; //!< Internal container storing the sequence of optimzable pose vertices

TimeDiffSequence timediffvec; //!< Internal container storing the sequence of optimzable timediff vertices

VertexPose 继承g2o::BaseVertex<3, PoseSE2 >
This class stores and wraps a SE2 pose (position and orientation) into a vertex that can be optimized via g2o

VertexTimeDiff继承g2o::BaseVertex<1, double>。This class stores and wraps a time difference \f$ \Delta T \f$ into a vertex that can be optimized via g2o

判断初始化:bool isInit() const {return !timediff_vec_.empty() && !pose_vec_.empty(); }

TimedElasticBand::clearTimedElasticBand(): 清空 timediffvec posevec

addPoseAndTimeDiff: Add one single Pose first. Timediff describes the time difference between last conf and given conf”;


所有约束的信息矩阵

所有约束的信息矩阵都是对角矩阵

  • 速度
1
2
3
4
5
Eigen::Matrix<double,2,2> information;
information(0,0) = cfg_->optim.weight_max_vel_x;
information(1,1) = cfg_->optim.weight_max_vel_theta;
information(0,1) = 0.0;
information(1,0) = 0.0;
  • 加速度

    1
    2
    3
    4
    Eigen::Matrix<double,2,2> information;
    information.fill(0);
    information(0,0) = cfg_->optim.weight_acc_lim_x;
    information(1,1) = cfg_->optim.weight_acc_lim_theta;
  • 时间

    1
    2
    Eigen::Matrix<double,1,1> information;
    information.fill(cfg_->optim.weight_optimaltime);
  • 最短距离

1
2
Eigen::Matrix<double,1,1> information;
information.fill(cfg_->optim.weight_shortest_path);
  • 优先转弯方向,二元边

    1
    2
    3
    // create edge for satisfiying kinematic constraints
    Eigen::Matrix<double,1,1> information_rotdir;
    information_rotdir.fill(cfg_->optim.weight_prefer_rotdir);
  • 障碍

1
2
Eigen::Matrix<double,1,1> information;
information.fill(cfg_->optim.weight_obstacle * weight_multiplier);
  • 动态障碍 AddEdgesDynamicObstacles
1
2
3
4
Eigen::Matrix<double,2,2> information;
information(0,0) = cfg_->optim.weight_dynamic_obstacle * weight_multiplier;
information(1,1) = cfg_->optim.weight_dynamic_obstacle_inflation;
information(0,1) = information(1,0) = 0;

前端 5. Ceres scan matcher中的占用空间代价函数

先看这个匹配的原理

ceres中的类 BiCubicInterpolator

输入无限的二维grid

1
2
3
4
struct Grid2D {
enum { DATA_DIMENSION = 2 };
void GetValue(int row, int col, double* f) const;
};

GetValue函数返回函数f(可能是向量)的值, 枚举DATA_DIMENSION表示所插值函数的维度,比如对一个图片(红绿蓝)进行插值,那么DATA_DIMENSION = 3

BiCubicInterpolator使用三次卷积算法生成平滑估计,或者说双三次插值法,用来在真实曲线的任一点评价

对二维数组进行插值

1
2
3
4
5
6
7
8
const double data[] = {1.0, 3.0, -1.0, 4.0,
3.6, 2.1, 4.2, 2.0,
2.0, 1.0, 3.1, 5.2};
// 生成 BiCubicInterpolator 需要的二维数组
Grid2D<double, 1> array(data, 0, 3, 0, 4);
BiCubicInterpolator interpolator(array);
double f, dfdr, dfdc;
interpolator.Evaluate(1.2, 2.5, &f, &dfdr, &dfdc);

函数void Evaluate(double r, double c, double* f, double* dfdr, double* dfdc),残差会对应第3个参数f。 Evaluate the interpolated function value and/or its 导数. Returns false 如果r 或者 c越界

这里的用法没看懂,怎么获得插值后的结果, GetValue获得还是之前的值

OccupiedSpaceCostFunction2D

这个Occupied Space Cost Function的模型和 Real time correlative scan matching 的思路基本上一致,只是求解方法变成了最小二乘问题的求解。

将点云中所有点的坐标映射到栅格坐标系, 假如点对应的空闲概率最小,说明对应栅格几乎被占据,点确实是hit点,此时的变换为最优变换。 出于精度考虑使用了ceres提供的双三线性插值。 还有地图大小限制的问题,即一旦点云变换后存在部分脱离地图范围的点,这些点的代价值需要定义。cartographer中的做法是在地图周围增加一个巨大的边框(kPadding),并且通过一个地图适配器定义点落在边框中的代价值。

先看创建代价函数 CreateOccupiedSpaceCostFunction2D

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 创建代价函数 for matching the 'point_cloud' to the 'grid' with a 'pose'
// 'point_cloud' 是 filtered_gravity_aligned_point_cloud
// 'grid' 是 matching_submap->grid()
// grid 和 point observation 匹配越差,代价越大
// 比如 points falling into less occupied space
ceres::CostFunction* CreateOccupiedSpaceCostFunction2D(
const double scaling_factor, const sensor::PointCloud& point_cloud,
const Grid2D& grid)
{
return new ceres::AutoDiffCostFunction<OccupiedSpaceCostFunction2D,
ceres::DYNAMIC, /* residuals 残差维度未知 */
3 /* pose variables */>
(
new OccupiedSpaceCostFunction2D(scaling_factor, point_cloud, grid),
point_cloud.size() );
}

OccupiedSpaceCostFunction2D的构造函数只有参数赋值

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
template <typename T>
bool operator()(const T* const pose, T* residual) const
{
Eigen::Matrix<T, 2, 1> translation(pose[0], pose[1]);
Eigen::Rotation2D<T> rotation(pose[2]);
Eigen::Matrix<T, 2, 2> rotation_matrix = rotation.toRotationMatrix();
Eigen::Matrix<T, 3, 3> transform;
transform << rotation_matrix, translation, T(0.), T(0.), T(1.);
// 构造二阶线性插值类, 成员变量只有 grid_
// 功能主要是在概率栅格图对应的 index 中取出相应的概率值
const GridArrayAdapter adapter(grid_);
// 使用Ceres提供的双三次插值迭代器
ceres::BiCubicInterpolator<GridArrayAdapter> interpolator(adapter);
const MapLimits& limits = grid_.limits();
// 遍历点云(当前 scan)中的所有点,计算该点匹配残差: 1-Smooth(Tp)
for (size_t i = 0; i < point_cloud_.size(); ++i)
{
// 这是2D点,第三个元素是 scaling factor
const Eigen::Matrix<T, 3, 1> point(
( T(point_cloud_[i].position.x()) ),
( T(point_cloud_[i].position.y()) ),
T(1.) );

// 将点转换到 local map 坐标系下
const Eigen::Matrix<T, 3, 1> world = transform * point;
/* 计算 1-Smooth(T * point)
前两个参数用来描述x y轴索引, 第三个参数用于记录插值后的结果
xy索引的计算是通过 GridArrayAdapter::GetValue 获取栅格数据
取出每个点对应的栅格地图空闲概率(双三次插值之后的) p */
interpolator.Evaluate(
// 传进来的时候x和y都分别加了 kPadding
// max的x y减掉真实的xy得到 cell_index
/* kPadding是为了解决有些点可能跑到地图外面去的情况,所以加了一个超大的值
即将地图上下左右边界分别扩大 kPadding,这是为了照顾ceres的Evaluate函数 */

// 将坐标转换为栅格坐标,与子图坐标方向相反,双三次插值器自动计算中对应坐标的value
(limits.max().x() - world[0]) / limits.resolution() - 0.5 +
static_cast<double>(kPadding),
(limits.max().y() - world[1]) / limits.resolution() - 0.5 +
static_cast<double>(kPadding),
&residual[i] );
// 因为有多个点,残差不是一维的,所有残差使用同一权重
residual[i] = scaling_factor_ * residual[i];
}
return true;
}

占用栅格中原本存储的就是栅格空闲的概率,而这里GetValue查询出来的概率其实就是 ,令其最小化就对了

GridArrayAdapter是cartographer定义的,使用适配器模式,interpolator构造函数的参数需要的是模板里的类型。重要函数的是GetValue,调用的地方在interpolator.Evaluate里面。根源还是BiCubicInterpolator

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
class GridArrayAdapter
{
public:
enum { DATA_DIMENSION = 1 }; //被插值的向量维度

explicit GridArrayAdapter(const Grid2D& grid) : grid_(grid) {}
// 返回空闲概率, kPadding 是个很大的数
void GetValue(const int row, const int column, double* const value) const
{
// 处于地图外部时,赋予最大的free值
if(row < kPadding || column < kPadding ||
row >= NumRows() - kPadding || column >= NumCols() - kPadding)
*value = kMaxCorrespondenceCost;
// 在地图里取空闲概率,这里需要减掉kPadding,因为在传进来的时候,已经加了kPadding
else
*value = static_cast<double>(grid_.GetCorrespondenceCost(
Eigen::Array2i(column - kPadding, row - kPadding) ) );
}
int NumRows() const {
return grid_.limits().cell_limits().num_y_cells + 2 * kPadding;
}
int NumCols() const {
return grid_.limits().cell_limits().num_x_cells + 2 * kPadding;
}
private:
const Grid2D& grid_;
}

参考: ceres 三次插值
cubic_interpolation.h