【运动规划算法项目实战】路径规划中常用的抽稀算法(附ROS C++代码)

本文详细介绍了路径规划中的抽稀算法,包括Douglas-Peucker算法和垂距限值法。通过算法原理及ROS C++代码实现,阐述了如何在保持路径形状特征的同时减少点的数量,提高路径规划的效率和实时性。
摘要由CSDN通过智能技术生成


简介

抽稀(Simplification)算法是指在保持数据尽可能不失真的前提下,对数据进行精简处理,减少数据的数量,便于数据的存储和处理。路径规划中,抽稀算法的作用是将原始路径中的冗余点去除,从而减小路径点的数量,降低计算量和存储量,同时保留路径的形状特征和轨迹信息,确保路径规划的精度和实时性。常见的抽稀算法有 Douglas-Peucker算法和垂距限值法。


一、Douglas-Peucker算法

Douglas-Peucker算法是一种常用于抽稀(简化)线段的算法。其基本思想是在曲线中找到一些关键点,用这些点来近似曲线,从而实现简化。算法可以实现将原本复杂的曲线简化为一组少量的点,从而达到减少数据量、加快计算速度等目的。该算法常用于地图绘制、路径规划等领域。
在这里插入图片描述

1.1 算法原理

  1. 给定一条由n个点构成的曲线;
  2. 在曲线的起点和终点之间找到距离最远的点,将其加入简化后的点集合;
  3. 将曲线分成两段,对这两段分别执行步骤2;
  4. 递归地执行步骤2和步骤3,直到所有的点都被加入简化后的点集合。

1.2 代码实现

下面给出ROS中的实现,假设输入的原始路径数据类型是nav_msgs::Path(rviz中以绿色路径显示),抽稀处理后输出的路径也为数据类型是nav_msgs::Path(rviz中以红色路径显示)。

// 定义垂距的计算函数
double PerpendicularDistance(const geometry_msgs::Point& p1, const geometry_msgs::Point& p2, const geometry_msgs::Point& p3)
{
    // 计算垂距
    double numerator = std::abs((p3.y - p2.y) * (p2.x - p1.x) - (p2.y - p1.y) * (p3.x - p2.x));
    double denominator = std::sqrt(std::pow(p3.y - p2.y, 2) + std::pow(p3.x - p2.x, 2));
    return numerator / denominator;
}

// 定义Douglas-Peucker算法的主函数
nav_msgs::Path DouglasPeucker(const nav_msgs::Path& input_path, const double epsilon)
{
    // 创建输出路径对象并初始化
    nav_msgs::Path output_path;
    output_path.header = input_path.header;

    // 判断输入路径是否为空或只有一个点
    if (input_path.poses.size() <= 1) {
        output_path = input_path;
        return output_path;
    }

    // 找到输入路径中垂距最大的点
    double dmax = 0.0;
    int index = 0;

    for (int i = 1; i < input_path.poses.size() - 1; ++i) {
        double d = PerpendicularDistance(input_path.poses.front().pose.position,
                                          input_path.poses[i].pose.position,
                                          input_path.poses.back().pose.position);
        if (d > dmax) {
            index = i;
            dmax = d;
        }
    }

    // 如果最大垂距小于等于阈值,则直接返回输入路径
    if (dmax <= epsilon) {
        output_path = input_path;
        return output_path;
    }

    // 递归处理左右两部分路径
    nav_msgs::Path left_path;
    left_path.header = input_path.header;
    std::vector<geometry_msgs::PoseStamped>::const_iterator begin = input_path.poses.begin();
    std::vector<geometry_msgs::PoseStamped>::const_iterator middle = begin + index + 1;
    std::vector<geometry_msgs::PoseStamped>::const_iterator end = input_path.poses.end();
    left_path.poses.assign(begin, middle);
    left_path = DouglasPeucker(left_path, epsilon);

    nav_msgs::Path right_path;
    right_path.header = input_path.header;
    right_path.poses.assign(middle, end);
    right_path = DouglasPeucker(right_path, epsilon);

    // 将左右两部分路径拼接到一起
    output_path.poses.reserve(left_path.poses.size() + right_path.poses.size() - 1);
    output_path.poses.insert(output_path.poses.end(), left_path.poses.begin(), left_path.poses.end());
    output_path.poses.insert(output_path.poses.end(), right_path.poses.begin() + 1, right_path.poses.end());

    return output_path;
}

