简介
抽稀(Simplification)算法是指在保持数据尽可能不失真的前提下,对数据进行精简处理,减少数据的数量,便于数据的存储和处理。路径规划中,抽稀算法的作用是将原始路径中的冗余点去除,从而减小路径点的数量,降低计算量和存储量,同时保留路径的形状特征和轨迹信息,确保路径规划的精度和实时性。常见的抽稀算法有 Douglas-Peucker算法和垂距限值法。
一、Douglas-Peucker算法
Douglas-Peucker算法是一种常用于抽稀(简化)线段的算法。其基本思想是在曲线中找到一些关键点,用这些点来近似曲线,从而实现简化。算法可以实现将原本复杂的曲线简化为一组少量的点,从而达到减少数据量、加快计算速度等目的。该算法常用于地图绘制、路径规划等领域。
1.1 算法原理
- 给定一条由n个点构成的曲线;
- 在曲线的起点和终点之间找到距离最远的点,将其加入简化后的点集合;
- 将曲线分成两段,对这两段分别执行步骤2;
- 递归地执行步骤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 算法原理
- 以第一个点开始,计算第一个点到前一个点和后一个点所在直线的垂距;
- 如果垂距小于阈值,则保留该点,并将其作为下一个点计算;
- 如果垂距大于阈值,则舍弃该点,并将下一个点作为当前点重新进行计算;
- 依次类推,直到计算完最后一个点。
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算法是一种经典的抽稀算法,它通过将原始路径分段处理,递归地对每个线段进行抽稀。通过调整递归深度和垂距限制值,可以获得不同程度的抽稀效果。垂距限值法算法则是基于每个点与上一点和下一点所形成的线段的垂距来进行抽稀。这种算法的优点是简单易实现,但是需要设置合适的垂距限制值。
在实际应用中,不同的抽稀算法可能适用于不同的场景。因此,开发人员需要根据实际情况选择合适的算法,以便更高效地进行路径规划。希望本篇文章可以对读者们在路径规划中应用抽稀算法提供一些参考。