Costmap2D类的 setConvexPolygonCost 函数
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
bool Costmap2D::setConvexPolygonCost(const std::vector<geometry_msgs::Point>& polygon, unsigned char cost_value)
{
// we assume the polygon is given in the global_frame... we need to transform it to map coordinates
std::vector<MapLocation> map_polygon;
for (unsigned int i = 0; i < polygon.size(); ++i)
{
MapLocation loc;
// 先将世界系下的多边形顶点转换到地图坐标系,并存放进map_polygon数组
if (!worldToMap(polygon[i].x, polygon[i].y, loc.x, loc.y))
{
// ("Polygon lies outside map bounds, so we can't fill it");
return false;
}
map_polygon.push_back(loc);
}
std::vector<MapLocation> polygon_cells;

// 通过机器人顶点坐标数组map_polygon得到多边形边缘及内部的全部cell,存放在 polygon_cells
// get the cells that fill the polygon
convexFillCells(map_polygon, polygon_cells);

// set the cost of those cells
// 通过循环对多边形边缘及内部各cell的cost赋值
for (unsigned int i = 0; i < polygon_cells.size(); ++i)
{
unsigned int index = getIndex(polygon_cells[i].x, polygon_cells[i].y);
costmap_[index] = cost_value;
}
return true;
}

convexFillCells

这个函数用于获取多边形边缘及内部cell

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
69
70
71
72
73
void Costmap2D::convexFillCells(const std::vector<MapLocation>& polygon, std::vector<MapLocation>& polygon_cells)
{
// we need a minimum polygon of a triangle
if (polygon.size() < 3)
return;

// first get the cells that make up the outline of the polygon
//首先获得轮廓点之间连线的列表,存放在polygon_cells中
polygonOutlineCells(polygon, polygon_cells);

// quick bubble sort to sort points by x
MapLocation swap;
unsigned int i = 0;
// 对边上的cell点的x做排序,使其按x坐标升序排列
while (i < polygon_cells.size() - 1)
{
if (polygon_cells[i].x > polygon_cells[i + 1].x)
{
swap = polygon_cells[i];
polygon_cells[i] = polygon_cells[i + 1];
polygon_cells[i + 1] = swap;

if (i > 0)
--i;
}
else
++i;
}
/* 遍历所有x,对每个相同的x,检查y,获得y最大和最小的polygon cell,
将范围内的所有cell填充进polygon_cells,获得多边形边缘及内部的所有cell */
i = 0;
MapLocation min_pt;
MapLocation max_pt;
unsigned int min_x = polygon_cells[0].x;
unsigned int max_x = polygon_cells[polygon_cells.size() - 1].x;

// walk through each column and mark cells inside the polygon
for (unsigned int x = min_x; x <= max_x; ++x)
{
if (i >= polygon_cells.size() - 1)
break;

if (polygon_cells[i].y < polygon_cells[i + 1].y)
{
min_pt = polygon_cells[i];
max_pt = polygon_cells[i + 1];
}
else
{
min_pt = polygon_cells[i + 1];
max_pt = polygon_cells[i];
}

i += 2;
while (i < polygon_cells.size() && polygon_cells[i].x == x)
{
if (polygon_cells[i].y < min_pt.y)
min_pt = polygon_cells[i];
else if (polygon_cells[i].y > max_pt.y)
max_pt = polygon_cells[i];
++i;
}

MapLocation pt;
// loop though cells in the column
for (unsigned int y = min_pt.y; y < max_pt.y; ++y)
{
pt.x = x;
pt.y = y;
polygon_cells.push_back(pt);
}
}
}

polygonOutlineCells

循环调用raytraceLine函数,不断获取相邻之间的连线,最终组成多边形边上的cell,需要注意的是需要将最后一点和第一点连接起来,形成闭合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Costmap2D::polygonOutlineCells(const std::vector<MapLocation>& polygon, std::vector<MapLocation>& polygon_cells)
{
PolygonOutlineCells cell_gatherer(*this, costmap_, polygon_cells);
for (unsigned int i = 0; i < polygon.size() - 1; ++i)
{
raytraceLine(cell_gatherer, polygon[i].x, polygon[i].y, polygon[i + 1].x, polygon[i + 1].y);
}
if (!polygon.empty())
{
unsigned int last_index = polygon.size() - 1;
// we also need to close the polygon by going from the last point to the first
raytraceLine(cell_gatherer, polygon[last_index].x, polygon[last_index].y, polygon[0].x, polygon[0].y);
}
}

