TEB的实际问题累计
abstract Welcome to my blog, enter password to read.
Read more
DWA算法(五) 如何触发latched机制

DWA规划路径时,机器人到了位置误差范围内会开始转动,直到满足角度误差。但是如果两个误差设置太小,很可能在转动过程中机器人又不能满足位置误差了,这样就又进入局部规划了,这种现象被称为dance,也就是当机器人接近目标点时,会在附近左右摆来摆去的。这显然不是我们想要的。

latch_xy_goal_tolerance机制是确保机器人一旦到达目标的position就停止,默认是关闭的。有些人反复设置总是不起作用,原因其实是LatchedStopRotateController的bug,它的构造函数中的latch_xy_goal_tolerance参数设置为false,即使在yaml中设置为了true,参数服务器里也是true,用日志在源码里打印会发现变量latch_xy_goal_tolerance_还是false,只能去构造函数里修改。

但是latched机制不能一直打开,否则又会给全局路径造成问题。因为接收到新目标时,不能再latch机器人了,move_base将reset latching. 但是全局路径在机器人到达目标前是不会关闭的,结果会出现:第一次latch机制是正常的,然后再发导航目标,机器人会依然不动。

解决的方法是在DWAPlannerROS::setPlan里,在reset latch之前检查全局路径的目标是否改变,若没有改变就不要reset latch。 更省事的方法是到move_base里把全局规划频率planner_frequency设置为0,这样全局规划只有在接收到新目标或局部规划失败时才进行规划,看各自需求了。

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 DWAPlannerROS::setPlan(const std::vector<geometry_msgs::PoseStamped>& orig_global_plan)
{
if (! isInitialized()) {
ROS_ERROR("This planner has not been initialized, please call initialize() before using this planner");
return false;
}
//when we get a new plan, we also want to clear any latch we may have on goal tolerances
geometry_msgs::PoseStamped current_global_goal = orig_global_plan.back();

if (first_goal_)
{
latchedStopRotateController_.resetLatching();
previous_global_goal_ = current_global_goal;
first_goal_ = false;
}
else
{
double dist = distance(current_global_goal, previous_global_goal_);
if (dist > 0.2) {
latchedStopRotateController_.resetLatching();
}
previous_global_goal_ = current_global_goal;
}

//when we get a new plan, we also want to clear any latch we may have on goal tolerances
//latchedStopRotateController_.resetLatching();

ROS_INFO("Got new plan");
return dp_->setPlan(orig_global_plan);
}

DWA算法(一) 论文

DWA算法的优点是计算的复杂度较低,由于考虑到速度和加速度的限制,只有安全的轨迹会被考虑,且每次采样的时间较短,因此轨迹空间较小。采样的速度即形成了一个动态窗口

DWA的弊端时很明显的,他对heading的实现是一个软约束,并不能够很好的在终点达到期望的heading.

该论文相对于之前的创新点在于:

  • 从移动机器人的运动动力学推导出来的
  • 考虑到机器人的惯性,也就是刹车距离,对于限制机器人在高速行驶时很重要
  • 在动态杂乱环境中速度可以较快

建立的假设:
a. 运动学模型的速度设为随时间变化的分段函数,因此机器人轨迹可看做许多的圆弧积分组成
b. 加速度也不是一直变化,假设有n个时间片,在每个时间片内的线加速度和角加速度不变
c. 省略 的二次项

DWA的整体轨迹评价函数主要是三个方面:

  1. 与目标的接近程度
  2. 机器人前进的速度
  3. 与下一个障碍物的距离

也就是在局部规划出一条路径,希望与目标越来越近,且速度较快,与障碍物尽可能远。评价函数权衡以上三个部分得到一条最优路径。 这样一看确实和CMU张楫团队提出的路径算法挺像

根据机器人的速度加速度假设和运动学模型,可以得出

其中的 都可以用 表示,这个式子就是圆在平面的公式,根据此公式可以求出圆的轨迹,即通过一系列的分段圆弧和直线来拟合轨迹。

速度空间降采样

圆弧轨迹:动态窗口法仅仅考虑圆弧轨迹,该轨迹由采样速度 (v,w) 决定,这些速度构成一个速度搜索空间
允许速度:如果机器人能够在碰到最近的障碍物之前停止,则该采样速度参与打分,机器人不与障碍物碰撞的速度集合记做
动态窗口:由于机器人加速度的限制,只保留以当前加速度可到达的速度,速度集合记做

最终的搜索空间

有点cartographer分支定界算法的感觉

目标函数

说是使用最优化方法,但是我看目标函数方程十分简单,不用使用什么优化算法,求最大值即可

  • heading用于评价机器人与目标位置的夹角,当机器人朝着目标前进时,该值取最大
  • dist 用于表示与机器人轨迹相交的最近的障碍物距离
  • vel 表示机器人的前向移动速度,支持快速移动

