前端 6. AddAccumulatedRangeData(3) 加入位姿估计器和插入子图
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
// pose_estimate_2d 是 scanMatch返回的
// ceres计算的位姿需要再跟重力方向对齐,才是LocalSLAM的位姿
const transform::Rigid3d pose_estimate = transform::Embed3D(*pose_estimate_2d) *
gravity_alignment;
// 对应流程图,反馈给 extrapolator_ 以修正估计值
// 添加到PoseExtrapolator的timed_pose_queue_队列
extrapolator_->AddPose(time, pose_estimate);

// 下面使用的还是ceres返回的位姿
// 把点云数据转化为在 local map 坐标系 中的点云数据
/* 注意这里的点云 range_data_in_local 来自 gravity_aligned_range_data
后者是没有经过voxel filter的点云,否则加入地图的点就太少了 */
// gravity_aligned_range_data的 origin,return,miss都左乘 pose_estimate_2d
sensor::RangeData range_data_in_local =
TransformRangeData( gravity_aligned_range_data,
transform::Embed3D(pose_estimate_2d->cast<float>()) );
// 更新Submap, 同时返回插入结果 将LocalSLAM坐标系下的点云插入 active_submap
// filtered_gravity_aligned_point_cloud 实际没参与 插入子图,只作为insert result的成员返回
std::unique_ptr<InsertionResult> insertion_result = InsertIntoSubmap(
time, range_data_in_local, filtered_gravity_aligned_point_cloud,
pose_estimate, gravity_alignment.rotation() );
// ......时间处理部分

// 传入后端
return absl::make_unique<MatchingResult>(
MatchingResult{ time, pose_estimate, std::move(range_data_in_local),
std::move(insertion_result) } );

时间处理部分

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
const auto wall_time = std::chrono::steady_clock::now();
if (last_wall_time_.has_value())
{
const auto wall_time_duration = wall_time - last_wall_time_.value();
// 用来监视延迟
kLocalSlamLatencyMetric->Set(common::ToSeconds(wall_time_duration));
if (sensor_duration.has_value())
{
kLocalSlamRealTimeRatio->Set(
common::ToSeconds(sensor_duration.value()) /
common::ToSeconds(wall_time_duration) );
}
}
const double thread_cpu_time_seconds = common::GetThreadCpuTimeSeconds();
if (last_thread_cpu_time_seconds_.has_value())
{
const double thread_cpu_duration_seconds =
thread_cpu_time_seconds - last_thread_cpu_time_seconds_.value();
if (sensor_duration.has_value() )
{
kLocalSlamCpuRealTimeRatio->Set(
common::ToSeconds(sensor_duration.value()) /
thread_cpu_duration_seconds );
}
}
last_wall_time_ = wall_time;
last_thread_cpu_time_seconds_ = thread_cpu_time_seconds;

坐标系之间的关系和 InsertionResult

Frames(坐标系)

  • global map frame

这是表示全局 SLAM 结果的坐标系。它是包含所有闭环和优化结果的固定地图坐标系。 当新的优化结果出现时,该帧与其他帧之间的转换可以跳转。

  • local map frame

局部SLAM结果的坐标系,不包括闭环优化,对于给定某个时间点,该帧与全局地图之间的变换可能会发生变化,但是该帧和其他帧的变换不会发生变化。

  • submap frame

每个子图都有一个单独的坐标系

  • tracking frame

传感器数据表示的坐标系,自定义,可能是imu, laser, base_footprint

  • gravity-aligned frame

重力对准坐标系,只在2D中使用,与tracking坐标系有相同位置,但是坐标系方向不一致

Transforms (变换)

  • local_pose (局部姿态)

将数据从 tracking坐标系 或者子图坐标系 转换到 local map frame 的变换

  • global_pose (全局位姿)

将数据从tracking坐标系或者子图坐标系 转换到global map frame的变换

关系图

前端的MatchingResult包含:

1
2
3
4
time          此次matching的scan时间。
local_pose 此次matching的scan的位姿。
range_data_in_local 未过滤的距离数据。
insertion_result range_data_in_local插入submap返回的结果。

local map frame 和 submap的坐标系不是一回事。

子图的坐标系原点来自 range_data_in_local.origin 也就是ActiveSubmaps2D::InsertRangeData中的AddSubmap(range_data.origin.head<2>());

InsertionResult中的constant_data是此次range插入submap包含的主要内容,类型为TrajectoryNode::Data的共享指针。 constant_data其中有个成员const transform::Rigid3d local_pose,表示submap(node)在local map frame中的位姿,用于这两个坐标系之间的变换, 对每个子图,local_pose在不断更新 第1个local_pose是子图坐标系的原点,也就是上面的 range_data_in_local.origin,也是MotionFilter的判断依据,判断位姿和上一次插入scan的位姿有一定差别

MatchingResult里也有一个local_poseconstant_data 的那个local_pose是相同的,赋值用的都是下一帧位姿的观测值。不过在后端里用的是InsertionResultlocal_pose,源码在PoseGraph2D::AddNode。这个是在AddSubmap函数中创建新的submap的时候加入的,是创建submap时的第一个ranges的原点。

constant_data还有一个成员global_pose,这是submap(node)在global map frame的位姿。


后端1 AddSensorData的后端部分

MapBuilder 在创建GlobalTrajectoryBuilder时中会实例化 PoseGraph 对象, 并作为 GlobalTrajectoryBuilder 的成员。

接着看GlobalTrajectoryBuilder::AddSensorData的后端部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 将前端的输出结果喂给后端进行闭环检测和全局优化  */
kLocalSlamMatchingResults->Increment();
// 前端匹配的结果,即submap位置和submap
std::unique_ptr<InsertionResult> insertion_result;
// 判定前端是否将传感器的数据插入到子图中
if (matching_result->insertion_result != nullptr)
{
kLocalSlamInsertionResults->Increment();
// 将匹配后的结果加入图优化节点中
// 把前端的输出结果喂给后端,即后端增加一个新的节点
// 后端优化位姿图节点ID,记录在临时对象 node_id
auto node_id = pose_graph_->AddNode(
matching_result->insertion_result->constant_data,
trajectory_id_,
matching_result->insertion_result->insertion_submaps );
CHECK_EQ(node_id.trajectory_id, trajectory_id_);

// 重新封装前端传来的 InsertionResult,增加了一个node_id
insertion_result = absl::make_unique<InsertionResult>( InsertionResult{
node_id, matching_result->insertion_result->constant_data,
std::vector<std::shared_ptr<const Submap>>(
matching_result->insertion_result->insertion_submaps.begin(),
matching_result->insertion_result->insertion_submaps.end() ) } );
}

PoseGraph2D__AddNode 流程图.png

