訂閱
糾錯
加入自媒體

自動駕駛:基于PCL的激光雷達感知

介紹

自動駕駛是現(xiàn)代技術(shù)中一個相對較新且非常迷人的領域。在2004年的DARPA Grand Challenge期間公開展示,并在2007年轉(zhuǎn)向更具挑戰(zhàn)性的城市環(huán)境,自那以后,工業(yè)界和學術(shù)界一直在追求自動駕駛。

這些應用程序在個人自動駕駛汽車、自動出租車、運輸、送貨等方面都有所不同,但這項技術(shù)還沒有成熟。

自動駕駛陷入低谷的原因之一是,感知組件是一個非常復雜的問題。雖然大多數(shù)團隊都采用基于激光雷達的感知方式,但仍有人試圖通過相機來感知(Tesla 和 Wayve)。

依賴激光雷達的解決方案也可以分為兩類:處理點云的傳統(tǒng)計算機視覺算法和基于深度學習的方法。

神經(jīng)網(wǎng)絡有望以較高的平均精度解決感知問題,然而,如果我們想在最壞的情況下證明合理的準確性,這是不夠的。

在本文中,我們將看一看在PCL(一個開源的點云庫)的幫助下制作的自動駕駛堆棧。

首先,我們將堅持系統(tǒng)級的測試驅(qū)動開發(fā)(TDD),以確保在第一次現(xiàn)場部署之前對我們的整個代碼進行徹底測試。

為此,我們需要一個數(shù)據(jù)集來運行代碼。卡爾斯魯厄理工學院(Karlsruhe Institute of Technology)和芝加哥豐田理工學院(Toyota Technology Institute)2012年的經(jīng)典數(shù)據(jù)集Kitti將非常適合這一目的。這是首批收集的大規(guī)模高質(zhì)量數(shù)據(jù)集之一,可作為自動駕駛領域計算機視覺算法的基準。

Kitti跟蹤由21個同步PNG圖像序列、Velodyne激光雷達掃描和來自RT3003 GPS-IMU模塊的NMEA記錄組成。

數(shù)據(jù)集的一個重要特征是傳感器之間的徹底相互校準,包括矩陣“Tr_imu_velo”,它是從GPS-imu坐標到Velodyne激光雷達坐標的轉(zhuǎn)換。

感知管道的架構(gòu)如下所示。

讓我們分別討論每一個組件,深入挖掘他們的C++實現(xiàn)。

點云抽取

為什么我們可能需要從深度傳感器(可能是一個或幾個激光雷達)中抽取點云?

自動駕駛軟件最重要的要求是滿足實時操作約束。

第一個要求是處理管道要跟上激光雷達掃描采樣的速率。在現(xiàn)實生活中,掃描速度可能從10到25次/秒不等,這導致最大延遲為100毫秒到40毫秒不等。如果某些操作導致延遲超過100 ms(對于每秒10次掃描的速度),要么會發(fā)生幀丟失,要么管道的總延遲將開始任意增長。這里的解決方案之一是丟掉一些點,而不是丟失整個幀。這將逐漸降低準確性指標(召回率和精度),并保持管道實時運行。

第二個要求是系統(tǒng)的總體延遲或反應時間。同樣,總延遲應該被限制在至少100或200毫秒。對于自動駕駛來說,500ms甚至1秒的反應時間是不可接受的。因此,在算法設計開始時,首先采用抽取的方法處理少量的點是有意義的。

抽取的標準選項包括:

1. 有規(guī)律的

2. (偽)隨機

3. 格柵下采樣

常規(guī)下采樣速度很快,但可能會導致點云上的鋸齒模式。隨機或偽隨機下采樣也很快,但可能會導致不可預測的小對象完全消失。像PCL的pcl::VoxelGrid<>類一樣的格柵下采樣是智能和自適應的,但需要額外的計算和內(nèi)存。

原始點云:

大量點云:

多掃描聚合

