详解Apollo8.0 车道线拟合代码

作者 | 萌萌的糖  编辑 | 汽车人

原文链接:https://zhuanlan.zhihu.com/p/657147115

点击下方卡片,关注“自动驾驶之心”公众号

ADAS巨卷干货,即可获取

点击进入→自动驾驶之心【车道线检测】技术交流群

本文只做学术分享,如有侵权,联系删文

1 开篇

我们已经对感知组件有了比较清晰的认识。从这篇文章开始,如果有大佬已经讲过某组件整体的运行逻辑,我将不会再重复该部分工作,而是分析那些还没有被提及的细节和难点,相信大家更有耐心看下去。

所以,本文的重点落在了车道线后处理上。
首先我们在lane_camera_perception.cc文件中的LaneCameraPerception::Perception()方法可以发现整个车道线的检测的流程为:

  1. 调用DarkSCNN进行车道线分割;

  2. 分割结果2D后处理;

  3. 相机在线标定,计算相机高度和pitch角度;

  4. 3D后处理。

// DarkSCNN进行车道线分割
if (!lane_detector_->Detect(lane_detetor_options, frame)) {
                    AERROR << "Failed to detect lane.";
                    return false;}
    PERF_BLOCK_END_WITH_INDICATOR(frame->data_provider->sensor_name(),
        "LaneDetector");
    // 分割结果2D后处理
    if (!lane_postprocessor_->Process2D(lane_postprocessor_options, frame)) {
        AERROR << "Failed to postprocess lane 2D.";
        return false;
    }
    PERF_BLOCK_END_WITH_INDICATOR(frame->data_provider->sensor_name(),
        "LanePostprocessor2D");

    // 根据当前帧检测出的车道线求取相机高度和pitch角
    frame->calibration_service->Update(frame); 
    PERF_BLOCK_END_WITH_INDICATOR(frame->data_provider->sensor_name(), "CalibrationService");
    // 3D后处理
    if (!lane_postprocessor_->Process3D(lane_postprocessor_options, frame)) {
        AERROR << "Failed to postprocess lane 3D.";
        return false;
    }
    PERF_BLOCK_END_WITH_INDICATOR(frame->data_provider->sensor_name(),
        "LanePostprocessor3D");

本篇文章注重分析车道线拟合代码,主要包含以下3部分:

  1. 2D车道线拟合代码分析;

  2. 相机在线标定代码分析;

  3. 3D车道线拟合代码分析。

2 2D车道线拟合

Apollo采用DarkSCNN算法完成一共13条车道线语义分割,神经网络的输出结果包含两部分:1.13条车道分割结果;2.灭点检测结果,具体参考Frank,对于车道线分割输出结果,每条车道线都是单独一个通道,这样的设计也方便车道线的后处理。当车道线检测完成后会调用DarkSCNNLanePostprocessor::Process2D()方法进行2D车道线拟合。其总体流程如下:

2.1 车道线下采样

bool DarkSCNNLanePostprocessor::Process2D(const LanePostprocessorOptions& options, CameraFrame* frame)
        {
    ADEBUG << "Begin to Process2D.";
    frame->lane_objects.clear();
    auto start = std::chrono::high_resolution_clock::now();

    cv::Mat lane_map(lane_map_height_, lane_map_width_, CV_32FC1);
    // copy data
    memcpy(lane_map.data, frame->lane_detected_blob->cpu_data(),
        lane_map_width_ * lane_map_height_ * sizeof(float));
    int y = static_cast<int>(lane_map.rows * 0.9 - 1); // 480 * 0.9 - 1 = 431

    // step_y为采样步长,动态变化
    // y = y - step_y
    // 越挨着图片底部,越有可能是车道区域,步长越小,
    int step_y = (y - 40) * (y - 40) / 6400 + 1; // step_y = 25,实际上并没有用这个
    xy_points.clear();
    xy_points.resize(lane_type_num_);
    uv_points.clear();
    uv_points.resize(lane_type_num_);
    // 分割结果采样
    while (y > 0) { 
        for (int x = 1; x < lane_map.cols - 1; ++x) {
            int value = static_cast<int>(round(lane_map.at<float>(y, x)));
            // lane on left(自车左车道)
            if ((value > 0 && value < 5) || value == 11) {
                // right edge (inner) of the lane
                if (value != static_cast<int>(round(lane_map.at<float>(y, x + 1)))) {
                    // 车道线在图像上坐标
                    Eigen::Matrix<float, 3, 1> img_point(static_cast<float>(x * roi_width_ / lane_map.cols),
                        static_cast<float>(y * roi_height_ / lane_map.rows + roi_start_),
                        1.0);
                    Eigen::Matrix<float, 3, 1> xy_p;
                    // trans_mat_为单应矩阵:表示图像上车道平面投影到车辆坐标系下的投影矩阵?
                    // trans_mat_的计算感觉有点问题
                    xy_p = trans_mat_ * img_point;  // 车辆坐标系下的点
                    Eigen::Matrix<float, 2, 1> xy_point;
                    Eigen::Matrix<float, 2, 1> uv_point;
                    if (std::fabs(xy_p(2)) < 1e-6)
                        continue;
                    // 归一化坐标
                    xy_point << xy_p(0) / xy_p(2), xy_p(1) / xy_p(2);
                    // Filter out lane line points
                    if (xy_point(0) < 0.0 || // This condition is only for front camera
                        xy_point(0) > max_longitudinal_distance_ || std::abs(xy_point(1)) > 30.0) {
                        continue;
                    }
                    // 保存两者的坐标点
                    uv_point << static_cast<float>(x * roi_width_ / lane_map.cols),
                        static_cast<float>(y * roi_height_ / lane_map.rows + roi_start_);
                    if (xy_points[value].size() < minNumPoints_ || xy_point(0) < 50.0f || std::fabs(xy_point(1) - xy_points[value].back()(1)) < 1.0f) {
                        xy_points[value].push_back(xy_point);
                        uv_points[value].push_back(uv_point);
                    }
                }
            } else if (value >= 5 && value < lane_type_num_) {
                // Left edge (inner) of the lane
                ...
                ...
            }
        }
        // 实际采用这里的step_y
        step_y = (y - 45) * (y - 45) / 6400 + 1; // 30
        y -= step_y;
    }
}