PoseGraph2D类的以FromProto结尾的函数, 全是在 MapBuilder::LoadState 里调用的,也就是纯定位的用途。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
NodeId PoseGraph2D::AddNode(
std::shared_ptr<const TrajectoryNode::Data> constant_data,
const int trajectory_id,
const std::vector<std::shared_ptr<const Submap2D>>& insertion_submaps)
{
// 将点云的原点坐标从local map坐标系转换为global map坐标系
const transform::Rigid3d optimized_pose(
GetLocalToGlobalTransform(trajectory_id) * constant_data->local_pose);
// 添加轨迹节点 子图节点
const NodeId node_id = AppendNode(constant_data, trajectory_id,
insertion_submaps, optimized_pose);
// 查看旧子图是否插入完成(两个submap,front用于匹配和更新的)
// 如果 finished,则需要进行闭环检测,完成会调用 Submap2D::Finish()
const bool newly_finished_submap =
insertion_submaps.front()->insertion_finished();
// 添加计算约束的任务,加入 work_queue 队列,实际在DrainWorkQueue函数执行
AddWorkItem( [=]() LOCKS_EXCLUDED(mutex_)
{
return ComputeConstraintsForNode(node_id, insertion_submaps,
newly_finished_submap);
} );
return node_id;
}

后端增加一个位姿图节点需要传递匹配后的结果包括submap和点云数据,最后返回一个位姿图节点ID。其中前端处理后的点云数据,包括水平投影的点云,重力方向和在local submap的位置。

GetLocalToGlobalTransform

获取激光点云当前local map下的pose,转换为全局绝对pose的转移矩阵。也就是从local map坐标系转到global map坐标系

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
transform::Rigid3d PoseGraph2D::GetLocalToGlobalTransform(
const int trajectory_id) const
{
absl::MutexLock locker(&mutex_);

return ComputeLocalToGlobalTransform(
data_.global_submap_poses_2d,
trajectory_id );
}

transform::Rigid3d PoseGraph2D::ComputeLocalToGlobalTransform(
const MapById<SubmapId, optimization::SubmapSpec2D>& global_submap_poses,
const int trajectory_id) const
{
// 查找已经优化过的trajectory id中的submap的位置,如果有可返回存储的值
auto begin_it = global_submap_poses.BeginOfTrajectory(trajectory_id);
auto end_it = global_submap_poses.EndOfTrajectory(trajectory_id);
if (begin_it == end_it)
{
const auto it = data_.initial_trajectory_poses.find(trajectory_id);
// 位姿插值
if (it != data_.initial_trajectory_poses.end())
{
return GetInterpolatedGlobalTrajectoryPose(it->second.to_trajectory_id,
it->second.time) *
it->second.relative_pose;
}
// 仅是唯一个,表明为初始submap,转移矩阵为单位矩阵
else
return transform::Rigid3d::Identity();
}
const SubmapId last_optimized_submap_id = std::prev(end_it)->id;
// 如果不存在,则返回由上个优化后的 submap_id 的位置的相对位置换算出的位置
return transform::Embed3D(
global_submap_poses.at(last_optimized_submap_id).global_pose) *
data_.submap_data.at(last_optimized_submap_id)
.submap->local_pose()
.inverse();
}

既然这个函数是返回一个transform,所以是左乘变换: global = transform * local,因此transform = global * inv(local)。 初始global为单位矩阵,也就是产生子图前,后续通过相对位置推导每个位置

InitializeGlobalSubmapPoses里可以看到获得转换矩阵的逻辑:
首先从global_submap_poses_2d中找出属于该轨迹的所有submap信息,那就找出上一个优化过的子图的全局位姿global_pose和局部位姿local_pose,通过这两个位姿就可以得到 local和global坐标系 的转换矩阵。

如果在global_submap_poses_2d没有找到该轨迹的submap信息,那就说明该轨迹还没有产生submap,那就从data_.initial_trajectory_poses中查找该轨迹的初始位姿,如果找到 了初始位姿,就根据该轨迹初始位姿的时间去节点列表中找改时间点最近的节点,根据该节点的global_pose找到 local 和 global 的转换信息。

initial_trajectory_poses来自SetInitialTrajectoryPose,根源在MapBuilder::AddTrajectoryBuildertrajectory_options.has_initial_trajectory_pose(),但是没找到函数has_initial_trajectory_pose()。怀疑这是纯定位的部分

如果initial_trajectory_poses中也没有找到该轨迹的初始位姿,就直接返回单位转换矩阵,认为二者重合。


cartographer 常用的类型

Rigid3类型在rigid_transform.h,很有学习价值

1
2
3
4
5
Vector translation_;
Quaternion rotation_;
// using Vector = Eigen::Matrix<FloatType, 3, 1>;
// using Quaternion = Eigen::Quaternion<FloatType>;
// using AngleAxis = Eigen::AngleAxis<FloatType>;

Rigid3d类型实质是包含了一个3行1列的Eigen矩阵,一个Eigen::Quaternion,可以直接用 cout
值得注意的运算符:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 参见 SLAM 十四讲
Rigid3 inverse() const
{
const Quaternion rotation = rotation_.conjugate();
const Vector translation = -(rotation * translation_);
return Rigid3(translation, rotation);
}

template <typename FloatType>
Rigid3<FloatType> operator*(const Rigid3<FloatType>& lhs,
const Rigid3<FloatType>& rhs) {
return Rigid3<FloatType>(
lhs.rotation() * rhs.translation() + lhs.translation(),
(lhs.rotation() * rhs.rotation()).normalized());
}

OptimizationProblem2D类的成员

struct NodeId,成员只有int trajectory_id;int node_index;,后者是从0开始的

struct SubmapId,成员只有int trajectory_id;int submap_index;,后者是从0开始的

class MapById是对std::map的一个封装,其实是模板<typename IdType, typename DataType>,前者可以是NodeId 或者 SubmapId

// Submaps get assigned an ID and state as soon as they are seen, even
// before they take part in the background computations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct NodeSpec2D
{
common::Time time;
transform::Rigid2d local_pose_2d;
transform::Rigid2d global_pose_2d;
Eigen::Quaterniond gravity_alignment;
};

struct SubmapSpec2D
{
transform::Rigid2d global_pose;
};
// optimization_problem_->submap_data() 就是 SubmapSpec2D
MapById<SubmapId, optimization::SubmapSpec2D> global_submap_poses_2d;

data_是类PoseGraph2D中的成员变量,类型 PoseGraphData

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
struct PoseGraphData
{
/* struct InternalSubmapData
{
std::shared_ptr<const Submap> submap;
SubmapState state = SubmapState::kNoConstraintSearch;

// IDs of the nodes that were inserted into this map together with
// constraints for them. They are not to be matched again when this submap
// becomes 'kFinished'.
std::set<NodeId> node_ids;
}; */

// Submaps get assigned an ID and state as soon as they are seen, even
// before they take part in the background computations.
MapById<SubmapId, InternalSubmapData> submap_data;

// Global submap poses currently used for displaying data.
MapById<SubmapId, optimization::SubmapSpec2D> global_submap_poses_2d;
MapById<SubmapId, optimization::SubmapSpec3D> global_submap_poses_3d;

// Data that are currently being shown.
MapById<NodeId, TrajectoryNode> trajectory_nodes;

// Global landmark poses with all observations.
std::map<std::string /* landmark ID */, PoseGraphInterface::LandmarkNode>
landmark_nodes;
// How our various trajectories are related.
TrajectoryConnectivityState trajectory_connectivity_state;
int num_trajectory_nodes = 0;
std::map<int, InternalTrajectoryState> trajectories_state;

// Set of all initial trajectory poses.
std::map<int, PoseGraph::InitialTrajectoryPose> initial_trajectory_poses;

std::vector<PoseGraphInterface::Constraint> constraints;
};

