c++11 新特性

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

lambda表达式

C++11提供了对匿名函数的支持,称为Lambda表达式. Lambda表达式具体形式如下:
[capture](parameters)->return-type{body}

如果没有参数,空的圆括号()可以省略。 返回值也可以省略,如果函数体只由一条return语句组成或返回类型为void的话。形如:
[capture](parameters){body}

几个Lambda函数的例子:     

1
2
3
4
[](int x, int y) { return x + y; } // 隐式返回类型
[](int& x) { ++x; } // 没有return语句 -> lambda 函数的返回类型是'void'
[]() { ++global_x; } // 没有参数, 仅访问某个全局变量
[]{ ++global_x; } // 与上一个相同,省略了()

先掌握这些简单的情况

判断inf, nan

1
2
3
4
5
bool isinf( float arg );

bool isfinite( float arg );

bool isnan( float arg );

constexpr

C++11新标准规定,允许将变量声明为constexpr类型以便由编译器验证变量的值是否是一个常量表达式。如果不是,编译器报错。同时,声明为constexpr的变量一定是常量,而且必须用常量表达式初始化。 constexpr可以修饰函数参数、函数返回值、变量、类的构造函数、函数模板等,是一种比const更加严格的约束,它修饰的表达式除了具有“运行时常量性”,也具有“编译时常量性”,即constexpr修饰的表达式的值在编译期间可知。

1
2
3
4
5
6
constexpr int mf = 20;              //正确,20是常量表达式
constexpr int limit = mf + 1; //正确,mf + 1是常量表达式
constexpr int sz = size(); //未知,若size()函数是一个constexpr函数时即正确,反之错误。

int i = 10;
constexpr int t = i; //错误,i不是常量

参考:constexpr

大括号初始化

在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 };
};

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

匿名命名空间

std::tie

限域枚举

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>)>;

std::weak_ptr

std::weakptr Schedule(std::unique_ptr task)
LOCKS_EXCLUDED(mutex
) override;


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


概率知识

贝叶斯法则:

是后验, 是似然, 是先验

  • 先验:指根据以往经验得到事件发生的概率

最大后验: 使最大的x估计,比如分布是高斯分布,那么求出的是均值,比估计整个分布简单的多。

有时求最大后验时,也不知道,所以只把似然最大化。

最大似然: 在什么状态下,最可能产生当前的观测。对于高斯分布,就是什么情况下最可能取得均值,让概率密度函数最大

在状态估计中,可以这么理解,先验就是没有得到观测值时的概率分布,似然就是观测的概率分布,后验就是在得到观测值后对先验校正后的概率分布。


KD树和最近邻搜索

kD树的构建步骤:

  1. 在K维数据集合中选择具有最大方差的维度k,然后在该维度上选择中值为pivot对该数据集合进行划分,得到两个子集合;同时创建一个树结点node,用于存储

  2. 对两个子集合重复(1)步骤的过程,直至所有子集合都不能再划分为止;

  • 选择数据方差大的维度,是因为沿该坐标轴方向上数据点分散的比较开;另外可以减少最近邻搜索时的回溯代价,减少子树的访问

  • 对于n个实例的k维数据来说,建立kd-tree的时间复杂度为 , 这有时会比较复杂

最近邻搜索

算法的时间复杂度是
举例如下:
KD树构建
坐标图
搜索过程

  • 不会去搜索根节点的另一颗子树
  • 不需要计算每一个节点的欧式距离,而多数情况只需要计算点到超平面的距离就可以了。
  • KD树在维度较小时(比如20、30),算法的查找效率很高,然而当数据维度增大(例如:K≥100),查找效率会随着维度的增加而迅速下降。假设数据集的维数为 D,一般来说要求数据的规模 N 满足 ,才能达到高效的搜索,否则大部分的点都会被查询。

PCL中的KD树

nearestKSearch参数

1
2
3
4
5
6
void pcl::search::Search< PointT >::nearestKSearch(
const PointCloud & cloud,
int k,
std::vector< Indices > & k_indices,
std::vector< std::vector< float > > & k_sqr_distances
) const

对给定的点,搜索 k-nearest neighbors

参数:

  • [in] cloud the point cloud data
  • [in] k the number of neighbors to search for
  • [out] k_indices the resultant indices of the neighboring points, k_indices[i] corresponds to the neighbors of the query point i
  • [out] 查询到的点的距离平方