进入Process2D方法,首先获取车道线分割结果的Blob,然后对车道线进行非均匀采样。然后将图片上的车道线坐标进行投影。不太确定的是,这里的投影矩阵trans_mat_是图片坐标系到车辆坐标系的投影矩阵?将图片上的车道线坐标通过单应性矩阵投影到车辆归一化平面???经过分析对应的trans_mat_设置流程如下:

Visualizer::Init_all_info_single_camera()->Visualizer::adjust_angles()->DarkSCNNLanePostprocessor::SetIm2CarHomography()

2.2 单应矩阵求解

其主要调用Visualizer::adjust_angles()方法求取单应矩阵,代码如下:

bool Visualizer::adjust_angles(const std::string &camera_name,
                               const double pitch_adj_degree,
                               const double yaw_adj_degree,
                               const double roll_adj_degree) {
  // Convert degree angles to radian angles
  double pitch_adj_radian = pitch_adj_degree * degree_to_radian_factor_;
  double yaw_adj_radian = yaw_adj_degree * degree_to_radian_factor_;
  double roll_adj_radian = roll_adj_degree * degree_to_radian_factor_;

  // We use "right handed ZYX" coordinate system for euler angles
  // adjust pitch yaw roll in camera coords
  // Remember that camera coordinate
  // (Z)----> X
  //  |
  //  |
  //  V
  //  Y
  Eigen::Matrix4d Rx;  // pitch
  Rx << 1, 0, 0, 0, 0, cos(pitch_adj_radian), -sin(pitch_adj_radian), 0, 0,
      sin(pitch_adj_radian), cos(pitch_adj_radian), 0, 0, 0, 0, 1;
  Eigen::Matrix4d Ry;  // yaw
  Ry << cos(yaw_adj_radian), 0, sin(yaw_adj_radian), 0, 0, 1, 0, 0,
      -sin(yaw_adj_radian), 0, cos(yaw_adj_radian), 0, 0, 0, 0, 1;
  Eigen::Matrix4d Rz;  // roll
  Rz << cos(roll_adj_radian), -sin(roll_adj_radian), 0, 0, sin(roll_adj_radian),
      cos(roll_adj_radian), 0, 0, 0, 0, 1, 0, 0, 0, 0, 1;

  // adjusted_camera2car_camera2car表示相机到车辆坐标系的位姿变换
  adjusted_camera2car_ = ex_camera2car_ * Rz * Ry * Rx;
  AWARN << "adjusted_camera2car_: " << adjusted_camera2car_;

  // Get homography from projection matrix
  // ====
  // Version 1. Direct

  // compute the homography matrix, such that  [u, v, 1] ~ H [X_l, Y_l, 1]
  Eigen::Matrix3d R = adjusted_camera2car_.block(0, 0, 3, 3); // 取出旋转矩阵 camera->car
  Eigen::Vector3d T = adjusted_camera2car_.block(0, 3, 3, 1); // 取出平移向量 camera->car
  Eigen::Matrix3d H; // 3*3
  Eigen::Matrix3d H_inv;

  // 直接展开计算得到 Rccamera_car
  H.block(0, 0, 3, 2) = (K_[camera_name] * R.transpose()).block(0, 0, 3, 2); // 这里好理解
  H.block(0, 2, 3, 1) = -K_[camera_name] * R.transpose() * T; //  为什么是这样的??
  H_inv = H.inverse();
  homography_ground2image_[camera_name] = H;
  homography_image2ground_[camera_name] = H_inv;

  // Version 2. Conceptual ##########################
  // ex_car2camera_ = adjusted_camera2car_.inverse();

  // // compute the homography matrix, such that H [u, v, 1]' ~ [X_l, Y_l, 1]
  // Eigen::Matrix3d R = ex_car2camera_.block(0, 0, 3, 3);
  // Eigen::Vector3d T = ex_car2camera_.block(0, 3, 3, 1);

  // projection_matrix_.setIdentity();
  // projection_matrix_.block(0, 0, 3, 3) = K_ * R;
  // projection_matrix_.block(0, 3, 3, 1) = K_ * T;

  // homography_ground2image_.block(0, 0, 3, 2)
  //   = projection_matrix_.block(0, 0, 3, 2);
  // homography_ground2image_.block(0, 2, 3, 1)
  //   = projection_matrix_.block(0, 3, 3, 1);

  // AINFO << "homography_ground2image_: ";
  // AINFO << homography_ground2image_;

  // homography_image2ground_ = homography_ground2image_.inverse();

  return true;
}

