优化问题类和后端总结-cartographer
Kong Liangqian Lv6

当我们把所有的约束都放入了PoseGraph2D 类中的constraints中, 通过

1
2
optimization_problem_->Solve(data_.constraints, GetTrajectoryStates(),
data_.landmark_nodes);

下面是optimizationproblem的定义

1
2
// Current optimization problem.
std::unique_ptr<optimization::OptimizationProblem2D> optimization_problem_;

OptimizationProblem2D

该类位于 mapping/internal/optimization/optimazation_problem_2d.h中,保存了所有的传感器数据,包括

1
2
3
4
5
6
7
8
9
10
11
optimization::proto::OptimizationProblemOptions options_; // 参数配置

// 优化的目标就是节点的位姿与子图的位姿
MapById<NodeId, NodeSpec2D> node_data_; // 节点坐标列表
MapById<SubmapId, SubmapSpec2D> submap_data_; // submap原点坐标列表

std::map<std::string, transform::Rigid3d> landmark_data_; // landmark数据
sensor::MapByTime<sensor::ImuData> empty_imu_data_; // 空的imu数据列表
sensor::MapByTime<sensor::OdometryData> odometry_data_; // 里程计数据列表
sensor::MapByTime<sensor::FixedFramePoseData> fixed_frame_pose_data_; // gps数据列表
std::map<int, PoseGraphInterface::TrajectoryData> trajectory_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

// 添加里程计数据
void OptimizationProblem2D::AddOdometryData(
const int trajectory_id, const sensor::OdometryData& odometry_data) {
odometry_data_.Append(trajectory_id, odometry_data);
}

// 添加gps数据
void OptimizationProblem2D::AddFixedFramePoseData(
const int trajectory_id,
const sensor::FixedFramePoseData& fixed_frame_pose_data) {
fixed_frame_pose_data_.Append(trajectory_id, fixed_frame_pose_data);
}

// 向优化问题中加入轨迹的节点位姿数据
void OptimizationProblem2D::AddTrajectoryNode(const int trajectory_id,
const NodeSpec2D& node_data) {
node_data_.Append(trajectory_id, node_data);
trajectory_data_[trajectory_id];
}

// 设置节点位姿,向node_id中添加节点位姿
void OptimizationProblem2D::InsertTrajectoryNode(const NodeId& node_id,
const NodeSpec2D& node_data) {
node_data_.Insert(node_id, node_data);
trajectory_data_[node_id.trajectory_id];
}

获取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const MapById<NodeId, NodeSpec2D>& node_data() const override {
return node_data_;
}
const MapById<SubmapId, SubmapSpec2D>& submap_data() const override {
return submap_data_;
}
const std::map<std::string, transform::Rigid3d>& landmark_data()
const override {
return landmark_data_;
}
const sensor::MapByTime<sensor::ImuData>& imu_data() const override {
return empty_imu_data_;
}
const sensor::MapByTime<sensor::OdometryData>& odometry_data()
const override {
return odometry_data_;
}

子图相关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 添加子图位姿
void OptimizationProblem2D::AddSubmap(
const int trajectory_id, const transform::Rigid2d& global_submap_pose) {
submap_data_.Append(trajectory_id, SubmapSpec2D{global_submap_pose});
}

// 添加子图位姿
void OptimizationProblem2D::InsertSubmap(
const SubmapId& submap_id, const transform::Rigid2d& global_submap_pose) {
submap_data_.Insert(submap_id, SubmapSpec2D{global_submap_pose});
}

// 删除指定id的子图位姿, 在纯定位时使用
void OptimizationProblem2D::TrimSubmap(const SubmapId& submap_id) {
submap_data_.Trim(submap_id);
}

根据里程计数据算出两个节点间的相对坐标变换

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
/**
* @brief 根据里程计数据算出两个节点间的相对坐标变换
*
* @param[in] trajectory_id 轨迹的id
* @param[in] first_node_data 前一个节点数据
* @param[in] second_node_data 后一个节点数据
* @return std::unique_ptr<transform::Rigid3d> 两个节点的坐标变换
*/
std::unique_ptr<transform::Rigid3d>
OptimizationProblem2D::CalculateOdometryBetweenNodes(
const int trajectory_id, const NodeSpec2D& first_node_data,
const NodeSpec2D& second_node_data) const {

if (odometry_data_.HasTrajectory(trajectory_id)) {
// 插值得到time时刻的里程计数据
const std::unique_ptr<transform::Rigid3d> first_node_odometry =
InterpolateOdometry(trajectory_id, first_node_data.time);
const std::unique_ptr<transform::Rigid3d> second_node_odometry =
InterpolateOdometry(trajectory_id, second_node_data.time);

if (first_node_odometry != nullptr && second_node_odometry != nullptr) {
// 根据里程计数据算出的相对坐标变换
// 需要注意的是, 实际上在optimization_problem中, node的位姿都是2d平面上的
// 而odometry的pose是带姿态的, 因此要将轮速计插值出来的位姿转到平面上
transform::Rigid3d relative_odometry =
transform::Rigid3d::Rotation(first_node_data.gravity_alignment) *
first_node_odometry->inverse() * (*second_node_odometry) *
transform::Rigid3d::Rotation(
second_node_data.gravity_alignment.inverse());

return absl::make_unique<transform::Rigid3d>(relative_odometry);
}
}

return nullptr;
}

