当前位置: 首页 > article >正文

自动驾驶控制与规划——Project 5: Lattice Planner

目录

  • 零、任务介绍
  • 一、算法原理
    • 1.1 Frenet坐标系与笛卡尔坐标系的转换
    • 1.2 Lattice Planner
  • 二、代码实现
    • 2.1 轨迹类
    • 2.2 轨迹规划相关函数
  • 三、效果展示

零、任务介绍

补全lattice_planner.cpp中的TODO部分,实现横纵向解耦的Lattice Planner。

一、算法原理

1.1 Frenet坐标系与笛卡尔坐标系的转换

Frenet坐标系与笛卡尔坐标系的转换
给定横向偏置 l l l,参考点在全局坐标系下的位置 ( x r , y r ) (x_r, y_r) (xr,yr),以及参考点的偏航角 θ r \theta_r θr、参考点处曲率 κ r \kappa_r κr,按照如下公式计算Frenet到笛卡尔坐标系的变换( [ s , s ˙ , l , l ′ ] [s, \dot s, l, l'] [s,s˙,l,l] [ x , y , θ x , κ , v ] [x, y, \theta_x, \kappa, v] [x,y,θx,κ,v]的变换)。
x = x r − l sin ⁡ θ r y = y r + l cos ⁡ θ r θ x = arctan2 ( l ′ , 1 − κ r l ) + θ r v = s ˙ 2 ( 1 − κ r l ) + l ˙ 2 \begin{aligned} x &= x_r - l\sin\theta_r \\ y &= y_r + l \cos\theta_r \\ \theta_x &= \text{arctan2}(l', 1-\kappa_r l) + \theta_r\\ v &= \sqrt{\dot s^2 (1 - \kappa_r l) + \dot l^2} \end{aligned} xyθxv=xrlsinθr=yr+lcosθr=arctan2(l,1κrl)+θr=s˙2(1κrl)+l˙2
其中 l ˙ \dot l l˙ l l l 对时间 t t t 的导数, l ′ l' l l l l s s s 的导数。
本项目中,使用三次样条曲线拟合全局路点作为Frenet的参考线,参考线的曲率、偏航角等参数的计算方法在Spline2D中给出。

1.2 Lattice Planner

此处的Lattice Planner参考Werling M, Ziegler J, Kammel S, et al. Optimal trajectory generation for dynamic street scenarios in a frenet frame[C]//2010 IEEE international conference on robotics and automation. IEEE, 2010: 987-993.,详细的论文阅读笔记可以参考【论文笔记】Optimal trajectory generation for dynamic street scenarios in a Frenét Frame

本项目中使用高速情况下的Lattice Planner,高速巡航时,车辆的横纵向轨迹规划可以解耦,可以用如下的四阶多项式描述横纵向轨迹:
s ( t ) = a 1 t 4 + a 2 t 3 + a 3 t 2 + a 4 t + a 5 d ( t ) = b 1 t 4 + b 2 t 3 + b 3 t 2 + b 4 t + b 5 \begin{aligned} s(t) &= a_1 t^4 + a_2 t^3 + a_3 t^2 + a_4 t + a_5\\ d(t) &= b_1 t^4 + b_2 t^3 + b_3 t^2 + b_4 t + b_5\\ \end{aligned} s(t)d(t)=a1t4+a2t3+a3t2+a4t+a5=b1t4+b2t3+b3t2+b4t+b5
约束如下,需要注意由于此场景是巡航场景,因此 s ( t 1 ) s(t_1) s(t1)无约束:
s ( t 0 ) = a 5 = s 0 v ( t 0 ) = a 4 = v 0 a ( t 0 ) = 2 a 3 = a 0 v ( t 1 ) = 4 a 1 t 1 3 + 3 a 2 t 1 2 + 2 a 3 t 1 + a 4 a ( t 1 ) = 12 a 1 t 1 2 + 6 a 2 t 1 + 2 a 3 d ( t 0 ) = b 5 = d 0 d v ( t 0 ) = b 4 = d v 0 d a ( t 0 ) = 2 b 3 = d a 0 d ( t 1 ) = b 1 t 1 4 + b 2 t 1 3 + b 3 t 1 2 + b 4 t 1 + b 5 d v ( t 1 ) = 4 b 1 t 1 3 + 3 b 2 t 1 2 + 2 b 3 t 1 + b 4 d a ( t 1 ) = 12 b 1 t 1 2 + 6 b 2 t 1 + 2 b 3 \begin{aligned} s(t_0) &= a_5 = s_0\\ v(t_0) &= a_4 = v_0\\ a(t_0) &= 2 a_3 = a_0\\ v(t_1) &= 4a_1t_1^3 + 3a_2t_1^2 + 2 a_3 t_1 + a_4\\ a(t_1) &= 12a_1t_1^2 + 6a_2t_1 + 2 a_3\\ d(t_0) &= b_5 = d_0\\ d_v(t_0) &= b_4 = d_{v0}\\ d_a(t_0) &= 2 b_3 = d_{a0}\\ d(t_1) &= b_1 t_1^4 + b_2 t_1^3 + b_3 t_1^2 + b_4 t_1 + b_5\\ d_v(t_1) &= 4b_1t_1^3 + 3b_2t_1^2 + 2 b_3 t_1 +b_4\\ d_a(t_1) &= 12b_1t_1^2 + 6b_2t_1 + 2 b_3\\ \end{aligned} s(t0)v(t0)a(t0)v(t1)a(t1)d(t0)dv(t0)da(t0)d(t1)dv(t1)da(t1)=a5=s0=a4=v0=2a3=a0=4a1t13+3a2t12+2a3t1+a4=12a1t12+6a2t1+2a3=b5=d0=b4=dv0=2b3=da0=b1t14+b2t13+b3t12+b4t1+b5=4b1t13+3b2t12+2b3t1+b4=12b1t12+6b2t1+2b3
规划流程如下:

  1. 将道路横向离散化,按时间步长采样,计算四阶多项式(d-t曲线)
  2. 对上述计算的每一条d-t曲线,将纵向速度离散化,计算四阶多项式(s-t曲线)
  3. 计算每个轨迹的代价函数,代价包含jerk,时间,速度误差等
  4. 检查轨迹的可行性,例如是否超过最大速度、是否发生碰撞等
  5. 选择代价函数最优的可行轨迹

二、代码实现

2.1 轨迹类

FrenetPath类,存储Frenet坐标系下的轨迹点横纵向位置及其导数,以及对应的笛卡尔坐标系下的位置、偏航角、速度、曲率。

class FrenetPath {
   public:
    float cd = 0.0;
    float cv = 0.0;
    float cf = 0.0;
    // Vec_f = std::vector<float>
    Vec_f t;        // time
    Vec_f d;        // lateral offset
    Vec_f d_d;      // lateral speed
    Vec_f d_dd;     // lateral acceleration
    Vec_f d_ddd;    // lateral jerk
    Vec_f s;        // s position along spline
    Vec_f s_d;      // s speed
    Vec_f s_dd;     // s acceleration
    Vec_f s_ddd;    // s jerk

    Vec_f x;      // x position
    Vec_f y;      // y position
    Vec_f yaw;    // yaw in rad
    Vec_f ds;     // speed
    Vec_f c;      // curvature

    float max_speed;
    float max_accel;
    float max_curvature;
};

三次样条曲线Spline2D类,世界坐标系下的横纵坐标表示为以s为参数的三次样条sxsy,计算给定s_t下的全局坐标、曲率、偏航角等参数。本项目中使用Spline2D拟合全局waypoint作为Frenet坐标系的参考线。

class Spline2D {
   public:
    Spline sx;
    Spline sy;
    Vec_f s;

    Spline2D(Vec_f x, Vec_f y) {
        s = calc_s(x, y);   
        sx = Spline(s, x);
        sy = Spline(s, y);
    };

    Poi_f calc_postion(float s_t) {
        float x = sx.calc(s_t); 
        float y = sy.calc(s_t);
        return {{x, y}};    // 全局坐标系下的横坐标
    };

    float calc_curvature(float s_t) {   // 计算曲率
        float dx = sx.calc_d(s_t);
        float ddx = sx.calc_dd(s_t);
        float dy = sy.calc_d(s_t);
        float ddy = sy.calc_dd(s_t);
        // 曲率计算公式是 kappa = (x'*y'' - x''*y') / (x'^2 + y'^2)^(3/2)
        return (ddy * dx - ddx * dy) / (dx * dx + dy * dy);
    };

    float calc_yaw(float s_t) { // 计算yaw
        float dx = sx.calc_d(s_t);
        float dy = sy.calc_d(s_t);
        return std::atan2(dy, dx);  // 全局坐标系下的v与x轴的夹角
    };

   private:
    Vec_f calc_s(Vec_f x, Vec_f y) {
        Vec_f ds;
        Vec_f out_s{0};
        Vec_f dx = vec_diff(x);
        Vec_f dy = vec_diff(y);

        for (unsigned int i = 0; i < dx.size(); i++) {
            ds.push_back(std::sqrt(dx[i] * dx[i] + dy[i] * dy[i]));
        }

        Vec_f cum_ds = cum_sum(ds);
        out_s.insert(out_s.end(), cum_ds.begin(), cum_ds.end());
        return out_s;
    };
};

四阶多项式类QuarticPolynomial,本项目中使用四阶多项式描述规划轨迹中的s-t轨迹和d-t轨迹。

class QuarticPolynomial {
   public:
    // current parameter at t=0
    float xs;
    float vxs;
    float axs;

    // parameters at target t=t_j
    float vxe;
    float axe;

    // function parameters
    float a0, a1, a2, a3, a4;

    QuarticPolynomial(){};

    QuarticPolynomial(float xs_, float vxs_, float axs_, float vxe_, float axe_, float T) : xs(xs_), vxs(vxs_), axs(axs_), vxe(vxe_), axe(axe_), a0(xs_), a1(vxs_), a2(axs_ / 2.0) {
        Eigen::Matrix2f A;
        A << 3 * std::pow(T, 2), 4 * std::pow(T, 3), 6 * T, 12 * std::pow(T, 2);
        Eigen::Vector2f B;
        B << vxe - a1 - 2 * a2 * T, axe - 2 * a2;

        Eigen::Vector2f c_eigen = A.colPivHouseholderQr().solve(B);
        a3 = c_eigen[0];
        a4 = c_eigen[1];
    };

    float calc_point(float t) { return a0 + a1 * t + a2 * std::pow(t, 2) + a3 * std::pow(t, 3) + a4 * std::pow(t, 4); };

    float calc_first_derivative(float t) { return a1 + 2 * a2 * t + 3 * a3 * std::pow(t, 2) + 4 * a4 * std::pow(t, 3); };

    float calc_second_derivative(float t) { return 2 * a2 + 6 * a3 * t + 12 * a4 * std::pow(t, 2); };

    float calc_third_derivative(float t) { return 6 * a3 + 24 * a4 * t; };
};

2.2 轨迹规划相关函数

calc_frenet_paths获取采样轨迹,此处将横向和纵向解耦,算法步骤如下:

  1. D_ROAD_W将道路横向离散化,按时间步长DTMINTMAXT采样,计算四阶多项式lateral_fp(d-t曲线),离散化为fp_without_s
  2. 对上述计算的每一条d-t曲线,按D_T_S将纵向速度从TARGET_SPEED - D_T_S * N_S_SAMPLETARGET_SPEED + D_T_S * N_S_SAMPLE离散化,计算四阶多项式longitudinal_fp(s-t曲线),与d-t曲线合成并离散化为fp_with_s
  3. 计算每个fp_with_s的代价函数,代价包含jerk,时间,速度误差等
// 01 获取采样轨迹
Vec_Path FrenetOptimalTrajectory::calc_frenet_paths(float c_speed, float c_d, float c_d_d, float c_d_dd, float s0) {
    std::vector<FrenetPath> fp_list;
    // 根据 当前车辆的速度,当前车辆在frenet坐标系中的s坐标,l坐标,l坐标的一阶导数和二阶导数来采样生成备选轨迹
    // 先遍历 d 方向,再遍历 t 方向,这样可以生成 d-t 曲线,每个d-t曲线下,再遍历备选速度,生成 s-t 曲线,这种方法其实只适用终点 s 自由的方式
    // 完成轨迹采样
    for (float di = 0; di <= 5; di += D_ROAD_W) {
    	// 起始右车道中心线是0,向左侧遍历
        for (float ti = MINT; ti <= MAXT; ti += DT) {
            FrenetPath fp_without_s;
            // std::cout << "采样过程中的c_d: " << c_d << std::endl;
            QuinticPolynomial lateral_fp(c_d, c_d_d, c_d_dd, di, 0.0, 0.0, ti);    // 四阶多项式
            // 这个 for 不涉及采样,是计算已经采样得到的d-t曲线在每个时刻点状态值,这里只计算一根轨迹
            for (float _t = 0.0; _t <= ti; _t += DT) {
                fp_without_s.t.push_back(_t);
                fp_without_s.d.push_back(lateral_fp.calc_point(_t));
                fp_without_s.d_d.push_back(lateral_fp.calc_first_derivative(_t));
                fp_without_s.d_dd.push_back(lateral_fp.calc_second_derivative(_t));
                fp_without_s.d_ddd.push_back(lateral_fp.calc_third_derivative(_t));
            }
            // 当前遍历下,每一条s-t曲线复用上面刚刚生成的d-t曲线(只有一根),for循环每运行一次,生成一条完整的备选轨迹
            for (float vi = TARGET_SPEED - D_T_S * N_S_SAMPLE; vi < TARGET_SPEED + D_T_S * N_S_SAMPLE; vi += D_T_S) {
                FrenetPath fp_with_s = fp_without_s;
                QuarticPolynomial longitudinal_qp(s0, c_speed, 0.0, vi, 0.0, ti);
                for (float _t : fp_without_s.t) {
                    fp_with_s.s.push_back(longitudinal_qp.calc_point(_t));
                    fp_with_s.s_d.push_back(longitudinal_qp.calc_first_derivative(_t));
                    fp_with_s.s_dd.push_back(longitudinal_qp.calc_second_derivative(_t));
                    fp_with_s.s_ddd.push_back(longitudinal_qp.calc_third_derivative(_t));
                }
                fp_with_s.max_speed = *max_element(fp_with_s.s_d.begin(), fp_with_s.s_d.end());
                fp_with_s.max_accel = *max_element(fp_with_s.s_dd.begin(), fp_with_s.s_dd.end());

                float Jp = sum_of_power(fp_with_s.d_ddd);    // 横向jerk平方和
                float Js = sum_of_power(fp_with_s.s_ddd);    // 纵向jerk平方和

                // 计算每条备选轨迹的代价,参考的是论文 “Optimal trajectory generation for dynamic street scenarios in a Frenét Frame“ 里面的巡航控制的代价函数计算方式
                fp_with_s.cd = KJ * Jp + KT * ti + KD * pow(fp_with_s.d.back(), 2);    // 横向代价
                // # square of diff from target speed
                float ds = pow(TARGET_SPEED - fp_with_s.s_d.back(), 2);
                fp_with_s.cv = KJ * Js + KT * ti + KD * ds;                  // 纵向代价
                fp_with_s.cf = KLAT * fp_with_s.cd + KLON * fp_with_s.cv;    // 总代价函数

                fp_list.push_back(fp_with_s);
            }
        }
    }
    return fp_list;
};

check_paths检查路径是否超过最大速度、最大加速度、最大曲率,是否发生碰撞

// 03
// 检查路径,通过限制最大速度,最大加速度,最大曲率与避障,选取可使用的轨迹数组
Vec_Path FrenetOptimalTrajectory::check_paths(Vec_Path path_list, const Vec_Poi ob) {
    Vec_Path output_fp_list;
    // TODO: 补全代码
    // std::cout << "before check: " << path_list.size() << std::endl;
    for (auto path : path_list) {
        // 检查最大速度、最大加速度、最大曲率
        if (path.max_speed > MAX_SPEED) {
            // std::cout << "exceed max speed" << std::endl;
            continue;
        } else if (path.max_accel > MAX_ACCEL) {
            // std::cout << "exceed max acceleration" << std::endl;
            continue;
        } else if (path.max_curvature > MAX_CURVATURE) {
            // std::cout << "exceed max curvature" << std::endl;
            continue;
        } else if (!check_collision(path, ob)) {
            // 检查是否与障碍物发生碰撞
            // std::cout << "collision" << std::endl;
            continue;
        } else {
            output_fp_list.emplace_back(path);
        }
    }
    // std::cout << "after check: " << output_fp_list.size() << std::endl;
    return output_fp_list;
};

calc_global_paths根据参考轨迹和多条采样轨迹,计算轨迹在笛卡尔坐标系中的坐标、偏航角等参数

// 02
// 根据参考轨迹与采样的轨迹数组,计算frenet中的其他曲线参数,如航向角,曲率,ds等参数
void FrenetOptimalTrajectory::calc_global_paths(Vec_Path& path_list, Spline2D csp) {
    int iiii = 0;
    // 计算采样轨迹的其他参数
    for (Vec_Path::iterator _fpp = path_list.begin(); _fpp != path_list.end(); _fpp++) {
        iiii++;
        // # calc global positions
        for (int i = 0; i < _fpp->s.size(); i++) {
            // 重规划局部轨迹的 s 坐标超过参考路径的最大 s 以后,超过的部分不需要计算其他参数了。
            if (_fpp->s[i] >= csp.s.back()) {
                break;
            }
            Poi_f location_cartesian = csp.calc_postion(_fpp->s[i]);
            float i_yaw_angle = csp.calc_yaw(_fpp->s[i]);
            float di = _fpp->d[i];
            float fx = location_cartesian[0] + di * cos(i_yaw_angle + M_PI / 2.0);
            float fy = location_cartesian[1] + di * sin(i_yaw_angle + M_PI / 2.0);
            _fpp->x.push_back(fx);
            _fpp->y.push_back(fy);
        }

        // # calc yaw and ds
        for (int i = 0; i < _fpp->s.size() - 1; i++) {
            // 重规划局部轨迹的 s 坐标超过参考路径的最大 s 以后,超过的部分不需要计算其他参数了。
            if (_fpp->s[i] >= csp.s.back()) {
                break;
            }
            float dx = _fpp->x[i + 1] - _fpp->x[i];
            float dy = _fpp->y[i + 1] - _fpp->y[i];
            _fpp->yaw.push_back(atan2(dy, dx));
            _fpp->ds.push_back(sqrt(pow(dx, 2) + pow(dy, 2)));
        }
        _fpp->yaw.push_back(_fpp->yaw.back());
        _fpp->ds.push_back(_fpp->ds.back());

        // # calc curvature
        for (int i = 0; i < _fpp->s.size() - 1; i++) {
            // 重规划局部轨迹的 s 坐标超过参考路径的最大 s 以后,超过的部分不需要计算其他参数了。
            if (_fpp->s[i] >= csp.s.back()) {
                break;
            }
            _fpp->c.push_back((_fpp->yaw[i + 1] - _fpp->yaw[i]) / _fpp->ds[i]);
        }
        _fpp->max_curvature = *max_element(_fpp->c.begin(), _fpp->c.end());
    }
};

check_collision碰撞检测,此处将车辆简化为一个圆形的机器人,检查轨迹上每个点到障碍物的距离是否小于机器人半径,若无碰撞返回true,发生碰撞返回false

bool FrenetOptimalTrajectory::check_collision(FrenetPath path, const Vec_Poi ob) {
    // 碰撞返回false,无碰撞返回true
    for (auto point : ob) {
        for (unsigned int i = 0; i < path.x.size(); i++) {
            float dist = std::pow((path.x[i] - point[0]), 2) + std::pow((path.y[i] - point[1]), 2);
            if (dist <= ROBOT_RADIUS * ROBOT_RADIUS) {
                return false;
            }
        }
    }
    return true;
};

frenet_optimal_planning使用上述函数获得路径,选择最小代价的路径

// TODO: step 1 finish frenet_optimal_planning
FrenetPath FrenetOptimalTrajectory::frenet_optimal_planning(Spline2D csp, float s0, float c_speed, float c_d, float c_d_d, float c_d_dd, Vec_Poi ob) {
    // 01 获取采样轨迹
    Vec_Path fp_list = calc_frenet_paths(c_speed, c_d, c_d_d, c_d_dd, s0);
    // 02 根据参考轨迹与采样的轨迹数组,计算frenet中的其他曲线参数,如航向角,曲率,ds等参数
    // 此处的csp是用三次样条拟合路点得到的,作用是提供frenet坐标系的参考点
    calc_global_paths(fp_list, csp);
    // 03 检查路径,通过限制最大速度,最大加速度,最大曲率与避障,选取可使用的轨迹数组
    Vec_Path save_paths = check_paths(fp_list, ob);
    float min_cost = std::numeric_limits<float>::max();
    FrenetPath final_path;
    for (auto path : save_paths) {
        if (min_cost >= path.cf) {
            min_cost = path.cf;
            final_path = path;
        }
    }
    return final_path;
};

三、效果展示

自动驾驶控制与规划——Project 5: Lattice


http://www.kler.cn/a/466975.html

相关文章:

  • 【three.js】场景搭建
  • RabbitMQ案例
  • Jdk动态代理源码缓存优化比较(JDK17比JDK8)
  • 面试题解,JVM中的“类加载”剖析
  • I2C(一):存储器模式:stm32作为主机对AT24C02写读数据
  • 框架模块说明 #09 日志模块_01
  • 【时时三省】(C语言基础)常见的动态内存错误2
  • 解读 C++23 std::expected 函数式写法
  • 【Rust自学】10.4. trait Pt.2:trait作为参数和返回类型、trait bound
  • 简易Type-C拉取5V/3A电流电路分享
  • 【动态重建】时间高斯分层的长体积视频
  • Excel使用VLOOKUP时注意绝对引用和相对引用区别
  • 基于Java的超级玛丽游戏的设计与实现【源码+文档+部署讲解】
  • SQLite AND/OR 运算符
  • 【信息系统项目管理师】高分论文:论信息系统项目的风险管理(数字化联合审查管理系统)
  • JVM学习指南(9)-JVM运行时数据区
  • Kotlin 协程基础知识总结六 —— 协程 Flow 的综合应用
  • rocketmq-pull模式-消费重平衡和拉取PullTaskImpl线程
  • ubuntu1604 apt镜像源切换
  • 使用PyTorch实现基于稀疏编码的生成对抗网络(GAN)在CIFAR-10数据集上的应用
  • 计算机毕业设计PyHive+Hadoop深圳共享单车预测系统 共享单车数据分析可视化大屏 共享单车爬虫 共享单车数据仓库 机器学习 深度学习
  • STM32-笔记34-4G遥控灯
  • Golang:使用minio替代文件系统实战教程
  • NLP CH3复习
  • springboot3 性能优化
  • 1.4 spring八股文学习