推导如下:设相机内参

c5b7c694cd41609212c93409832bb9b1.png

车辆坐标系到相机坐标系的旋转矩阵

654324b48ad8056b684eb2e645d7452d.png

车辆坐标系到相机坐标系的平移向量

22532cd18ff5d9e4334bc49710fc5f10.png

将车辆自身归一化坐标向图片投影

8ab2275132c5e7f30033bcd231f7c156.png

将内参和旋转矩阵带入该方程,展开如下

b0e10215056d56fb16bfaef5467efca4.png 96406a85684121b2dc062594e794151c.png

可到到单应矩阵

99267ffe6c61bc2a0cf424828d73228c.png

对应上面代码,发现单应矩阵最后一列H.block(0, 2, 3, 1) = -K_[camera_name] * R.transpose() * T;不太理解,是我推导错了吗??希望大佬指正!!!这里的单应矩阵是车辆坐标系的车道平面和图像坐标系上的车道平面的投影关系,并不是两张图片间的变换矩阵。参考:

2.3 2D车道线拟合

2D车道线拟合的思想是遍历所有下采样后车道线检测结果,每根车道线进行随机采样一致性算法(Ransac),得到车道线曲线方程。

// 2. Remove outliers and Do a ransac fitting
    std::vector<Eigen::Matrix<float, 4, 1>> coeffs;
    std::vector<Eigen::Matrix<float, 4, 1>> img_coeffs;
    std::vector<Eigen::Matrix<float, 2, 1>> selected_xy_points;
    coeffs.resize(lane_type_num_);
    img_coeffs.resize(lane_type_num_);
    // 遍历每条车道线在归一化坐标系拟合其曲线方程  
    for (int i = 1; i < lane_type_num_; ++i) {
        coeffs[i] << 0, 0, 0, 0;
        if (xy_points[i].size() < minNumPoints_) // minNumPoints_ = 8
            continue;
        Eigen::Matrix<float, 4, 1> coeff;
        // Solve linear system to estimate polynomial coefficients
        // 每根车道线进行随机采样一致性算法(Ransac),去除外点,得到曲线函数
        if (RansacFitting<float>(xy_points[i], &selected_xy_points, &coeff, 200,
                static_cast<int>(minNumPoints_), 0.1f)) {
            coeffs[i] = coeff;

            xy_points[i].clear();
            xy_points[i] = selected_xy_points;
        } else {
            xy_points[i].clear();
        }
    }

RansacFitting()为车道线曲线拟合,代码如下:

