地面分割算法
摘要:本文整理 Ray、Scan 与 RANSAC 三种地面分割方法,说明几何判据、参数作用与适用场景,并分别通过核心代码片段说明三种算法的分组、地面判断与输出过程,理解 PCL 在点云处理链路中的作用。
1. 地面分割的目标
地面分割将输入点云划分为地面点与非地面点,为障碍物聚类提供输入。非地面点中仍可能包含噪声,不能直接等同于有效障碍物。处理前需要统一坐标系、移除 NaN/Inf,并明确传感器高度和车辆自身的过滤范围。
下面以 Autoware 的三种地面分割方法为例整理。表中的默认值来自参考文档,实际部署应以所用版本的源码与配置为准。
2. Ray Ground Filter(射线法)
流程:点云沿径向分离,可以根据各个角度的射线,得到点之间的距离与角度信息,从靠近车开始,依次对每条射线上的地面进行分类。
对点 ,柱坐标表示为:
atan2 可以区分四个象限。按方位角分组后,每组按半径由近到远排序。相邻点的变化量为:
当 时,局部坡角可写为:
用于理解局部高度容差的简化判据为:
其中 是最小高度容差。实际分类还要结合全局坡度、前一点的类别与重新分类条件,不能只凭一次坡度比较判断地面。坐标系中的地面高度也不一定为零。

核心参数:
| Name | Type | Description | Default | Range |
|---|---|---|---|---|
| min_x | float | 手动设置车辆自身过滤范围的边界 [m] | -0.01 | N/A |
| max_x | float | 手动设置车辆自身过滤范围的边界 [m] | 0.01 | N/A |
| min_y | float | 手动设置车辆自身过滤范围的边界 [m] | -0.01 | N/A |
| max_y | float | 手动设置车辆自身过滤范围的边界 [m] | 0.01 | N/A |
| use_vehicle_footprint | boolean | 是否使用车辆自身的占地范围过滤点云 | false | N/A |
| general_max_slope | float | 由 general_max_slope 形成的三角区域称为 global cone,全局锥形区域。如果点落在 global cone 外部,则认为该点不是地面点。 | 8.0 | N/A |
| local_max_slope | float | 由 local_max_slope 形成的三角区域称为 local cone,局部锥形区域。该参数用于根据相邻点之间的连续性判断点是否属于地面。 | 6.0 | N/A |
| initial_max_slope | float | 由于传感器近处存在盲区,物体首次被扫描到的位置通常离车较远,该处点云分辨率与后续区域不同,因此单独设置一个初始坡度阈值。 | 3.0 | N/A |
| radial_divider_angle | float | 用于按角度将点云切分成一条条 ray/射线。 | 1.0 | N/A |
| min_height_threshold | float | 当相邻点距离太近时,仅靠 local cone 难以判断连续性,因此使用该参数设置局部高度容差下限。 | 0.15 | N/A |
| concentric_divider_distance | float | 同心分组的径向距离尺度 [m];不可直接理解为整帧点云的最小检测半径。 | 0.0 | N/A |
| reclass_distance_threshold | float | 用于判断当前点是否距离前一个点太远。如果距离太远, 则不再直接沿用前一个点的连续性判断,而是重新分类。 | 0.1 | N/A |
| publish_processing_time_detail | boolean | 是否发布详细处理耗时信息。主要用于调试和性能分析。 | false | N/A |
2.1 代码讲解:按方位角分组并排序
以下是按算法步骤整理的 C++ 核心片段,用于解释处理逻辑,并非 Autoware 源码原样摘录。输入使用 pcl::PointCloud<pcl::PointXYZ>,已去除无效点,坐标单位为米。片段中的配置变量由调用方提供,角度先换算为弧度。
struct RayPoint {
int index; // 保留原始点云索引
double radius;
double height;
};
const double pi = std::acos(-1.0);
const double angle_step = radial_divider_angle * pi / 180.0;
// angle_step 必须大于 0。
const int ray_count = static_cast<int>(std::ceil(2.0 * pi / angle_step));
std::vector<std::vector<RayPoint>> rays(ray_count);
for (int i = 0; i < static_cast<int>(cloud->size()); ++i) {
const auto& p = cloud->points[i];
double theta = std::atan2(p.y, p.x);
if (theta < 0.0) theta += 2.0 * pi;
const int bin = std::min(ray_count - 1,
static_cast<int>(theta / angle_step));
rays[bin].push_back({i, std::hypot(p.x, p.y), p.z});
}
for (auto& ray : rays) {
std::sort(ray.begin(), ray.end(), [](const auto& a, const auto& b) {
return a.radius < b.radius;
});
}
atan2 得到方位角,负角度转换到正区间后即可计算射线编号。hypot 计算水平距离;sort 保证每条射线由近到远处理,让前一点成为当前点的几何参考。index 始终指向原始 PCL 点云,后续输出不需要复制整份点数据。
2.2 代码讲解:局部连续性与全局约束
下面展示简化分类器:前一点是地面时沿局部连续性扩展;前一点不是地面时,需要全局约束辅助恢复地面。它省略了完整实现中的初始坡度、车辆轮廓与特殊重新分类分支。
const double local_tan = std::tan(local_max_slope * pi / 180.0);
const double global_tan = std::tan(general_max_slope * pi / 180.0);
pcl::PointIndices ground_indices, non_ground_indices;
for (const auto& ray : rays) {
double prev_r = 0.0;
double prev_z = ground_z; // 当前坐标系中的参考地面高度
bool prev_ground = true;
for (const auto& p : ray) {
const double dr = p.radius - prev_r;
const double local_limit = std::max(min_height_threshold,
dr * local_tan);
const double global_limit = std::max(min_height_threshold,
p.radius * global_tan);
const bool local_ok = std::abs(p.height - prev_z) <= local_limit;
const bool global_ok = std::abs(p.height - ground_z) <= global_limit;
const bool is_ground = local_ok && (prev_ground || global_ok);
(is_ground ? ground_indices : non_ground_indices).indices.push_back(p.index);
prev_r = p.radius;
prev_z = p.height;
prev_ground = is_ground;
}
}
dr * local_tan 将坡角约束转换为允许的高度差,min_height_threshold 避免点间距很小时容差过小。prev_ground 保存分类状态:遇到障碍物后不能仅因其表面平坦就继续认作地面。这里展示的是状态更新思路,不包含全部恢复条件。
最后可用 pcl::ExtractIndices 配合两组索引输出地面与非地面点云。Ray 的核心是分组、排序和条件判断,PCL 主要提供点云容器与索引提取。
3. Scan Ground Filter(扫描法)
流程:
- 原始点云
- 按方向切成很多扫描线
- 每条扫描线的点,由近到远排序
- 在每条线上划成一个个格子grid
- 判断每个点:地面/非地面/超出范围