这个函数的返回值是 查询到的个数。

PCL中的KD-Tree一般适用于三维特征点。

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
#include <pcl/kdtree/kdtree_flann.h>  //kdtree近邻搜索
#include <pcl/io/pcd_io.h> //文件输入输出
#include <pcl/point_types.h> //点类型相关定义
#include <pcl/visualization/cloud_viewer.h> //点类型相关定义

#include <iostream>
#include <vector>

int main()
{
// 读取点云
pcl::PointCloud<pcl::PointXYZRGB>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZRGB>);
if (pcl::io::loadPCDFile<pcl::PointXYZRGB>("rabbit.pcd", *cloud) == -1)
{
PCL_ERROR("Cloudn't read file!");
return -1;
}
// 原始点云着色
for (size_t i = 0; i < cloud->points.size(); ++i){
cloud->points[i].r = 255;
cloud->points[i].g = 255;
cloud->points[i].b = 255;
}

// 建立kd-tree
pcl::KdTreeFLANN<pcl::PointXYZRGB> kdtree; //建立kdtree对象
kdtree.setInputCloud(cloud); //设置需要建立kdtree的点云指针

// K近邻搜索
pcl::PointXYZRGB searchPoint = cloud->points[1000]; // 设置查找点
int K = 900; //设置需要查找的近邻点个数
std::vector<int> pointIdxNKNSearch(K); // 保存每个近邻点的索引
std::vector<float> pointNKNSquaredDistance(K); // 保存每个近邻点与查找点之间的欧式距离平方

if (kdtree.nearestKSearch(searchPoint, K, pointIdxNKNSearch, pointNKNSquaredDistance) > 0)
{
for (size_t i = 0; i < pointIdxNKNSearch.size(); ++i){
cloud->points[pointIdxNKNSearch[i]].r = 0;
cloud->points[pointIdxNKNSearch[i]].g = 255;
cloud->points[pointIdxNKNSearch[i]].b = 0;
}
}
std::cout << "K = 900近邻点个数:" << pointIdxNKNSearch.size() << endl;

// 显示点云
pcl::visualization::CloudViewer viewer("cloud viewer");
viewer.showCloud(cloud);
// 或者 pause()
system("pause");
return 0;
}

如果运行时出现报警 Failed to find match for field ‘rgb’ ,则把pcl::PointXYZRGBA>改为pcl::PointXYZ>

KD树和八叉树的比较

从搜索点数来看,当原点云与目标点云重叠区域较大时,优先选择nearestKSearch接口来寻找最近邻,否则优先使用radiusSearch接口来寻找最近邻。 PCL的ICP用的就是 nearestKSearch

参考:
点云数据通过pcl的kdtree搜索关键点某半径邻域内的区域


偶然出现倒退的小速度

机器人导航时偶然出现倒退的小速度-0.02,但周围又没有障碍物,其实就是参数max_vel_x_backwards=0.02


Matlab中的矩阵

定义矩阵

a=[1,2,3; 4,5,6]是一个2×3的矩阵

1
2
1 2 3
4 5 6

matrix = [1;2;3;4;5] 是5x1的矩阵, matrix = [1,2,3,4,5]是1x5的矩阵

matrix = [1,2,3,4,5]' 是做了转置,结果是5x1的矩阵

matrix(:, 1)是矩阵的第一列

  • zeros(m,n): 生成一个 m 行 n 列的零矩阵,m=n 时可简写为 zeros(n)

  • ones(m,n): 生成一个 m 行 n 列的元素全为 1 的矩阵, 当m=n 时可写为 ones(n)

  • eye(m,n): 生成一个主对角线全为 1 的 m 行 n 列矩阵, m=n 时可简写为 eye(n),即为 n 维单位矩阵

  • rand(m,n): 产生 0~1 间均匀分布的随机矩阵, m=n 时简写为 rand(n)

  • randn(m,n): 产生均值为0,方差为1的标准正态分布矩阵,m=n 时简写为 randn(n)

zeros(m, n)中的n可以为0,此时矩阵实际是空。

1
2
3
matrix = zeros(5,0)
matrix = [matrix, [1;2;3;4;5] ]
matrix = [matrix, [11;12;13;14;15] ]

最终matrix是5x2的矩阵,也就是逐步扩展
1
2
3
4
5
1    11
2 12
3 13
4 14
5 15


论文解读 Autonomous Exploration Development Environment and the Planning Algorithms