实现细节:
当机器人陷入局部最优时(即不存在路径可以通过),使其原地旋转,直到找到可行路径。但这样看着会很奇怪,这就是一个缺点
安全裕度:在路径规划时,设定一安全裕度,即在路径和障碍物之间保留一定间隙,且该间隙随着速度增大线性增长。

占比重太大,机器人运动自由度大,窄的区域不容易通过, 占比重太小,机器人轨迹则不够平滑。因此 越大,越适合在窄区域, 越小,越适合在宽区域。


在不涉及TEB的情况下,ROS中经常使用的局部规划器,有base_local_plannerdwa_local_planner,简单来看,两者属并列关系,其实就是选择使用 TrajectoryPlannerROS 还是 DWAPlannerROS,我们用ROS就只看DWAPlannerROS。两种配置如下:

1
2
3
<arg name="base_local_planner" default="base_local_planner/TrajectoryPlannerROS"/>

<arg name="base_local_planner" default="dwa_local_planner/DWAPlannerROS"/>

其实base_local_planner也是包含dwa选项的,不过通常还是使用独立的DWA模块。 base_local_planner::TrajectoryPlanner是对DWA的实现和Trajectory Rollout。 可以向dwa_local_planner添加自己的代价函数或者trajectory generators

Matlab对DWA算法的仿真
DWA泊车算法的实现
Dynamic Window Approach_机器人局部避障的动态窗口法

参考:DWA算法的论文


DWA算法(四) dwaComputeVelocityCommands

接上一篇,分析如果还没到x y误差范围内,进行DWA规划的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
else
{
// 是否规划成功
bool isOk = dwaComputeVelocityCommands(current_pose_, cmd_vel);

if (isOk)
publishGlobalPlan(transformed_plan);
else
ROS_WARN_NAMED("dwa_local_planner", "DWA planner failed to produce path.");

std::vector<geometry_msgs::PoseStamped> empty_plan;
publishGlobalPlan(empty_plan);
}
return isOk;

关键就是dwaComputeVelocityCommands

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
bool DWAPlannerROS::dwaComputeVelocityCommands(
tf::Stamped<tf::Pose> &global_pose,
geometry_msgs::Twist& cmd_vel )
{
// dynamic window sampling approach to get useful velocity commands
if(! isInitialized() )
{
ROS_ERROR("This planner has not been initialized, please call initialize() before using this planner");
return false;
}

tf::Stamped<tf::Pose> robot_vel;
odom_helper_.getRobotVel(robot_vel);

/* For timing uncomment
struct timeval start, end;
double start_t, end_t, t_diff;
gettimeofday(&start, NULL);
*/

//compute what trajectory to drive along
tf::Stamped<tf::Pose> drive_cmds;
drive_cmds.frame_id_ = costmap_ros_->getBaseFrameID();

// call with updated footprint
base_local_planner::Trajectory path = dp_->findBestPath(global_pose, robot_vel, drive_cmds, costmap_ros_->getRobotFootprint());
//ROS_ERROR("Best: %.2f, %.2f, %.2f, %.2f", path.xv_, path.yv_, path.thetav_, path.cost_);

/* For timing uncomment
gettimeofday(&end, NULL);
start_t = start.tv_sec + double(start.tv_usec) / 1e6;
end_t = end.tv_sec + double(end.tv_usec) / 1e6;
t_diff = end_t - start_t;
ROS_INFO("Cycle time: %.9f", t_diff);
*/

//pass along drive commands
cmd_vel.linear.x = drive_cmds.getOrigin().getX();
cmd_vel.linear.y = drive_cmds.getOrigin().getY();
cmd_vel.angular.z = tf::getYaw(drive_cmds.getRotation());

//if we cannot move... tell someone
std::vector<geometry_msgs::PoseStamped> local_plan;
if(path.cost_ < 0)
{
ROS_DEBUG_NAMED("dwa_local_planner",
"The dwa local planner failed to find a valid plan, cost functions discarded all candidates. This can mean there is an obstacle too close to the robot.");
local_plan.clear();
publishLocalPlan(local_plan);
return false;
}
ROS_DEBUG_NAMED("dwa_local_planner", "A valid velocity command of (%.2f, %.2f, %.2f) was found for this cycle.",
cmd_vel.linear.x, cmd_vel.linear.y, cmd_vel.angular.z);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
    // Fill out the local plan
for(unsigned int i = 0; i < path.getPointsSize(); ++i)
{
double p_x, p_y, p_th;
path.getPoint(i, p_x, p_y, p_th);

tf::Stamped<tf::Pose> p =
tf::Stamped<tf::Pose>(tf::Pose(
tf::createQuaternionFromYaw(p_th),
tf::Point(p_x, p_y, 0.0)),
ros::Time::now(),
costmap_ros_->getGlobalFrameID());
geometry_msgs::PoseStamped pose;
tf::poseStampedTFToMsg(p, pose);
local_plan.push_back(pose);
}

//publish information to the visualizer
publishLocalPlan(local_plan);
return true;
}