多掃描聚合是指當車相對于地面移動時,將多個歷史激光雷達掃描記錄到共同坐標系的過程。通用的坐標系統(tǒng)可以是局部導航框架或當前的激光雷達傳感器坐標。我們將以后者為例。

這個階段在理論上是可選的,但在實踐中是非常重要的。問題是,后續(xù)的聚類階段依賴于LiDAR點的密度,如果密度不夠,可能會產(chǎn)生過聚類的影響。過聚類意味著任何對象(汽車、公共汽車、建筑墻等)都可以被分割成幾個部分。

就其本身而言,這可能不是一個檢測障礙的問題,然而,對于感知-跟蹤-聚類的下游模塊來說,這是一個實質(zhì)性的挑戰(zhàn)。跟蹤器可能會不準確地關(guān)聯(lián)對象的各個部分,這最終導致車輛突然剎車。我們絕對不希望聚類中的小錯誤在下游組件中造成雪崩式的錯誤。

多次連續(xù)掃描(5到10次)的聚合成比例地增加了落在每個物體上的激光雷達點的密度,并促進了精確的聚類。汽車運動的一個很好的特點是,汽車能夠從不同的視角觀察同一物體,激光雷達掃描模式覆蓋物體的不同部分。

讓我們看看執(zhí)行聚合的代碼。

第一階段是保留一個限制長度的隊列,其中包含歷史點云以及后續(xù)掃描儀的姿勢轉(zhuǎn)換。請注意,我們?nèi)绾问褂脧腞T3003 GPS-IMU模塊獲得的平移速度[Vx,Vy]和旋轉(zhuǎn)速度Wz來構(gòu)造姿勢變換。

// We accumulate the incoming scans along with their localization metadata

// into a deque to perform subsequent aggregation.

  Transform3f next_veh_pose_vs_curr = Transform3f::Identity();

if (gpsimu_ptr)

    float frame_interval_sec = 0.1f;


    // First, we need to calculate yaw change given the yaw rate

    // (angular speed over Z axis) and the time inteval between frames.

    float angle_z = gpsimu_ptr->wz * frame_interval_sec;

    auto rot = Eigen::AngleAxisf(angle_z, Eigen::Vector3f::UnitZ());
           next_veh_pose_vs_curr.rotate(rot);

    // Second, we need a translation transform to the next frame

    // given the speed of the ego-vehicle and the frame interval.

    next_veh_pose_vs_curr.translate(Eigen::Vector3f(
              gpsimu_ptr->vf * frame_interval_sec,
              gpsimu_ptr->vl * frame_interval_sec,

       0.0f
           ));

  }


// Since later we want to aggregate all scans into the coordinate

// frame of the last scans, we need the inverse transform.

auto curr_veh_pose_vs_next = next_veh_pose_vs_curr.inverse();

// Put the resulting pair of the cloud and the transform into a queue.

auto cloud_and_metadata = CloudAndMetadata{decimated_cloud_ptr, curr_veh_pose_vs_next};

m_queue.push_back(cloud_and_metadata);

while (m_queue.size() > m_params->m_num_clouds)

      m_queue.pop_front();

  }

在第二階段,我們從最新的掃描時間向后遍歷隊列,進行聚合,并將聚合轉(zhuǎn)換應用到每個歷史幀。

使用這種方法,計算成本為O(N*D),其中N是點的數(shù)量,D是歷史的深度(掃描的數(shù)量)。

// We accumulate the transforms starting from the latest back in time and

// transform each historical point cloud into the coordinates of the current frame.

auto aggregated_cloud_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZI> >();

Eigen::Matrix4f aggragated_transform = Eigen::Matrix4f::Identity();

