在自动驾驶和机器人路径规划领域,我们经常需要在不同的坐标系之间进行转换。就像我们在城市中导航时,既可以用"东西南北"的绝对方向描述位置,也可以用"沿当前道路向前500米"这样的相对描述。Cartesian坐标系(笛卡尔坐标系)和Frenet坐标系就是这两种描述方式的数学表达。
Cartesian坐标系是我们最熟悉的x-y直角坐标系,它以固定的参考点作为原点。而Frenet坐标系则是沿着参考曲线建立的动态坐标系,它用沿曲线的距离s和偏离曲线的距离d来描述位置。这种描述方式特别适合描述车辆在道路上的位置,因为道路本身就是一条参考曲线。
在实际应用中,我们经常需要在两种坐标系间转换。比如:
Cartesian坐标系就是我们熟悉的(x,y)坐标系。在二维平面中,任何一点P的位置都可以用一对数值(x,y)唯一确定,其中:
这个坐标系的特点是:
Frenet坐标系是沿着参考曲线建立的动态坐标系,用(s,d)表示位置:
这个坐标系的特点是:
数学上,参考曲线可以表示为r(s),其中s是弧长参数。在曲线上的每一点,我们可以定义:
给定Cartesian坐标系中的点P(x,y),要找到其在Frenet坐标系中的对应表示(s,d),我们需要:
这本质上是一个投影问题:将点P投影到参考曲线上。
设参考曲线为r(s) = (x_r(s), y_r(s)),我们需要最小化P到曲线的距离:
min_s ||P - r(s)||²
这等价于求解:
(P - r(s)) · r'(s) = 0
其中r'(s)是曲线在s处的切线向量。这个方程通常需要数值方法求解。
一旦找到最优的s,d可以通过下式计算:
d = sign((P - r(s)) · N(s)) * ||P - r(s)||
其中N(s)是曲线的法向量,sign函数确定d的符号(曲线左侧为正或右侧为正取决于约定)。
注意:当参考曲线曲率较大时,一个Cartesian点可能对应多个Frenet坐标,需要根据实际应用场景选择最合适的解。
给定Frenet坐标(s,d),要找到对应的Cartesian坐标(x,y),我们需要:
Cartesian坐标可以表示为:
P = r(s) + d * N(s)
其中:
具体计算步骤:
注意:当参考曲线曲率较大时,简单的法向偏移可能导致位置不准确,需要考虑曲率修正。
参考曲线通常有三种表示方式:
在实际应用中,三次样条曲线是较好的折中选择:
Cartesian到Frenet转换中最耗时的步骤是寻找最近点。优化方法包括:
在以下情况需要特殊处理:
解决方案:
在车道保持系统中:
在机器人路径跟踪中:
在动态避障场景:
以下是C++实现的关键代码片段(伪代码):
cpp复制// 参考曲线类
class ReferenceLine {
public:
// 三次样条插值
void FitSpline(const vector<Point2D>& points);
// 计算s处的点和导数
Point2D Evaluate(double s) const;
Point2D EvaluateDerivative(double s) const;
// Cartesian转Frenet
FrenetPoint CartesianToFrenet(const Point2D& cart) const;
// Frenet转Cartesian
Point2D FrenetToCartesian(const FrenetPoint& frenet) const;
};
// Cartesian转Frenet实现
FrenetPoint ReferenceLine::CartesianToFrenet(const Point2D& cart) const {
double best_s = 0;
double min_dist = numeric_limits<double>::max();
// 粗略搜索最近点
for(double s = 0; s < max_s_; s += search_step_) {
Point2d ref_point = Evaluate(s);
double dist = Distance(cart, ref_point);
if(dist < min_dist) {
min_dist = dist;
best_s = s;
}
}
// 精确优化
auto objective = [&](double s) {
Point2d ref_point = Evaluate(s);
return DistanceSquared(cart, ref_point);
};
best_s = OptimizeNewton(objective, best_s);
// 计算d
Point2d ref_point = Evaluate(best_s);
Point2d ref_derivative = EvaluateDerivative(best_s);
Point2d normal(-ref_derivative.y, ref_derivative.x);
normal = Normalize(normal);
double d = Dot(cart - ref_point, normal);
return {best_s, d};
}
可能原因:
调试方法:
优化方向:
包括但不限于:
解决方案:
在实际工程实现中,我发现正确处理高曲率区域和参考线不连续情况最为关键。一个实用的技巧是在转换前对参考线进行平滑处理,虽然这会引入微小误差,但能显著提高转换的鲁棒性。另外,对于实时性要求高的应用,合理设置搜索范围和步长比追求数学精度更重要。