欧拉角和四元数

概念

欧拉角其实就是表示向三个方向旋转的角度,比较形象直观。四元数是等价的另一种表示方法,不能直观体现,但是具备一些优点。数学理论实在太复杂,只需要记住以下几条:

  • 欧拉角就是绕Z轴、Y轴、X轴的旋转角度,记做ψ、θ、φ,分别为Yaw(偏航)、Pitch(俯仰)、Roll(翻滚)
  • 四元数是w,x,y,z,它们的平方和是1
  • 欧拉角会出现万向节死锁,比如三维空间中有一个平行于 X 轴的向量,将它绕 Y 轴旋转直到它平行于 Z 轴,这时任何绕 Z 轴的旋转都改变不了该向量的方向

三个旋转方向如图所示:

欧拉角到四元数的转换:

四元数到欧拉角的转换:

描述机器人转弯的角就是绕Z轴的旋转,按照右手法则可以知道机器人向左转为正,右转为负。

ROS中的欧拉角 四元数

geometry_msgs/Quaternion消息

1
2
3
4
float64 x
float64 y
float64 z
float64 w

geometry_msgs/Transform消息

1
2
geometry_msgs/Vector3 translation
geometry_msgs/Quaternion rotation

这两个各自有一个加时间戳的消息类型。


除了geometry_msgs/Quaternion,还有一个tf::Quaternion类,重要函数:

1
2
3
4
5
Vector3  getAxis () const    //Return the axis of the rotation represented by this quaternion. 
//Set the quaternion using fixed axis RPY.
void setRPY (const tfScalar &roll, const tfScalar &pitch, const tfScalar &yaw)
//Return an identity quaternion.
tf::Quaternion createIdentityQuaternion()

还有一个有点特殊的函数,直接返回yaw,但是范围是[0, 2Pi],所以尽量不要使用

1
tf::Quaternion::getAngle () const

geometry_msgs::Quaternion —— tf::Quaternion 转换如下
1
2
// [inline, static]
static void tf::quaternionMsgToTF(const geometry_msgs::Quaternion& msg, tf::Quaternion& bt)

四元数——欧拉角

1
2
3
4
tf::Quaternion quat;
double roll, pitch, yaw;
// 赋值给quat,getRPY函数 将quat转为欧拉角
tf::Matrix3x3(quat).getRPY(roll, pitch, yaw);

orientation是表示朝向的消息成员,也就是四元数,类型const geometry_msgs::Quaternion &。 常常配合tf::quaternionMsgToTF函数使用,因为我们一般是先获得geometry_msgs::Quaternion类型

或者用tf2的方式

1
2
3
4
5
6
7
#include “tf2/LinearMath/Matrix3x3.h”

tf2::Matrix3x3 mat(tf2::Quaternion( x, y, z, w) );
mat.getEulerYPR(tf2Scalar& yaw,
tf2Scalar& pitch,
tf2Scalar& roll,
unsigned int solution_number = 1);

欧拉角 —— 四元数

1
2
3
4
5
6
7
8
9
//只通过yaw计算四元数,用于平面小车
static geometry_msgs::Quaternion tf::createQuaternionMsgFromYaw(double yaw);

static tf::Quaternion createQuaternionFromYaw (double yaw)

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

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

python常见的写法:

1
2
3
pos = Pose()
q = tf.transformations.quaternion_from_euler(0, 0, point.z)
pos.orientation = q

但是经测试,这样写是有问题的,正确的写法如下:
1
2
3
4
5
6
pos = Pose()
q = tf.transformations.quaternion_from_euler(0, 0, point.z)
pos.orientation.x = q[0]
pos.orientation.y = q[1]
pos.orientation.z = q[2]
pos.orientation.w = q[3]

报错积累

报错 MSG to TF: Quaternion Not Properly Normalized , 原因是接受的位姿的四元数不合理。

参考:
四元数与欧拉角(Yaw、Pitch、Roll)的转换