DWA算法(三) latched Stop Rotate

接着上一篇,继续DWAPlannerROS::computeVelocityCommands

如果x y坐标已经在目标的距离误差范围内

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (latchedStopRotateController_.isPositionReached(
&planner_util_, current_pose_) )
{
// 发布空的路径,这段可以删除
std::vector<geometry_msgs::PoseStamped> local_plan;
std::vector<geometry_msgs::PoseStamped> transformed_plan;
publishGlobalPlan(transformed_plan);
publishLocalPlan(local_plan);
// 这个类保存了所有 DWA参数,只有一个getAccLimits函数
base_local_planner::LocalPlannerLimits limits = planner_util_.getCurrentLimits();
return latchedStopRotateController_.computeVelocityCommandsStopRotate(
cmd_vel,
limits.getAccLimits(),
dp_->getSimPeriod(),
&planner_util_,
odom_helper_,
current_pose_,
boost::bind(&DWAPlanner::checkTrajectory, dp_, _1, _2, _3) );
}

latchedStopRotateController_base_local_planner::LatchedStopRotateController,它用到一个参数latch_xy_goal_tolerance

isPositionReached

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
/**
* returns true if we have passed the goal position.
* Meaning we might have overshot on the position beyond tolerance, yet still return true.
* Also goal orientation might not yet be true
*/
bool LatchedStopRotateController::isPositionReached(LocalPlannerUtil* planner_util,
tf::Stamped<tf::Pose> global_pose)
{
double xy_goal_tolerance = planner_util->getCurrentLimits().xy_goal_tolerance;

// 认为 global goal 是 global plan的最后一个点
// 显然是把全局路径的最后一个点赋值给 goal_pose
tf::Stamped<tf::Pose> goal_pose;
if ( ! planner_util->getGoal(goal_pose))
{
return false;
}
double goal_x = goal_pose.getOrigin().getX();
double goal_y = goal_pose.getOrigin().getY();

// check to see if we've reached the goal position
if ( (latch_xy_goal_tolerance_ && xy_tolerance_latch_) ||
base_local_planner::getGoalPositionDistance(global_pose, goal_x, goal_y) <= xy_goal_tolerance)
{
xy_tolerance_latch_ = true;
return true;
}
return false;
}

这里有两种情况可以出现 if(true)getGoalPositionDistance不用看也知道是求 global_pose 和 goal的空间距离,再跟参数xy_goal_tolerance比较。

latch_xy_goal_tolerance_本意是在LatchedStopRotateController构造函数里获取设置的参数,但是源码在这里出现了bug,应当是命名空间导致的。在DWA参数里设置其为true没有效果,从参数服务器看到的确实是true,但是在源码里的latch_xy_goal_tolerance_还是默认的false. 可以直接修改构造函数的设置值。 剩下的关键就是变量xy_tolerance_latch_了,有一个关键函数void resetLatching() { xy_tolerance_latch_ = false; }。 如何实现这一情况,在下面函数。

注意isPositionReached只是判断,没有发布零速度,没有停止

computeVelocityCommandsStopRotate