1
2
3
4
5
6
7
8
9
struct InternalSubmapData
{
std::shared_ptr<const Submap> submap;
SubmapState state = SubmapState::kNoConstraintSearch;
// IDs of the nodes that were inserted into this map together with
// constraints for them. They are not to be matched again when this submap
// becomes 'kFinished'.
std::set<NodeId> node_ids;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Constraint {
struct Pose {
transform::Rigid3d zbar_ij;
double translation_weight;
double rotation_weight;
};
SubmapId submap_id; // 'i' in the paper.
NodeId node_id; // 'j' in the paper.
// Pose of the node 'j' relative to submap 'i'.
Pose pose;
/* Differentiates between intra-submap (where node 'j' was inserted into
submap 'i') and inter-submap constraints (where node 'j' was not inserted
into submap 'i').*/
enum Tag { INTRA_SUBMAP, INTER_SUBMAP } tag;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class OptimizationProblem2D
: public OptimizationProblemInterface<NodeSpec2D, SubmapSpec2D,
transform::Rigid2d>
{
public:
void Solve(
const std::vector<Constraint>& constraints,
const std::map<int, PoseGraphInterface::TrajectoryState>&
trajectories_state,
const std::map<std::string, LandmarkNode>& landmark_nodes) override;
// 省略很多函数
private:
optimization::proto::OptimizationProblemOptions options_;
MapById<NodeId, NodeSpec2D> node_data_;
MapById<SubmapId, SubmapSpec2D> submap_data_;
sensor::MapByTime<sensor::ImuData> imu_data_;
sensor::MapByTime<sensor::OdometryData> odometry_data_;
std::map<int, PoseGraphInterface::TrajectoryData> trajectory_data_;

sensor::MapByTime<sensor::FixedFramePoseData> fixed_frame_pose_data_;
std::map<std::string, transform::Rigid3d> landmark_data_;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// 结果、时间、函数
struct WorkItem
{
enum class Result {
kDoNotRunOptimization,
kRunOptimization,
};

std::chrono::steady_clock::time_point time;
std::function<Result()> task;
};

using WorkQueue = std::deque<WorkItem>;

ConstraintBuilder2D::Result 其实是 vector<Constraint>

PoseExtrapolator

1
2
3
std::deque<TimedPose> timed_pose_queue_;
std::deque<sensor::ImuData> imu_data_;
std::deque<sensor::OdometryData> odometry_data_;
cartographer里凡是变量名包含queue的,类型都是deque

插入子图

sensor::RangeData

local坐标系下的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Rays begin at 'origin'. 'returns' are the points where obstructions were
// detected. 'misses' are points in the direction of rays for which no return
// was detected, and were inserted at a configured distance
// It is assumed that between the 'origin' and 'misses' is free space
struct RangeData {
Eigen::Vector3f origin;
PointCloud returns;
PointCloud misses;
};

using PointCloud = std::vector<RangefinderPoint>;
// Stores 3D position of a point observed by a rangefinder sensor.
struct RangefinderPoint
{
Eigen::Vector3f position;
};

TimedRangeData

1
2
3
4
5
struct TimedPointCloudData {
common::Time time;
Eigen::Vector3f origin;
TimedPointCloud ranges;
};
1
2
3
4
5
6
7
8
9
struct TimedPointCloudOriginData {
struct RangeMeasurement {
TimedRangefinderPoint point_time;
size_t origin_index;
};
common::Time time;
std::vector<Eigen::Vector3f> origins;
std::vector<RangeMeasurement> ranges;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// Like 'RangeData', but with 'TimedPointClouds'.
struct TimedRangeData {
Eigen::Vector3f origin;
TimedPointCloud returns;
TimedPointCloud misses;
};

using TimedPointCloud = std::vector<TimedRangefinderPoint>;

struct TimedRangefinderPoint {
Eigen::Vector3f position;
float time;
};
1
2
3
4
5
6
7
8
9
10
11
struct SubmapPose
{
int version;
transform::Rigid3d pose;
};

struct SubmapData
{
std::shared_ptr<const Submap> submap;
transform::Rigid3d pose;
};

LocalTrajectoryBuilder2D

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
struct MatchingResult
{
common::Time time; // 扫描匹配发生的时间
// tracking 坐标系下的位姿(Local SLAM坐标系下的位姿), 需要乘以
// tracking frame in map的转换(Local SLAM坐标系到Global SLAM坐标系的转换)才能转到全局坐标系
transform::Rigid3d local_pose;
// tracking 坐标系下的点云(即LocalSLAM坐标系下的点云)
sensor::RangeData range_data_in_local;
// 'nullptr' if dropped by the motion filter.
std::unique_ptr<const InsertionResult> insertion_result;
};

struct InsertionResult
{
// Node数据 插入的节点数据,TrajectoryNode::Data包含了处理之后的点云数据
std::shared_ptr<const TrajectoryNode::Data> constant_data;
//该Node数据插入的两个submap
std::vector<std::shared_ptr<const Submap2D>> insertion_submaps;
};

struct TrajectoryNode
{
struct Data
{
common::Time time;
// Transform to approximately gravity align the tracking frame as
// determined by local SLAM.
Eigen::Quaterniond gravity_alignment;
// Used for loop closure in 2D: voxel filtered returns in the
// 'gravity_alignment' frame.
sensor::PointCloud filtered_gravity_aligned_point_cloud;
// 省略 3D.

// The node pose in the local SLAM frame.
transform::Rigid3d local_pose;
};
common::Time time() const { return constant_data->time; }
// This must be a shared_ptr. If the data is used for visualization while the
// node is being trimmed, it must survive until all use finishes.
std::shared_ptr<const Data> constant_data;
// The node pose in the global SLAM frame.
transform::Rigid3d global_pose;
};

MatchingResult里有一个local_poseInsertionResultconstant_data成员里也有一个local_pose 这两个是相同的,赋值用的都是下一帧位姿的观测值。不过在后端里用的是InsertionResultlocal_pose,源码在PoseGraph2D::AddNode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// An individual submap, which has a 'local_pose' in the local map frame, 
// keeps track of how many range data were inserted into it
// and sets 'insertion_finished' when the map no longer changes
// and is ready for loop closing
class Submap
{
public:
Submap(const transform::Rigid3d& local_submap_pose)
: local_pose_(local_submap_pose) {}

private:
// Pose of this submap in the local map frame
const transform::Rigid3d local_pose_;
// Number of RangeData inserted.
int num_range_data_ = 0;
bool insertion_finished_ = false;
};

常用函数

1
2
3
4
5
6
7
8
9
10
11
12
// 对点云中每一个点做变换, 左乘 transform
PointCloud TransformPointCloud(const PointCloud& point_cloud,
const transform::Rigid3f& transform)
{
PointCloud result;
result.reserve(point_cloud.size() );
for (const RangefinderPoint& point : point_cloud)
{
result.emplace_back(transform * point);
}
return result;
}
1
2
3
4
5
6
7
8
9
RangeData TransformRangeData(const RangeData& range_data,
const transform::Rigid3f& transform)
{
return RangeData{
transform * range_data.origin,
TransformPointCloud(range_data.returns, transform),
TransformPointCloud(range_data.misses, transform),
};
}

Trajectory

enum class TrajectoryState { ACTIVE, FINISHED, FROZEN, DELETED };


前端 2. 传感器数据的同步及融合-RangeDataCollator
如果只用一个雷达,可以不看这段

激光雷达点云数据均为作为主要输入,使用时无需考虑具体几个传感器和类型,可认为是一个雷达产生的点云数据。但实际cartographer通过RangeDataCollator类将多种传感器进行了融合,并进行了时间同步,最后形成对应的时间戳,pose和点云集合。在真正使用时通过畸变校准,并构建scan match和插入submap的传感器数据rangedata。

有多个雷达的点云信息,它们的各个点可能时间会重合,因此需要将所有雷达的点云信息进行时间的整理,保证所有点的时间是单调的。多个雷达数据合并到一起,它们的原点可能不一样,因此要保存各自的原点。

头文件部分:

1
2
3
4
5
6
7
8
9
10
11
12
  // 插入集合
sensor::TimedPointCloudOriginData AddRangeData(
const std::string& sensor_id,
const sensor::TimedPointCloudData& timed_point_cloud_data);

private:
// 融合
sensor::TimedPointCloudOriginData CropAndMerge();
// 期望处理传感器类型清单
const std::set<std::string> expected_sensor_ids_;
// 同步和融合后集合,每种传感器至多一帧点云数据
std::map<std::string, sensor::TimedPointCloudData> id_to_pending_data_;

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
sensor::TimedPointCloudOriginData RangeDataCollator::AddRangeData(
const std::string& sensor_id,
const sensor::TimedPointCloudData& timed_point_cloud_data)
{
// 检测该点云数据 sensor_id 是否在期望的sensor_ids里面
CHECK_NE(expected_sensor_ids_.count(sensor_id), 0);
// TODO(gaschler): These two cases can probably be one.
// 此传感器类型数据已有
if (id_to_pending_data_.count(sensor_id) != 0)
{
current_start_ = current_end_;
// When we have two messages of the same sensor, move forward the older of
// the two (do not send out current).
/* 如果当前融合时间段内融合结果中已包含同一ID的传感器数据,则应采用最新的点云数据进行替换
但是结束的时间戳仍然采用原来的时间刻度,开始进行数据截取合并CropAndMerge()并返回 */
// std::map<std::string, sensor::TimedPointCloudData> id_to_pending_data_;
current_end_ = id_to_pending_data_.at(sensor_id).time;
auto result = CropAndMerge();
id_to_pending_data_.emplace(sensor_id, timed_point_cloud_data);
return result;
}
id_to_pending_data_.emplace(sensor_id, timed_point_cloud_data);
// 若现在收到的数据类型未全,即期望收到种类未全,直接退出,无需融合
if (expected_sensor_ids_.size() != id_to_pending_data_.size()) {
return {};
}
current_start_ = current_end_;
// We have messages from all sensors, move forward to oldest.
common::Time oldest_timestamp = common::Time::max();
// 找传感器数据中最早的时间戳
for (const auto& pair : id_to_pending_data_) {
oldest_timestamp = std::min(oldest_timestamp, pair.second.time);
}
/* current_end_是下次融合的开始时间,是本次融合的最后时间刻度
但其实是此次融合所有传感器中最早的时间戳*/
current_end_ = oldest_timestamp;
return CropAndMerge();
}

进一步处理在CropAndMerge,这个函数十分复杂。 简单理解如果有一个传感器频率较高,已经来过一帧并进行了缓存,另外一个未来,
这个传感器又来一帧,则先进行截取合并送出结果(实际上就是上帧缓存的点云直接发出),
然后将新来的一帧替换换来的buffer。

参考: cartographer 代码思想解读 RangeDataCollator


前端 4. Ceres scan matcher

原理

在把一帧的scan插入到子图之前,scan 相对子图的位姿是由 ceres scan matcher 优化获得的。由于这是一个实时的局部优化,需要一个好的初始位姿估计。前面的real time CSM就是把位姿估计器传来的预测值更新为一个好的初值,如果没有real time CSM,就还用位姿估计器传来的预测值作初值。Ceres Scan Matcher以初值作为先验,并找到最佳的点,该点就是通过scan match获得的在子地图中的位置,实现方式是 interpolating the submap and sub-pixel aligning the scan. 前端的两个scan matcher其实都是 scan to map 问题,让当前观测和已知环境最匹配

scan matcher 负责在地图中找到一个scan pose,使得在该位姿下所有 hit 点在 local map中的占用概率之和最大,于是转化为一个最小二乘问题:

式中的 就是我们需要求的位姿,它将scan坐标系中的点 (hk)全部转化到 Submap坐标系下, 让点 (hk)最大概率地匹配到Submap。 由于栅格中原本存储的就是空闲的概率,其实从栅格查出来的就是 。 相对地,real time scan match是占据概率之和最大,其实二者本质差不多,只是CSM换成最小二乘形式了,为了更精确。

在代码里实现的时候,又加上了旋转和平移的残差,防止点云匹配的结果和初值差太多。其实只要初值够好,这两个残差可以去掉。

这种方式的速度很快,计算复杂度高,鲁棒性好,但不能修正比子图分辨率大很多的误差。如果传感器和时间戳是合理的,只使用Ceres Scan Matcher通常是最好的选择。

Msmooth

Msmooth是一个将local子图中的各点概率进行平滑处理的函数,能够提供比栅格分辨率更好的精度。实质是双三次插值算法,该函数的输出结果为(0, 1)以内的数,实际返回了Submap中的占用概率,如果趋近1,那是很好了。

提高Scan matcher的频率可以获得更好优化结果,但计算量加大;或者用IMU来估计 scan match 中的旋转角度

ceres匹配对初值要求相当高,匹配后的结果会考虑其与初始位置偏差进行权重化,说明 cartographer认为其匹配后的值应该与初值相差不大。 一旦初值离真实值过远,很容易陷入局部最优解,导致优化失败。这也是为什么ceres对线速度与角速度有限制。

1
2
3
4
5
6
7
8
9
auto pose_observation = absl::make_unique<transform::Rigid2d>();
ceres::Solver::Summary summary;
ceres_scan_matcher_.Match(pose_prediction.translation(),
initial_ceres_pose,
filtered_gravity_aligned_point_cloud,
*matching_submap->grid(), pose_observation.get(),
&summary);
// 省略: 统计残差, 观测值 - 预测值
return pose_observation;

Match 函数

CeresScanMatcher2D类主要函数只有Match:根据初值,将点云hit点和栅格对齐,返回观测值

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
/*
input:
1. 上一个 scan 的位姿 target_translation
2. 经过 RealTimeCorrelativeScanMatcher2D 处理过的
当前的 scan 的位姿的初始估计 initial_pose_estimate
3. 当前 scan 点云(2D) point_cloud
4. local map 概率分布栅格图 grid

output: 更新的位姿估计 pose_estimate
*/
void CeresScanMatcher2D::Match(
const Eigen::Vector2d& target_translation,
const transform::Rigid2d& initial_pose_estimate,
const sensor::PointCloud& point_cloud,
const Grid2D& grid,
transform::Rigid2d* const pose_estimate,
ceres::Solver::Summary* const summary) const
{
// 优化的变量(3维)
double ceres_pose_estimate[3] = {initial_pose_estimate.translation().x(),
initial_pose_estimate.translation().y(),
initial_pose_estimate.rotation().angle() };
ceres::Problem problem;
CHECK_GT(options_.occupied_space_weight(), 0.);
// 点云匹配残差块
problem.AddResidualBlock(
CreateOccupiedSpaceCostFunction2D(
options_.occupied_space_weight() /
std::sqrt(static_cast<double>(point_cloud.size())),
point_cloud, grid ),
nullptr /* loss function */, ceres_pose_estimate );

// 省略 GridType::TSDF
CHECK_GT(options_.translation_weight(), 0.);
// ceres 的套路都在函数里定义,很简单 AutoDiff
problem.AddResidualBlock(
TranslationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
options_.translation_weight(), target_translation),
nullptr, ceres_pose_estimate);

CHECK_GT(options_.rotation_weight(), 0.);
// ceres 的套路都在函数里定义,很简单
problem.AddResidualBlock(
RotationDeltaCostFunctor2D::CreateAutoDiffCostFunction(
options_.rotation_weight(), ceres_pose_estimate[2]),
nullptr, ceres_pose_estimate);
// ceres_solver_options_.linear_solver_type = ceres::DENSE_QR;
ceres::Solve(ceres_solver_options_, &problem, summary);

*pose_estimate = transform::Rigid2d(
{ceres_pose_estimate[0], ceres_pose_estimate[1]}, ceres_pose_estimate[2]);
}

ceres scan matcher 将上面的最小二乘问题分解成了三个代价函数:点云与栅格的匹配残差、平移的残差、角度的残差。其中,点云匹配的权重总和为1,平移的权重和旋转的权重都是手动给的。

三个对应的核函数都是 nullptr ceres的linear_solver_typeDENSE_QR

TranslationDeltaCostFunctor2D

两个Cost Function的实现,就是保证平移和旋转最小的代价函数

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
// Computes the cost of translating 'pose' to 'target_translation'.
// Cost increases with the solution's distance from 'target_translation'.
class TranslationDeltaCostFunctor2D {
public:
static ceres::CostFunction* CreateAutoDiffCostFunction(
const double scaling_factor, const Eigen::Vector2d& target_translation)
{
return new ceres::AutoDiffCostFunction<TranslationDeltaCostFunctor2D,
2 /* residuals */,
3 /* pose variables */>(
new TranslationDeltaCostFunctor2D(scaling_factor, target_translation));
}
template <typename T>
bool operator()(const T* const pose, T* residual) const {
residual[0] = scaling_factor_ * (pose[0] - x_);
residual[1] = scaling_factor_ * (pose[1] - y_);
return true;
}
private:
// Constructs a new TranslationDeltaCostFunctor2D from the given
// 'target_translation' (x, y).
explicit TranslationDeltaCostFunctor2D(
const double scaling_factor, const Eigen::Vector2d& target_translation)
: scaling_factor_(scaling_factor),
x_(target_translation.x()),
y_(target_translation.y()) {}
// 省略: 禁止拷贝构造函数 和 赋值运算符
const double scaling_factor_;
const double x_;
const double y_;
};

这个代价函数比较简单,都是套路,RotationDeltaCostFunctor2D也是同理

  • 平移量的残差计算,当前位姿逼近目标位姿 (位姿估计器输出的预测值xy)。目标函数
    residual[0] = scaling_factor_ * (pose[0] - x_); residual[1] = scaling_factor_ * (pose[1] - y_);

  • 逼近目标角度,目标函数scaling_factor_ * (pose[2] - angle_)

  • 这里是逼近之前的位姿,所以要求先验位姿比较准确,经试验发现ceres输出的位姿和预测值相差极小,疑似存在问题,可以将这两个ceres优化去掉,但地图匹配必须保留

统计残差

观测值 - 预测值

1
2
3
4
5
6
7
8
9
10
11
12
13
if (pose_observation)
{
kCeresScanMatcherCostMetric->Observe(summary.final_cost);
const double residual_distance =
(pose_observation->translation() - pose_prediction.translation())
.norm();
kScanMatcherResidualDistanceMetric->Observe(residual_distance);

const double residual_angle =
std::abs(pose_observation->rotation().angle() -
pose_prediction.rotation().angle());
kScanMatcherResidualAngleMetric->Observe(residual_angle);
}

参考:
无处不在的小土
jiajiading的分析
叶落寒蝉的分析
Ceres的使用


前端 3. Real Time Correlative Scan Matcher

AddAccumulatedRangeData

现在进入AddAccumulatedRangeData函数,返回类型为 std::unique_ptr<LocalTrajectoryBuilder2D::MatchingResult>

所用参数为

  • const common::Time time     当前同步时间
  • const sensor::RangeData& gravity_aligned_range_data     经过重力修正后的点云returns和miss
  • const transform::Rigid3d& gravity_alignment,     重力向量
  • const absl::optional<common::Duration>& sensor_duration

先看开头,我交换了部分代码顺序,并不影响。这里实际是获得scanMatch的两个参数

1
2
3
4
5
6
7
8
9
10
11
if (gravity_aligned_range_data.returns.empty())
{
LOG(WARNING) << "Dropped empty horizontal range data.";
return nullptr;
}
const sensor::PointCloud& filtered_gravity_aligned_point_cloud =
sensor::AdaptiveVoxelFilter(options_.adaptive_voxel_filter_options())
.Filter(gravity_aligned_range_data.returns);

if (filtered_gravity_aligned_point_cloud.empty())
return nullptr;

这一段就是流程图的Adaptive Voxel Filter环节,解释参考(九) Voxel Filter和Adaptive Voxel Filter

然后是估计位姿部分

1
2
3
4
5
const transform::Rigid3d  non_gravity_aligned_pose_prediction =
extrapolator_->ExtrapolatePose(time);
// 经过重力方向计算投影后的2d位置
const transform::Rigid2d pose_prediction = transform::Project2D(
non_gravity_aligned_pose_prediction * gravity_alignment.inverse() );

返回的pose_prediction和上面的filtered_gravity_aligned_point_cloud一起进入 Scan Mathching 环节,这个在流程图上很明显。

表面上只是重力对齐以后再处理,其实这里面还为后续的匹配与查找减小了查找范围,因为* gravity_alignment.inverse()这一乘使得与上一帧数据的角度变化缩小了(基本是同一角度),所以 为后面的ScanMatch减小了角度的查找范围。

在后面再把角度乘回来:

1
2
3
4
5
pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);

transform::Rigid3d pose_estimate =
transform::Embed3D(*pose_estimate_2d) * gravity_alignment;

这样就实现了角度的估计,gravity_alignment上下是同一数值,只是为了减少计算量,点云数据也按这个gravity_alignment做了处理

scan match

1
2
3
4
5
6
7
8
9
// local map frame <--- gravity-aligned frame
// 求 Scan 插入Submap的最优 Pose
std::unique_ptr<transform::Rigid2d> pose_estimate_2d =
ScanMatch(time, pose_prediction, filtered_gravity_aligned_point_cloud);
if (pose_estimate_2d == nullptr)
{
LOG(WARNING) << "Scan matching failed.";
return nullptr;
}

这里的ScanMatch包含了real time correlative scan matcherceres scan match两部分,前者为后者提供好的初值

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
// 输出是对该帧传感器数据的最优pose
std::unique_ptr<transform::Rigid2d> LocalTrajectoryBuilder2D::ScanMatch(
const common::Time time, const transform::Rigid2d& pose_prediction,
const sensor::PointCloud& filtered_gravity_aligned_point_cloud)
{
// 如果还没有子图,返回 PoseExtrapolator 的预测值
if (active_submaps_.submaps().empty() )
return absl::make_unique<transform::Rigid2d>(pose_prediction);

// 永远是头部的子图做匹配 SCAN-to-MAP
std::shared_ptr<const Submap2D> matching_submap =
active_submaps_.submaps().front();

transform::Rigid2d initial_ceres_pose = pose_prediction;

// 函数最终更新 initial_ceres_pose,返回匹配的最高分数
if (options_.use_online_correlative_scan_matching())
{
const double score = real_time_correlative_scan_matcher_.Match(
pose_prediction, filtered_gravity_aligned_point_cloud,
// Grid2D类型
*matching_submap->grid(), &initial_ceres_pose );
kRealTimeCorrelativeScanMatcherScoreMetric->Observe(score);
}
// ceres scan matcher 部分
}

Real Time Correlative Scan Matcher

这里借用了karto的做法,即本质是暴力匹配,而hector是用了高斯牛顿法和多分辨率地图。但是karto还是用了多分辨率地图,而cartographer这里没有。

它使用类似于在回环检测中激光与子地图匹配的方式,然后最好的匹配用作Ceres Scan Matcher的先验。这个scan matcher非常消耗资源,但它在特征丰富的环境中是很鲁棒。

我们要找到机器人位姿的后验概率分布,使用贝叶斯公式得到

运动模型是已知的多元高斯分布, 算法只研究观测模型。观测模型难以计算,一般具有多极值。 雷达观测数据 里包含了很多激光点数据,假设他们是完全独立的

将一帧观测数据的概率拆分成了当前帧的每一个点的概率,在计算这个概率之前,我们需要对数据帧以及地图进行栅格化。

位姿估计器传来的预测值不适合做初值,真正初值是在它附近。

实时相关性scan match 以预测值为中心,在搜索窗口内枚举出所有候选位姿,并生成每个位姿对应的点云,也就是说按照 把当前观测 投影到栅格地图 m (t 时刻的激光数据与 t-1 时刻的地图匹配, scan to map),然后分别计算每个点云中hit点在栅格地图的CellIndex,并计算所有hit点的占据概率之和,概率最大值对应的位姿就是我们需要的。代表着在这个位姿下,激光点得映射到占据栅格中的可能性最大,当前观测与已知环境最为一致,前后帧匹配成功。 scan match非常重要,如果结果位姿很差,建图就会失败,后端怎么调整也没用

搜索的精度是栅格地图的分辨率

这个scan matcher可以修改做重定位,不过搜索窗口扩大很多
  1. 暴力搜索
    我们要计算 ,这就需要一个三层for循环 计算每一个位姿,还有第四个循环,也就是遍历点云中每个点。 计算量巨大,因为每一个位姿都要重新投影,需要计算sin和cos函数

  2. 计算2D切片
    把三层循环交换顺序,最外层遍历 ,这样(x, y)的循环只有加法。

  3. 多分辨率搜索
    首先选择一个相对低的分辨率对 map 和 scan 进行栅格化,每个栅格上保留高分辨率情况下的该区域的最高概率,这样在匹配时就不会错过正确的位姿估计
    低分辨率找到最佳匹配后再在该位置进行高分辨率的栅格化,并设定相对较小的 search window 进行计算。

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
// 更新 pose_estimate, 返回最高的分数
double RealTimeCorrelativeScanMatcher2D::Match(
const transform::Rigid2d& initial_pose_estimate,
const sensor::PointCloud& point_cloud,
const Grid2D& grid,
transform::Rigid2d* pose_estimate ) const
{
CHECK(pose_estimate != nullptr);
// 预测位姿的旋转量
const Eigen::Rotation2Dd initial_rotation = initial_pose_estimate.rotation();
/* 左乘 initial_rotation, 将当前点云转换为
初始角度坐标系下点云,这里只有 旋转 */
const sensor::PointCloud rotated_point_cloud = sensor::TransformPointCloud(
point_cloud,
transform::Rigid3f::Rotation(Eigen::AngleAxisf(
initial_rotation.cast<float>().angle(),
Eigen::Vector3f::UnitZ() ) )
);

const SearchParameters search_parameters(
options_.linear_search_window(), options_.angular_search_window(),
rotated_point_cloud, grid.limits().resolution() );
// 获取搜索窗口下机器人朝向各个方向角时的点云数据

// 先通过角度切片,然后在xy方向上暴力搜索,为了减少计算量
// 切片数目 num_scans , 按照搜索角度/步长,以初始值为基础,生成一些列的旋转后的点云
const std::vector<sensor::PointCloud> rotated_scans =
GenerateRotatedScans(rotated_point_cloud, search_parameters);

// DiscreteScan2D 就是 std::vector<Eigen::Array2i>
/* 把切片后的旋转点云坐标,左乘initial_pose_estimate.translation
转化到地图坐标,这里只有 平移
最终通过GetCellIndex返回一系列的 激光点return所在的cell index*/
const std::vector<DiscreteScan2D> discrete_scans = DiscretizeScans(
grid.limits(), rotated_scans,
Eigen::Translation2f( initial_pose_estimate.translation().x(),
initial_pose_estimate.translation().y() )
);
/* 获得candidates, size是 search_parameters.num_scans *
(linear_bounds.max_x - inear_bounds.min_x) * (linear_bounds.max_y - inear_bounds.min_y)*/

/* 这里是num_scans循环,x和y循环,没有 theta循环了
成员是 scan_index, x_index_offset(逐步增加), y_index_offset(逐步增加),
search_parameters意思其实是 每个激光点附近的矩形区域*/
std::vector<Candidate2D> candidates =
GenerateExhaustiveSearchCandidates(search_parameters);

// 计算当前帧在theta、x、y三层窗口中,每个状态在grid地图中的匹配的评分
// search_parameters 实际没用
ScoreCandidates(grid, discrete_scans, search_parameters, &candidates);
// 取最好的候选
const Candidate2D& best_candidate =
*std::max_element(candidates.begin(), candidates.end());

// 最后的结果是预测值加候选位姿
*pose_estimate = transform::Rigid2d(
{initial_pose_estimate.translation().x() + best_candidate.x,
initial_pose_estimate.translation().y() + best_candidate.y },
// 前端观测,所以右乘
initial_rotation * Eigen::Rotation2Dd(best_candidate.orientation) );

return best_candidate.score;
}
  1. 获取位姿估计器传来的初值的旋转分量,将激光点云左乘旋转分量,即先做旋转变换
  2. 根据搜索窗口x、y、yaw的大小计算 num_scans和LinearBounds, x y搜索的num_linear_perturbations
  3. 切片旋转:GenerateRotatedScans函数,其实就是theta循环,将角度进行序列化(初始角度跟search_window的大小相关,按步长增加),点云依次左乘角度对应的旋转向量(旋转轴为z轴),即可生成num_scans段点云
  4. 把点云中所有点做变换:左乘初值的平移部分,根据变换后的坐标,找出它在栅格地图中的CellIndex并保存
  5. 根据搜索范围,生成候选者,数量是25*num_scans
  • 根据保存的索引队列,从栅格地图中获取最大置信度,这就用到了ProbabilityGrid类,根据每个点的CellIndex获取占据概率,最终对分数进行加权处理
  • 求得每个候选位置的score,score最高的为最佳位置。

候选者是针对scan_index (0~num_scans)所做的x y方向的offset,生成所有候选者的过程其实就是num_scans和x y三个循环

Match 函数的计算量很大 。 试验发现最终的分数,大部分在0.6~0.9

SearchParameters

根据lua参数,确定后面的切片步长angular_perturbation_step_size,切片数num_scans,搜索区域

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
SearchParameters::SearchParameters(
const double linear_search_window,
const double angular_search_window,
const sensor::PointCloud& point_cloud,
const double resolution)
: resolution(resolution)
{
// We set this value to something on the order of
// resolution to make sure that the std::acos() below is defined.
float max_scan_range = 3.f * resolution;
for (const sensor::RangefinderPoint& point : point_cloud) {
const float range = point.position.head<2>().norm();
max_scan_range = std::max(range, max_scan_range);
}
const double kSafetyMargin = 1. - 1e-3;
angular_perturbation_step_size =
kSafetyMargin * std::acos(1. - common::Pow2(resolution) /
(2. * common::Pow2(max_scan_range)));
// 这两者的计算不同
num_angular_perturbations =
std::ceil(angular_search_window / angular_perturbation_step_size);
const int num_linear_perturbations =
std::ceil(linear_search_window / resolution);
// 切片数目,由于resolution不修改,实际取决于 max_scan_range 和 angular_search_window
num_scans = 2 * num_angular_perturbations + 1;
/* Linear search window in pixel offsets; bounds are inclusive.
struct LinearBounds {
int min_x;
int max_x;
int min_y;
int max_y;
};
std::vector<LinearBounds> linear_bounds; Per rotated scans */
linear_bounds.reserve(num_scans);
// 对同一帧scan, linear_bounds的元素都相同,不同帧就不同了
for (int i = 0; i != num_scans; ++i) {
linear_bounds.push_back(
LinearBounds{-num_linear_perturbations, num_linear_perturbations,
-num_linear_perturbations, num_linear_perturbations} );
}
}

GenerateRotatedScans 切片旋转

生成序列化旋转的多段点云

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
std::vector<sensor::PointCloud> GenerateRotatedScans(
const sensor::PointCloud& point_cloud,
const SearchParameters& search_parameters)
{
std::vector<sensor::PointCloud> rotated_scans;
rotated_scans.reserve(search_parameters.num_scans);
// 旋转角度步长
double delta_theta = -search_parameters.num_angular_perturbations *
search_parameters.angular_perturbation_step_size;
// 个数 num_scans
for (int scan_index = 0; scan_index < search_parameters.num_scans;
++scan_index,
delta_theta += search_parameters.angular_perturbation_step_size)
{
// 左乘旋转向量,旋转轴为z轴
rotated_scans.push_back(sensor::TransformPointCloud(
point_cloud, transform::Rigid3f::Rotation(Eigen::AngleAxisf(
delta_theta, Eigen::Vector3f::UnitZ()) ) ) );
}
return rotated_scans;
}

ScoreCandidates

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
void ScoreCandidates(
const Grid2D& grid, const std::vector<DiscreteScan2D>& discrete_scans,
const SearchParameters& search_parameters,
std::vector<Candidate2D>* const candidates) const
{
for (Candidate2D& candidate : *candidates)
{
candidate.score = ComputeCandidateScore(
static_cast<const ProbabilityGrid&>(grid),
discrete_scans[candidate.scan_index],
candidate.x_index_offset,
candidate.y_index_offset);
break;
// 省略 GridType::TSDF
// 进一步做权重处理
candidate.score *=
std::exp(
-common::Pow2(
std::hypot(candidate.x, candidate.y) *
options_.translation_delta_cost_weight()
+
std::abs(candidate.orientation) *
options_.rotation_delta_cost_weight()
)
);
}
}


// 其中用到的函数, 计算每个候选的得分
float ComputeCandidateScore(const ProbabilityGrid& probability_grid,
const DiscreteScan2D& discrete_scan,
int x_index_offset, int y_index_offset)
{
float candidate_score = 0.f;
// 这里就用到前面获取的x y 索引队列
for (const Eigen::Array2i& xy_index : discrete_scan)
{
const Eigen::Array2i proposed_xy_index(xy_index.x() + x_index_offset,
xy_index.y() + y_index_offset );
// Returns probability of the cell with 'cell_index'
// 这个就是占据概率
const float probability =
probability_grid.GetProbability(proposed_xy_index);
candidate_score += probability;
}
// 返回的是一帧点云所有激光点得分的平均值
candidate_score /= static_cast<float>(discrete_scan.size());
CHECK_GT(candidate_score, 0.f);
return candidate_score;
}

参考: RealTimeCorrelativeScanMatcher2D


机器人到障碍物的距离 calculateDistance

TEB中的footprint模型 中提到calculateDistance函数计算机器人到障碍物的 Euclidean 距离,用于图优化环节。

以最简单的Point类型为例

1
2
3
4
5
virtual double calculateDistance(const PoseSE2& current_pose, 
const Obstacle* obstacle) const
{
return obstacle->getMinimumDistance(current_pose.position());
}

Obstacle是个抽象类,它的派生类有
1
2
3
4
5
PointObstacle
CircularObstacle
PillObstacle
LineObstacle
PolygonObstacle

显然不同的障碍类型对函数有不同实现,这里又涉及到costmap_converterHomotopy,这就更复杂了

createGraph() —— PointObstacle::checkLineIntersection —— PointObstacle::checkCollision


TEB中的footprint模型

TEB算法中的footprint模型是单独构建的,而没有直接调用通用代价地图中的 footprint,二者footprint模型不同,但在TEB中二者都有使用。

TEB的footprint用于图优化,costmap的 footprint用于 `feasibility check`环节

LayeredCostmap类的成员函数: double getInscribedRadius() { return inscribed_radius_; }

对于polygon轮廓,两种footprint模型的内接半径相同, TEB的模型没有返回外接半径的函数

TebLocalPlannerROS::initialize中定义: RobotFootprintModelPtr robot_model = getRobotFootprintFromParamServer(nh);,然后进行模型验证validateFootprints

在planner初始化时,也就是在TebOptimalPlanner::initialize中赋值给robot_model_.

RobotFootprintModelPtr 其实就是 BaseRobotFootprintModel类型的 boost::shared_ptr

很显然BaseRobotFootprintModel是个抽象类,定义了机器人footprint/contour models的接口,根据yaml设置的类型而定义相应的类,这个类只用于optimization,代价地图的footprint只用于检查路径的可行性

  • Point 和 circular模型的footprints要求算力最少。

  • two-circles模型要求的是两次 distance calculations

  • line模型增加了一些 simple case checking, 但是算力在大多数情况仍要求不高。

  • polygon模型对每一个edge都要求 distance check,所以计算时间主要取决于 edges和vertices 的数量

也就是说这几种模型的算力要求依次增大。

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
RobotFootprintModelPtr TebLocalPlannerROS::getRobotFootprintFromParamServer(const ros::NodeHandle& nh)
{
std::string model_name;
if (!nh.getParam("footprint_model/type", model_name))
{
// 如果未指定类型,则使用点类型
return boost::make_shared<PointRobotFootprint>();
}
// point
if (model_name.compare("point") == 0)
{
ROS_INFO("Footprint model 'point' loaded for trajectory optimization.");
return boost::make_shared<PointRobotFootprint>();
}
// circular 省略
if (model_name.compare("circular") == 0)
// line 省略
if (model_name.compare("line") == 0)
// two circles 省略
if (model_name.compare("two_circles") == 0)

// polygon 只对 Point2dContainer vertices_; 赋值
if (model_name.compare("polygon") == 0)
{
// check parameters
XmlRpc::XmlRpcValue footprint_xmlrpc;
if (!nh.getParam("footprint_model/vertices", footprint_xmlrpc) )
{
ROS_ERROR_STREAM("Footprint model 'polygon' cannot be loaded for
trajectory optimization, since param '" << nh.getNamespace() <<
"/footprint_model/vertices' does not exist. Using point-model instead");
return boost::make_shared<PointRobotFootprint>();
}
// get vertices
if (footprint_xmlrpc.getType() == XmlRpc::XmlRpcValue::TypeArray)
{
try
{
Point2dContainer polygon = makeFootprintFromXMLRPC(footprint_xmlrpc,
"/footprint_model/vertices");
ROS_INFO_STREAM("Footprint model 'polygon' loaded for trajectory optimization.");
return boost::make_shared<PolygonRobotFootprint>(polygon);
}
catch(const std::exception& ex)
{
ROS_ERROR_STREAM("Footprint model 'polygon' cannot be loaded for trajectory optimization: "
<< ex.what() << ". Using point-model instead.");
return boost::make_shared<PointRobotFootprint>();
}
}
else
{
ROS_ERROR_STREAM("Footprint model 'polygon' cannot be loaded for trajectory optimization,
since param '" << nh.getNamespace() << "/footprint_model/vertices' does not
define an array of coordinates. Using point-model instead.");
return boost::make_shared<PointRobotFootprint>();
}
}
return boost::make_shared<PointRobotFootprint>();
}

validateFootprints

1
2
3
4
5
6
7
// TebLocalPlannerROS::initialize中的调用
validateFootprints(robot_model->getInscribedRadius(), robot_inscribed_radius_,
cfg_.obstacles.min_obstacle_dist);

void TebLocalPlannerROS::validateFootprints(double opt_inscribed_radius,
double costmap_inscribed_radius, double min_obst_dist)
{}

函数就一句ROS_WARN_COND,要求 TEB优化所用的机器人内接半径 + 参数 min_obstacle_dist > costmap中的内接半径(包括footprint_padding) ,否则Infeasible optimziation results会频繁出现

这里就是设置为点类型的缺陷之处,第1参数个会是0,但是如果min_obstacle_dist超过costmap中的内接半径,会无法走窄通道。


BaseRobotFootprintModel的派生类实现了calculateDistance函数,计算机器人到障碍物的 Euclidean距离,用于图优化环节。以最简单的Point类型为例

1
2
3
4
5
virtual double calculateDistance(const PoseSE2& current_pose, 
const Obstacle* obstacle) const
{
return obstacle->getMinimumDistance(current_pose.position());
}

至于细节,需要单独一篇文章分析

变量robot_model_在之后用于图优化时的一系列函数:
AddEdgesObstacles
AddEdgesObstaclesLegacy
AddEdgesDynamicObstacles
AddEdgesVelocityObstacleRatio,也就是所有和障碍相关的约束

以及可视化中的publishInfeasibleRobotPose


(二) costmap converter基本机制

配置使用的是costmap_converter::CostmapToLinesDBSRANSAC,类继承关系:

1
class CostmapToLinesDBSRANSAC : public CostmapToPolygonsDBSMCCH : public BaseCostmapToPolygons

这个机制太复杂了,还没有搞清楚

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
void workerCallback(const ros::TimerEvent&)
{
updateCostmap2D();
compute();
}


void CostmapToPolygonsDBSMCCH::updateCostmap2D()
{
occupied_cells_.clear();
if (!costmap_->getMutex())
{
ROS_ERROR("Cannot update costmap since the mutex pointer is null");
return;
}
int idx = 0;
costmap_2d::Costmap2D::mutex_t::scoped_lock lock(*costmap_->getMutex());

// get indices of obstacle cells
for(std::size_t i = 0; i < costmap_->getSizeInCellsX(); i++)
{
for(std::size_t j = 0; j < costmap_->getSizeInCellsY(); j++)
{
int value = costmap_->getCost(i,j);
if(value >= costmap_2d::LETHAL_OBSTACLE)
{
double x, y;
costmap_->mapToWorld((unsigned int)i, (unsigned int)j, x, y);
occupied_cells_.push_back( KeyPoint( x, y ) );
}
++idx;
}
}
}