这篇论文发表于2021.10,大部分内容在官网上,对一些重点内容做一下记录。

I. INTRODUCTION

算法框架包括探索算法TARE (exploration planner); 路径算法FAR (route planner)

我们的算法不依赖只有仿真才提供的信息,比如 semantic and terrain segmentation groundtruth. 算法是为了让用户重点开发上层的规划算法。

在2021 DARPA地下挑战赛中, CMU-OSU Team Explorer 虽然没拿到冠军,但获得”探索最多区域奖“(26 out of 28 sectors).

For outdoor settings, the most well-known dataset is KITTI Vision Benchmark Dataset [4] collected
from a self-driving suite in road driving scenarios. The
dataset contains sensor data from stereo cameras, 3D Lidar,
and GPS/INS ground truth for benchmarking depth reconstruction, odometry estimation, object segmentation, etc.

For localization purposes, the long-term localization benchmark includes a comprehensive list of datasets: Aachen Day-Night dataset, Extended CMU Seasons dataset, RobotCar Seasons dataset, and SILDa Weather and Time of Day dataset.

For indoor navigation, iGibson [15], [16], Sapien [17], AI2Thor [18], Virtual Home [19], ThreeDWorld [20], MINOS [21], House3D [22] and CHALET [23] use synthetic scenes, while reconstructed scenes are also available in iGibson, AI Habitatand MINOS. Compared to datasets, simulation environments have the advantage of providing access to ground truth data, e.g. vehicle pose and semantic segmentation to simplify the algorithm development and allowing full navigation system tests in closed control loop。

For indoor scenes, datasets such as NYUDepth dataset [7], TUM SLAM dataset [8], inLoc dataset [9], MIT Stata center dataset, and KTH-INDOL dataset [11], [12] are available. Datasets are useful in developing and

Simulation environments: Carla [13] and AirSim [14] are two representative simulation environments for autonomous driving and flying. These simulators support various conditions such as lighting and weather changes, moving objects such as pedestrians, and incident scenes.

其他的导航框架: ROS Navigation Stack 和港科大的 fast_planner ,不过是针对无人机的。

我们的导航框架: 支持photorealistic house models from Matterport3D 以及向AI Habitat提供接口。这两个在机器人学和计算视觉中应用很多。Users are provided with scan data and RGB, depth, and semantic images rendered by AI Habitat. Users have the option of running AI Habitat side by side with our system or in post-processing

参考:
阅读笔记 《Matterport3D: Learning from RGB-D Data inIndoor Environments》
AI Habitat室内仿真平台使用

III. DEVELOPMENT ENVIRONMENT

Local Planner

local planner保证了到达目标时的安全,它预计算 a motion primitive library and associates the motion primitives to 车周围的3D locations(这里说的就是free_paths对应的曲线簇)。 The motion primitives are modeled as Monte Carlo samples and organized in groups

现实中快遇到障碍物时,local planner可以在几毫秒内判断出哪些 motion primitives 会和障碍相撞,然后从motion primitives中选出最可能朝向目标的一组。

The module also has interface to take in additional range data for collision avoidance as an extension option

Terrain Traversability Analysis

模块建立了一个代价地图,地图中的每一个点和一个traversal cost相关联. 代价值取决于 local smoothness of the terrain.

我们使用体素网格表示环境,分析 distributions of data points in adjacent voxels to 估计地面高度. The points are associated with higher traversal costs if they are further apart from the ground.

模块还能处理negative obstacles,这些障碍生成空区域with no data points on the terrain map. 如果开启了negative obstacle的处理,模块认为那些区域不可经过

Visualization and Debugging Tools

可视化算法的性能. 可视化工具显示地图,已探索区域,车的轨迹. Metrics包括 explored volume, traveling distance, and algorithm runtime are plotted and logged to files.

另外,支持手柄和导航交互,在多种操作模式之间切换 to ease the process of system debugging.

IV. HIGH-LEVEL PLANNERS

V. BEST PRACTICES

Safety margin: local planner使用车和waypoint之间的距离作为规划尺度(planning horizon)。即使waypoint很接近障碍,也可以让车到达。但是,如果车不能在waypoint停下,上层规划器倾向于让车到waypoint保持一定距离(默认≥
3.75m)。If the waypoint is closer, users can project the waypoint further away and keep the waypoint in the same direction w.r.t. the vehicle to fully use the safety margin. (这句话没理解什么意思)