机器人到达位置时,停止平移,只做旋转

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
bool LatchedStopRotateController::computeVelocityCommandsStopRotate(
geometry_msgs::Twist& cmd_vel,
Eigen::Vector3f acc_lim,
double sim_period,
LocalPlannerUtil* planner_util,
OdometryHelperRos& odom_helper_,
tf::Stamped<tf::Pose> global_pose,
// obstacle_check 在调用里是 DWAPlanner::checkTrajectory
boost::function<bool (Eigen::Vector3f pos,
Eigen::Vector3f vel,
Eigen::Vector3f vel_samples)> obstacle_check)
{
// global goal is the last point in the global plan
tf::Stamped<tf::Pose> goal_pose;
if ( ! planner_util->getGoal(goal_pose) )
{
ROS_ERROR("Could not get goal pose");
return false;
}
base_local_planner::LocalPlannerLimits limits = planner_util->getCurrentLimits();
// latch goal tolerance, if we ever reach the goal location,
// we'll just rotate in place
// 这里就是latched机制了,在第5篇再详细解释。这里先不考虑latch,看下面的情况。
if (latch_xy_goal_tolerance_ && ! xy_tolerance_latch_ )
{
ROS_INFO("Goal position reached, stopping and turning in place");
xy_tolerance_latch_ = true;
}
// 检查是否达到了 goal orientation
double goal_th = tf::getYaw(goal_pose.getRotation());
double angle = base_local_planner::getGoalOrientationAngleDifference(global_pose, goal_th);
if (fabs(angle) <= limits.yaw_goal_tolerance)
{
// 到达, 停止转动
cmd_vel.linear.x = 0.0;
cmd_vel.linear.y = 0.0;
cmd_vel.angular.z = 0.0;
rotating_to_goal_ = false;
}

如果发布了零速度,会直接运行到最后的return true

最后看角度没有到导航误差范围的情况,比较复杂

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
else 
{
tf::Stamped<tf::Pose> robot_vel;
odom_helper_.getRobotVel(robot_vel);
nav_msgs::Odometry base_odom;
odom_helper_.getOdom(base_odom);
// if we're not stopped yet... we want to stop...
// taking into account the acceleration limits of the robot

/* 这里是机器人在继续运动的情况
rotating_to_goal_ 在构造函数里是 false
stopped函数: 如果里程计的角速度小于 rot_stopped_vel,且线速度小于
trans_stopped_vel, 则返回 true,认为已经停止 */
if ( ! rotating_to_goal_ && !base_local_planner::stopped(
base_odom, limits.rot_stopped_vel, limits.trans_stopped_vel) )
{
if ( ! stopWithAccLimits(
global_pose,
robot_vel,
cmd_vel,
acc_lim,
sim_period,
obstacle_check) )
{
ROS_INFO("Error when stopping.");
return false;
}
ROS_DEBUG("Stopping...");
}
//if we're stopped... then we want to rotate to goal
else
{
//set this so that we know its OK to be moving
rotating_to_goal_ = true;
if ( ! rotateToGoal(
global_pose,
robot_vel,
goal_th,
cmd_vel,
acc_lim,
sim_period,
limits,
obstacle_check) )
{
ROS_INFO("Error when rotating.");
return false;
}
ROS_DEBUG("Rotating...");
}
}
return true;
}


DWA算法(二) dwa_ros 源码

我们需要看的是DWAPlannerROS类的源码,继承自nav_core::BaseLocalPlanner。 但是它还用到了base_local_plannerLatchedStopRotateController模块,用于latch机制和到达位置后转向。

initialize

原型是void DWAPlannerROS::initialize(std::string name, tf2_ros::Buffer* tf, costmap_2d::Costmap2DROS* costmap_ros)

其实主要是if( !isInitialized())的部分,直接看里面的内容

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
ros::NodeHandle private_nh("~/" + name);
g_plan_pub_ = private_nh.advertise<nav_msgs::Path>("global_plan", 1);
l_plan_pub_ = private_nh.advertise<nav_msgs::Path>("local_plan", 1);
tf_ = tf;
costmap_ros_ = costmap_ros;
// 在局部代价地图中的位姿
costmap_ros_->getRobotPose(current_pose_);

// 局部代价地图
costmap_2d::Costmap2D* costmap = costmap_ros_->getCostmap();
// base_local_planner::LocalPlannerUtil
// 其实就是把三个参数赋值给 LocalPlannerUtil 的三个成员变量
planner_util_.initialize(tf, costmap, costmap_ros_->getGlobalFrameID());

//这里创建了dwa的共享指针,也就是 boost::shared_ptr<DWAPlanner> dp_;
dp_ = boost::shared_ptr<DWAPlanner>(new DWAPlanner(name, &planner_util_));

if( private_nh.getParam( "odom_topic", odom_topic_ ))
{
odom_helper_.setOdomTopic( odom_topic_ );
}

initialized_ = true;

// Warn about deprecated parameters -- remove this block in N-turtle
// 这些不必关注
nav_core::warnRenamedParameter(private_nh, "max_vel_trans", "max_trans_vel");
nav_core::warnRenamedParameter(private_nh, "min_vel_trans", "min_trans_vel");
nav_core::warnRenamedParameter(private_nh, "max_vel_theta", "max_rot_vel");
nav_core::warnRenamedParameter(private_nh, "min_vel_theta", "min_rot_vel");
nav_core::warnRenamedParameter(private_nh, "acc_lim_trans", "acc_limit_trans");
nav_core::warnRenamedParameter(private_nh, "theta_stopped_vel", "rot_stopped_vel");
// 动态调整
dsrv_ = new dynamic_reconfigure::Server<DWAPlannerConfig>(private_nh);
dynamic_reconfigure::Server<DWAPlannerConfig>::CallbackType cb = boost::bind(&DWAPlannerROS::reconfigureCB, this, _1, _2);
dsrv_->setCallback(cb);

但是搜索发现,DWA的参数基本都在TrajectoryPlannerROS::initialize里加载。

computeVelocityCommands

根据机器人离目标是否足够接近,路径规划由dwa sampling或者stop and rotate负责。