获取里程计数据的位姿

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 根据时间对里程计数据进行插值, 获取这个时刻的里程计数据的位姿
std::unique_ptr<transform::Rigid3d> OptimizationProblem2D::InterpolateOdometry(
const int trajectory_id, const common::Time time) const {
// 找到时间上第一个大于等于time的里程计数据的迭代器
const auto it = odometry_data_.lower_bound(trajectory_id, time);
if (it == odometry_data_.EndOfTrajectory(trajectory_id)) {
return nullptr;
}
if (it == odometry_data_.BeginOfTrajectory(trajectory_id)) {
if (it->time == time) {
return absl::make_unique<transform::Rigid3d>(it->pose);
}
return nullptr;
}
// 前一个里程计数据
const auto prev_it = std::prev(it);
// 根据时间进行线性插值
return absl::make_unique<transform::Rigid3d>(
Interpolate(transform::TimestampedTransform{prev_it->time, prev_it->pose},
transform::TimestampedTransform{it->time, it->pose}, time)
.transform);
}

Solve

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @brief 搭建优化问题并进行求解
*
* @param[in] constraints 所有的约束数据
* @param[in] trajectories_state 轨迹的状态
* @param[in] landmark_nodes landmark数据
*/
void OptimizationProblem2D::Solve(
const std::vector<Constraint>& constraints,
const std::map<int, PoseGraphInterface::TrajectoryState>&
trajectories_state,
const std::map<std::string, LandmarkNode>& landmark_nodes)
{}

成员变量

nodedata

定义为:

1
MapById<NodeId, NodeSpec2D> node_data_;                   // 节点坐标列表

可以通过函数node_data()获得此数据

1
2
3
const MapById<NodeId, NodeSpec2D>& node_data() const override {
return node_data_;
}

NodeSpec2D 数据结构:

1
2
3
4
5
6
struct NodeSpec2D {
common::Time time;
transform::Rigid2d local_pose_2d;
transform::Rigid2d global_pose_2d;
Eigen::Quaterniond gravity_alignment;
};

nodedata 更新:

1
2
3
4
5
6
// 向优化问题中加入节点位姿数据
void OptimizationProblem2D::AddTrajectoryNode(const int trajectory_id,
const NodeSpec2D& node_data) {
node_data_.Append(trajectory_id, node_data);
trajectory_data_[trajectory_id];
}

此在节点加入后端后,计算此节点的相关约束的时候进行调用,加入的node_data为此node的global和local的位姿

OptimizationProblem2D对象的使用

打开posegraph_2d.cc,搜索optimization_problem,查看如何使用

添加submap

函数InitializeGlobalSubmapPoses中,返回指定轨迹id下的正处于活跃状态下的子图的SubmapId

这个是一个子图的情况

1
2
3
4
5
optimization_problem_->AddSubmap(
trajectory_id, transform::Project2D(
ComputeLocalToGlobalTransform(
data_.global_submap_poses_2d, trajectory_id) *
insertion_submaps[0]->local_pose()));

这个是有两个活跃子图的情况

1
2
3
4
5
6
7
8
// 解算新的submap的global pose, 插入到OptimizationProblem2D::submap_data_中
optimization_problem_->AddSubmap(
trajectory_id,
// first_submap_pose * constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() = globla指向local的坐标变换
// globla指向local的坐标变换 * 第二个子图原点在local下的坐标 = 第二个子图原点在global下的坐标
first_submap_pose *
constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() *
constraints::ComputeSubmapPose(*insertion_submaps[1]));

添加传感器数据

在向PoseGraph添加传感器数据的时候,

1
2
3
4
optimization_problem_->AddImuData(trajectory_id, imu_data);
optimization_problem_->AddOdometryData(trajectory_id, odometry_data);
optimization_problem_->AddFixedFramePoseData(trajectory_id,
fixed_frame_pose_data);

加入节点数据

在计算节点约束的时候,添加Node的信息加入到优化问题中。

1
2
3
4
5
6
// 把该节点的信息加入到OptimizationProblem中
optimization_problem_->AddTrajectoryNode(
matching_id.trajectory_id,
optimization::NodeSpec2D{constant_data->time, local_pose_2d,
global_pose_2d,
constant_data->gravity_alignment});

对于landmark ,他没有添加到这里,他只是放到了pose_graph_2d中

总结

Node的global位姿

