首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

dwa_local_planner

dwa_local_planner中的DWAPlannerROS是amcl_demo默认的局部规划器。 它是对nav_core中用虚类BaseLobalPlanner所定义的接口的实现。DWAPlannerROS只是一个针对move_base框架的封装,具体的实现由DWAPlanner完成。 DWAPlanner使用一种称为动态窗口路径(Dynamic Window Approach)的方法进行局部的路径规划, 跟踪全局规划器输出的全局轨迹,依据机器人的动态特性,和周围障碍物特征,生成实际控制机器人运动的速度指令。

DWA算法的基本思想是,在机器人的控制空间(线速度、角速度指令)中离散地采样,对于每个样本指令,在机器人当前状态和地图信息的基础上,推演未来很短的一段时间内的运动轨迹。 根据发生碰撞的可能性、目标点的接近程度、全局轨迹的跟踪近似度、速度限制等多方面的指标评价这些轨迹并打分,选取得分最高的轨迹,将其对应的指令下发给底盘,控制机器人运动。

实际上DWAPlanner也不是DWA算法的具体实现,它只是包base_local_planner一个封装而已。 本文分析DWAPlannerROS和DWAPlanner相关的源码,旨在了解DWA算法运行的基本过程,以及融合到move_base框架下的必要操作。在详细分析这两个类之前,让我们先来看一下BaseLobalPlanner的定义。

1. 接口——BaseLocalPlanner

BaseLobalPlanner是move_base获取局部规划能力的接口, 它是定义在包nav_core的头文件base_lobal_planner.h中的一个虚类, 其类定义如下:

        class BaseLobalPlanner {
        public:
            virtual bool computeVelocityCommands(geometry_msgs::Twist& cmd_vel) = 0;
            virtual bool isGoalReached() = 0;
            virtual bool setPlan(const std::vector<geometry_msgs::PoseStamped>& plan) = 0;
            virtual void initialize(std::string name, tf2_ros::Buffer* tf, costmap_2d::Costmap2DROS* costmap_ros) = 0;
            virtual ~BaseLocalPlanner(){}
        protected:
            BaseLocalPlanner(){}
        };

从类定义可以看到,BaseLocalPlanner中定义了四个接口函数:

2. 封装——DWAPlannerROS

DWAPlannerROS是接口BaseLobalPlanner的一种实现,是对局部规划器DWAPlanner的封装。下面是DWAPlannerROS的类定义片段,可以看到它继承自BaseLobalPlanner,并实现了上述的四个接口函数。

        class DWAPlannerROS : public nav_core::BaseLocalPlanner {
        public:
            void initialize(std::string name, tf2_ros::Buffer* tf, costmap_2d::Costmap2DROS* costmap_ros);
            bool computeVelocityCommands(geometry_msgs::Twist& cmd_vel);
            bool setPlan(const std::vector<geometry_msgs::PoseStamped>& orig_global_plan);
            bool isGoalReached();
            // 省略其它成员
        };

2.1 初始化

下面是DWAPlannerROS的初始化函数,它有三个参数。其中name是规划器的名称,tf是系统坐标变换器, costmap_ros是进行局部路径规划需要参考的代价地图对象。tf有tf和tf2两个库,两者的API有比较大的差异,ROS官方希望通过tf2替代tf。在18年7月份发布的1.16版的navigation之后就改用tf2了。

        void DWAPlannerROS::initialize(std::string name, tf2_ros::Buffer* tf, costmap_2d::Costmap2DROS* costmap_ros) {
            if (!isInitialized()) {
                // 初始化
                initialized_ = true;
            } else
                ROS_WARN("This planner has already been initialized, doing nothing.");
        }

在初始化时,首先构建一个ROS句柄,在根据此句柄发布主题"global_plan"和"local_plan"。

        ros::NodeHandle private_nh("~/" + name);
        g_plan_pub_ = private_nh.advertise("global_plan", 1);
        l_plan_pub_ = private_nh.advertise("local_plan", 1);

接下来,用DWAPlannerROS的成员变量记录下坐标变换对象tf,代价地图costmap_ros_。并据此构建了规划器辅助对象planner_util_, 这是一个定义在包base_local_planner中的类LocalPlannerUtil,主要用于维护局部规划器的各种限制参数。 dp_则是实际的DWA规划器对象,它提供了局部规划算法。

        tf_ = tf;
        costmap_ros_ = costmap_ros;
        costmap_ros_->getRobotPose(current_pose_);
        costmap_2d::Costmap2D* costmap = costmap_ros_->getCostmap();
        planner_util_.initialize(tf, costmap, costmap_ros_->getGlobalFrameID());
        dp_ = boost::shared_ptr<DWAPlanner>(new DWAPlanner(name, &planner_util_));