raytraceLine

找到两点连线上的cell。对于离散的平面点,指定两个点,这个函数可以找到两个点之间的其他点,使得这些中间组成一个尽可能趋近直线的点集

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
template<class ActionType>  inline void raytraceLine(ActionType at, 
unsigned int x0, unsigned int y0,
unsigned int x1, unsigned int y1,
unsigned int max_length = UINT_MAX )
{
int dx = x1 - x0;
int dy = y1 - y0;

unsigned int abs_dx = abs(dx);
unsigned int abs_dy = abs(dy);

int offset_dx = sign(dx);
int offset_dy = sign(dy) * size_x_;

unsigned int offset = y0 * size_x_ + x0;

// we need to chose how much to scale our dominant dimension, based on the maximum length of the line
double dist = hypot(dx, dy);
double scale = (dist == 0.0) ? 1.0 : std::min(1.0, max_length / dist);

// if x is dominant
if (abs_dx >= abs_dy)
{
int error_y = abs_dx / 2;
bresenham2D(at, abs_dx, abs_dy, error_y, offset_dx, offset_dy, offset, (unsigned int)(scale * abs_dx));
return;
}
// otherwise y is dominant
int error_x = abs_dy / 2;
bresenham2D(at, abs_dy, abs_dx, error_x, offset_dy, offset_dx, offset, (unsigned int)(scale * abs_dy));
}


Gazebo行人作为动态障碍

Gazebo自带的一些模型,比如人行走和奔跑的模型,在/usr/share/gazebo-11/media/modelsGazebo的行人模型不是障碍,机器人开过去撞不上,Rviz里也不会作为障碍,只能找插件让它变成障碍。

Gazebo模型碰撞的插件actor_collisions,按照说明进行编译后,如果执行gazebo ../actor_collisions.world正常,那么说明安装成功了。然后将libActorCollisionsPlugin.so文件复制到/opt/ros/noetic/lib

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<actor name="actor">
<!-- 插件 -->
<plugin name="actor_collisions_plugin" filename="libActorCollisionsPlugin.so">
<!-- 行人全身关节,不需修改 -->
<scaling collision="LHipJoint_LeftUpLeg_collision" scale="
0.01
0.001
0.001
"/>
......
<scaling collision="RightFingerBase_RightHandIndex1_collision" scale="
4.0
4.0
3.0
"/>
</plugin>
1
2
3
4
5
6
7
8
9
10
11
12
13
<!-- 行人的初始位姿 -->
<pose>3 -3 0.9 1.57 0 0</pose>
<!-- 使用哪个模型 -->
<skin>
<filename>walk.dae</filename>
<scale>1.0</scale>
</skin>

<animation name="walking">
<filename>walk.dae</filename>
<scale>1.000000</scale>
<interpolate_x>true</interpolate_x>
</animation>

循环行走的逻辑,使用script,手动确定每个时间点的位姿,尽量保证循环行走的平滑性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
<loop>true</loop>
<delay_start>0.000000</delay_start>
<auto_start>true</auto_start>

<trajectory id="0" type="walking">
<waypoint>
<time>0.000000</time>
<pose>0.000000 1.000000 0.000000 0.000000 0.000000 0.000000</pose>
</waypoint>
<waypoint>
<time>0.500000</time>
<pose>0.195090 0.980785 0.000000 0.000000 0.000000 -0.196350</pose>
</waypoint>
......
<waypoint>
<time>18.000000</time>
<pose>-0.195090 0.980785 0.000000 0.000000 0.000000 0.196345</pose>
</waypoint>
</trajectory>
</script>

插件的缺点:

  1. 插件的collision参数难以准确设置
  2. 行走的时间和位姿难以保证平滑性


第1个缺点不好解决,不同的模型需要单独设置参数。针对第2个缺点,我发现了插件gazebo_ros_actor_plugin,这个插件可以对行人进行速度控制。

两种模式(follow_mode): velocity模式,使用teleop_twist_keyboard进行手动控制; path模式,按照path_publisher.py指定路径行走。

对于可控制actor移动的模式,world文件加入下面内容

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
<actor name="actor">
<pose>3 -3 0.9 1.57 0 0</pose>