先是全局路径的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 开始规划时再次获取位姿
if ( !costmap_ros_->getRobotPose(current_pose_))
{
ROS_ERROR("Could not get robot pose");
return false;
}

// TEB的部分全局路径也叫 transformed_plan,应该是因为下面的getLocalPlan参数也是这个名字
std::vector<geometry_msgs::PoseStamped> transformed_plan;
// 获取全局路径
if ( !planner_util_.getLocalPlan(current_pose_, transformed_plan) )
{
ROS_ERROR("Could not get local plan");
return false;
}

if(transformed_plan.empty())
{
ROS_WARN_NAMED("dwa_local_planner", "Received an empty transformed plan.");
return false;
}
ROS_DEBUG_NAMED("dwa_local_planner", "Received a transformed plan with %zu points.", transformed_plan.size());

上面的planner_util_.getLocalPlan的内容实际是:

1
2
3
4
5
6
7
8
9
10
11
12
base_local_planner::transformGlobalPlan(
*tf_,
global_plan_,
global_pose,
*costmap_,
global_frame_,
transformed_plan)

if(limits_.prune_plan)
{
base_local_planner::prunePlan(global_pose, transformed_plan, global_plan_);
}

涉及到了DWA的参数prune_plan,如果需要剪辑全局路径,就进入base_local_planner::prunePlan,此函数在goal_funtions.cpp,一看就是在namespace里。这里不如TEB的剪切全局路径复杂,其实就是把机器人当前位姿身后的部分剪掉,这部分是从路径起点到离位姿的欧氏距离的平方不到1米的点,这个1米已经写死,不能用参数更改,不过并不重要。

updatePlanAndLocalCosts

1
2
3
// update plan in dwa_planner even if we just stop and rotate, to allow checkTrajectory
dp_->updatePlanAndLocalCosts(current_pose_, transformed_plan,
costmap_ros_->getRobotFootprint() );
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
void DWAPlanner::updatePlanAndLocalCosts(
const geometry_msgs::PoseStamped& global_pose,
const std::vector<geometry_msgs::PoseStamped>& new_plan,
const std::vector<geometry_msgs::Point>& footprint_spec)
{
global_plan_.resize(new_plan.size());
for (unsigned int i = 0; i < new_plan.size(); ++i)
{
global_plan_[i] = new_plan[i];
}

obstacle_costs_.setFootprint(footprint_spec);

// costs for going away from path
path_costs_.setTargetPoses(global_plan_);

// costs for not going towards the local goal as much as possible
goal_costs_.setTargetPoses(global_plan_);

// alignment costs
geometry_msgs::PoseStamped goal_pose = global_plan_.back();

Eigen::Vector3f pos(global_pose.pose.position.x, global_pose.pose.position.y, tf2::getYaw(global_pose.pose.orientation));
double sq_dist =
(pos[0] - goal_pose.pose.position.x) * (pos[0] - goal_pose.pose.position.x)
+
(pos[1] - goal_pose.pose.position.y) * (pos[1] - goal_pose.pose.position.y);

// we want the robot nose to be drawn to its final position
// (before robot turns towards goal orientation), not the end of the
// path for the robot center. Choosing the final position after
// turning towards goal orientation causes instability when the
// robot needs to make a 180 degree turn at the end
std::vector<geometry_msgs::PoseStamped> front_global_plan = global_plan_;
double angle_to_goal = atan2(goal_pose.pose.position.y - pos[1], goal_pose.pose.position.x - pos[0]);

front_global_plan.back().pose.position.x = front_global_plan.back().pose.position.x +
forward_point_distance_ * cos(angle_to_goal);
front_global_plan.back().pose.position.y = front_global_plan.back().pose.position.y + forward_point_distance_ *
sin(angle_to_goal);

goal_front_costs_.setTargetPoses(front_global_plan);

// keeping the nose on the path
if (sq_dist > forward_point_distance_ * forward_point_distance_ * cheat_factor_)
{
alignment_costs_.setScale(path_distance_bias_);
// costs for robot being aligned with path (nose on path, not ju
alignment_costs_.setTargetPoses(global_plan_);
}
else
{
// once we are close to goal, trying to keep the nose close to anything destabilizes behavior.
alignment_costs_.setScale(0.0);
}
}

Eigen(二) 旋转,平移,特征值

旋转

有旋转向量、四元数和旋转矩阵三种表示方法,在Eigen里用前两种进行定义,这样参数少,可以转化为第三种。

牢记下面这张图,根据《视觉SLAM十四讲》总结:
三种方式的转换

使用的是模板类:class Eigen::AngleAxis< Scalar >,这被称为角轴表示法,顾名思义给定旋转角度和旋转轴即可。其创建方式符合Eigen的原则,传入数据类型作为模板参数。旋转角度以弧度表示,旋转轴为Vector类型的向量,注意向量必须要被归一化(vec.normalized()即可)。Eigen中也预先定义好了AngleAxisdAngleAxisf两种方便使用。