template <typename Dtype>
        bool RansacFitting(const std::vector<Eigen::Matrix<Dtype, 2, 1>>& pos_vec,
            std::vector<Eigen::Matrix<Dtype, 2, 1>>* selected_points,
            Eigen::Matrix<Dtype, 4, 1>* coeff, const int max_iters = 100,
            const int N = 5,const Dtype inlier_thres = static_cast<Dtype>(0.1)){
    if (coeff == nullptr) {
        AERROR << "The coefficient pointer is NULL.";
        return false;
    }
    selected_points->clear();
    // 将车道线按点数分成3段
    int n = static_cast<int>(pos_vec.size());
    int q1 = static_cast<int>(n / 4);
    int q2 = static_cast<int>(n / 2);
    int q3 = static_cast<int>(n * 3 / 4);
    if (n < N) {
        AERROR << "The number of points should be larger than the order. #points = "
                << pos_vec.size();
        return false;
    }
    std::vector<int> index(3, 0);
    int max_inliers = 0;
    Dtype min_residual = std::numeric_limits<float>::max();
    Dtype early_stop_ratio = 0.95f;
    Dtype good_lane_ratio = 0.666f;
    for (int j = 0; j < max_iters; ++j) {
        // 在三段中,随机采样3个点,进行曲线拟合,3个点会不会太少
        index[0] = std::rand() % q2; // 0~n/2中取点
        index[1] = q2 + std::rand() % q1; // n/2~n3/4中取点
        index[2] = q3 + std::rand() % q1; //n3/4~n中取点

        // 曲线模型:y = a_0 + a_1*x + a_2*x^2
        Eigen::Matrix<Dtype, 3, 3> matA;
        matA << pos_vec[index[0]](0) * pos_vec[index[0]](0), pos_vec[index[0]](0),
            1, pos_vec[index[1]](0) * pos_vec[index[1]](0), pos_vec[index[1]](0), 1,
            pos_vec[index[2]](0) * pos_vec[index[2]](0), pos_vec[index[2]](0), 1;

        Eigen::FullPivLU<Eigen::Matrix<Dtype, 3, 3>> mat(matA);
        mat.setThreshold(1e-5f);
        if (mat.rank() < 3) { // 方程无非零解
            ADEBUG << "matA: " << matA;
            ADEBUG << "Matrix is not full rank (3). The rank is: " << mat.rank();
            continue;
        }
        // y值
        Eigen::Matrix<Dtype, 3, 1> matB;
        matB << pos_vec[index[0]](1), pos_vec[index[1]](1), pos_vec[index[2]](1);
        // 直接求解,但会出现矩阵不可逆的情况,可以向matA对角线上加上阻尼因子,保证矩阵可逆,也可以用最小二乘
        Eigen::Vector3f c = static_cast<Eigen::Matrix<Dtype, 3, 1>>(matA.inverse() * matB);
        if (!(matA * c).isApprox(matB)) {
            ADEBUG << "No solution.";
            continue;
        }
        int num_inliers = 0; // 内点数
        Dtype residual = 0;  // 总误差
        Dtype y = 0;
        // 对于每一个模型,遍历所有点,求预测值,判断是否为内点并计算总误差
        for (int i = 0; i < n; ++i) {
            // 预测值
            y = pos_vec[i](0) * pos_vec[i](0) * c(0) + pos_vec[i](0) * c(1) + c(2);
            if (std::abs(y - pos_vec[i](1)) <= inlier_thres)
                ++num_inliers;
            // 总体误差
            residual += std::abs(y - pos_vec[i](1));
        }
        // 内点数量足够多或者误差足够小,保存车道线曲线参数
        if (num_inliers > max_inliers || (num_inliers == max_inliers && residual < min_residual)) {
            (*coeff)(3) = 0;
            (*coeff)(2) = c(0);
            (*coeff)(1) = c(1);
            (*coeff)(0) = c(2);
            max_inliers = num_inliers;
            min_residual = residual;
        }
        // 95%以上都是内点时,算法已经收敛
        if (max_inliers > early_stop_ratio * n)
            break;
    }
    // 内点数太少时,认为拟合失败
    if (static_cast<Dtype>(max_inliers) / n < good_lane_ratio)
        return false;
    // 用最后得到的模型内点标记
    for (int i = 0; i < n; ++i) {
        Dtype y = pos_vec[i](0) * pos_vec[i](0) * (*coeff)(2) + pos_vec[i](0) * (*coeff)(1) + (*coeff)(0);
        // 判断是否为内点
        if (std::abs(y - pos_vec[i](1)) <= inlier_thres) {
            selected_points->push_back(pos_vec[i]);
        }
    }
    return true;
}

假设车道线曲线方程

dd987064ba3729b19efdf2e16951bc0a.png

将车道线按点数分成3段,在三段中,随机采样3个点,若这三个点都满足曲线方程,则有

54a0b72674a5f3f15c0d656a28bac429.png

解法1:
写成矩阵形式有

e382eb893a2ed388484c853bde6e9586.png

显然,如果矩阵A可逆

cdfa5034978e6592ae22b2c34654b1c5.png

同样这里也可以采样4个点 或5个点,用QR分解求解其系数。
解法2:

de3755a122da1aa3a52dd41c548b9ce1.png

构建目标函数

f7042d797c72b13a7de66aa6dd35dda5.png

利用梯度下降、高斯-牛顿法或者Levenberg-Marquardt(LM)法求解得到系数

在Apollo中代码中采用的是方法一,但是加上了Ransac去除外点。至此第一部分内容已经介绍完了,总的来说还算简单,车道线合并请参考Frank的文章

3 相机在线标定

3.1 总体流程