<skin>
<filename>walk.dae</filename>
<scale>1.0</scale>
</skin>

<animation name="walking">
<filename>walk.dae</filename>
<scale>1.000000</scale>
<interpolate_x>true</interpolate_x>
</animation>

<plugin name="actor_plugin" filename="libgazebo_ros_actor_command.so">
<!-- <follow_mode>path</follow_mode> -->
<follow_mode>velocity</follow_mode>
<vel_topic>/gazebo_cmd_vel</vel_topic>
<path_topic>/cmd_path</path_topic>
<animation_factor>4.0</animation_factor>
<linear_tolerance>0.1</linear_tolerance>
<linear_velocity>1</linear_velocity>
<angular_tolerance>0.0872</angular_tolerance>
<angular_velocity>2.5</angular_velocity>
<default_rotation>1.57</default_rotation>
</plugin>
</actor>

但问题在于,这个插件的行人和默认的一样,不是障碍,如果能将两个插件结合就好了。

但是同时加载两个插件后,Gazebo里的行人不能正常显示,要么是频繁在站立和躺着之间切换,要么就是原地走,但没有往前动。如果旋转,行人没反应。显然是两个插件冲突了。

修改

将第1个插件的Load函数加入第2个插件,再把SetWorldPose函数的后两个参数从false改为true,即 this->actor_->SetWorldPose(pose, true, true);,编译之后把生成的libgazebo_actor_cmd_collision.so放入/usr/lib/x86_64-linux-gnu/gazebo-11/plugins

这样把两个插件合并,可以让actor作为障碍被用户控制。

如果配置Gazebo有多个actor,可以都设置为手动控制模式,只要话题名不同,也能分别控制。

缺点: 不同actor不会相撞,遇到时会穿过。

也可以设置有的actor为手动控制模式,有的为路径模式。
最终效果.gif


tf常用函数和geometry_msgs的互相转换

来自transform_datatypes.h,这个头文件真是太有用了

1
2
typedef tf::Vector3 	tf::Point
typedef tf::Transform tf::Pose

  • static tf::Quaternion tf::createQuaternionFromRPY (double roll, double pitch, double yaw)

  • static geometry_msgs::Quaternion tf::createQuaternionMsgFromRollPitchYaw (double roll, double pitch, double yaw)

  • static geometry_msgs::Quaternion tf::createQuaternionMsgFromYaw (double yaw)

  • static double tf::getYaw (const geometry_msgs::Quaternion &orientation)

  • static void tf::transformTFToMsg (const Transform &bt, geometry_msgs::Transform &msg)

  • static void tf::transformStampedMsgToTF (const geometry_msgs::TransformStamped &msg, StampedTransform &bt)

  • static void tf::transformMsgToTF (const geometry_msgs::Transform &msg, Transform &bt)

  • static void tf::quaternionTFToMsg (const Quaternion &bt, geometry_msgs::Quaternion &msg)

  • static void tf::poseTFToMsg (const Pose &bt, geometry_msgs::Pose &msg)

  • static void tf::poseStampedMsgToTF (const geometry_msgs::PoseStamped &msg, Stamped< Pose > &bt)

1
2
3
4
5
6
#include "tf2_geometry_msgs/tf2_geometry_msgs.h"

tf2::Quaternion quat_tf;
quat_tf.setRPY(1.0, 0.0, 0.0);
geometry_msgs::Quaternion quat_msg;
tf2::convert(quat_tf, quat_msg);
1
2
3
4
5
geometry_msgs::PoseStamped  p;
// 对 p 赋值
// ......
tf2::Quaternion q;
tf2::convert(p.pose.orientation, q);
  • geometry_msgs::TransformStampedtf::StampedTransform, tf2::Transform
1
2
3
4
5
6
geometry_msgs::TransformStamped   transform;
tf::StampedTransform tfTransform;
transformStampedTFToMsg(tfTransform , transform);

tf2::Transform tf2_transform;
tf2::convert(transform.transform, tf2_transform);

tf::StampedTransformgeometry_msgs/TransformStamped的区别:前者是C++中的一个类,只能用于C++中,而后者则是ROS的消息,只依赖于ROS,与语言无关,都可以使用。


costmap和OpenCV的坐标关系

costmap和OpenCV的坐标系关系
costmap原点在左下角,OpenCV图片的原点在左上。