参考:基于eigen实现欧拉角(RPY), 旋转矩阵, 旋转向量, 四元数之间的变换

1
2
3
4
5
6
7
8
9
Vector3f v1 = Vector3f::UnitX();

AngleAxisf aa(45*3.1415/180, v1);
// 返回弧度制表示的旋转角度 和 旋转轴(列向量)
cout << aa.angle() <<" "<<endl<< aa.axis() <<endl;
cout << aa.toRotationMatrix()<<endl; // 转换为3 × 3的旋转矩阵

// fromRotationMatrix():从一个3×3的旋转矩阵构建AngleAxis对象。也就是提取旋转矩阵所表示的旋转角
// inverse(): 返回以AngleAxis表示的当前旋转的逆旋转

Quaternion是四元数表示法,Quaternion的构造是标准Eigen格式:数据类型作为模板参数+构造参数,而且重载了多个构造函数,因此可以方便地从角轴、旋转矩阵等数据类型进行构造。

Eigen提供了Quaternionf和Quaterniond方便使用。Quaterniond的初始化:

1
2
3
4
5
Eigen::Quaterniond q1(x, y, z, w);

Eigen::Quaterniond q2(Vector4d(x, y, z, w));

Eigen::Quaterniond q2(Matrix3d(R));

在Quaternion内部的保存中,虚部在前,实部在后

如果以第一种方式构造四元数,则实部是x,虚部是[y,z,w]。 对于第二种方式,则实部是w,虚部是[x,y,z]; 对于第三种方式,则是用3x3的旋转矩阵初始化四元数。

所以最常用是 Eigen::Quaterniond q1(w, x, y, z); ,四个数的传入顺序是w、x、y、z,对应w+xi+yj+zk

Eigen::Quaterniond 不能使用cout

  • Identity(): 返回一个表示单位旋转的四元数

  • w(): 返回四元数中的w分量。 同理 x(), y(), z()是返回相应的分量。

  • coeffs(): 返回四元数的四个数,可以对其进行索引[]获取值。 需要注意的是返回顺序是x、y、z、w,和构造函数的不同

  • toRotationMatrix() 将当前Quaternion对象转换为3×3的旋转矩阵。另外,没有fromRotationMatrix()函数,只能用构造函数传入旋转矩阵

  • slerp():对两个四元数进行球面线性插值(Spherical linear interpolation,通常简称Slerp),是四元数的一种线性插值运算,主要用于在两个表示旋转的四元数之间平滑差值。

欧拉角 —— 旋转矩阵

可以让三个AngleAxis相乘:

1
2
3
4
5
6
// 参数顺序和 static_transform_publisher 的一致
Eigen::Vector3f angle(roll, pitch, yaw);
Eigen::Matrix3d rotation;
rotation = Eigen::AngleAxisd(angle[0], Eigen::Vector3d::UnitZ()) *
Eigen::AngleAxisd(angle[1], Eigen::Vector3d::UnitY()) *
Eigen::AngleAxisd(angle[2], Eigen::Vector3d::UnitX()) ;

这样计算的结果和每个AngleAxisf转换成旋转矩阵后再相乘得到的结果是相同的

旋转矩阵转欧拉角的结果可能不是想要的,最好不使用

四元数 —— 旋转矩阵

其实这样更简单,我们可以直接从tf2_ros::Buffergeometry_msgs::TransformStamped直接获得四元数

1
2
3
4
5
6
Eigen::Quaterniond quaternion(transform.transform.rotation.w,
transform.transform.rotation.x,
transform.transform.rotation.y,
transform.transform.rotation.z
);
Eigen::Matrix3d rotation = quaternion.matrix();

平移

采用Eigen标准构造原则,构造示例如下:

1
2
3
4
Translation<float,2>(tx, ty)
Translation<float,3>(tx, ty, tz)
Translation<float,N>(s)
Translation<float,N>(vecN)

多个平移合并用乘号而不是加号,需要注意。和旋转相比,它的成员函数也相对少一些:

  • x():获取平移的x分量(可修改)

  • y():获取平移的y分量(可修改)

  • z():获取平移的z分量(可修改),注意,千万不要对一个二维的Translation获取z分量,否则直接会运行报错

  • vector()&.translation():返回当前平移的向量表示(可修改),可以索引[]获取各分量

1
2
3
4
5
Translation2d t1(1,4);
Translation2d t2(2,7);
Translation2d t3;
t3=t1*t2;
cout<<t3.translation()<<endl;

计算特征值与特征向量

构造一个EigenSolver,然后分别调用其成员函数.eigenvalues()、.eigenvectors()即可获得特征值与特征向量。

