点云提取车道线 识别车道线

目录

安装依赖项:

windows

1. 提取地面

2. 提取车道线


安装依赖项:

pip install CSF

windows

pip install csf

import csf

csf=csf.csf(points)

1. 提取地面

import numpy as np
import laspy
import open3d as o3d
# import CSF
import csf
import matplotlib.cm as cm

# ====================== 1. 读取LAS文件 ======================
def read_las_to_open3d(file_path):
    """读取LAS文件并转换为Open3D点云"""
    las = laspy.read(file_path)
    xyz = np.vstack((las.x, las.y, las.z)).transpose()
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(xyz)
    
    # 反射强度处理
    if hasattr(las, 'intensity'):
        intensity = np.array(las.intensity)
        
        # 归一化处理
        min_val, max_val = np.min(intensity), np.max(intensity)
        # 另一种做法:将原始强度值归一化到0-255,再除以255得到0-1
        if max_val > min_val:
            # 归一化到0-255
            gray_values = (intensity - min_val) * 255.0 / (max_val - min_val)
        else:
            gray_values = np.zeros_like(intensity)
        # 然后除以255,得到0-1
        gray_values = (gray_values)+125
        normalized_gray = gray_values / 255.0
        rgb_colors = np.repeat(normalized_gray[:, np.newaxis], 3, axis=1)
    else:
        # 无反射强度时使用黑色
        rgb_colors = np.zeros((len(xyz), 3))
    
    # 设置点云颜色
    pcd.colors = o3d.utility.Vector3dVector(rgb_colors)

    #     # 检查并转换颜色
    # if hasattr(las, 'red'):
    #     rgb = np.vstack((las.red, las.green, las.blue)).T
    #     # 若值超过 255,转为 8 位
    #     if rgb.max() > 255:
    #         rgb = (rgb >> 8).astype(np.uint8)
    #     normalized_rgb = rgb / 255.0
    # else:
    #     normalized_rgb = np.zeros((len(xyz), 3))  # 无颜色时设为黑色

    # # 创建 Open3D 点云对象
    # pcd.colors = o3d.utility.Vector3dVector(normalized_rgb)

    # 可视化
    o3d.visualization.draw_geometries([pcd])

    return pcd

# ====================== 2. 地面点云提取算法 ======================
def extract_ground(pcd, method="region_ransac", **kwargs):
    """
    提取最大地平面(二选一方法)
    参数:
      method: "region_ransac"(复杂地形) 或 "csf"(平坦/斜坡地形)
      kwargs: 算法参数(见下方说明)
    """
    # 方法1:区域化RANSAC(适合起伏地形)
    if method == "region_ransac":
        return _region_ransac(pcd, 
                              voxel_size=kwargs.get('voxel_size', 0.1),
                              distance_threshold=kwargs.get('distance_threshold', 0.2),
                              region_size=kwargs.get('region_size', 5.0)
                             )
    # 方法2:布料模拟滤波(CSF)(适合连续斜坡)
    elif method == "csf":
        return _csf_filter(pcd, 
                           rigidness=kwargs.get('rigidness', 3),
                           cloth_resolution=kwargs.get('cloth_resolution', 0.5),
                           height_threshold=kwargs.get('height_threshold', 0.15)
                          )

def _region_ransac(pcd, voxel_size, distance_threshold, region_size):
    """区域化RANSAC地面分割"""
    # 下采样与去噪
    # down_pcd = pcd.voxel_down_sample(voxel_size)
    # cl, ind = pcd.remove_statistical_outlier(nb_neighbors=100, std_ratio=2.0)
    # filtered_pcd = pcd.select_by_index(ind)
    filtered_pcd = pcd
    # 分区域处理
    bbox = filtered_pcd.get_axis_aligned_bounding_box()
    min_bound, max_bound = bbox.min_bound, bbox.max_bound
    ground_points = []
    ground_colors = []
    
    # 网格划分(避免全局拟合失效)
    x_bins = np.arange(min_bound[0], max_bound[0], region_size)
    y_bins = np.arange(min_bound[1], max_bound[1], region_size)
    
    for i in range(len(x_bins)-1):
        for j in range(len(y_bins)-1):
            # 裁剪当前区域
            area_box = o3d.geometry.AxisAlignedBoundingBox(
                min_bound=[x_bins[i], y_bins[j], min_bound[2]],
                max_bound=[x_bins[i+1], y_bins[j+1], max_bound[2]]
            )
            area_pcd = filtered_pcd.crop(area_box)
            if len(area_pcd.points) < 1500: continue
            
            # 局部平面拟合
            plane_model, inliers = area_pcd.segment_plane(
                distance_threshold, ransac_n=3, num_iterations=500
            )
            # 法向量验证(Z分量>0.8为地面)
            normal = np.array(plane_model[:3])
            if normal[2] > 0.9:  
                selected = area_pcd.select_by_index(inliers)
                ground_points.append(selected.points)
                ground_colors.append(selected.colors)
    
    # 合并地面点
    ground_pcd = o3d.geometry.PointCloud()
    ground_pcd.points = o3d.utility.Vector3dVector(np.vstack(ground_points))
    ground_pcd.colors = o3d.utility.Vector3dVector(np.vstack(ground_colors))
    # 可视化
    o3d.visualization.draw_geometries([ground_pcd])
    return ground_pcd