相机标定的流程为:

  1. 加载自身车道的左右两车道线的点

  2. 根据定位计算当前帧与前一帧相机的位移和转角,并得到速度和角速度

  3. 通过转角判断相机是否直线运动,必须直线运动才能估计相机pitch角

  4. 通过检测到的车道线进行灭点估计

  5. 用估计的灭点计算相机pitch

  6. pitch角直方滤波

值得注意的是整个过程中并没有相机高度的修正,在代码中也是直接采用默认高度。

这部分内容主要为通过已经检测出的2D车道线计算相机的pitch角和高度。在车道线检测组件中调用的接口为OnlineCalibrationService::Update(CameraFrame* frame),在该函数内部,其核心函数为calibrator_成员的Calibrate()的成员函数。

bool LaneLineCalibrator::Calibrate(const CalibratorOptions& options, float* pitch_angle)
        {
            if (pitch_angle == nullptr) {
                AERROR << "pitch_angle is not available";
                return false;
            }
            EgoLane ego_lane;
            // 1.加载自车道和左右车道上的点,注意必须是左右两车道线都存在才返回true
            if (!LoadEgoLaneline(*options.lane_objects, &ego_lane)) {
                AINFO << "Failed to get the ego lane.";
                return false;
            }
            // cam_ori为相机坐标原点
            double cam_ori[4] = { 0 };
            cam_ori[3] = 1.0;

            // Camera to world pose
            // Twc来自于定位channel
            Eigen::Affine3d c2w = *options.camera2world_pose;

            double p2w[12] = {c2w(0, 0),c2w(0, 1),c2w(0, 2),c2w(0, 3),c2w(1, 0),c2w(1, 1),c2w(1, 2),c2w(1, 3),
                                c2w(2, 0),c2w(2, 1),c2w(2, 2),c2w(2, 3),};

            ADEBUG << "c2w transform this frame:\n"
                   << p2w[0] << ", " << p2w[1] << ", " << p2w[2] << ", " << p2w[3] << "\n"
                   << p2w[4] << ", " << p2w[5] << ", " << p2w[6] << ", " << p2w[7] << "\n"
                   << p2w[8] << ", " << p2w[9] << ", " << p2w[10] << ", " << p2w[11];
            // cam_coord_cur_ 为相机坐标系的原点在世界坐标系下的投影
            common::IMultAx3x4(p2w, cam_ori, cam_coord_cur_);
            time_diff_ = kTimeDiffDefault; // 默认时间差 1/15 15表示15FPS
            yaw_rate_ = kYawRateDefault;  // 角速度 0
            velocity_ = kVelocityDefault; // 速度 8.333
            timestamp_cur_ = *options.timestamp;
            if (!is_first_frame_) {
                time_diff_ = fabsf(static_cast<float>(timestamp_cur_ - timestamp_pre_));
                ADEBUG << timestamp_cur_ << " " << timestamp_pre_ << std::endl;
                // 2.根据当前时刻 、上一时刻,上上时刻相机在世界坐标系的魏位置,计算相机获取角速度和速度
                camera::GetYawVelocityInfo(time_diff_, cam_coord_cur_, cam_coord_pre_, cam_coord_pre_pre_, &yaw_rate_, &velocity_);
                std::string timediff_yawrate_velocity_text = absl::StrCat("time_diff_: ", std::to_string(time_diff_).substr(0, 4),
                    " | yaw_rate_: ", std::to_string(yaw_rate_).substr(0, 4),
                    " | velocity_: ", std::to_string(velocity_).substr(0, 4));
                ADEBUG << timediff_yawrate_velocity_text << std::endl;
            }
            
            // 输入,左右车道线坐标,速度,角速度和时间间隔
            bool updated = calibrator_.Process(ego_lane, velocity_, yaw_rate_, time_diff_);
            if (updated) {
                *pitch_angle = calibrator_.get_pitch_estimation();
                float vanishing_row = calibrator_.get_vanishing_row();
                AINFO << "#updated pitch angle: " << *pitch_angle;
                AINFO << "#vanishing row: " << vanishing_row;
            }

            if (!is_first_frame_) {
                memcpy(cam_coord_pre_pre_, cam_coord_pre_, sizeof(double) * 3);
            }
            is_first_frame_ = false;
            memcpy(cam_coord_pre_, cam_coord_cur_, sizeof(double) * 3);
            timestamp_pre_ = timestamp_cur_;
            return updated;
        }

上面的代码已经完成了两步:LoadEgoLaneline(*options.lane_objects, &ego_lane)函数为加载左右两车道线的点,camera::GetYawVelocityInfo()为根据定位计算当前帧速度和角速度,这两部分代码相对简单,这里不再一一介绍。我们直接进入calibrator_.Process(ego_lane, velocity_, yaw_rate_, time_diff_)