for (int i = m_queue.size()-1; i >= 0; i--)

    constauto& cloud_and_metadata = m_queue[i];

    constauto& cloud_ptr = cloud_and_metadata.cloud_ptr;

    constauto& trans = cloud_and_metadata.transform_to_next;

    pcl::PointCloud<pcl::PointXYZI>::Ptr transformed_cloud_ptr;

    if (i 。 m_queue.size()-1)



      aggragated_transform *= trans.matrix();

      transformed_cloud_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZI> >();

      pcl::transformPointCloud(*cloud_ptr, *transformed_cloud_ptr, aggragated_transform);

  }

else

  {

    // For the current scan no need to transform
           transformed_cloud_ptr = cloud_ptr;

  }


// Concatenate the transformed point cloud into the aggregate cloud

  *aggregated_cloud_ptr += *transformed_cloud_ptr;

聚合后,如果移動的物體看起來有點模糊,點云會顯得有些模糊?梢栽诰垲愲A段進一步解決。在這個階段,我們需要的是一個更密集的點云,它可以從多個幀中積累信息。

地面移除

感知堆棧的目的是提供有關(guān)動態(tài)對象和靜止障礙物的信息。汽車應該在道路上行駛,通常路面不被視為障礙物。

因此,我們可以移除所有從路面反射的激光雷達點。要做到這一點,我們首先將地面檢測為平面或曲面,并移除表面周圍或下方約10厘米的所有點。有幾種方法可以檢測點云上的地面:

1. 用Ransac探測平面

2. 用Hough變換檢測平面

3. 基于Floodfill的非平面表面檢測

讓我們在EGIN和PCL庫的幫助下,研究RANSAC的C++實現(xiàn)。

首先,讓我們定義候選平面。我們將使用基點加法向量的形式。

// A plane is represented with a point on the plane (base_point)

// and a normal vector to the plane.

struct Plane

    Eigen::Vector3f base_point;

    Eigen::Vector3f normal;

    EIGEN_MAKE_ALIGNED_OPERATOR_NEW

};

然后,我們定義了一個輔助函數(shù),它允許我們在點云轉(zhuǎn)換為平面坐標后,在Z坐標上找到滿足條件的所有點的索引。代碼中的注釋給出了實現(xiàn)的細節(jié)。

// This helper function finds indices of points that are considered inliers,

// given a plane description and a condition on distance from the plane.

std::vector<size_t> find_inlier_indices(

  const pcl::PointCloud<pcl::PointXYZ>::Ptr& input_cloud_ptr,

  const Plane& plane,

  std::function<bool(float)> condition_z_fn)

  typedef Eigen::Transform<float, 3, Eigen::Affine, Eigen::DontAlign> Transform3f;

  auto base_point = plane.base_point;

  auto normal = plane.normal;

  // Before rotation of the coordinate frame we need to relocate the point cloud to

  // the position of base_point of the plane.

  Transform3f world_to_ransac_base = Transform3f::Identity();

  world_to_ransac_base.translate(-base_point);

  auto ransac_base_cloud_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZ> >();

  pcl::transformPointCloud(*input_cloud_ptr, *ransac_base_cloud_ptr, world_to_ransac_base);

  // We are going to use a quaternion to determine the rotation transform

  // which is required to rotate a coordinate system that plane's normal

  // becomes aligned with Z coordinate axis.

  auto rotate_to_plane_quat = Eigen::Quaternionf::FromTwoVectors(
            normal,
             Eigen::Vector3f::UnitZ(

  ).normalized();

  // Now we can create a rotation transform and align the cloud that

  // the candidate plane matches XY plane

  Transform3f ransac_base_to_ransac = Transform3f::Identity();

  ransac_base_to_ransac.rotate(rotate_to_plane_quat);

  auto aligned_cloud_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZ> >();

  pcl::transformPointCloud(*ransac_base_cloud_ptr, *aligned_cloud_ptr, ransac_base_to_ransac);

  // Once the point cloud is transformed into the plane coordinates,

  // We can apply a simple criterion on Z coordinate to find inliers.

  std::vector<size_t> indices;

  for (size_t i_point = 0; i_point < aligned_cloud_ptr->size(); i_point++)

      constauto& p = (*aligned_cloud_ptr)[i_point];

     if (condition_z_fn(p.z))

   {

          indices.push_back(i_point);

     }

   }

   return indices;

