直接基于雷达点云生成障碍物的3D包围盒
本文提出两种无需深度学习的轻量级方法,直接从雷达点云生成3D障碍物包围盒。方案一通过顶视图投影和连通域分析,将3D点云转换为2D图像后检测障碍物;方案二采用RANSAC地面分割结合DBSCAN聚类,实现更精细的3D障碍物检测。两种方法均能有效识别点云中的障碍物,方案一计算简单快速但精度有限,方案二处理复杂场景效果更好但参数调整较复杂。实验结果表明,这两种非深度学习方法在自动驾驶和机器人感知中具有实
直接基于雷达点云生成障碍物的3D包围盒
一、背景
在自动驾驶和机器人感知中,雷达(LiDAR) 是一种常用的传感器,它能通过激光扫描获取周围环境的三维点云——也就是一大堆带有三维坐标(X, Y, Z)的点,每个点代表激光照射到物体表面的一个位置。
通常,要从点云中识别出障碍物(如车辆、行人、路障),最先进的方法是使用基于深度学习的点云检测模型(比如PointPillars、VoxelNet等)。这些模型能够直接处理点云,输出每个障碍物的类别和3D包围盒(一个能框住物体的最小立方体,包含中心位置、长宽高和朝向)。但是,深度学习模型需要大量的标注数据进行训练,而且对硬件有一定要求。
那么,有没有一种更直观、更轻量的方法,不需要复杂的神经网络,也能快速找到点云中的障碍物呢?
我们观察到,障碍物在点云中通常表现为一团聚集的点(有点像“团状物”)。这让我们联想到图像处理中的连通域分析——如果把点云“拍扁”成一张二维图像,那些聚集的点就会形成一块块连通的区域,就像照片中的物体一样。基于这个想法,我们设计了两种简单但有效的方案,可以直接从雷达点云中生成障碍物的3D包围盒,完全不需要深度学习。
二、效果
1、方案一效果
下面两张图分别展示了两种方案的实际效果。原始点云(灰色)中,红色的立方体就是算法自动生成的障碍物包围盒。

(顶视图投影 + 连通域分析的结果)
2、方案二效果