def _csf_filter(pcd, rigidness, cloth_resolution, height_threshold):
    """布料模拟滤波(CSF)[6](@ref)"""
    # 将Open3D点云转为CSF所需格式
    points = np.asarray(pcd.points)
    # csf = CSF.CSF()
    csf=csf.csf(points)
    # csf.setPointCloud(points)
    
    # 参数设置
    csf.params.bSloopSmooth = True      # 启用坡度平滑
    csf.params.rigidness = rigidness    # 布料刚度(1-3:软,适合山地;>3:硬,适合城市)
    csf.params.resolution = cloth_resolution  # 布料网格分辨率(米)
    csf.params.height_threshold = height_threshold # 地面点高度阈值
    
    # 执行滤波
    ground_indices = CSF.VecInt()  # 表示计算后地面点索引的列表
    non_ground_indices = CSF.VecInt()  # 表示计算后非地面点索引的列表
    csf.do_filtering(ground_indices, non_ground_indices)  # 执行实际过滤。
    # ground_indices = csf.getGroundIndices()
    
    # 提取地面点
    ground_points = points[ground_indices]
    ground_pcd = o3d.geometry.PointCloud()
    ground_pcd.points = o3d.utility.Vector3dVector(ground_points)
    return ground_pcd

def cluster_and_filter_ground(ground_pcd, 
                             min_cluster_points=100, 
                             eps=0.3, 
                             dynamic_threshold=False):
    labels = np.array(
        ground_pcd.cluster_dbscan(
            eps=0.5,          # 邻域半径(单位:米)
            min_points=10,    # 最小核心点数
            print_progress=True  # 显示进度
        )
    )

    # 解析结果
    max_label = labels.max()
    print(f"聚类数量:{max_label + 1}")
    print(f"噪声点数量:{np.sum(labels == -1)}")

    # 可视化:不同簇随机着色
    colors = np.random.rand(max_label + 1, 3)  # 生成随机颜色
    pcd_colors = np.zeros((len(labels), 3))
    print(labels)
    for i in range(len(labels)):
        if labels[i] >= 0:  # 非噪声点
            pcd_colors[i] = colors[labels[i]]
    # ground_pcd.colors = o3d.utility.Vector3dVector(pcd_colors)
    # o3d.visualization.draw_geometries([ground_pcd])
    
        # 统计非噪声簇的点数分布(噪声标签为-1)
    unique_labels, counts = np.unique(labels[labels >= 0], return_counts=True)

    if len(unique_labels) == 0:
        max_cluster_indices = np.array([])  # 无有效簇时返回空集
    else:
        # 找到点数最多的簇标签
        max_cluster_label = unique_labels[np.argmax(counts)]
        # 提取该簇所有点的索引
        max_cluster_indices = np.where(labels == max_cluster_label)[0]

    # 创建仅包含最大簇的点云
    max_cluster_pcd = ground_pcd.select_by_index(max_cluster_indices)
    # max_cluster_pcd.paint_uniform_color([0.5, 0.5, 0.5])
    o3d.visualization.draw_geometries([max_cluster_pcd])
    return labels

# ====================== 3. 可视化与保存 ======================
def visualize_ground(original_pcd, ground_pcd):
    """可视化原始点云与地面点云"""
    # 着色:地面=绿色,非地面=灰色
    # ground_pcd.paint_uniform_color([0.2, 0.8, 0.2])
    non_ground = original_pcd + ground_pcd
    non_ground.paint_uniform_color([0.7, 0.7, 0.7])
    
    # 可视化
    vis = o3d.visualization.Visualizer()
    vis.create_window(window_name="最大地平面提取")
    vis.add_geometry(ground_pcd)
    # vis.add_geometry(non_ground)
    
    # 视角设置
    ctr = vis.get_view_control()
    ctr.set_front([0, -1, 0.5])  # 俯视30度角
    ctr.set_lookat(original_pcd.get_center())
    
    # 渲染配置
    opt = vis.get_render_option()
    opt.point_size = 1.5
    opt.background_color = np.array([0.1, 0.1, 0.1])
    
    print("按ESC退出可视化")
    vis.run()
    vis.destroy_window()