最后,主要的Ransac實現(xiàn)如下所示。第一步是基于Z坐標對點進行粗略過濾。此外,我們需要再次抽取點,因為我們不需要聚集云中的所有點來驗證候選平面。這些操作可以一次完成。

接下來,我們開始迭代。在C++標準庫的 std::mt19937偽隨機生成器的幫助下,每次迭代采樣3個隨機點。對于每個三元組,我們計算平面并確保其法線指向上方。然后我們使用相同的輔助函數(shù)find_inlier_index來計算內(nèi)點的數(shù)量。

迭代結(jié)束后,我們剩下的是最佳候選平面,我們最終使用它來復制點云中所有索引不存在于列表中的點的副本。請注意std::unordered_set<>的用法。它允許執(zhí)行恒定時間O(1)搜索,而不是對std::vector<>進行的線性O(N)搜索。

// This function performs plane detection with RANSAC sampling of planes

// that lie on triplets of points randomly sampled from the cloud.

// Among all trials the plane that is picked is the one that has the highest

// number of inliers. Inlier points are then removed as belonging to the ground.

auto remove_ground_ransac(

    pcl::PointCloud<pcl::PointXYZ>::Ptr input_cloud_ptr)

  // Threshold for rough point dropping by Z coordinate (meters)

  constfloat rough_filter_thr = 0.5f;

  // How much to decimate the input cloud for RANSAC sampling and inlier counting

  constsize_t decimation_rate = 10;

  // Tolerance threshold on the distance of an inlier to the plane (meters)

  constfloat ransac_tolerance = 0.1f;

  // After the final plane is found this is the threshold below which all

  // points are discarded as belonging to the ground.

  constfloat remove_ground_threshold = 0.2f;


  // To reduce the number of outliers (non-ground points) we can roughly crop

  // the point cloud by Z coordinate in the range (-rough_filter_thr, rough_filter_thr).

  // Simultaneously we perform decimation of the remaining points since the full

  // point cloud is excessive for RANSAC.

  std::mt19937::result_type decimation_seed = 41;

  std::mt19937 rng_decimation(decimation_seed);

  auto decimation_gen = std::bind(

       std::uniform_int_distribution<size_t>(0, decimation_rate), rng_decimation);

 

  auto filtered_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZ> >();

  for (constauto& p : *input_cloud_ptr)

       if ((p.z > -rough_filter_thr) && (p.z < rough_filter_thr))
            {

           // Use random number generator to avoid introducing patterns

          // (which are possible with structured subsampling

          // like picking each Nth point).

           if (decimation_gen() == 0)
               {
                        filtered_ptr->push_back(p);
                 }
            }
        }


  // We need a random number generator for sampling triplets of points.

  std::mt19937::result_type sampling_seed = 42;

  std::mt19937 sampling_rng(sampling_seed);

  auto random_index_gen = std::bind(

       std::uniform_int_distribution<size_t>(0, filtered_ptr->size()), sampling_rng);


  // Number of RANSAC trials

  constsize_t num_iterations = 25;

  // The best plane is determined by a pair of (number of inliers, plane specification)

  typedefstd::pair<size_t, Plane> BestPair;

  auto best = std::unique_ptr<BestPair>();

  for (size_t i_iter = 0; i_iter < num_iterations; i_iter++)
      {


       // Sample 3 random points.

      // pa is special in the sense that is becomes an anchor - a base_point of the plane
              Eigen::Vector3f pa = (*filtered_ptr)[random_index_gen()].getVector3fMap();
              Eigen::Vector3f pb = (*filtered_ptr)[random_index_gen()].getVector3fMap();
              Eigen::Vector3f pc = (*filtered_ptr)[random_index_gen()].getVector3fMap();

       // Here we figure out the normal to the plane which can be easily calculated
             // as a normalized cross product.
              auto vb = pb - pa;
              auto vc = pc - pa;
              Eigen::Vector3f normal = vb.cross(vc).normalized();

      // Flip the normal if points down
             if (normal.dot(Eigen::Vector3f::UnitZ()) < 0)
            {
                   normal = -normal;
              }
      

      Plane plane{pa, normal};

    

    // Call find_inlier_indices to retrieve inlier indices.
          // We will need only the number of inliers.
          auto inlier_indices = find_inlier_indices(filtered_ptr, plane,
                [ransac_tolerance](float z) -> bool {
                         return (z >= -ransac_tolerance) && (z <= ransac_tolerance);
                  });


          // If new best plane is found, update the best
          bool found_new_best = false;
          if (best)
         {
                  if (inlier_indices.size() > best->first)
                {
                          found_new_best = true;
                  }
           }
          else
         {
                // For the first trial update anyway
                found_new_best = true;
           }

   if (found_new_best)
        {
                  best = std::unique_ptr<BestPair>(new BestPair{inlier_indices.size(), plane});
          }

  }

  // For the best plane filter out all the points that are

 // below the plane + remove_ground_threshold.

  pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_no_ground_ptr;