(RANSAC地面分割 + DBSCAN聚类 + 合并过滤的结果)
三、原理
方案一:顶视图投影 + 连通域分析
这个方案的思路非常直接:既然障碍物在点云中是“一团一团的”,我们可以从上方往下看,把三维点云投影到二维平面(X-Y平面),就像用相机俯拍一样。投影之后,每个点对应一个像素,有点的像素被标记为1,没有的标记为0,这样就得到了一张二值图像。
然后,我们用图像处理中的连通域标记(8邻域连接)来找出哪些像素是连在一起的。每个连通的像素块就对应一个潜在的障碍物。最后,根据这个像素块在原图中的位置和大小,结合原始点云中该区域点的Z坐标(高度),就能反算出一个三维的轴对齐包围盒(即长方体的各个面平行于坐标轴)。
优点:实现简单,计算快。
缺点:如果障碍物之间有重叠(比如两辆车并排且高度相近),它们在投影图上可能会连成一片,导致无法区分。此外,它只能产生轴对齐的包围盒,无法适应旋转的物体。
方案二:RANSAC地面分割 + DBSCAN聚类 + 合并与过滤
为了克服方案一的局限性,我们采用了更精细的三维处理:
-
地面分割:点云中除了障碍物,还有大量的地面点。地面点会干扰障碍物的识别,因此需要先去除。我们使用RANSAC(随机采样一致性)算法来拟合一个平面,认为这个平面就是地面,并将其从点云中移除。剩下的就是非地面点(障碍物候选点)。
-
欧式聚类(DBSCAN):现在,障碍物点云是散乱的,我们需要把属于同一个物体的点聚在一起。DBSCAN是一种基于密度的聚类算法,它会根据点与点之间的距离来判断是否属于同一簇。我们需要设定一个距离阈值
eps(如果两个点的距离小于这个值,就认为它们是邻居)。为了让参数更鲁棒,我们实现了一个自适应eps估算:计算每个点的k个最近邻的平均距离,然后取所有点这个距离的中位数作为eps。 -
合并过分割的簇:DBSCAN有时会把一个物体分成几块(比如车头和车身离得稍远),造成“过分割”。我们通过计算每个簇的包围盒,如果两个簇在X-Y平面上的投影重叠较多(IOU大于阈值),且Z方向高度相近,就将它们合并。
-
形状过滤:最后,我们根据包围盒的体积、长宽比、高度等条件,过滤掉太小的(可能是噪声)或形状异常的(比如长条状可能是栏杆)物体,输出最终的3D包围盒。
优点:能处理复杂场景,适应旋转物体(我们可以用有向包围盒,但这里示例用了轴对齐),通过合并减少过分割,过滤去除噪声。
缺点:参数较多,需要针对不同场景调整。
四、代码实现
下面我们分别给出两种方案的Python代码,并附上详细注释,帮助理解每一步在做什么。
方案一
import matplotlib.pyplot as plt
import open3d as o3d
import numpy as np
from scipy import ndimage
import copy
def load_point_cloud(filename):
"""加载点云文件(支持.pcd等格式)"""
print("加载点云文件...")
pcd = o3d.io.read_point_cloud(filename)
print(f"原始点云数量: {len(pcd.points)}")
return pcd
def preprocess_point_cloud(pcd, voxel_size=0.05, z_min=-1.5, z_max=-0.01):
"""
预处理点云:
1. 根据Z坐标过滤(只保留特定高度范围内的点,例如去除地面以上的点)
2. 体素下采样(减少点云密度,加快处理)
3. 统计滤波去噪(移除离群点)
"""
print("预处理点云...")
points = np.asarray(pcd.points)
# 1. Z值过滤:只保留高度在z_min到z_max之间的点
mask = (points[:, 2] > z_min) & (points[:, 2] <= z_max)
filtered_points = points[mask]
filtered_cloud = o3d.geometry.PointCloud()
filtered_cloud.points = o3d.utility.Vector3dVector(filtered_points)
print(f"Z过滤后点数: {len(filtered_cloud.points)}")
# 2. 体素下采样:将空间划分为小立方体(体素),每个体素内只保留一个点
downpcd = filtered_cloud.voxel_down_sample(voxel_size=voxel_size)
print(f"下采样后点数: {len(downpcd.points)}")
# 3. 统计滤波:计算每个点周围邻居的距离,距离过大的点被认为是噪声并移除
cl, ind = downpcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=1.0)
inlier_cloud = downpcd.select_by_index(ind)
print(f"去噪后点数: {len(inlier_cloud.points)}")
return inlier_cloud
def visualize_results(original_pcd, clusters, bounding_boxes, title="检测结果"):
"""可视化原始点云、各个聚类点云以及包围盒"""
print("可视化结果...")
geometries = [original_pcd.paint_uniform_color([0.5, 0.5, 0.5])] # 原始点云设为灰色
geometries.extend(clusters) # 每个聚类用随机颜色显示
geometries.extend(bounding_boxes) # 包围盒显示为红色
o3d.visualization.draw_geometries(
geometries, window_name=title, width=1024, height=768
)
def detect_objects_in_point_cloud(filename, voxel_size=0.1, z_range=(-1.5, -0.01), min_volume=0.05):
"""
主函数:通过顶视图投影+连通域分析检测障碍物
参数说明:
filename: 点云文件路径
voxel_size: 体素大小,用于下采样,也影响投影分辨率
z_range: (z_min, z_max) 只保留该高度范围内的点
min_volume: 最小体积阈值,小于此值的物体将被过滤
"""
# 1. 加载点云
pcd = load_point_cloud(filename)
# 2. 预处理
processed_pcd = preprocess_point_cloud(pcd, voxel_size=voxel_size,
z_min=z_range[0], z_max=z_range[1])
points = np.asarray(processed_pcd.points)
cluster_geometries = [] # 存储每个聚类的点云(用于可视化)
bounding_boxes = [] # 存储每个物体的包围盒
# 3. 顶视图投影,计算连通域
print("开始顶视图投影和连通域分析...")
# 投影分辨率:每个像素代表实际空间中的多大尺寸
pixel_size = voxel_size * 2 # 这里设为体素大小的2倍,可根据需要调整
# 获取点云在XY平面上的范围
x_min, x_max = np.min(points[:, 0]), np.max(points[:, 0])
y_min, y_max = np.min(points[:, 1]), np.max(points[:, 1])
# 计算图像尺寸(行列数)
width = int(np.ceil((x_max - x_min) / pixel_size)) + 1
height = int(np.ceil((y_max - y_min) / pixel_size)) + 1
# 初始化二值图像,所有像素初始为0
img = np.zeros((height, width), dtype=np.uint8)
# 将每个点的X,Y坐标映射到像素行列索引
col_indices = np.floor((points[:, 0] - x_min) / pixel_size).astype(int)
row_indices = np.floor((points[:, 1] - y_min) / pixel_size).astype(int)
# 确保索引不越界
col_indices = np.clip(col_indices, 0, width - 1)
row_indices = np.clip(row_indices, 0, height - 1)
# 标记有点的像素为1
img[row_indices, col_indices] = 1
# 连通域标记(使用8邻域连接)
labeled_img, num_features = ndimage.label(img, structure=np.ones((3, 3)))
print(f"检测到 {num_features} 个候选连通域")
# 为每个点获取其对应的标签
point_labels = labeled_img[row_indices, col_indices]
filtered_count = 0
# 遍历每一个连通域(标签从1开始)
for label in range(1, num_features + 1):
# 提取属于当前标签的点
mask = point_labels == label
if np.sum(mask) == 0:
continue
cluster_points = points[mask]
# ---- 在图像上获取该连通域的二维包围盒 ----
rows, cols = np.where(labeled_img == label)
min_row, max_row = np.min(rows), np.max(rows)
min_col, max_col = np.min(cols), np.max(cols)
# 将像素坐标转换回物理坐标,得到二维中心、长、宽
center_x = x_min + (min_col + max_col) / 2.0 * pixel_size
center_y = y_min + (min_row + max_row) / 2.0 * pixel_size
length_x = (max_col - min_col + 1) * pixel_size
length_y = (max_row - min_row + 1) * pixel_size
# ---- 获取该区域的高度范围 ----
z_min_obj = np.min(cluster_points[:, 2])
z_max_obj = np.max(cluster_points[:, 2])
center_z = (z_min_obj + z_max_obj) / 2.0
length_z = z_max_obj - z_min_obj
# 计算体积
volume = length_x * length_y * length_z
# 过滤体积太小的物体(可能是噪声)
if volume < min_volume:
filtered_count += 1
continue
# 生成该聚类的点云(用于可视化)
cluster_pcd = o3d.geometry.PointCloud()
cluster_pcd.points = o3d.utility.Vector3dVector(cluster_points)
color = np.random.rand(3) # 随机颜色
cluster_pcd.paint_uniform_color(color)
cluster_geometries.append(cluster_pcd)
# 生成轴对齐包围盒
min_bound = [center_x - length_x/2, center_y - length_y/2, center_z - length_z/2]
max_bound = [center_x + length_x/2, center_y + length_y/2, center_z + length_z/2]
bbox = o3d.geometry.AxisAlignedBoundingBox(min_bound, max_bound)
bbox.color = [1, 0, 0] # 红色
bounding_boxes.append(bbox)
print(f"过滤后剩余物体数量: {len(bounding_boxes)} (过滤了 {filtered_count} 个)")
# 4. 可视化结果
visualize_results(processed_pcd, cluster_geometries, bounding_boxes)
return {
'point_cloud': processed_pcd,
'clusters': cluster_geometries,
'bounding_boxes': bounding_boxes
}
def main():
# 调用主函数,请将"LIDAR_TOP.pcd"替换为你自己的点云文件路径
result = detect_objects_in_point_cloud(
"LIDAR_TOP.pcd",
voxel_size=0.2, # 体素大小(米)
z_range=(-1.5, -0.01), # 只保留高度在-1.5米到-0.01米之间的点(假设雷达安装高度1.5米,地面以下)
min_volume=0.05 # 最小体积0.05立方米
)
return result
if __name__ == "__main__":
main()
方案二
import open3d as o3d
import numpy as np
from sklearn.neighbors import NearestNeighbors
import copy # 用于合并时的深拷贝
def load_point_cloud(filename):
"""加载点云文件"""
print("加载点云文件...")
pcd = o3d.io.read_point_cloud(filename)
print(f"原始点云数量: {len(pcd.points)}")
return pcd
def preprocess_point_cloud(pcd, voxel_size=0.1):
"""基础预处理:下采样 + 统计滤波(不包含地面分割)"""
print("基础预处理...")
# 1. 体素下采样
downpcd = pcd.voxel_down_sample(voxel_size=voxel_size)
print(f"下采样后点数: {len(downpcd.points)}")
# 2. 统计滤波去噪
cl, ind = downpcd.remove_statistical_outlier(nb_neighbors=20, std_ratio=1.0)
inlier_cloud = downpcd.select_by_index(ind)
print(f"去噪后点数: {len(inlier_cloud.points)}")
return inlier_cloud
def segment_ground_ransac(pcd, distance_threshold=0.2, ransac_n=3, num_iterations=1000):
"""
使用RANSAC算法分割地面
参数:
pcd: 输入点云
distance_threshold: 点到平面的距离阈值,小于该值的点被认为是地面点
ransac_n: 随机采样点数,用于拟合平面
num_iterations: 迭代次数
返回:
ground_cloud: 地面点云
objects_cloud: 非地面点云
"""
print("RANSAC地面分割...")
plane_model, inliers = pcd.segment_plane(distance_threshold=distance_threshold,
ransac_n=ransac_n,
num_iterations=num_iterations)
ground_cloud = pcd.select_by_index(inliers)
objects_cloud = pcd.select_by_index(inliers, invert=True)
print(f"地面点: {len(ground_cloud.points)}, 非地面点: {len(objects_cloud.points)}")
return ground_cloud, objects_cloud
def estimate_eps(point_cloud, k=8, percentile=50):
"""
基于k近邻距离估算DBSCAN的eps参数
原理:计算每个点的k个最近邻的平均距离,然后取所有点这个平均距离的某个分位数作为eps。
这样可以根据点云密度自动调整聚类半径。
"""
points = np.asarray(point_cloud.points)
if len(points) < k:
return 0.5 # 点太少,返回默认值
nbrs = NearestNeighbors(n_neighbors=k).fit(points)
distances, _ = nbrs.kneighbors(points)
# 每个点的平均距离(忽略自身,即取第1到k-1近邻)
avg_dist = np.mean(distances[:, 1:], axis=1)
eps = np.percentile(avg_dist, percentile)
print(f"自适应eps估算: {eps:.4f} (基于{k}近邻的{percentile}分位数)")
return eps
def cluster_dbscan(pcd, eps=None, min_points=10, eps_factor=2.0, use_auto_eps=True):
"""
执行DBSCAN聚类,支持自动估算eps
"""
if use_auto_eps:
if eps is None:
eps = estimate_eps(pcd, k=min_points, percentile=50)
else:
# 若使用固定倍数,需要传入体素大小等,这里简化处理
pass
print(f"DBSCAN聚类参数: eps={eps:.4f}, min_points={min_points}")
labels = np.array(pcd.cluster_dbscan(eps=eps, min_points=min_points, print_progress=True))
return labels
def merge_clusters_by_bbox(clusters_pcd, bboxes, iou_thresh=0.3, z_merge_thresh=0.5):
"""
基于包围盒合并过分割的簇
参数:
clusters_pcd: 每个簇的点云列表
bboxes: 每个簇的轴对齐包围盒列表
iou_thresh: XY平面投影的IOU阈值,高于此值考虑合并
z_merge_thresh: Z方向中心高度差阈值,小于此值考虑合并
返回:
合并后的簇点云列表和包围盒列表
"""
if len(clusters_pcd) <= 1:
return clusters_pcd, bboxes
n = len(clusters_pcd)
merged = [False] * n
new_clusters = []
new_bboxes = []
for i in range(n):
if merged[i]:
continue
# 当前簇作为合并的基础
combined_pcd = copy.deepcopy(clusters_pcd[i])
combined_bbox = bboxes[i]
merged[i] = True
for j in range(i+1, n):
if merged[j]:
continue
# 计算两个包围盒在XY平面上的IOU
bbox_i = bboxes[i]
bbox_j = bboxes[j]
min_i = bbox_i.get_min_bound()
max_i = bbox_i.get_max_bound()
min_j = bbox_j.get_min_bound()
max_j = bbox_j.get_max_bound()
# 计算重叠区间
x_overlap = max(0, min(max_i[0], max_j[0]) - max(min_i[0], min_j[0]))
y_overlap = max(0, min(max_i[1], max_j[1]) - max(min_i[1], min_j[1]))
area_i = (max_i[0]-min_i[0]) * (max_i[1]-min_i[1])
area_j = (max_j[0]-min_j[0]) * (max_j[1]-min_j[1])
overlap_area = x_overlap * y_overlap
iou_xy = overlap_area / (area_i + area_j - overlap_area + 1e-6)
# Z方向中心高度差
z_center_i = (min_i[2] + max_i[2]) / 2
z_center_j = (min_j[2] + max_j[2]) / 2
z_diff = abs(z_center_i - z_center_j)
# 如果满足合并条件,则合并
if iou_xy > iou_thresh and z_diff < z_merge_thresh:
combined_pcd += clusters_pcd[j]
merged[j] = True
# 重新计算合并后的包围盒
new_bbox = combined_pcd.get_axis_aligned_bounding_box()
new_bbox.color = [1, 0, 0]
new_clusters.append(combined_pcd)
new_bboxes.append(new_bbox)
print(f"合并前簇数量: {n}, 合并后: {len(new_clusters)}")
return new_clusters, new_bboxes
def filter_by_shape(bbox, min_volume=0.05, max_aspect_ratio=5.0, min_height=0.2):
"""
根据形状过滤:体积、长宽比、高度
参数:
bbox: 包围盒
min_volume: 最小体积(立方米)
max_aspect_ratio: 最大长宽比(最长边/最短边)
min_height: 最小高度(米)
返回:
True表示通过过滤,保留;False表示过滤掉
"""
extent = bbox.get_extent()
volume = extent[0] * extent[1] * extent[2]
if volume < min_volume:
return False
aspect_ratio = max(extent) / (min(extent) + 1e-6)
if aspect_ratio > max_aspect_ratio:
return False
if extent[2] < min_height:
return False
return True
def detect_objects_optimized(filename, voxel_size=0.2,
ground_dist=0.3,
z_range=None, # 例如 (-1.5, None) 或 (-1.5, 0.0)
z_max_global=None, # 额外Z上限过滤(如去除Z>0的点)
eps=None, min_points=15,
min_volume=0.1, use_auto_eps=True,
merge_enabled=True):
"""
优化后的物体检测主函数,支持Z范围过滤和额外Z上限过滤
参数:
filename: 点云文件路径
voxel_size: 体素下采样大小
ground_dist: RANSAC地面分割距离阈值
z_range: 预处理阶段的Z值过滤范围,元组 (z_min, z_max),None表示不限制
z_max_global: 地面分割后对物体点云的Z上限过滤(如0表示只保留Z<=0的点)
eps: DBSCAN聚类半径,若为None则自动估算
min_points: DBSCAN最小点数
min_volume: 最小体积阈值
use_auto_eps: 是否自动估算eps
merge_enabled: 是否启用簇合并
"""
# 1. 加载点云
pcd = load_point_cloud(filename)
# 2. 基础预处理(下采样+去噪)
processed_pcd = preprocess_point_cloud(pcd, voxel_size=voxel_size)
# 3. 可选:预处理阶段的Z范围过滤(例如只保留地面附近区域)
if z_range is not None:
z_min, z_max = z_range
points = np.asarray(processed_pcd.points)
mask = np.ones(len(points), dtype=bool)
if z_min is not None:
mask &= (points[:, 2] > z_min)
if z_max is not None:
mask &= (points[:, 2] <= z_max)
processed_pcd = processed_pcd.select_by_index(np.where(mask)[0])
print(f"Z范围过滤后点数: {len(processed_pcd.points)}")
# 4. RANSAC地面分割
ground, objects = segment_ground_ransac(processed_pcd, distance_threshold=ground_dist)
# 5. 额外Z上限过滤(例如去除高于地面的点,只保留地面以下的物体)
if z_max_global is not None:
obj_points = np.asarray(objects.points)
mask_z = obj_points[:, 2] <= z_max_global
objects = objects.select_by_index(np.where(mask_z)[0])
print(f"Z上限过滤后物体点数量: {len(objects.points)}")
if len(objects.points) == 0:
print("警告:无物体点云,请检查参数")
return None
# 6. DBSCAN聚类
if use_auto_eps:
eps = estimate_eps(objects, k=min_points, percentile=50)
else:
if eps is None:
eps = voxel_size * 3.0 # 默认倍数
labels = np.array(objects.cluster_dbscan(eps=eps, min_points=min_points, print_progress=True))
max_label = labels.max()
print(f"初始聚类数: {max_label + 1} (不含噪声)")
# 提取每个簇
points = np.asarray(objects.points)
raw_clusters = []
raw_bboxes = []
for label in range(max_label + 1):
cluster_mask = labels == label
cluster_points = points[cluster_mask]
cluster_pcd = o3d.geometry.PointCloud()
cluster_pcd.points = o3d.utility.Vector3dVector(cluster_points)
cluster_pcd.paint_uniform_color(np.random.rand(3)) # 随机颜色
bbox = cluster_pcd.get_axis_aligned_bounding_box()
bbox.color = [1, 0, 0]
raw_clusters.append(cluster_pcd)
raw_bboxes.append(bbox)
# 7. 可选:合并过分割簇
if merge_enabled:
merged_clusters, merged_bboxes = merge_clusters_by_bbox(raw_clusters, raw_bboxes)
else:
merged_clusters, merged_bboxes = raw_clusters, raw_bboxes
# 8. 形状过滤
final_clusters = []
final_bboxes = []
filtered_count = 0
for cluster, bbox in zip(merged_clusters, merged_bboxes):
if filter_by_shape(bbox, min_volume=min_volume):
final_clusters.append(cluster)
final_bboxes.append(bbox)
else:
filtered_count += 1
print(f"过滤后物体数量: {len(final_bboxes)} (过滤了 {filtered_count} 个)")
# 9. 可视化
ground.paint_uniform_color([0.5, 0.5, 0.5]) # 灰色地面
vis_list = [ground] + final_clusters + final_bboxes
o3d.visualization.draw_geometries(vis_list, window_name="优化后的欧式聚类结果", width=1024, height=768)
return {
'ground': ground,
'objects': objects,
'clusters': final_clusters,
'bounding_boxes': final_bboxes
}
def main():
# 调用主函数,请将"LIDAR_TOP.pcd"替换为你自己的点云文件路径
result = detect_objects_optimized(
"LIDAR_TOP.pcd",
voxel_size=0.2, # 下采样体素大小(米)
ground_dist=-1.5, # RANSAC地面分割距离阈值
z_range=(None,0), # 预处理过滤:只保留Z > -1.5米的点(即地面以下的部分)
z_max_global=0.0, # 额外过滤:只保留Z <= 0的非地面点(即地面以下)
min_points=10, # DBSCAN最小点数
min_volume=0.05, # 最小体积(立方米)
use_auto_eps=True, # 自动估算eps
merge_enabled=True # 启用合并过分割簇
)
return result
if __name__ == "__main__":
main()
五、总结
本文介绍了两种无需深度学习的点云障碍物检测方法:
- 方案一 将三维点云投影到二维平面,利用图像连通域分析快速定位障碍物,适合简单场景。
- 方案二 采用RANSAC地面分割、DBSCAN聚类、合并与过滤,能处理更复杂的场景,且可灵活调整参数。
这两种方法实现简单、计算高效,可作为深度学习方法的补充或替代,特别适合资源受限或需要快速原型验证的场景。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)