cv::Mat map_mat = cv::Mat(m_y, m_x, CV_8U);type经常设置为CV_8U,无符号整数 0~255,Mat代表矩阵,该类声明在头文件opencv2/core/core.hpp。对(行,列)元素访问、赋值常使用m.at<类型>(行,列);

OpenCV的黑白图像是二维数组,255是白色,0是黑色. 数组中每个元素就是图像对应位置的像素值,数组索引、像素行列、像素坐标关系如下:

  • 数组行索引 = 像素所在行数 - 1 = 像素纵坐标
  • 数组列索引 = 像素所在列数 - 1 = 像素横坐标

在ROS中的地图像素扫描方向是从左下角向右上角扫描的。resolution表示地图上一个像素宽度代表实际的距离(通常为0.05,代表一个像素代表5cm*5cm),costmap的本质是栅格地图,其示意图如下:

OpenCV的图相当于costmap先逆时针转180°,再对y轴取对称。

1
2
3
4
5
6
7
8
9
10
11
// pgm的像素值,或者说保存方式和OpenCV是一样的
costmap_->saveMap("/home/user/costmap.pgm");

// 范围 0~255
cv::Mat map_mat = cv::Mat(m_y, m_x, CV_8U);

for (int x = 0; x < map_mat.cols; x++)
for (int y = 0; y < map_mat.rows; y++)
{
map_mat.at<uint8_t>(map_mat.rows -1 - y, x) = 255 - costmap_->getCharMap()[ y * map_mat.cols + x];
}


Action通信(二) MoveBase客户端和服务端的常用代码

MoveBase.action

1
2
3
4
geometry_msgs/PoseStamped target_pose
---
---
geometry_msgs/PoseStamped base_position

target_pose是目标位姿。base_position作为feedback,是base_link在world坐标系的位姿。


MoveBase的Action实际就是另一种形式的话题,会生成7个msg文件:

1
2
3
4
5
6
7
MoveBaseActionFeedback.msg  
MoveBaseActionResult.msg
MoveBaseResult.msg
MoveBaseActionGoal.msg
MoveBaseFeedback.msg
MoveBaseAction.msg
MoveBaseGoal.msg

另外有个RecoveryStatus.msg,目前没有使用过。

1
2
3
4
geometry_msgs/PoseStamped pose_stamped
uint16 current_recovery_number
uint16 total_number_of_recoveries
string recovery_behavior_name

客户端

1
2
template<class ActionSpec>
bool actionlib::SimpleActionClient< ActionSpec >::waitForServer (const ros::Duration & timeout = ros::Duration(0,0) ) [inline]

一般花点时间,比如几百毫秒实现二者的连接,在此时间内的goal会发送失败。

参数:

  • timeout Max time to block before returning. 参数为 0 is interpreted as an infinite timeout.
  • 返回 True if the server connected in the allocated time. False on timeout

所以这样写代码比较优雅:

1
2
3
4
5
while( ! ac->isServerConnected() )
{
ROS_INFO("Waiting for move_base action server to start");
ac->waitForServer(ros::Duration(1) );
}

发送goal的常用程序

1
2
3
4
5
6
7
8
9
10
11
12
13
ac = new actionlib::SimpleActionClient<move_base_msgs::MoveBaseAction>("move_base", true);
// 注意是 MoveBaseGoal, 成员只有 geometry_msgs/PoseStamped target_pose
move_base_msgs::MoveBaseGoal goal;
// 一系列赋值 ......
ac->sendGoal(goal);
// ac->sendGoal(goal, &doneCb, &activeCb, &feedbackCb);

bool finished_before_timeout = ac->waitForResult(ros::Duration(30.0));
if (finished_before_timeout)
{
actionlib::SimpleClientGoalState state = ac->getState();
ROS_INFO("MoveBase Action finished: %s", state.toString().c_str());
}

三个回调函数 :当action完成后,调用回调函数doneCb一次。 当action激活后,调用回调函数activeCb一次,此时还没有获得路径。 收到feedback后,不断调用回调函数feedbackCb,一直到action结束

  • bool waitForResult (const ros::Duration &timeout = ros::Duration(0, 0))

Blocks until this goal finishes. 可以指定阻塞的时间,如果是0,则无线阻塞。

  • SimpleClientGoalState getState() const