if (best)
      {
                cloud_no_ground_ptr = std::make_shared<pcl::PointCloud<pcl::PointXYZ> >();
                auto inlier_indices = find_inlier_indices(input_cloud_ptr, best->second,
                     [remove_ground_threshold](float z) -> bool {
                            return z <= remove_ground_threshold;
                       });
                std::unordered_set<size_t> inlier_set(inlier_indices.begin(), inlier_indices.end());
                for (size_t i_point = 0; i_point < input_cloud_ptr->size(); i_point++)
              {
                       bool extract_non_ground = true;
                       if ((inlier_set.find(i_point) == inlier_set.end()) == extract_non_ground)
                     {
                             constauto& p = (*input_cloud_ptr)[i_point];
                             cloud_no_ground_ptr->push_back(p);
                       }
                }
           }
          else
         {
               cloud_no_ground_ptr = input_cloud_ptr;
           }

    return cloud_no_ground_ptr;

讓我們看看地面移除的結(jié)果。

在移除地面之前:

地面移除后:

移除地面后,我們準備對剩余的點進行聚類,并通過凸包提取來壓縮對象元數(shù)據(jù)。這兩個階段應該有自己的文章。我將在即將到來的第二部分中介紹它們的實現(xiàn)。同時下面是聚類的最終結(jié)果——凸包提取。

可視化的最終對象:

凸包絕對是任何跟蹤器都渴望接受作為其輸入的元數(shù)據(jù)類型。它們在RAM使用方面更加緊湊,并且比定向邊界框更準確地表示對象的邊界。

KITTI 0003中的聚類點云:

結(jié)論

我相信,在生活質(zhì)量和整體生產(chǎn)力方面,自動駕駛將是人類的一次飛躍。

參考資料:

image.png

感謝閱讀!

       原文標題 : 自動駕駛:基于PCL的激光雷達感知

聲明: 本文由入駐維科號的作者撰寫,觀點僅代表作者本人,不代表OFweek立場。如有侵權(quán)或其他問題,請聯(lián)系舉報。

發(fā)表評論

0條評論,0人參與

請輸入評論內(nèi)容...

請輸入評論/評論長度6~500個字

您提交的評論過于頻繁,請輸入驗證碼繼續(xù)

暫無評論

暫無評論

人工智能 獵頭職位 更多
掃碼關(guān)注公眾號
OFweek人工智能網(wǎng)
獲取更多精彩內(nèi)容
文章糾錯
x
*文字標題:
*糾錯內(nèi)容:
聯(lián)系郵箱:
*驗 證 碼:

粵公網(wǎng)安備 44030502002758號