1
2
3
4
5
6
7
8
9
#include <Eigen/Eigenvalues>

A << 2,-2,0, -2,1,-2, 0,-2,0;
EigenSolver<Matrix3d> eigensolver(A);
if (eigensolver.info() == Success)
{
cout << eigensolver.eigenvalues() << endl<<endl;
cout << eigensolver.eigenvectors() << endl;
}

输出结果并不是我们平时熟悉的形式,而是

1
2
3
4
5
6
7
(4,0)
(1,0)
(-2,0)

(-0.666667,0) (-0.666667,0) (-0.333333,0)
(0.666667,0) (-0.333333,0) (-0.666667,0)
(-0.333333,0) (0.666667,0) (-0.666667,0)

实际特征值是4,1,-2. 4对应的特征向量分别为(-0.666667, -0.666667,-0.333333)

参考:
C++ Eigen库计算矩阵特征值及特征向量
Eigen函数与Matlab对应关系


彻底解决 sudo rosdep init 和 rosdep update失败问题

如果sudo rosdep initrosdep update没有执行成功,有些ROS功能会无法执行,比如rqt_tf_tree会报错:

1
2
the rosdep view is empty: call 'sudo rosdep init' and 'rosdep update'
[WARN] wait_for_service(/rqt_gui_py_node_26204/tf2_frames): failed to contact, will keep trying

这两个老大难其实都是网络的问题,sudo rosdep init的目的是为了在/etc目录下新建/ros/rosdep/sources.list.d/20-default.list文件。

小鱼基于rosdep源码制作了rosdepc,专门服务国内ROS用户,使用pip3 和 rosdepc 可以彻底消灭这个问题

1
2
3
4
5
sudo apt-get install python3-pip 
sudo pip3 install rosdepc

sudo rosdepc init
rosdepc update


rosdep update根据20-default.list文件中的网址链接去下载相应的文件,最终在/etc/ros/rosdep下放5个文件,分别是base.yaml,gentoo.yaml,osx-homebrew.yaml,python.yaml,ruby.yaml. 但又做了一些配置,所以不是下载文件能解决的。

以下步骤先从网上下载rosdistro文件夹,放到/etc/ros

执行sudo rosdep init后报错

1
2
ERROR: cannot download default sources list from:
https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/sources.list.d/20-default.list Website may be down

从网上直接下载这个文件,然后放到/etc/ros/rosdep/sources.list.d,再修改如下:

1
2
3
4
5
6
7
8
9
10
11
# os-specific listings first

yaml file:///etc/ros/rosdistro/rosdep/osx-homebrew.yaml osx

# generic
yaml file:///etc/ros/rosdistro/rosdep/base.yaml
yaml file:///etc/ros/rosdistro/rosdep/python.yaml
yaml file:///etc/ros/rosdistro/rosdep/ruby.yaml
gbpdistro file:///etc/ros/rosdistro/releases/fuerte.yaml fuerte

# newer distributions (Groovy, Hydro, ...) must not be listed anymore, they are being fetched from the rosdistro index.yaml instead

rosdep update 报错

kinetic 要修改python2.7的路径, melodic和noetic要修改python3 ,当然另一种方法是从其他电脑拷贝已经生成的文件。

kinetic 的修改

1
2
reading in sources list data from /etc/ros/rosdep/sources.list.d
ERROR: unable to process source

对下面三个文件做修改
1
2
3
4
5
6
7
8
9
10
/usr/lib/python2.7/dist-packages/rosdep2/gbpdistro_support.py
修改: FUERTE_GBPDISTRO_URL = 'file:///etc/ros/rosdistro/releases/fuerte.yaml'


/usr/lib/python2.7/dist-packages/rosdep2/rep3.py
修改: REP3_TARGETS_URL = 'file:///etc/ros/rosdistro/releases/targets.yaml'


/usr/lib/python2.7/dist-packages/rosdistro/__init__.py
修改: DEFAULT_INDEX_URL = 'file:///etc/ros/rosdistro/index-v4.yaml'

启动新终端,再次执行rosdep update

melodic和noetic 的修改:clone代码仓https://github.com/ros/rosdistro到本地,并更改其文件rosdep/sources.list.d/20-default.list,将其url改成本地文件路径,内容类似如下:

1
2
3
4
5
6
7
8
# os-specific listings first
yaml file:///home/baby/rosdistro/rosdep/osx-homebrew.yaml osx

# generic
yaml file:///home/baby/rosdistro/rosdep/base.yaml
yaml file:///home/baby/rosdistro/rosdep/python.yaml
yaml file:///home/baby/rosdistro/rosdep/ruby.yaml
gbpdistro file:///home/baby/rosdistro/releases/fuerte.yaml fuerte