# ====================== 主流程 ======================
if __name__ == "__main__":
    # 参数配置
    las_file = r"B:\360MoveData\Users\Administrator\Desktop\tmp\1026\las-detect\input\street.las"
    
    # 1. 读取点云
    pcd = read_las_to_open3d(las_file)
    
    # 2. 选择地面提取方法(二选一)
    # 方法A:区域化RANSAC(适合山地、丘陵)
    ground_pcd = extract_ground(
        pcd, 
        method="region_ransac",
        voxel_size=0.15,       # 下采样尺寸(米)
        distance_threshold=0.2, # 点到平面距离阈值
        region_size=6.0         # 网格大小
    )
    
    # # # 方法B:CSF滤波(适合城市道路、连续斜坡)
    # # ground_pcd = extract_ground(
    # #     pcd,
    # #     method="csf",
    # #     rigidness=2,           # 刚度(1软-5硬)
    # #     cloth_resolution=0.5,  # 布料网格分辨率
    # #     height_threshold=0.15  # 地面点高度阈值
    # # )

    # 3. 可视化与保存
    # visualize_ground(pcd, ground_pcd)

     # 假设已通过地面分割获得 ground_pcd
    cluster_labels = cluster_and_filter_ground(
        ground_pcd,
        min_cluster_points=1500,    # 最小点数阈值(低于此值视为噪声)
        eps=0.25,                  # 邻域半径基准值
        dynamic_threshold=True     # 启用动态阈值(推荐远距离场景)
    )
    print('start save ground')
    o3d.io.write_point_cloud("ground.ply", ground_pcd)  # 保存地面点云

2. 提取车道线

import open3d as o3d
import numpy as np
from sklearn.decomposition import PCA

# 1. 读取点云数据
pcd = o3d.io.read_point_cloud("ground.ply")

# 2. 基于灰度值分割点云 (r=g=b且灰度>125)
def grayscale_segmentation(pcd, threshold=125):
    colors = np.asarray(pcd.colors)
    # 检测灰度点 (r=g=b) 且灰度值 > threshold
    gray_mask = (colors[:, 0] == colors[:, 1]) & (colors[:, 1] == colors[:, 2]) & (colors[:, 0] > threshold/255.0)
    gray_indices = np.where(gray_mask)[0]
    return pcd.select_by_index(gray_indices), pcd.select_by_index(np.where(~gray_mask)[0])

# 3. 迭代拟合多条直线 (RANSAC改进版)
def fit_multiple_lines(pcd, max_lines=5, distance_threshold=0.02):
    lines = []
    line_points_set = []
    remaining_pcd = pcd
    
    for _ in range(max_lines):
        if len(remaining_pcd.points) < 5:  # 点数不足时停止
            break
        
        # PCA拟合直线方向 [2,9](@ref)
        points = np.asarray(remaining_pcd.points)
        pca = PCA(n_components=3)
        pca.fit(points)
        direction = pca.components_[0]
        centroid = np.mean(points, axis=0)
        
        # 计算点到直线的距离 [1](@ref)
        vectors = points - centroid
        projections = np.dot(vectors, direction)
        distances = np.linalg.norm(vectors - np.outer(projections, direction), axis=1)
        
        # 筛选内点 (距离小于阈值)
        inlier_mask = distances < distance_threshold
        inlier_indices = np.where(inlier_mask)[0]
        
        if len(inlier_indices) == 0:
            break
            
        # 保存当前直线参数和内点
        line_params = {
            "centroid": centroid,
            "direction": direction,
            "inliers": remaining_pcd.select_by_index(inlier_indices)
        }
        lines.append(line_params)
        line_points_set.append(line_params["inliers"])
        
        # 移除已拟合的点 [3](@ref)
        outlier_indices = np.where(~inlier_mask)[0]
        remaining_pcd = remaining_pcd.select_by_index(outlier_indices)
    
    return lines, line_points_set, remaining_pcd

# 4. 可视化结果
def visualize_results(original_pcd, line_points_set, remaining_pcd):
    # 原始点云设为灰色
    original_pcd.paint_uniform_color([0.5, 0.5, 0.5])
    
    # 为每条直线分配不同颜色
    colors = [
        [1, 0, 0], [0, 1, 0], [0, 0, 1],
        [1, 1, 0], [1, 0, 1]
    ]
    for i, line_pcd in enumerate(line_points_set):
        line_pcd.paint_uniform_color(colors[i % len(colors)])
    
    # 合并可视化对象
    # vis_objects = [original_pcd] + line_points_set + [remaining_pcd]
    vis_objects = line_points_set + [remaining_pcd]
    o3d.visualization.draw_geometries(vis_objects)

# 主流程
if __name__ == "__main__":
    # 灰度分割
    gray_pcd,remaining_pcd = grayscale_segmentation(pcd, threshold=145)
    gray_pcd.paint_uniform_color([0.5, 0.5, 0.5])
    
    o3d.visualization.draw_geometries([gray_pcd])

    # 拟合多条直线
    lines, line_points_set, remaining_pcd = fit_multiple_lines(
        gray_pcd, max_lines=5, distance_threshold=5
    )
    
    # 输出结果
    print(f"拟合到 {len(lines)} 条直线")
    for i, line in enumerate(lines):
        print(f"直线 {i+1}: 中心点={line['centroid']}, 方向向量={line['direction']}")
    
    # 可视化
    visualize_results(pcd, line_points_set, remaining_pcd)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AI算法网奇

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值