Get the state information for this goal. Possible States Are: PENDING, ACTIVE, RECALLED, REJECTED, PREEMPTED, ABORTED, SUCCEEDED, LOST

如果在服务端的回调函数里没有对goal的状态进行设置,会有下面报警

Your executeCallback did not set the goal to a terminal status. This is a bug in your ActionServer implementation. Fix your code!
For now, the ActionServer will set this goal to aborted
因为没有设置goal的最终状态,比如使用setSucceededsetAborted设置状态.

  • cancelGoal

cancelGoal()之后,getState()的状态还是ACTIVE

如果没有goal处于ACTIVE,不要使用cancelGoal,会报错但是不会让进程退出:

两个回调函数

actionlib::SimpleActionServer的成员函数

1
2
3
4
5
// Allows users to register a callback to be invoked when a new goal is available.
void registerGoalCallback (boost::function< void()> cb)

// Allows users to register a callback to be invoked when a new preempt request is available.
void registerPreemptCallback (boost::function< void()> cb)

客户端向SimpleActionServer发送新目标时,isNewGoalAvailable()为true,isPreemptRequested()也为true。 如果客户端调用cancelGoal()isNewGoalAvailable()为false,但isPreemptRequested()还是true.

问题

有时候我在move_base设置了setAborted之后,会出现报警 To transition to an aborted state, the goal must be in a preempting or active state, it is currently in state: 4 。 这是因为多次调用了 setAborted,state 4 其实就是状态 Aborted

此时如果再发送goal,就会出现报警
Your executeCallback did not set the goal to a terminalstatus. This is a bug in your ActionServer implementation. Fix your code! For now, the ActionServer will set this goal to aborted
,这打乱了 Action 的状态机,必须避免这个报警。


我使用了一段时间后,感觉缺resetGoalgetGoal函数,不太方便。

参考:
ROS Action动作通讯编程C++
Writing a Callback Based SimpleActionClientTutorials(2f)Writing(20)a(20)Callback(20)Based(20)Simple(20)Action(20)Client.html)


使用GDB解决算法切换导致的多线程问题 (一)
abstract Welcome to my blog, enter password to read.
Read more
Gazebo使用心得

Gazebo不好上手,费了一番功夫终于熟悉了,内容太多,只记录注意事项。

Gazebo里用的是 前x 左y 上z的右手坐标系 ,gazebo的栅格范围是20mX20m