由于rosdep使用python3,可直接该动源码。我们需要改动三个文件:

  • /usr/lib/python3/dist-packages/rosdep2/sources_list.py,修改 DEFAULT_SOURCES_LIST_URL = 'file:///home/baby/rosdistro/rosdep/sources.list.d/20-default.list'

  • /usr/lib/python3/dist-packages/rosdep2/rep3.py,修改 REP3_TARGETS_URL = 'file:///home/baby/rosdistro/releases/targets.yaml'

  • /usr/lib/python3/dist-packages/rosdep2/gbpdistro_support.py,修改 FUERTE_GBPDISTRO_URL = 'file:///home/baby/rosdistro/releases/fuerte.yaml'

  • /usr/lib/python3/dist-packages/rosdistro/__init__.py,修改 DEFAULT_INDEX_URL = 'file:///home/baby/rosdistro/index-v4.yaml'

完成后先把/etc/ros/rosdep/sources.list.d/20-default.list删除,再执行sudo rosdep initrosdep update就可以成功了。

参考:
解决 rosdep update


gtsam

安装

下载gtsam源码: git clone https://bitbucket.org/gtborg/gtsam.git

依赖:

1
2
Boost >= 1.43 (Ubuntu: sudo apt-get install libboost-all-dev)
CMake >= 3.0 (Ubuntu: sudo apt-get install cmake)

安装可选的依赖:

  • Intel Threaded Building Blocks (TBB)

sudo apt-get install libtbb-dev

  • Intel Math Kernel Library (MKL)

https://software.intel.com/content/www/us/en/develop/articles/installing-intel-free-libs-and-python-apt-repo.html

编译

1
2
3
4
mkdir build
cd build
cmake ..
make install

ubuntu的显卡及驱动

ubuntu-drivers devices查看显卡硬件型号,也就是其中的model。 如果同意安装推荐版本,那我们只需要终端输入:sudo ubuntu-drivers autoinstall 就可以自动安装了。
当然我们也可以使用 apt 命令安装自己想要安装的版本,比如我想安装 340 这个版本号的版本,终端输入:sudo apt install nvidia-340 就自动安装了。

lshw -numeric -C display | grep product也可以查看电脑显卡的十六进制ID

驱动安装

电脑装好好,启动一直失败,报错failed to start User Manager for UID 121,连文本界面都进不去,后来才知道是ubuntu下缺少显卡驱动

先完全卸载之前安装的显卡驱动

ppa源文件卸载:sudo apt-get remove --purge nvidia*

runfile源文件卸载:sudo ./NVIDIA-Linux-x86_64-384.59.run --uninstall

ppa源驱动安装

先查询电脑最适合的显卡驱动版本,命令 ubuntu-drivers devices,这个命令不仅适用于显卡

最佳显卡驱动版本为 nvidia-driver-435, 用命令行进行安装

1
2
3
4
sudo add-apt-repository ppa:graphics-drivers/ppa
sudo apt-get update
sudo apt-get install nvidia-driver-435 #此处数字要对应上面查询到的版本号
sudo apt-get install mesa-common-dev

如果前面没有禁用secure boot,则在安装过程中会提示设置一个密码,在重启时需要输入密码验证以禁用secure boot,重启后会出现蓝屏,这时候不能直接选择continue, 而应该按下按键,选择Enroll MOK, 确认后在下一个选项中选择continue,接着输入安装驱动时设置的密码,开机。

安装完成后重启,nvidia-smi命令显示驱动信息,若出现驱动版本,就是成功

官网驱动下载

到 NVIDIA 的官网下载相应型号的驱动,官网地址
操作系统那里,一定选择Linux 64-bit

需要先安装一些 NVIDIA 显卡依赖的软件,在终端依次执行如下命令:

1
2
3
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install build-essential libc6:i386

Ubuntu 系统默认安装好是使用的一个开源的驱动:nouveau,我们需要先禁用这个开源驱动,方法如下,依次执行:
1
2
sudo bash -c "echo blacklist nouveau > /etc/modprobe.d/blacklist-nvidia-nouveau.conf"
sudo bash -c "echo options nouveau modeset=0 >> /etc/modprobe.d/blacklist-nvidia-nouveau.conf"

重启一下系统,运行之前下载的驱动。可能会出现下面的信息:
1
2
3
The distribution-provided pre-install script failed!
Are you sure you want to continue? -> CONTINUE INSTALLATION
Would you like to run the nvidia-xconfig utility? -> YES

安装完成后重启系统就可以点击软件列表中的 NVIDIA 的配置软件配置显卡驱动了,如果你遇到
1
WARNING: Unable to find suitable destination to install 32-bit compatibility libraries

解决办法:
1
2
3
sudo dpkg --add-architecture i386
sudo apt update
sudo apt install libc6:i386