实际导航问题的积累
abstract Welcome to my blog, enter password to read.
Read more
添加新约束:Jerk(加加速度)

加速度的变化率过大会使机器人底盘电机输出的力矩突变而引起机器人受冲击振荡,为了使TEB生成的轨迹 less jerky, 考虑jerk约束。jerky的意思是有突然的停止和行走现象,也就是trajectory不够平滑稳定。

Jerk约束  7元边.png

github上的TEB jerk约束的代码,我估计作者自己都没认真测试,有很多低级错误,比如变量omega写成了omege,都改完之后,如果yaml里用了Jerk约束,运行时规划出了路径,但是没有速度。还需要自己调试


ROS的global_planner
abstract Welcome to my blog, enter password to read.
Read more
PCL1.7和1.8版本冲突问题

同事的点云程序必须用PCL-1.8,而ROS默认的是1.7,结果有时出现了版本冲突。

安装了PCL-1.8后,重要的库文件libcostmap_2d.so链接到了pcl-1.8,而不是默认的1.7。此时删除了pcl-1.8之后,再编译会报错:

1
2
3
4
CMake Error at /home/user/catkin_ws/devel/share/costmap_2d/cmake/costmap_2dConfig.cmake:113 (message):
Project 'costmap_2d' specifies '/usr/local/include/pcl-1.8' as an include
dir, which is not found. It does neither exist as an absolute directory
nor in '/home/user/catkin_ws/src/navigation/costmap_2d//usr/local/include/pcl-1.8'.

编译还会找pcl-1.8的路径,这是CMake caches的问题,不是软链接之类的问题,它还会去找旧的路径,所以删除catkin_ws/build下面的catkin_make.cache, Makefile, CMakeCache.txt. 然后重新编译,这样libcostmap_2d.so就不会再去链接1.8了


多机器人的互相避障
abstract Welcome to my blog, enter password to read.
Read more
找不到动态库.so--cannot open shared object file

明明安装了库,也在CMake里指定了路径,但还是报这个错误

  1. 修改/etc/ld.so.conf 然后刷新

Linux回到这个文件定义的路径里寻找库文件

1
2
3
sudo vim /etc/ld.so.conf
# 添加你的库文件路径
sudo ldconfig

  1. 修改环境变量LD_LIBRARY_PATH
1
2
export LD_LIBRARY_PATH=/where/you/install/lib:$LD_LIBRARY_PATH
sudo ldconfig
  1. 用ln将需要的so文件链接到/usr/lib或者/lib这两个默认的目录下边