bool LaneBasedCalibrator::Process(const EgoLane& lane, const float& velocity,const float& yaw_rate,
    const float& time_diff){
    // 根据时间计算位移和转角
    float distance_traveled_in_meter = velocity * time_diff;
    float vehicle_yaw_changed = yaw_rate * time_diff;
    // 3.只有直线运动才能标定,转角超过3°认为不是直线运动
    if (!IsTravelingStraight(vehicle_yaw_changed)) {
        AINFO << "Do not calibate if not moving straight: "
                << "yaw angle changed " << vehicle_yaw_changed;
        vp_buffer_.clear();
        return false;
    }
    VanishingPoint vp_cur;
    VanishingPoint vp_work;
    // Get the current estimation on vanishing point from lane
    // 4.灭点估计?为什么不用神经网络输出结果??
    if (!GetVanishingPoint(lane, &vp_cur)) {
        AINFO << "Lane is not valid for calibration.";
        return false;
    }
    vp_cur.distance_traveled = distance_traveled_in_meter;
    //  std::cout << "#current v-row: " << vp_cur.pixel_pos[1] << std::endl;
    // Push vanishing point into buffer
    PushVanishingPoint(vp_cur);
    if (!PopVanishingPoint(&vp_work)) {
        AINFO << "Driving distance is not long enough";
        return false;
    }
    // Get current estimation on pitch
    // 5.pitch角度估计
    pitch_cur_ = 0.0f;
    if (!GetPitchFromVanishingPoint(vp_work, &pitch_cur_)) {
        AINFO << "Failed to estimate pitch from vanishing point.";
        return false;
    }
    vanishing_row_ = vp_work.pixel_pos[1];
    // Get the filtered output using histogram
    // 6.加入直方图,并进行滤波
    if (!AddPitchToHistogram(pitch_cur_)) {
        AINFO << "Calculated pitch is out-of-range.";
        return false;
    }
    accumulated_straight_driving_in_meter_ += distance_traveled_in_meter;
    //  std::cout << "acc_d: " << accumulated_straight_driving_in_meter_ << "\n";
    if (accumulated_straight_driving_in_meter_ > params_.min_distance_to_update_calibration_in_meter && pitch_histogram_.Process()) {
        pitch_estimation_ = pitch_histogram_.get_val_estimation();
        const float cy = k_mat_[5];
        const float fy = k_mat_[4];
        vanishing_row_ = tanf(pitch_estimation_) * fy + cy;
        accumulated_straight_driving_in_meter_ = 0.0f;
        return true;
    }
    return false;
}

3.2 灭点计算

直线运动的判断仅仅通过转角判断。接着进行灭点计算,这里的灭点要求比较严格认为是两条世界坐标系平行直线的在图像上的交点,所以前面必须保证车辆是直线运动。灭点计算首先采样得到左右车道线的起始端点,再计算两条线段所在直线的端点。灭点计算整个流程如下:

bool LaneBasedCalibrator::GetVanishingPoint(const EgoLane& lane, VanishingPoint* v_point)
{
    assert(v_point != nullptr);
    float line_seg_l[4] = { 0 };
    float line_seg_r[4] = { 0 };

    // Get line segment
    // 通过采样得到车道线线段端点坐标,左车道
    bool get_line_seg_left = SelectTwoPointsFromLineForVanishingPoint(lane.left_line, line_seg_l);
    if (!get_line_seg_left) {
        AINFO << "Left lane is too short.";
        return false;
    }
    // 通过采样得到车道线线段端点坐标 右车道
    bool get_line_seg_right = SelectTwoPointsFromLineForVanishingPoint(lane.right_line, line_seg_r);
    if (!get_line_seg_right) {
        AINFO << "Right lane is too short.";
        return false;
    }
    // Get vanishing point by line segments intersection
    // 向量形式计算两个线段交点
    return GetIntersectionFromTwoLineSegments(line_seg_l, line_seg_r, v_point);
}

Apollo中采用的是向量法求取直线的交点,重点就是直线的向量表达即参数方程,具体代码内中给出的参考

3.3 pitrh角计算

5312c19d48a6aa3098cbe0b856657bfe.png

将其展开

6898d7c46b39115ba19598941771fef8.png 23de7cfb8bf902bb654b098aa7b7ce55.png

整理可得

4471ce071ee93cbe1ccb0e502ac6abd7.png

将其展开可得:

c5f83590260296fab765583e09a9551f.png

我们发现位姿变换T只剩下了第三列,我们知道欧拉角与旋转矩阵的关系参考:

0d726b12bf4add7b5bcf66943de92926.png

根据欧拉角转换为旋转矩阵的公式:

e2927919f1dcf2cc932f96a1bb3664f4.png 114feacb34cb621bedc8a120e98cc81f.png

所以可以近似得到