代码说明:
首先定义了计算垂距的函数perpendicularDistance,该函数用于计算一个点到一条直线的垂距。这个函数在后面的算法中会被多次使用。

然后定义了Douglas-Peucker算法的主函数douglasPeucker,该函数接收一个路径对象和一个阈值epsilon作为输入,并返回一个简化后的路径对象。

在函数内部,首先创建一个输出路径对象,并初始化为输入路径对象的header。

接着判断输入路径是否为空或只有一个点,如果是,则直接返回输入路径。

如果输入路径不为空且有多于一个点,则在路径中找到垂距最大的点,将这个点的索引值存储在变量index中,并将垂距存储在变量dmax中。垂距最大的点将成为简化后路径的一个关键点。

如果最大垂距小于等于阈值epsilon,则认为路径已经足够简化,直接返回输入路径。

如果最大垂距大于阈值,则将输入路径分成两部分,左边一部分包括第一个点到关键点,右边一部分包括关键点到最后一个点。

对左边一部分路径递归调用Douglas-Peucker算法,并将结果存储在变量left_path中。
对右边一部分路径递归调用Douglas-Peucker算法,并将结果存储在变量right_path中。

将左右两部分路径拼接到一起,生成简化后的路径对象,并返回最终简化后的路径。

rviz中的输出结果如下(threshold值为1.0,阈值自行设定):

在这里插入图片描述
总体来说,Douglas-Peucker算法通过不断寻找路径上垂距最大的点,并将路径分成两部分,递归地对每一部分进行简化,最终生成简化后的路径。


二、垂距限值法

垂距限值法是一种简单的轨迹平滑算法,用于将给定的轨迹进行简化,从而减少轨迹的噪声,并减少机器人的运动轨迹。该算法的基本思想是对轨迹上的每个点进行垂线投影,如果垂线的长度小于某个阈值,则认为该点是不必要的,可以将其删除,从而实现轨迹的简化。

2.1 算法原理

  1. 以第一个点开始,计算第一个点到前一个点和后一个点所在直线的垂距;
  2. 如果垂距小于阈值,则保留该点,并将其作为下一个点计算;
  3. 如果垂距大于阈值,则舍弃该点,并将下一个点作为当前点重新进行计算;
  4. 依次类推,直到计算完最后一个点。

2.2 代码实现

下面给出ROS中的实现,假设输入的原始路径数据类型是nav_msgs::Path(rviz中以绿色路径显示),抽稀处理后输出的路径也为数据类型是nav_msgs::Path(rviz中以红色路径显示)。

// 计算垂距
double DistanceToLine(const geometry_msgs::Point& p1, const geometry_msgs::Point& p2, const geometry_msgs::Point& p3)
{
    double numerator = abs((p3.y - p2.y) * (p2.x - p1.x) - (p2.y - p1.y) * (p3.x - p2.x));
    double denominator = sqrt(pow(p3.y - p2.y, 2) + pow(p3.x - p2.x, 2));
    return numerator / denominator;
}

// 垂距限值法函数
nav_msgs::Path PerpendicularDistanceThreshold(const nav_msgs::Path& input_path, const double threshold)
{
    nav_msgs::Path output_path;
    int input_size = input_path.poses.size();

    if (input_size <= 2) {
        output_path = input_path;
        return output_path;
    }

    int index = 0; // 记录当前点在input_path中的索引
    output_path.poses.push_back(input_path.poses[index]); // 将第一个点添加到输出路径中
    output_path.header = input_path.header;

    for (int i = 1; i < input_size - 1; ++i) {
        double distance = DistanceToLine(input_path.poses[index].pose.position,
                                                 input_path.poses[i].pose.position,
                                                 input_path.poses[i + 1].pose.position);
        if (distance > threshold) {
            output_path.poses.push_back(input_path.poses[i]); // 若垂距大于阈值,则保留该点
            index = i; // 更新当前点的索引
        }
    }

    output_path.poses.push_back(input_path.poses[input_size - 1]); // 将最后一个点添加到输出路径中

    return output_path;
}

代码说明:
这段代码定义了两个函数分别是DistanceToLine函数和PerpendicularDistanceThreshold函数。

DistanceToLine: 该函数计算三个点构成的线段与垂线之间的距离(即垂距)。函数参数为三个 geometry_msgs::Point 类型的点,返回值为计算得到的垂距值。