核心参数:
| Name | Type | Description | Default | Range |
|---|---|---|---|---|
| global_slope_max_angle_deg | float | 要分类为地面或物体的全局角度[deg]。较大的阈值可能会减少高边坡道路分类的假阳性,但可能会导致非地面分类的假阴性增加,特别是对于小物体。 | 10.0 | N/A |
| local_slope_max_angle_deg | float | 与相邻点比较时,分类为地面或物体的局部角度[deg]。较小的值可以提高倾斜表面物体的分类精度。这应该与split_points_distance_tolerance值一起考虑。 | 13.0 | N/A |
| split_points_distance_tolerance | float | 区分远近的xy距离阈值[m] | 0.2 | N/A |
| use_virtual_ground_point | boolean | 是否使用前轮的接地中心作为虚拟接地点 | true | N/A |
| split_height_distance | float | 与相邻点比较时区分地面和非地面点云的高度阈值[m]。小阈值提高了非地面点的分类,特别是对于垂直分辨率较高的激光雷达。然而,它可能会导致小台阶路面或未对齐的多激光雷达配置出现误报。 | 0.2 | N/A |
| non_ground_height_threshold | float | 相对预测地面的非地面高度阈值 [m],仅用于 elevation_grid_mode | 0.2 | N/A |
| grid_size_m | float | 第一个网格大小[m],仅适用于elevation_grid_mode。较大的值可以提高地表的预测稳定性。适用于粗糙表面或多激光雷达配置 | 0.1 | N/A |
| gnd_grid_buffer_size | integer | 用于估算局部地面坡度的网格数量,仅适用于elevation_grid_mode | 4 | N/A |
| detection_range_z_max | float | 探测范围的最大高度[m],仅适用于elevation_grid_mode | 2.5 | N/A |
| elevation_grid_mode | boolean | 高程网格扫描模式选项 | true | N/A |
| low_priority_region_x | float | 后侧的非零x阈值,小物体检测的优先级较低[m] | -20.0 | N/A |
| center_pcl_shift | float | 附加 LiDAR 相对于车辆质心在 x 轴方向上的偏移量 [m];建议仅在 elevation_grid_mode 下用于附加 LiDAR。 | 0.0 | N/A |
| radial_divider_angle_deg | float | 用于将整帧点云按角度切分成多个扇区/射线组的角度间隔 [deg]。 | 1.0 | N/A |
| use_recheck_ground_cluster | boolean | 是否启用地面聚类复检。 | true | N/A |
| recheck_start_distance | float | 开始对地面聚类进行复检的距离 [m]。 | 20.0 | ≥0.0 |
| use_lowest_point | boolean | 复检地面聚类时,是否选择最低点作为参考点;若为 false,则选择中间点作为参考点。 | true | N/A |
| publish_processing_time_detail | boolean | 是否发布详细的处理耗时信息。 | false | N/A |
3.1 配置与调参
以下参数属于 Scan,不应与 Ray 的参数名混用。配置路径是原笔记对应版本的示例,升级后应检查实际节点加载的 YAML。
调参文件:src/universe/autoware.universe/launch/tier4_perception_launch/config/obstacle_segmentation/ground_segmentation/ground_segmentation.param.yaml
| 参数 | 默认值 | 调参作用 |
|---|---|---|
global_slope_max_angle_deg | 10.0 | 全局最大坡角;调大可保留较陡坡路,但也可能误删障碍物;调小则容易把坡路误判为非地面 |
local_slope_max_angle_deg | 13.0 | 局部坡角,决定相邻点连续性判断 |
split_points_distance_tolerance | 0.20 | 当前点和前一个点在 xy 平面上的距离超过多少,就认为它们“隔得比较远”。 |
non_ground_height_threshold | 0.20 | 相对预测地面的高度阈值,调大→过滤更多低矮噪声,调小→检出更小的障碍 |
split_height_distance | 0.20 | 相邻点比较时,区分地面和非地面的高度差阈值 |
use_virtual_ground_point | true | 用前轮接地点补盲区,一般开 |
grid_size_m | 0.1 | elevation_grid 模式的网格大小 |
grid_mode_switch_radius | 20.0 | 近处按距离/远处按角度切换的半径 |
gnd_grid_buffer_size | 4 | 估计局部坡度用的网格数 |
detection_range_z_max | 2.5 | 检测最大高度 |
elevation_grid_mode | true | 是否启用高程网格模式;效果需结合路面与点云密度验证 |
3.2 代码讲解:沿射线建立径向网格
Scan 同样先按方位角分组、按半径排序,但高程网格模式还会用一段地面历史预测当前网格。下面用固定径向网格演示核心过程;实际 Autoware 还包含远近网格切换、虚拟接地点与复检逻辑。
struct Grid {
std::vector<int> indices;
double radius_sum = 0.0;
};
std::map<int, Grid> grids; // 单条射线,按编号从近到远遍历
for (const auto& p : ray) {
const int key = static_cast<int>(std::floor(p.radius / grid_size_m));
grids[key].indices.push_back(p.index);
grids[key].radius_sum += p.radius;
}
这里的 ray 沿用上一节的 RayPoint 表示。网格内保存原始索引,map 按网格编号有序遍历。Scan 的径向网格不等同于普通 PCL 的三维 VoxelGrid:前者服务于沿扫描方向的地面预测,后者用于空间下采样。
3.3 代码讲解:预测地面并更新历史
设历史地面网格的代表位置为 ,用局部直线预测高度:
以下片段用最近两个有效网格估计斜率,再根据当前网格中通过分类的点更新历史。history 初始包含参考接地点,gnd_grid_buffer_size 至少为 2;这是教学简化版,不是官方完整高程预测器。
struct GroundSample { double radius, height; };
std::deque<GroundSample> history{{virtual_ground_r, ground_z}};
const double slope_limit = std::tan(local_slope_max_angle_deg * pi / 180.0);
for (const auto& entry : grids) {
const auto& grid = entry.second;
const double r = grid.radius_sum / grid.indices.size();
const auto last = history.back();
double slope = 0.0;
if (history.size() >= 2) {
const auto before = history[history.size() - 2];
const double dr = last.radius - before.radius;
if (dr > 1e-6) slope = (last.height - before.height) / dr;
}
slope = std::clamp(slope, -slope_limit, slope_limit);
double accepted_height = 0.0;
int accepted_count = 0;
for (int index : grid.indices) {
const auto& p = cloud->points[index];
const double point_r = std::hypot(p.x, p.y);
const double predicted_z = last.height + slope * (point_r - last.radius);
const bool is_ground = std::abs(p.z - predicted_z)
<= non_ground_height_threshold;
(is_ground ? ground_indices : non_ground_indices).indices.push_back(index);
if (is_ground) {
accepted_height += p.z;
++accepted_count;
}
}
if (accepted_count > 0) {
history.push_back({r, accepted_height / accepted_count});
while (history.size() > static_cast<std::size_t>(gnd_grid_buffer_size))
history.pop_front();
}
}
predicted_z 是当前点处的预测地面高度,高度残差决定其是否通过简化分类。只有通过分类的点才更新地面历史,否则墙面或车辆表面可能拉高后续预测。clamp 限制预测坡度,防止局部异常产生过大的外推。
这段代码没有实现全局坡角、远距离断点与高度检测范围检查。没有有效地面点的网格不会更新历史,长距离缺测后仍需重新初始化或复检。片段使用 C++17 的 std::clamp;相关容器需要 <map>、<deque>,几何计算与排序需要 <cmath>、<algorithm>。
4. RANSAC Ground Filter(平面拟合法)
参考:
原理:在地面候选区域内反复随机采样并拟合平面,以内点支持评价模型,再检查平面方向与位置,避免将墙面或车顶误当作地面。
流程:
- 随机选择三个不共线的点,建立候选平面
- 检查有多少个点支持这个平面———点到平面的距离
- 在
max_iterations限制内重复采样,选择内点支持较好的平面 - 验证平面方向与位置,再通过
height_threshold分类地面点与非地面点
优点:地面近似平面且内点比例足够时,可以容忍一定比例的离群点;有限次随机采样不保证一定找到真实地面。
局限:单个平面难以描述起伏路面或连续变化的坡度。恒定坡度的路面仍可拟合,但需要满足坡角约束;输入点云应先限制到合适的候选区域。
核心参数:
| Name | Type | Description | Default | Range |
|---|---|---|---|---|
| base_frame | string | base_link frame | base_link | N/A |
| unit_axis | string | The axis which we need to search ground plane | z | N/A |
| max_iterations | integer | The maximum number of iterations | 1000 | N/A |
| min_trial | integer | min_trial | 5000 | N/A |
| min_points | integer | min_points | 1000 | N/A |
| outlier_threshold | float | 点到当前拟合平面的距离小于该阈值时,认为该点支持该平面模型;距离超过该阈值时,认为是离群点。 | 0.01 | N/A |
| plane_slope_threshold | float | 用于防止 RANSAC 拟合出错误的地面平面。如果拟合出的平面坡度过大,则认为该平面不像地面。 | 10.0 | N/A |
| voxel_size_x | float | voxel size x [m] | 0.04 | N/A |
| voxel_size_y | float | voxel size y [m] | 0.04 | N/A |
| voxel_size_z | float | voxel size z [m] | 0.04 | N/A |
| height_threshold | float | 点到最终地面平面的高度/距离超过该阈值时,认为是非地面点;小于该阈值时,认为是地面点。 | 0.01 | N/A |
| debug | boolean | true:输出调试信息;false:不输出调试信息。 | false | N/A |
| publish_processing_time_detail | boolean | 是否发布详细处理耗时信息。主要用于调试和性能分析。 | false | N/A |
4.1 代码讲解:下采样后拟合平面
下面按“候选点下采样 → RANSAC 拟合 → 平面验证 → 原始点分类”拆解核心片段。candidates 是已移除无效点、限定到地面候选区域的 PCL 点云,cloud 是待分类的原始分辨率点云;两者使用同一坐标系。配置阈值由调用方提供。
pcl::PointCloud<pcl::PointXYZ>::Ptr sampled(new pcl::PointCloud<pcl::PointXYZ>);
pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(candidates);
voxel.setLeafSize(voxel_size_x, voxel_size_y, voxel_size_z);
voxel.filter(*sampled);
pcl::SACSegmentation<pcl::PointXYZ> seg;
seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setMaxIterations(max_iterations);
seg.setDistanceThreshold(outlier_threshold);
seg.setInputCloud(sampled);
pcl::PointIndices inliers;
pcl::ModelCoefficients coefficients;
seg.segment(inliers, coefficients);
VoxelGrid 减少拟合点数,三个叶尺寸以米为单位且必须为正。SACMODEL_PLANE 选择平面模型,SAC_RANSAC 选择鲁棒估计方法。outlier_threshold 决定哪些采样点支持候选平面,max_iterations 限制随机试验次数。setOptimizeCoefficients(true) 用最终内点进一步优化系数。
4.2 代码讲解:检查法向量与平面有效性
设平面模型为:
点 到平面的垂直距离为:
拟合阶段用 判断内点。拟合阈值与最终地面分类阈值可以不同;拟合完成后可以用独立的分类阈值重新检查原始点云。
当坐标系的 z 轴向上时,平面相对水平面的倾角为:
绝对值消除了法向量正负的影响。倾角检查可以排除竖直墙面,却不能单独排除水平车顶,仍需结合候选区域与地面高度。
// 片段位于返回 bool 的处理函数中;失败交给调用方处理。
if (inliers.indices.empty() || coefficients.values.size() < 4)
return false;
const auto& v = coefficients.values;
const double a = v[0], b = v[1], c = v[2], d = v[3];
const double norm = std::sqrt(a*a + b*b + c*c);
if (norm <= 1e-6) return false;
const double pi = std::acos(-1.0);
const double max_slope = plane_slope_threshold * pi / 180.0;
if (std::abs(c) / norm < std::cos(max_slope))
return false;
coefficients.values 保存 。abs(c)/norm 衡量法向量与竖直轴的对齐程度,用来拒绝倾角过大的平面。它不能区分水平车顶与真实地面,候选区域、预期地面高度和最少内点数仍需额外验证。拒绝最大平面后,这段代码不会自动寻找第二个平面。
4.3 代码讲解:用拟合系数分类原始点
pcl::PointIndices ground_indices, non_ground_indices;
for (int i = 0; i < static_cast<int>(cloud->size()); ++i) {
const auto& p = cloud->points[i];
const double distance = std::abs(a*p.x + b*p.y + c*p.z + d) / norm;
const bool is_ground = distance <= height_threshold;
(is_ground ? ground_indices : non_ground_indices).indices.push_back(i);
}
pcl::ExtractIndices<pcl::PointXYZ> extract;
pcl::PointIndices::Ptr ground_ptr(new pcl::PointIndices(ground_indices));
extract.setInputCloud(cloud);
extract.setIndices(ground_ptr);
extract.setNegative(false);
extract.filter(*ground_cloud);
extract.setNegative(true);
extract.filter(*non_ground_cloud);
拟合用的是 sampled,其内点索引不能直接用于 cloud。这里将平面系数应用到原始点,重新计算距离并建立原始索引,保留输出分辨率。height_threshold 是最终分类容差,与拟合内点阈值分开;本示例采用垂直于平面的距离,不能直接解释为世界坐标系中的 z 高度差。
setNegative(false) 提取地面索引,true 提取其补集。PCL 对应头文件为 <pcl/filters/voxel_grid.h>、<pcl/segmentation/sac_segmentation.h> 和 <pcl/filters/extract_indices.h>。以上片段共同说明处理链路,没有包含 ROS 消息转换与失败时的回退策略。
5. 方法选择与调参检查
| 方法 | 主要依据 | 重点检查 |
|---|---|---|
| Ray | 同一方向上的高度连续性 | 坡角、稀疏点与近处盲区 |
| Scan | 扫描方向与网格地面预测 | 网格尺寸、坡度变化、小障碍物误删 |
| RANSAC | 候选平面的内点支持 | 候选区域、方向与距离阈值 |
调参时同时检查地面残留与低矮障碍物误删,并记录点数和耗时。角度参数通常以度给出,而 C++ 三角函数使用弧度;距离参数应与点云的米制单位一致。
参考:PCL 平面分割教程。Autoware 各方法的参考链接见对应章节。
本篇文章受 CC BY-NC-SA 4.0 协议保护,转载请注明出处。