下面,通过ROS句柄对象,获取里程计主题,并设置里程计辅助类对象odom_helper_。 它是一个在包base_local_planner中定义的辅助类OdometryHelperRos。

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

然后,标记已经完成初始化。源码中还有一些关于参数重命名和参数动态配置的Waring和初始化操作,它们与路径规划的业务流程无关,这里略去。

        initialized_ = true;

2.2 业务逻辑

move_bae通过接口computeVelocityCommands进行局部路径规划,下面代码片段是DWAPlannerROS的实现。它有一个参数cmd_vel,这是一个输出参数,用于输出局部规划器计算的机器人控制指令。

        bool DWAPlannerROS::computeVelocityCommands(geometry_msgs::Twist& cmd_vel) {

在函数一开始先通过代价地图对象获取机器人的位姿。

            if (!costmap_ros_->getRobotPose(current_pose_)) {
                ROS_ERROR("Could not get robot pose");
                return false;
            }

然后通过planner_util_对象的getLocalPlan接口,在全局轨迹中截取从当前位置到终点之间的路径,用局部变量transformed_plan记录。getLocalPlan除了截取剩余轨迹之外, 还会将全局轨迹点转换到代价地图的坐标系下。所以我猜这个局部变量的前缀transformed就是用来描述这一特性的。

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

更新规划器的轨迹和局部代价。

            dp_->updatePlanAndLocalCosts(current_pose_, transformed_plan, costmap_ros_->getRobotFootprint());

然后通过辅助对象latchedStopRotateController_判定是否到达了终点。 这个辅助对象是包base_local_planner中定义的辅助类LatchedStopRotateController。 如果我们到达了终点,就发布空路径,同时通过辅助对象控制机器人停下来。

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

没有到达终点,我们就通过成员函数dwaComputeVelocityCommands计算速度控制指令,并发布路径。

            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 empty_plan;
                    publishGlobalPlan(empty_plan);
                }
                return isOk;
            }
        }

2.3 局部路径规划

DWAPlannerROS通过成员函数dwaComputeVelocityCommands完成局部的路径规划。该函数有两个参数,通过参数global_pose输入机器人当前位姿,通过cmd_vel是输出机器人速度控制指令。

        bool DWAPlannerROS::dwaComputeVelocityCommands(geometry_msgs::PoseStamped &global_pose, geometry_msgs::Twist& cmd_vel) {

首先确认局部规划器已经初始化了,若否则报错退出。

            if(!isInitialized()){
                ROS_ERROR("This planner has not been initialized, please call initialize() before using this planner");
                return false;
            }

然后根据里程计获取机器人当前的运动速度。

            geometry_msgs::PoseStamped robot_vel;
            odom_helper_.getRobotVel(robot_vel);

接着,通过规划器dp_进行仿真并筛选出最优的局部路径。findBestPath调用结束之后,drive_cmds中将记录机器人的速度控制指令。该函数还将返回局部路径,这里将之记录在局部变量path中。

            geometry_msgs::PoseStamped drive_cmds;
            drive_cmds.header.frame_id = costmap_ros_->getBaseFrameID();
            base_local_planner::Trajectory path = dp_->findBestPath(global_pose, robot_vel, drive_cmds);

最后更新控制指令drive_cmds到输出参数cmd_vel中。

            cmd_vel.linear.x = drive_cmds.pose.position.x;
            cmd_vel.linear.y = drive_cmds.pose.position.y;
            cmd_vel.angular.z = tf2::getYaw(drive_cmds.pose.orientation);

仅就机器人的控制而言,更新了cmd_vel,故事就已经结束了。下面的这些操作,只是在ROS的环境下发布局部的运动轨迹。局部变量path的成员变量cost_记录了局部路径的代价,如果它是一个负值, 表示没有规划出一条合理的局部轨迹,我们将发布一条空路径。

            std::vector<geometry_msgs::PoseStamped> local_plan;
            if(path.cost_ < 0) {
                local_plan.clear();
                publishLocalPlan(local_plan);
                return false;
            }

下面根据path的数据填充局部轨迹消息并发布。

            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);
        
                geometry_msgs::PoseStamped p;
                p.header.frame_id = costmap_ros_->getGlobalFrameID();
                p.header.stamp = ros::Time::now();
                p.pose.position.x = p_x;
                p.pose.position.y = p_y;
                p.pose.position.z = 0.0;
                tf2::Quaternion q;
                q.setRPY(0, 0, p_th);
                tf2::convert(q, p.pose.orientation);
                local_plan.push_back(p);
            }
            publishLocalPlan(local_plan);
            return true;
        }