PerpendicularDistanceThreshold: 该函数简化一个 nav_msgs::Path 类型的路径,将路径上的一些点进行保留或删除,使得最终的路径更加简洁。函数参数为输入路径 input_path 和简化阈值 threshold,其中简化阈值为一个 double 类型的数值。函数返回值为简化后的路径 output_path。

在 PerpendicularDistanceThreshold函数中,先对输入路径进行一些特殊情况的处理,如路径长度小于等于2时直接返回输入路径。对于长度大于2的路径,则遍历路径上除第一个和最后一个点之外的所有点,计算每个点与其前后两个点构成的线段的垂距,若垂距小于等于阈值,则保留该点,否则删除该点。最终返回简化后的路径。

rviz中的输出结果如下(threshold值为1.0,阈值自行设定):
在这里插入图片描述


总结

本篇文章介绍了在路径规划中常用的抽稀算法,包括Douglas-Peucker算法和垂距限值法算法。这两种算法都是通过对原始路径进行简化,来提高路径规划的效率。

在实际的路径规划中,原始路径可能非常复杂,包含大量的点。使用这些点进行路径规划不仅效率低下,而且也可能导致机器人在行进过程中出现偏差。因此,抽稀算法可以有效地减少原始路径中的点数,使路径更加简单明了,提高路径规划的速度和准确性。

Douglas-Peucker算法是一种经典的抽稀算法,它通过将原始路径分段处理,递归地对每个线段进行抽稀。通过调整递归深度和垂距限制值,可以获得不同程度的抽稀效果。垂距限值法算法则是基于每个点与上一点和下一点所形成的线段的垂距来进行抽稀。这种算法的优点是简单易实现,但是需要设置合适的垂距限制值。

在实际应用中,不同的抽稀算法可能适用于不同的场景。因此,开发人员需要根据实际情况选择合适的算法,以便更高效地进行路径规划。希望本篇文章可以对读者们在路径规划中应用抽稀算法提供一些参考。

ROS,使用C++编写路径规划算法通常需要遵循以下步骤: 1. 安装ROS和必要的依赖项 2. 创建ROS工作空间并建立ROS包 3. 在ROS创建一个节点,用于实现路径规划算法 4. 将路径规划算法实现为一个ROS服务或ROS动作 5. 在节点实现与ROS通信,接收来自其他节点的请求,并将路径规划结果发布到ROS话题 下面是一个简单的C++路径规划算法示例,在ROS使用: 1. 在ROS创建一个名为“path_planning”的节点 2. 在节点代码定义一个ROS服务,用于接收要规划路径的起点和终点 ``` #include <ros/ros.h> #include <path_planning/PathPlanning.h> bool planPath(path_planning::PathPlanning::Request &req, path_planning::PathPlanning::Response &res) { // 在这里实现路径规划算法 // 将规划结果存储在res return true; } int main(int argc, char **argv) { ros::init(argc, argv, "path_planning"); ros::NodeHandle nh; ros::ServiceServer service = nh.advertiseService("path_planning", planPath); ROS_INFO("Path planning service ready"); ros::spin(); return 0; } ``` 3. 在ROS创建一个名为“path_planning_client”的节点,用于请求路径规划服务并接收规划结果 ``` #include <ros/ros.h> #include <path_planning/PathPlanning.h> int main(int argc, char **argv) { ros::init(argc, argv, "path_planning_client"); ros::NodeHandle nh; ros::ServiceClient client = nh.serviceClient<path_planning::PathPlanning>("path_planning"); path_planning::PathPlanning srv; srv.request.start.x = 0.0; srv.request.start.y = 0.0; srv.request.end.x = 1.0; srv.request.end.y = 1.0; if (client.call(srv)) { ROS_INFO("Path planning result: %s", srv.response.result ? "true" : "false"); } else { ROS_ERROR("Failed to call service path_planning"); } return 0; } ``` 4. 在终端启动ROS节点,执行以下命令: ``` rosrun path_planning path_planning rosrun path_planning path_planning_client ``` 其第一个命令启动路径规划节点,第二个命令启动路径规划客户端,并发送起点和终点信息,最终输出路径规划结果。 需要注意的是,上面的示例仅仅是一个简单的示例,实际需要根据具体的路径规划算法进行相应的实现。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Travis.X

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值