位于PoseGraph2D::AddNode

1
2
3
// 将节点在local坐标系下的坐标转成global坐标系下的坐标
const transform::Rigid3d optimized_pose(
GetLocalToGlobalTransform(trajectory_id) * constant_data->local_pose);

submap在global坐标系下的位姿

1
2
3
4
5
6
7
8
9
10
optimization_problem_->AddSubmap(
trajectory_id, transform::Project2D(
ComputeLocalToGlobalTransform(
• data_.global_submap_poses_2d, trajectory_id) *
• insertion_submaps[0]->local_pose()));
optimization_problem_->AddSubmap(
trajectory_id,
first_submap_pose *
• constraints::ComputeSubmapPose(*insertion_submaps[0]).inverse() *
• constraints::ComputeSubmapPose(*insertion_submaps[1]));

子图在global坐标系下的第一帧的位姿,就是这个子图在local坐标系下的位姿.

之后的子图在global坐标系下的位姿 是通过 第一个子图在global坐标系下的pose 乘以 第一个子图到第二个子图在local坐标系下的位姿变换 得到的.

子图内约束

计算一个节点约束的时候,in PoseGraph2D::ComputeConstraintsForNode,需要添加此Node对所有submap的约束

即submap指向Node的一个约束。这是local坐标系下得到的坐标变换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 遍历2个子图, 将节点加入子图的节点列表中, 计算子图原点与及节点间的约束(子图内约束)
for (size_t i = 0; i < insertion_submaps.size(); ++i) {
const SubmapId submap_id = submap_ids[i];
// 将node_id放到子图保存的node_ids的set中
data_.submap_data.at(submap_id).node_ids.emplace(node_id);
// 计算 子图原点 指向 node坐标 间的坐标变换(子图内约束)
const transform::Rigid2d constraint_transform =
constraints::ComputeSubmapPose(*insertion_submaps[i]).inverse() *
local_pose_2d;
// 新生成的 子图内约束 放入容器中
data_.constraints.push_back(
Constraint{submap_id,
node_id,
{transform::Embed3D(constraint_transform),
options_.matcher_translation_weight(),
options_.matcher_rotation_weight()},
Constraint::INTRA_SUBMAP}); // 子图内约束
} // end for

子图间约束

先通过子图在global坐标系下的坐标的逆, 乘以节点在global坐标系下的坐标, 获取子图原点在获取子图原点在 global坐标系下指向节点的相对坐标变换

首先一个先验估计值

1
2
3
4
5
6
// submap原点在global坐标系下的坐标的逆 * 节点在global坐标系下的坐标 = submap原点指向节点的坐标变换
const transform::Rigid2d initial_relative_pose =
optimization_problem_->submap_data()
.at(submap_id)
.global_pose.inverse() *
optimization_problem_->node_data().at(node_id).global_pose_2d;

然后根据子图在local坐标系下的位姿乘以这个坐标变换, 得到节点在local坐标系下的预测位姿

1
2
const transform::Rigid2d initial_pose =
ComputeSubmapPose(*submap) * initial_relative_pose;

再通过分枝定界粗匹配与ceres的精匹配, 对这个节点位姿进行校准, 校准后的位姿还是local坐标系下的

最后, 通过子图在local坐标系下位姿的逆, 乘以这个节点校准后的位姿, 得到子图间约束, 是local坐 标系下, 子图原点指向节点的相对坐标变换.

1
2
const transform::Rigid2d constraint_transform =
ComputeSubmapPose(*submap).inverse() * pose_estimate;

返回

1
2
3
4
5
6
7
// Step:5 返回计算后的约束
constraint->reset(new Constraint{submap_id,
node_id,
{transform::Embed3D(constraint_transform),
options_.loop_closure_translation_weight(),
options_.loop_closure_rotation_weight()},
Constraint::INTER_SUBMAP});

constraint 为添加到 constraints_ 双端队列的最末尾的数据

最后他返回的数据,将通过ConstraintBuilder2D::RunWhenDoneCallback()函数,把存储子图间约束的数据

1
std::deque<std::unique_ptr<Constraint>> constraints_

全部传入到result中,然后通过调用回调函数PoseGraph2D::HandleWorkQueue(

把数据全部添加到了PoseGraph2D的 data_.constraints中

计算相对位姿的方法总结

  1. 节点 通过 GetLocalToGlobalTransform * constant_data->local_pose 进行global下位姿的计算
  2. 子图 通过对前一个子图到后一个子图的坐标变换进行累计, 得到子图在global坐标系下的位姿
  3. 子图内约束 local坐标系系下, 子图原点指向节点间的坐标变换
  4. 子图间约束 根据global坐标计算初值, 然后通过分支定界算法粗匹配与ceres的精匹配, 获取校准后的位姿, 最后计算local坐标系系下, 子图原点指向校准后的节点间的坐标变换
 Comments