1
2
ln -s /where/you/install/lib/*.so /usr/lib
sudo ldconfig
  1. 强行将路径写进去
  2. 路径写进去不行的话,在~/.bashrc中,导入export环境变量 LD_LIBRARY_PATH
  3. /etc/ld.so.conf/目录下,添加该文件的路径,首先命令行输入locate package,定位软件包的位置,然后找个.conf文件,将路径加进去之后,重新sudo ldconfig

运行时动态库的搜索路径的先后顺序是

  1. 编译目标代码时指定的动态库搜索路径
  2. 环境变量LD_LIBRARY_PATH指定的动态库搜索路径
  3. 配置文件/etc/ld.so.conf中指定的动态库搜索路径
  4. 默认的动态库搜索路径/lib/usr/lib

这个顺序是compile gcc时写在程序内的,通常软件源代码自带的动态库不会太多,而我们的/lib/usr/lib只有root权限才可以修改,如果对环境变量LD_LIBRARY_PATH进行操作就能解决问题,不要改/etc/ld.so.conf/lib/usr/lib


catkin_make_isolated和ninja

这个命令用于非ROS的包,所以Cartographer用的是它。 缺点是处理pacakge 较慢,只适用于ROS1. 可以和ninja结合

1
catkin_make_isolated  --use-ninja --pkg cartographer_ros

只编译一个包,会真的跳过其他包,比catkin_make --pkg好用多了。 编译生成的文件在/workspace/devel_isolated/cartographer_ros/lib/cartographer_ros

如果加上--install,编译得到的可执行文件在/workspace/devel_isolated/cartographer_ros/install/lib/cartographer_ros


它会在工作空间生成文件夹devel_isolated, install_isolated, build_isolated

平时我们使用rosrun 运行的是 devel/lib 里的可执行文件,因为一般source的是 catkin_ws/devel/setup.zsh. 修改文件后,使用 catkin_make_isolated 编译,运行发现没有生效,需要先修改为 source catkin_ws/devel_isolated/setup.zsh, 以后就是用catkin_make_isolated

问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 Installing: /home/xiaoqiang/Documents/ros/install_isolated/share/cartographer_ros/launch
-- Installing: /home/xiaoqiang/Documents/ros/install_isolated/share/cartographer_ros/launch/software_institute.launch
-- Up-to-date: /home/xiaoqiang/Documents/ros/install_isolated/share/cartographer_ros/launch/\
CMake Error at cmake_install.cmake:142 (file):
file INSTALL cannot set permissions on
"/home/xiaoqiang/Documents/ros/install_isolated/share/cartographer_ros/launch/\"


FAILED: cd /home/xiaoqiang/Documents/ros/build_isolated/cartographer_ros && /usr/bin/cmake -P cmake_install.cmake
ninja: build stopped: subcommand failed.
<== Failed to process package 'cartographer_ros':
Command '['/home/xiaoqiang/Documents/ros/install_isolated/env.sh', 'ninja', 'install']' returned non-zero exit status 1

Reproduce this error by running:
==> cd /home/xiaoqiang/Documents/ros/build_isolated/cartographer_ros && /home/xiaoqiang/Documents/ros/install_isolated/env.sh ninja install

Command failed, exiting.

不知为何出现了文件夹 install_isolated/share/cartographer_ros/launch/\,删除后重新编译还是如此

参考:
catkin_make_isolated 官方说明
catkin_make vs catkin_make_isolated


(八) Homotopy planner

实际中用的是Homotopy planner,这里做一些简单说明

算法初始化

构造函数就一句initialize(cfg, obstacles, robot_model, visual, via_points);, 代码比较绕:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void HomotopyClassPlanner::initialize(const TebConfig& cfg, ObstContainer* obstacles,
RobotFootprintModelPtr robot_model, TebVisualizationPtr visual,
const ViaPointContainer* via_points)
{
cfg_ = &cfg;
obstacles_ = obstacles;
via_points_ = via_points;
robot_model_ = robot_model;

if (cfg_->hcp.simple_exploration)
graph_search_ = boost::shared_ptr<GraphSearchInterface>(new lrKeyPointGraph(*cfg_, this));
else
graph_search_ = boost::shared_ptr<GraphSearchInterface>(new ProbRoadmapGraph(*cfg_, this));

initialized_ = true;
setVisualization(visual); // 只有一句,对 visualization_ 赋值
}

simple_exploration默认false,也就是用复杂的ProbRoadmapGraph类。

plan 函数

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
// initial_plan 其实就是 transformed_plan
bool HomotopyClassPlanner::plan(const std::vector<geometry_msgs::PoseStamped>& initial_plan,
const geometry_msgs::Twist* start_vel, bool free_goal_vel)
{
ROS_ASSERT_MSG(initialized_, "Call initialize() first.");

// store initial plan for further initializations (must be valid for the
// lifetime of this object or clearPlanner() is called!)
initial_plan_ = &initial_plan;

PoseSE2 start(initial_plan.front().pose);
PoseSE2 goal(initial_plan.back().pose);

return plan(start, goal, start_vel, free_goal_vel);
}

// 然后是下面重要的重载函数
bool HomotopyClassPlanner::plan(const PoseSE2& start, const PoseSE2& goal,
const geometry_msgs::Twist* start_vel, bool free_goal_vel)
{
ROS_ASSERT_MSG(initialized_, "Call initialize() first.");

// Update old TEBs with new start, goal and velocity
updateAllTEBs(&start, &goal, start_vel);

// Init new TEBs based on newly explored homotopy classes
exploreEquivalenceClassesAndInitTebs(start, goal, cfg_->obstacles.min_obstacle_dist, start_vel);
// update via-points if activated
updateReferenceTrajectoryViaPoints(cfg_->hcp.viapoints_all_candidates);
// Optimize all trajectories in alternative homotopy classes
optimizeAllTEBs(cfg_->optim.no_inner_iterations, cfg_->optim.no_outer_iterations);
// Select which candidate (based on alternative homotopy classes) should be used
selectBestTeb();

initial_plan_ = nullptr; // clear pointer to any previous initial plan (any previous plan is useless regarding the h-signature);
return true;
}

HomotopyClassPlanner::plan —— exploreEquivalenceClassesAndInitTebs —— createGraph() —— PointObstacle::checkLineIntersection —— PointObstacle::checkCollision

1
2
3
4
5
6
7
8
9
PointObstacle::checkLineIntersection
{
return getMinimumDistance(point) < min_dist;
}

virtual bool checkCollision(const Eigen::Vector2d& point, double min_dist) const
{
return getMinimumDistance(point) < min_dist;
}

optimizeAllTEBs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void HomotopyClassPlanner::optimizeAllTEBs(int iter_innerloop, int iter_outerloop)
{
// optimize TEBs in parallel since they are independend of each other

if (cfg_->hcp.enable_multithreading)
{
// Must prevent .join_all() from throwing exception if interruption was
// requested, as this can lead to multiple threads operating on the same
// TEB, which leads to SIGSEGV
boost::this_thread::disable_interruption di;

boost::thread_group teb_threads;
for (TebOptPlannerContainer::iterator it_teb = tebs_.begin(); it_teb != tebs_.end(); ++it_teb)
{
teb_threads.create_thread( boost::bind(&TebOptimalPlanner::optimizeTEB,
it_teb->get(), iter_innerloop, iter_outerloop, true,
cfg_->hcp.selection_obst_cost_scale, cfg_->hcp.selection_viapoint_cost_scale,
cfg_->hcp.selection_alternative_time_cost) );
}
teb_threads.join_all();
}
}

EquivalenceClassContainer equivalence_classes_; //!< Store all known quivalence classes (e.g. h-signatures) to allow checking for duplicates after finding and adding new ones.

// The second parameter denotes whether to exclude the class from detour deletion or not (true: force keeping).

typedef boost::shared_ptr<TebOptimalPlanner> TebOptimalPlannerPtr;

typedef std::vector< TebOptimalPlannerPtr > TebOptPlannerContainer;

teb()函数 返回 const TimedElasticBand&


障碍层(三) purgeStaleObservations

purgeStaleObservations

将timestamp较早的点都移除出observation_list_ 类型std::list<Observation>
唯一调用的地方是bufferCloud

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
if (!observation_list_.empty() )
{
list<Observation>::iterator obs_it = observation_list_.begin();
// if keeping observations for no time, only keep one observation

// observation_keep_time_ 就是参数 observation_persistence
if (observation_keep_time_ == ros::Duration(0.0))
{
observation_list_.erase(++obs_it, observation_list_.end());
return;
}
// or loop through the observations to see which ones are stale
for ( obs_it = observation_list_.begin(); obs_it != observation_list_.end();
++obs_it )
{
Observation& obs = *obs_it;
// if observation is out of date, remove it and those follow from the list
// kinetic: ros::Duration time_diff = last_updated_ - pcl_conversions::fromPCL(obs.cloud_->header).stamp;
if ((last_updated_ - obs.cloud_->header.stamp) > observation_keep_time_)
{
observation_list_.erase(obs_it, observation_list_.end());
return;
}
}
}