启动simple_world.launch,然后在Gazebo里添加一个圆柱体,保存覆盖simple.world文件。再次启动时,报错 SpawnModel: Failure - model name sunny already exist. [Spawn service failed. Exiting. [spawn_urdf-6] process has died

网上说的方法没能解决,也不能重命名model,修改simple.world无效。 最后删除了<node pkg="gazebo_ros" type="spawn_model" name="spawn_urdf" args="-urdf -model sunny -x $(arg x_pos) -y $(arg y_pos) -z $(arg z_pos) -param robot_description" /> 解决问题。

urdf 和 xacro

urdf.xacro文件里,<link name="base_link">代表机器人的主体。<geometry> <box size="0.4 0.39 0.5" />是尺寸。

gazebo插件,可以简单理解为就是连接gazebo和ros的一个桥梁,一边插件通过gazebo的api来控制仿真环境的机器人关节等数据,一边通过关节数据通过运动学模型生成里程计、雷达的scan、image等类似的信息

solidworks生成urdf

solidworks to urdf安装后,如果在solidworks里找不到,就安装最新版的,不用管版本是否对应。

solidworks只生成的URDF文件,没有xacro文件. URDF文件做一些简单修改可保存为xacro,注意:

  1. jointtype="fixed",应当为continuous
  2. joint的某些参数值错误,导致车加载差速模型后,运动错误

xacro

xacro里面的模型仍然是urdf模型,有如下变化:

  1. 创建宏定义
  2. 文件包含
  3. 提供可编程接口:常量, 变量, 数学计算, 条件语句

启动Gazebo时,机器人模型的tf关系不完整,即使xacro文件正确。还需要添加joint_state_publisherrobot_state_publisher

设置雷达时,看情况决定是否需要可视化

1
2
3
<sensor type="ray" name="lds_lfcd_sensor">
<pose>0 0 0 0 0 0</pose>
<visualize>false</visualize>

Gazebo默认的障碍物质量轻,车撞上之后会把障碍物推开。改变density选项为铁,则不会推动障碍物。


在Gazebo world中用以下方式加载模型时:

1
2
3
<include>
<uri>model://one_building</uri>
</include>

要让Gazebo能够找到uri中的自建model,需要将自己的model路径加入环境变量, export GAZEBO_MODEL_PATH=~/<path>/my_package/models:${GAZEBO_MODEL_PATH}


数字的类型

数字后面带个U,L,F的含义

  • U表示该常数用无符号整型方式存储,相当于 unsigned int
  • L表示该常数用长整型方式存储,相当于 long
  • F表示该常数用浮点方式存储,相当于 float

比如 -1UL, 其实是一个很大很大的数

OpennCV的数据类型

S = 有符号整型 U = 无符号整型 F = 浮点型

  • CV_8U - 8位无符号整数(0…255)
  • CV_32F - 32位浮点数(-FLT_MAX…FLT_MAX,INF,NAN)

  • CV_8U - 8位无符号整数(0…255)

  • CV_8S - 8位有符号整数(-128…127)

  • CV_16U - 16位无符号整数(0…65535)

  • CV_16S - 16位有符号整数(-32768…32767)

  • CV_32S - 32位有符号整数(-2147483648…2147483647)

  • CV_32F - 32位浮点数(-FLT_MAX…FLT_MAX,INF,NAN)

  • CV_64F - 64位浮点数(-DBL_MAX…DBL_MAX,INF,NAN)


trimmer机制 - 移除重复子图

PoseGraph2D::HandleWorkQueue里,RunOptimization();之后

1
2
3
4
5
6
7
8
9
10
11
12
13
// 子图的裁剪,如果没有裁剪器就不裁剪
TrimmingHandle trimming_handle(this);
for (auto& trimmer : trimmers_) {
trimmer->Trim(&trimming_handle);
}
// 这里只有纯定位时才执行
// 裁剪器完成状态,删除裁剪器
trimmers_.erase(
std::remove_if(trimmers_.begin(), trimmers_.end(),
[](std::unique_ptr<PoseGraphTrimmer>& trimmer) {
return trimmer->IsFinished();
}),
trimmers_.end());

trimmers_的添加是 PoseGraph2D构造函数 —— PoseGraph2D::AddTrimmer

先看构造函数部分

1
2
3
4
5
6
7
8
if (options.has_overlapping_submaps_trimmer_2d()  )
{
const auto& trimmer_options = options.overlapping_submaps_trimmer_2d();
AddTrimmer(absl::make_unique<OverlappingSubmapsTrimmer2D>(
trimmer_options.fresh_submaps_count(),
trimmer_options.min_covered_area(),
trimmer_options.min_added_submaps_count() ) );
}

三个参数:

  1. fresh_submaps_count = 2 最新的2个submaps
  2. min_covered_area = 3.0 最小未被覆盖的面积
  3. min_added_submaps_count = 5 代表在5个submaps之后才会进行该项操作,这个参数不实用,主要是上面两个

Trims submaps that have less than min_covered_cells_count cells not overlapped by at least fresh_submaps_count submaps. 也就是说一些子图如果只有min_covered_cells_count个栅格没有被fresh_submaps_count个子图覆盖,那么这些子图被裁剪掉。

1
2
3
4
5
6
7
8
9
10
void PoseGraph2D::AddTrimmer(std::unique_ptr<PoseGraphTrimmer> trimmer)
{
// C++11 does not allow us to move a unique_ptr into a lambda.
PoseGraphTrimmer* const trimmer_ptr = trimmer.release();
AddWorkItem([this, trimmer_ptr]() LOCKS_EXCLUDED(mutex_) {
absl::MutexLock locker(&mutex_);
trimmers_.emplace_back(trimmer_ptr);
return WorkItem::Result::kDoNotRunOptimization;
});
}

PoseGraphTrimmer的派生类有 PureLocalizationTrimmerOverlappingSubmapsTrimmer2D。前者属于纯定位部分,这里不看。后者其实主要就实现了一个Trim函数。

回头看trimmers_.erase这一句,发现OverlappingSubmapsTrimmer2D::IsFinished()永远返回 false,所以这一句只有纯定位时才会执行,建图不执行。


frozen轨迹的特点

frozen轨迹的子图和节点不参与优化问题中的优化,也就是5个残差的计算。但是参与inter global约束的计算,也就是纯定位。