自動(dòng)駕駛:基于PCL的激光雷達(dá)感知
介紹
自動(dòng)駕駛是現(xiàn)代技術(shù)中一個(gè)相對(duì)較新且非常迷人的領(lǐng)域。在2004年的DARPA Grand Challenge期間公開展示,并在2007年轉(zhuǎn)向更具挑戰(zhàn)性的城市環(huán)境,自那以后,工業(yè)界和學(xué)術(shù)界一直在追求自動(dòng)駕駛。
這些應(yīng)用程序在個(gè)人自動(dòng)駕駛汽車、自動(dòng)出租車、運(yùn)輸、送貨等方面都有所不同,但這項(xiàng)技術(shù)還沒有成熟。
自動(dòng)駕駛陷入低谷的原因之一是,感知組件是一個(gè)非常復(fù)雜的問(wèn)題。雖然大多數(shù)團(tuán)隊(duì)都采用基于激光雷達(dá)的感知方式,但仍有人試圖通過(guò)相機(jī)來(lái)感知(Tesla 和 Wayve)。
依賴激光雷達(dá)的解決方案也可以分為兩類:處理點(diǎn)云的傳統(tǒng)計(jì)算機(jī)視覺算法和基于深度學(xué)習(xí)的方法。
神經(jīng)網(wǎng)絡(luò)有望以較高的平均精度解決感知問(wèn)題,然而,如果我們想在最壞的情況下證明合理的準(zhǔn)確性,這是不夠的。
在本文中,我們將看一看在PCL(一個(gè)開源的點(diǎn)云庫(kù))的幫助下制作的自動(dòng)駕駛堆棧。
首先,我們將堅(jiān)持系統(tǒng)級(jí)的測(cè)試驅(qū)動(dòng)開發(fā)(TDD),以確保在第一次現(xiàn)場(chǎng)部署之前對(duì)我們的整個(gè)代碼進(jìn)行徹底測(cè)試。
為此,我們需要一個(gè)數(shù)據(jù)集來(lái)運(yùn)行代碼?査刽敹蚶砉W(xué)院(Karlsruhe Institute of Technology)和芝加哥豐田理工學(xué)院(Toyota Technology Institute)2012年的經(jīng)典數(shù)據(jù)集Kitti將非常適合這一目的。這是首批收集的大規(guī)模高質(zhì)量數(shù)據(jù)集之一,可作為自動(dòng)駕駛領(lǐng)域計(jì)算機(jī)視覺算法的基準(zhǔn)。
Kitti跟蹤由21個(gè)同步PNG圖像序列、Velodyne激光雷達(dá)掃描和來(lái)自RT3003 GPS-IMU模塊的NMEA記錄組成。
數(shù)據(jù)集的一個(gè)重要特征是傳感器之間的徹底相互校準(zhǔn),包括矩陣“Tr_imu_velo”,它是從GPS-imu坐標(biāo)到Velodyne激光雷達(dá)坐標(biāo)的轉(zhuǎn)換。
感知管道的架構(gòu)如下所示。
讓我們分別討論每一個(gè)組件,深入挖掘他們的C++實(shí)現(xiàn)。
點(diǎn)云抽取
為什么我們可能需要從深度傳感器(可能是一個(gè)或幾個(gè)激光雷達(dá))中抽取點(diǎn)云?
自動(dòng)駕駛軟件最重要的要求是滿足實(shí)時(shí)操作約束。
第一個(gè)要求是處理管道要跟上激光雷達(dá)掃描采樣的速率。在現(xiàn)實(shí)生活中,掃描速度可能從10到25次/秒不等,這導(dǎo)致最大延遲為100毫秒到40毫秒不等。如果某些操作導(dǎo)致延遲超過(guò)100 ms(對(duì)于每秒10次掃描的速度),要么會(huì)發(fā)生幀丟失,要么管道的總延遲將開始任意增長(zhǎng)。這里的解決方案之一是丟掉一些點(diǎn),而不是丟失整個(gè)幀。這將逐漸降低準(zhǔn)確性指標(biāo)(召回率和精度),并保持管道實(shí)時(shí)運(yùn)行。
第二個(gè)要求是系統(tǒng)的總體延遲或反應(yīng)時(shí)間。同樣,總延遲應(yīng)該被限制在至少100或200毫秒。對(duì)于自動(dòng)駕駛來(lái)說(shuō),500ms甚至1秒的反應(yīng)時(shí)間是不可接受的。因此,在算法設(shè)計(jì)開始時(shí),首先采用抽取的方法處理少量的點(diǎn)是有意義的。
抽取的標(biāo)準(zhǔn)選項(xiàng)包括:
1. 有規(guī)律的
2. (偽)隨機(jī)
3. 格柵下采樣
常規(guī)下采樣速度很快,但可能會(huì)導(dǎo)致點(diǎn)云上的鋸齒模式。隨機(jī)或偽隨機(jī)下采樣也很快,但可能會(huì)導(dǎo)致不可預(yù)測(cè)的小對(duì)象完全消失。像PCL的pcl::VoxelGrid<>類一樣的格柵下采樣是智能和自適應(yīng)的,但需要額外的計(jì)算和內(nèi)存。
原始點(diǎn)云:
大量點(diǎn)云:
多掃描聚合
多掃描聚合是指當(dāng)車相對(duì)于地面移動(dòng)時(shí),將多個(gè)歷史激光雷達(dá)掃描記錄到共同坐標(biāo)系的過(guò)程。通用的坐標(biāo)系統(tǒng)可以是局部導(dǎo)航框架或當(dāng)前的激光雷達(dá)傳感器坐標(biāo)。我們將以后者為例。
這個(gè)階段在理論上是可選的,但在實(shí)踐中是非常重要的。問(wèn)題是,后續(xù)的聚類階段依賴于LiDAR點(diǎn)的密度,如果密度不夠,可能會(huì)產(chǎn)生過(guò)聚類的影響。過(guò)聚類意味著任何對(duì)象(汽車、公共汽車、建筑墻等)都可以被分割成幾個(gè)部分。
就其本身而言,這可能不是一個(gè)檢測(cè)障礙的問(wèn)題,然而,對(duì)于感知-跟蹤-聚類的下游模塊來(lái)說(shuō),這是一個(gè)實(shí)質(zhì)性的挑戰(zhàn)。跟蹤器可能會(huì)不準(zhǔn)確地關(guān)聯(lián)對(duì)象的各個(gè)部分,這最終導(dǎo)致車輛突然剎車。我們絕對(duì)不希望聚類中的小錯(cuò)誤在下游組件中造成雪崩式的錯(cuò)誤。
多次連續(xù)掃描(5到10次)的聚合成比例地增加了落在每個(gè)物體上的激光雷達(dá)點(diǎn)的密度,并促進(jìn)了精確的聚類。汽車運(yùn)動(dòng)的一個(gè)很好的特點(diǎn)是,汽車能夠從不同的視角觀察同一物體,激光雷達(dá)掃描模式覆蓋物體的不同部分。
讓我們看看執(zhí)行聚合的代碼。
第一階段是保留一個(gè)限制長(zhǎng)度的隊(duì)列,其中包含歷史點(diǎn)云以及后續(xù)掃描儀的姿勢(shì)轉(zhuǎn)換。請(qǐng)注意,我們?nèi)绾问褂脧腞T3003 GPS-IMU模塊獲得的平移速度[Vx,Vy]和旋轉(zhuǎn)速度Wz來(lái)構(gòu)造姿勢(shì)變換。
// 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();
}
}
在第二階段,我們從最新的掃描時(shí)間向后遍歷隊(duì)列,進(jìn)行聚合,并將聚合轉(zhuǎn)換應(yīng)用到每個(gè)歷史幀。
使用這種方法,計(jì)算成本為O(N*D),其中N是點(diǎ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;
}
聚合后,如果移動(dòng)的物體看起來(lái)有點(diǎn)模糊,點(diǎn)云會(huì)顯得有些模糊。可以在聚類階段進(jìn)一步解決。在這個(gè)階段,我們需要的是一個(gè)更密集的點(diǎn)云,它可以從多個(gè)幀中積累信息。
地面移除
感知堆棧的目的是提供有關(guān)動(dòng)態(tài)對(duì)象和靜止障礙物的信息。汽車應(yīng)該在道路上行駛,通常路面不被視為障礙物。
因此,我們可以移除所有從路面反射的激光雷達(dá)點(diǎn)。要做到這一點(diǎn),我們首先將地面檢測(cè)為平面或曲面,并移除表面周圍或下方約10厘米的所有點(diǎn)。有幾種方法可以檢測(cè)點(diǎn)云上的地面:
1. 用Ransac探測(cè)平面
2. 用Hough變換檢測(cè)平面
3. 基于Floodfill的非平面表面檢測(cè)
讓我們?cè)贓GIN和PCL庫(kù)的幫助下,研究RANSAC的C++實(shí)現(xiàn)。
首先,讓我們定義候選平面。我們將使用基點(diǎ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_M(jìn)AKE_ALIGNED_OPERATOR_NEW
};
然后,我們定義了一個(gè)輔助函數(shù),它允許我們?cè)邳c(diǎn)云轉(zhuǎn)換為平面坐標(biāo)后,在Z坐標(biāo)上找到滿足條件的所有點(diǎn)的索引。代碼中的注釋給出了實(shí)現(xiàn)的細(xì)節(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實(shí)現(xiàn)如下所示。第一步是基于Z坐標(biāo)對(duì)點(diǎn)進(jìn)行粗略過(guò)濾。此外,我們需要再次抽取點(diǎn),因?yàn)槲覀儾恍枰奂浦械乃悬c(diǎn)來(lái)驗(yàn)證候選平面。這些操作可以一次完成。
接下來(lái),我們開始迭代。在C++標(biāo)準(zhǔn)庫(kù)的 std::mt19937偽隨機(jī)生成器的幫助下,每次迭代采樣3個(gè)隨機(jī)點(diǎn)。對(duì)于每個(gè)三元組,我們計(jì)算平面并確保其法線指向上方。然后我們使用相同的輔助函數(shù)find_inlier_index來(lái)計(jì)算內(nèi)點(diǎn)的數(shù)量。
迭代結(jié)束后,我們剩下的是最佳候選平面,我們最終使用它來(lái)復(fù)制點(diǎn)云中所有索引不存在于列表中的點(diǎn)的副本。請(qǐng)注意std::unordered_set<>的用法。它允許執(zhí)行恒定時(shí)間O(1)搜索,而不是對(duì)std::vector<>進(jìn)行的線性O(shè)(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é)果。
在移除地面之前:
地面移除后:
移除地面后,我們準(zhǔn)備對(duì)剩余的點(diǎn)進(jìn)行聚類,并通過(guò)凸包提取來(lái)壓縮對(duì)象元數(shù)據(jù)。這兩個(gè)階段應(yīng)該有自己的文章。我將在即將到來(lái)的第二部分中介紹它們的實(shí)現(xiàn)。同時(shí)下面是聚類的最終結(jié)果——凸包提取。
可視化的最終對(duì)象:
凸包絕對(duì)是任何跟蹤器都渴望接受作為其輸入的元數(shù)據(jù)類型。它們?cè)赗AM使用方面更加緊湊,并且比定向邊界框更準(zhǔn)確地表示對(duì)象的邊界。
KITTI 0003中的聚類點(diǎn)云:
結(jié)論
我相信,在生活質(zhì)量和整體生產(chǎn)力方面,自動(dòng)駕駛將是人類的一次飛躍。
參考資料:
感謝閱讀!
原文標(biāo)題 : 自動(dòng)駕駛:基于PCL的激光雷達(dá)感知
發(fā)表評(píng)論
請(qǐng)輸入評(píng)論內(nèi)容...
請(qǐng)輸入評(píng)論/評(píng)論長(zhǎng)度6~500個(gè)字
最新活動(dòng)更多
-
即日-11.13立即報(bào)名>>> 【在線會(huì)議】多物理場(chǎng)仿真助跑新能源汽車
-
11月28日立即報(bào)名>>> 2024工程師系列—工業(yè)電子技術(shù)在線會(huì)議
-
12月19日立即報(bào)名>> 【線下會(huì)議】OFweek 2024(第九屆)物聯(lián)網(wǎng)產(chǎn)業(yè)大會(huì)
-
即日-12.26火熱報(bào)名中>> OFweek2024中國(guó)智造CIO在線峰會(huì)
-
即日-2025.8.1立即下載>> 《2024智能制造產(chǎn)業(yè)高端化、智能化、綠色化發(fā)展藍(lán)皮書》
-
精彩回顧立即查看>> 【限時(shí)免費(fèi)下載】TE暖通空調(diào)系統(tǒng)高效可靠的組件解決方案
推薦專題
- 1 【一周車話】沒有方向盤和踏板的車,你敢坐嗎?
- 2 特斯拉發(fā)布無(wú)人駕駛車,還未迎來(lái)“Chatgpt時(shí)刻”
- 3 特斯拉股價(jià)大跌15%:Robotaxi離落地還差一個(gè)蘿卜快跑
- 4 馬斯克給的“驚喜”夠嗎?
- 5 打完“價(jià)格戰(zhàn)”,大模型還要比什么?
- 6 馬斯克致敬“國(guó)產(chǎn)蘿卜”?
- 7 神經(jīng)網(wǎng)絡(luò),誰(shuí)是盈利最強(qiáng)企業(yè)?
- 8 比蘋果偉大100倍!真正改寫人類歷史的智能產(chǎn)品降臨
- 9 諾獎(jiǎng)進(jìn)入“AI時(shí)代”,人類何去何從?
- 10 Open AI融資后成萬(wàn)億獨(dú)角獸,AI人才之爭(zhēng)開啟
- 高級(jí)軟件工程師 廣東省/深圳市
- 自動(dòng)化高級(jí)工程師 廣東省/深圳市
- 光器件研發(fā)工程師 福建省/福州市
- 銷售總監(jiān)(光器件) 北京市/海淀區(qū)
- 激光器高級(jí)銷售經(jīng)理 上海市/虹口區(qū)
- 光器件物理工程師 北京市/海淀區(qū)
- 激光研發(fā)工程師 北京市/昌平區(qū)
- 技術(shù)專家 廣東省/江門市
- 封裝工程師 北京市/海淀區(qū)
- 結(jié)構(gòu)工程師 廣東省/深圳市