对象名称数据类型说明
oscillation_costs_OscillationCostFunction尽量降低机器人在原地晃动的情况。
obstacle_costs_ObstacleCostFunction防止机器人撞到障碍物上。
path_costs_MapGridCostFunction使机器人尽可能的贴近全局轨迹。
goal_costs_MapGridCostFunction更倾向于选择接近目标点的轨迹。
goal_front_costs_MapGridCostFunction尽可能的让机器人朝向局部的nose goal。
alignment_costs_MapGridCostFunction尽可能的让机器人保持在nose path上。
twirling_costs_TwirlingCostFunction尽量不让机器人原地打转。

3. 实现——DWAPlanner

DWAPlanner并不具体实现局部路径规划算法。它使用包base_local_planner中的SimpleTrajectoryGenerator来生成预测轨迹, 并为预测轨迹定义了一系列的评价标准。实际的规划则是由SimpleScoredSamplingPlanner进行的。

右边是它所用的各个评价对象,它们都是在base_local_planner中实现的,具有相同的基类TrajectoryCostFunction。

3.1 构造函数和初始化

下面是DWAPlanner的构造函数,它有两个参数。其中name是规划器的名称,根据它我们可以从ROS环境中获取一些关于规划器的配置。planner_util则是规划器辅助对象, 它为规划器提供了代价地图、坐标变换、全局规划等算法输入。并为一些成员变量赋予了初值。

        DWAPlanner::DWAPlanner(std::string name, base_local_planner::LocalPlannerUtil *planner_util)  :
            planner_util_(planner_util),
            obstacle_costs_(planner_util->getCostmap()),
            path_costs_(planner_util->getCostmap()),
            goal_costs_(planner_util->getCostmap(), 0.0, 0.0, true),
            goal_front_costs_(planner_util->getCostmap(), 0.0, 0.0, true),
            alignment_costs_(planner_util->getCostmap()) {

首先,我们构建ROS句柄,并据此获取相关的运行参数。

            ros::NodeHandle private_nh("~/" + name);
            private_nh.param("global_frame_id", frame_id_, std::string("odom"));
            // 省略其它参数获取的语句

然后构建一些可视化的消息发布器和对象,它们主要用于在rviz工具中显示轨迹信息和潜在的代价,不必深究。

            traj_cloud_pub_ = private_nh.advertise<sensor_msgs::PointCloud2>("trajectory_cloud", 1);
            map_viz_.initialize(name, planner_util->getGlobalFrame(), 
                        boost::bind(&DWAPlanner::getCellCosts, this, _1, _2, _3, _4, _5, _6));

接下来设置一些必要的准则对象属性:

            goal_front_costs_.setStopOnFailure( false );
            alignment_costs_.setStopOnFailure( false );
            oscillation_costs_.resetOscillationFlags();
            obstacle_costs_.setSumScores(sum_scores);

并一个vector容器装载所有的准则对象:

            std::vector<base_local_planner::TrajectoryCostFunction*> critics;
            critics.push_back(&oscillation_costs_); // discards oscillating motions (assisgns cost -1)
            critics.push_back(&obstacle_costs_); // discards trajectories that move into obstacles
            critics.push_back(&goal_front_costs_); // prefers trajectories that make the nose go towards (local) nose goal
            critics.push_back(&alignment_costs_); // prefers trajectories that keep the robot nose on nose path
            critics.push_back(&path_costs_); // prefers trajectories on global path
            critics.push_back(&goal_costs_); // prefers trajectories that go towards (local) goal, based on wave propagation
            critics.push_back(&twirling_costs_); // optionally prefer trajectories that don't spin

构建轨迹生成器列表:

            std::vector<base_local_planner::TrajectorySampleGenerator*> generator_list;
            generator_list.push_back(&generator_);

最后,根据准则对象列表和轨迹生成器列表,构建局部规划器:

            scored_sampling_planner_ = base_local_planner::SimpleScoredSamplingPlanner(generator_list, critics);
        }

3.2 更新轨迹和局部代价

在分析DWAPlannerROS的源码时,我们看到在生成速度控制指令,发布局部轨迹之前,先调用了DWAPlanner的成员函数updatePlanAndLocalCosts更新轨迹和局部代价。 下面是该函数的代码片段,它有三个参数。其中global_pose是当前机器人在全局坐标系下的位姿,new_plan则是在DWAPlannerROS中截断后的全局剩余路径, footprint_spec则是由代价地图提供的机器人脚印。

        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);
            path_costs_.setTargetPoses(global_plan_);
            goal_costs_.setTargetPoses(global_plan_);

接着计算当前机器人到终点的距离,和方向角偏差。

            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);
            double angle_to_goal = atan2(goal_pose.pose.position.y - pos[1], goal_pose.pose.position.x - pos[0]);