649e117025f45980bb39639bddf0a752.png

实现代码如下:

bool LaneBasedCalibrator::GetPitchFromVanishingPoint(const VanishingPoint& vp, float* pitch) const
{
    assert(pitch != nullptr);
    const float cx = k_mat_[2];
    const float cy = k_mat_[5];
    const float fx = k_mat_[0];
    const float fy = k_mat_[4];
    float yaw_check = static_cast<float>(atan2(vp.pixel_pos[0] - cx, fx));
    // 车辆为直线运动,yaw角为约等于0
    if (fabs(yaw_check) > params_.max_allowed_yaw_angle_in_radian) {
        return false;
    }
    //pitch角计算
    *pitch = static_cast<float>(atan2(vp.pixel_pos[1] - cy, fy));
    return true;
}

pitch计算完毕后,会进行直方图滤波,具体参考bool HistogramEstimator::Process()函数,这里不再过多叙述。至此,pitch角的计算已经分析完毕。

4 车道线3D处理

3D处理主要是将检测到的2D车道线坐标点,投影到相机坐标系下,然后在相机坐标系下进行3D车道线拟合,具体的接口如下:

bool DarkSCNNLanePostprocessor::Process3D(const LanePostprocessorOptions& options, CameraFrame* frame)
    {
        ConvertImagePoint2Camera(frame);
        PolyFitCameraLaneline(frame);
        return true;
    }

首先调用ConvertImagePoint2Camera(frame)将图像上的车道线点投影到相机归一化平面上,再根据前面计算出的相机pitch角修正该点的坐标,然后根据相机高度与相机成像原理计算车道线的3D坐标。
PolyFitCameraLaneline(frame)主要功能是3D车道线的拟合,但是由于车道平面视为水平面,其车道线上Y坐标为常数,所以车道线方程是关于X和Z的方程,拟合思路其实和2D车道线拟合思路一致,因为外点已经剔除,没有必要再用随机采用一致性算法,同时也是采用矩阵求逆的方法求解直线方程的系数,Apollo中采用了QR分解。直线拟合代码如下:

template <typename Dtype>
    bool PolyFit(const std::vector<Eigen::Matrix<Dtype, 2, 1>>& pos_vec, const int& order,
        Eigen::Matrix<Dtype, max_poly_order + 1, 1>* coeff, const bool& is_x_axis = true){
        if (coeff == nullptr) {
            AERROR << "The coefficient pointer is NULL.";
            return false;
        }
        if (order > max_poly_order) {
            AERROR << "The order of polynomial must be smaller than " << max_poly_order;
            return false;
        }
        int n = static_cast<int>(pos_vec.size());
        // 保证方程有解
        if (n <= order) {
            AERROR << "The number of points should be larger than the order. #points = "
                    << pos_vec.size();
            return false;
        }
        // 构建 Ax = b 的线性方程组
        Eigen::Matrix<Dtype, Eigen::Dynamic, Eigen::Dynamic> A(n, order + 1);
        Eigen::Matrix<Dtype, Eigen::Dynamic, 1> y(n);
        Eigen::Matrix<Dtype, Eigen::Dynamic, 1> result;
        for (int i = 0; i < n; ++i) {
            for (int j = 0; j <= order; ++j) {
                A(i, j) = static_cast<Dtype>(
                    std::pow(is_x_axis ? pos_vec[i].x() : pos_vec[i].y(), j));
            }
            y(i) = is_x_axis ? pos_vec[i].y() : pos_vec[i].x();
        }
        // solve linear least squares
        // QR分解求解系数
        result = A.householderQr().solve(y);
        assert(result.size() == order + 1);

        for (int j = 0; j <= max_poly_order; ++j) {
            (*coeff)(j) = (j <= order) ? result(j) : static_cast<Dtype>(0);
        }
        return true;
    }

至此,车道线检测的细节部分已经分析完毕。

总结

  1. 处于安全考虑,Apollo中车道的判定相当谨慎。

  2. 车道线从2D到3D的拟合非常细节,是从效率、鲁棒性和准确性的综合考虑。

  3. pitch计算依赖于:1.车辆走直线;2.车道线的检测成功。能适应与规范化城市道路。

① 全网独家视频课程