但是,如果车需要经过窄通道,降低这一距离可以让local planner找到安全合适的路径。

大转弯: Typically, a high-level planner selects the waypoint along the path that is a distance, namely look-ahead distance, ahead of the vehicle and sends the waypoint to the local planner (possibly after projecting the waypoint further away from the vehicle as discussed above).

When handling sharp turns (≥ 90 deg), the look-ahead distance needs to be properly set or the waypoint may jump to the back of the vehicle, causing the vehicle to osculate back-and-forth. We recommend to select the waypoint on the starting segment of the path that is in line-of-sight from the vehicle.

动态障碍: terrain analysis模块在动态障碍走过之后,通过ray-tracing清除动态障碍。这是在车的附近范围实现的(到车的距离≤5m), 因为雷达数据在远处比较稀疏,以及难以权衡清除动态障碍和thin structures之间的矛盾。 用户根据自己的情况来清除动态障碍,TARE和FAR都有这一步骤。

VI. SYSTEM INTEGRATION

用于DARPA挑战赛的平台


The state estimation module can detect and introduce loop closures. The module outputs state estimation in the odometry frame generated by 3D Lidar-based odometry containing accumulated drift. When loop closure is triggered, it outputs loop closure adjustments to globally relax the vehicle trajectory and corresponding maps. Loop closure adjustments are used by the high-level planners since they are in charge of planning at the global scale.

The local planner and terrain analysis modules are extended to handle complex terrains including negative obstacles such as cliffs, pits, and water puddles with a downward-looking depth sensor. The TARE planner, FAR planner, and other planners (for stuck recovery, etc) are run in parallel for tasks such as exploration, go-to waypoint, and return home.

On top of these planners, behavior executive and multi-robot coordination modules are built specifically for the challenge. The modules share explored and unexplored areas across multirobots and call TARE and FAR planners cooperatively. In particular, when a long-distance transition is determined due to new areas containing artifacts are discovered or operator waypoints are received, the behavior executive switches to
FAR planner for relocating the vehicle. During the relocation, FAR planner uses a sparse roadmap merged from multi-robots for high-level guidance. After the relocation is complete, TARE planner takes place for exploration

DARPA最后决赛是在Louisville Mega Cavern, KY. 赛道包括隧道、urban、洞穴环境。我们的三台车使用TAREFAR进行探索。
DARPA Subterranean Challenge Final Competition
红色轨迹:车先使用TARE一直探索到B,然后使用FAR经过C,一到达C,车换成TARE进行剩下的探索。

绿色轨迹:车只用TARE;蓝色轨迹:车先使用FAR经过A,在换成TARE进行之后的搜索。

三台车各行走596.6m, 499.8m, 445.2m。共花费时间2259s
比赛地图,与上图一样
这里只看论文不太明白,看实际视频才清楚。实际是两辆车和一架无人机进行探索。

VII. EXTENDED APPLICATION EXAMPLES

这部分我暂时用不到,就不研究了。

训练基于学习的探索和导航算法。大家可以使用我们的仿真平台作为专家系统(expert system)来训练基于学习的规划算法。专家系统里包含状态估计,局部避障,上层的探索或者路径规划算法。训练的对象可以是一些端对端的方法,比如只有一层深度网络的探索或者路径规划算法。

基于激光或者视觉的sim-to-real学习。大家可以用我们的仿真平台训练一些基于学习的方法,并将训练好的系统应用在现实世界中。因为我们提供了多个仿真世界以及支持来自Matterport3D的90个photorealistic环境模型,并且兼容AI Habitat的RGB,深度,和语义(Semantic)图像的渲染功能,所以不管是基于激光还是视觉的算法,大家都可以找到适用的环境模型,当然也可以根据需求换上自己的模型。举个简单的例子,需要做Semantic-in-loop导航的人可以使用我们提供的带有语义信息的环境,使用我们的地面机器人仿真平台,去训练他们的算法,并且在训练结束后,直接将算法移植到实际的机器人上。下面是我们的仿真机器人在Matterport3D的两个环境模型中导航的场景,用AI Habitat实时生成RGB,深度及语义图像。

开发人群导航(social navigation)算法。大家可以在我们的仿真环境中加入动态的障碍物或者行人,模拟真实环境中的社区场景,辅助开发人群导航算法,甚至可以在这种场景中预测动态物体的行为及其轨迹。