沿着朝向终点的方向延长forward_point_distance_,更新准则对象goal_front_costs_。forward_point_distance_是DWAPlanner的一个配置参数。

            std::vector<geometry_msgs::PoseStamped> front_global_plan = global_plan_;
            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);

最后更新alignment_costs_。

            if (sq_dist > forward_point_distance_ * forward_point_distance_ * cheat_factor_) {
                    alignment_costs_.setScale(pdist_scale_);
                    // 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);
                }
            }
        }

3.3 获取最优路径

更新了剩余路径和准则对象之后,我们就可以通过findBestPath获取一条较优的局部路径,并输出速度控制指令。下面是该函数的代码片段,它有三个参数。 其中global_pose和global_vel是两个输入量,分别描述了当前机器人的全局位姿和速度。drive_velocities则是一个输出参数,在函数退出之后,它将记录速度控制指令。 该函数的返回值是一条在各个准则对象综合评价下的最优路径。

在函数的一开始,对信号量configuration_mutex_加锁。这是为了防止move_base的动态配置参数的特性,与规划过程冲突。

        base_local_planner::Trajectory DWAPlanner::findBestPath(const geometry_msgs::PoseStamped& global_pose,
                    const geometry_msgs::PoseStamped& global_vel, geometry_msgs::PoseStamped& drive_velocities) {
            boost::mutex::scoped_lock l(configuration_mutex_);

下面构建三个Eigen的向量对象pos, vel, goal,分别记录了机器人当前的位姿、速度和目标点。 同时用一个局部变量limits记录机器人的各种运动学限制。

            Eigen::Vector3f pos(global_pose.pose.position.x, global_pose.pose.position.y, tf2::getYaw(global_pose.pose.orientation));
            Eigen::Vector3f vel(global_vel.pose.position.x, global_vel.pose.position.y, tf2::getYaw(global_vel.pose.orientation));
            geometry_msgs::PoseStamped goal_pose = global_plan_.back();
            Eigen::Vector3f goal(goal_pose.pose.position.x, goal_pose.pose.position.y, tf2::getYaw(goal_pose.pose.orientation));
            base_local_planner::LocalPlannerLimits limits = planner_util_->getCurrentLimits();

根据这些临时变量初始化轨迹生成器generator_。其中参数vsamples_是DWAPlanner的一个成员变量Eigen::Vector3f vsamples_, 它的三个元素分别记录了x,y方向上和转角的指令采样数量。

            generator_.initialise(pos, vel, goal, &limits, vsamples_);

下面通过实际的规划器scored_sampling_planner_对控制指令采样,并从中找出最优的局部路径。result_traj_是DWAPlanner的一个成员变量,它将在如下的操作之后记录最优的局部路径。 而临时vector对象all_explored则记录了所有的样本路径。

            result_traj_.cost_ = -7;
            std::vector<base_local_planner::Trajectory> all_explored;
            scored_sampling_planner_.findBestTrajectory(result_traj_, &all_explored);

如果配置可视化轨迹样本和代价地图,则通过traj_cloud_pub_和map_viz_发布相关主题的消息,我们可以在rviz中订阅对应主题看到可视化的结果。

            if (publish_traj_pc_) {
                // 填充轨迹云消息
                traj_cloud_pub_.publish(traj_cloud);
            }
            if (publish_cost_grid_pc_) {
                 map_viz_.publishCostCloud(planner_util_->getCostmap());
            }

接着更新碰撞准则对象。

            oscillation_costs_.updateOscillationFlags(pos, &result_traj_, planner_util_->getCurrentLimits().min_vel_trans);

如果result_traj_的字段cost_是一个负数,意味着我们没有成功规划出一条局部路径,此时需要控制机器人停下来。否则根据最优样本的线速度和角速度控制量输出控制指令。

            if (result_traj_.cost_ < 0) {
                drive_velocities.pose.position.x = 0;
                drive_velocities.pose.position.y = 0;
                drive_velocities.pose.position.z = 0;
                drive_velocities.pose.orientation.w = 1;
                drive_velocities.pose.orientation.x = 0;
                drive_velocities.pose.orientation.y = 0;
                drive_velocities.pose.orientation.z = 0;
            } else {
                drive_velocities.pose.position.x = result_traj_.xv_;
                drive_velocities.pose.position.y = result_traj_.yv_;
                drive_velocities.pose.position.z = 0;
                tf2::Quaternion q;
                q.setRPY(0, 0, result_traj_.thetav_);
                tf2::convert(q, drive_velocities.pose.orientation);
            }
            return result_traj_;
        }

4. 完

本文中,我们分析了局部规划器DWAPlannerROS和DWAPlanner的源码,大体了解了其业务基本流程。真正的DWA算法在包base_local_planner中实现。 DWAPlannerROS提供了接入move_base框架的接口,DWAPlanner定义了评价轨迹优劣的准则对象。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1