BEV感知、毫米波雷达视觉融合多传感器标定多传感器融合多模态3D目标检测点云3D目标检测目标跟踪Occupancy、cuda与TensorRT模型部署协同感知语义分割、自动驾驶仿真、传感器部署、决策规划、轨迹预测等多个方向学习视频(扫码即可学习

8f07d9aee06b8e4fa7d87ce73424eb93.png 视频官网:www.zdjszx.com

② 国内首个自动驾驶学习社区

近2000人的交流社区,涉及30+自动驾驶技术栈学习路线,想要了解更多自动驾驶感知(2D检测、分割、2D/3D车道线、BEV感知、3D目标检测、Occupancy、多传感器融合、多传感器标定、目标跟踪、光流估计)、自动驾驶定位建图(SLAM、高精地图、局部在线地图)、自动驾驶规划控制/轨迹预测等领域技术方案、AI模型部署落地实战、行业动态、岗位发布,欢迎扫描下方二维码,加入自动驾驶之心知识星球,这是一个真正有干货的地方,与领域大佬交流入门、学习、工作、跳槽上的各类难题,日常分享论文+代码+视频,期待交流!

51c950eaa63966d1b02256e14e049164.png

③【自动驾驶之心】技术交流群

自动驾驶之心是首个自动驾驶开发者社区,聚焦目标检测、语义分割、全景分割、实例分割、关键点检测、车道线、目标跟踪、3D目标检测、BEV感知、多模态感知、Occupancy、多传感器融合、transformer、大模型、点云处理、端到端自动驾驶、SLAM、光流估计、深度估计、轨迹预测、高精地图、NeRF、规划控制、模型部署落地、自动驾驶仿真测试、产品经理、硬件配置、AI求职交流等方向。扫码添加汽车人助理微信邀请入群,备注:学校/公司+方向+昵称(快速入群方式)

39f27a12bc37be578a1401f410822e17.jpeg

④【自动驾驶之心】平台矩阵,欢迎联系我们!

3e39af106b6d2ab4aea7c8688605e8e4.jpeg

<think>好的,用户需要查找关于Apollo 8.0部署的指南和文档。首先,我需要回顾提供的引用内容,看看里面有没有相关的信息。 引用[1]提到Apollo环境管理工具是用来管理环境容器的命令行工具,还提到了添加apt源的步骤。引用[2]和[3]都涉及Apollo 8.0的安装步骤,包括创建工作空间、启动Docker环境,以及二进制安装的优势。引用[4]则介绍了访问仿真界面的方法和后续启动退出的步骤。 用户的问题是关于部署的指南和文档,所以需要整合这些引用的信息,分步骤说明安装和部署过程。可能需要包括系统要求、安装方法(二进制或源码)、Docker环境的配置、仿真界面的访问,以及常见问题处理等部分。 另外,用户示例中的快速排序部分有代码块和相关问题,可能需要类似的结构。需要确保使用正确的LaTeX格式,比如行内公式用$...$,独立公式用$$...$$,并正确引用来源。 接下来,需要检查是否有遗漏的步骤或用户可能遇到的问题,比如依赖项安装、权限问题或网络配置。同时,根据引用内容,Apollo 8.0的二进制安装简化了流程,应该强调这一点,并说明如何通过命令行工具进行环境管理。 最后,生成相关问题时要围绕部署过程中可能遇到的疑问,比如系统需求、优化配置、错误解决和实际应用场景。确保每个引用都正确标注来源,例如在提到创建工作空间时引用[2],在二进制安装时引用[3]。</think>### Apollo 8.0 部署指南与文档整合 #### 1. **系统要求与准备工作** - **操作系统**: 推荐使用 Ubuntu 18.04/20.04 LTS 或其他兼容的 Linux 发行版。 - **依赖项**: 需预先安装 Docker、Git 和必要的开发工具[^2][^3]。 - **工作空间创建**: ```bash mkdir apollo_v8.0 cd apollo_v8.0 ``` 引用[2]提到这是启动 Docker 环境前的必要步骤。 #### 2. **安装 Apollo 8.0** Apollo 8.0 支持**二进制安装**,简化了部署流程: ```bash # 添加 Apollo apt 源(需管理员权限) sudo apt-get update && sudo apt-get install apollo-platform-dev ``` 引用[1][^3]指出,二进制安装避免了全量代码下载,适合快速部署和模块化开发。 #### 3. **启动 Apollo Docker 环境** 通过命令行工具进入容器: ```bash # 启动并进入 Docker 容器 ./apollo.sh start ./apollo.sh enter ``` 此步骤会激活 Apollo 的仿真和开发环境[^4]。 #### 4. **访问仿真界面** 在浏览器中输入 `http://localhost:8888` 可访问 Apollo 的仿真界面,用于验证部署是否成功。 #### 5. **模块化开发与调试** 若需修改特定模块(如规划模块): ```bash # 进入模块目录并编译 cd modules/planning bazel build //modules/planning:planning ``` 引用[3]强调,Apollo 8.0 支持单独模块的源码安装和调试。 #### 6. **常见问题** - **网络配置**: 确保 Docker 容器与主机网络连通。 - **权限问题**: 若 Docker 命令报错,需将用户加入 `docker` 用户组。 - **依赖冲突**: 使用 `apollo.sh clean` 清理缓存后重新编译。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值