ros1-lite3-slam

硬件设备

绝影云深处 lite3

硬件设备

建图场所

学校工业中心 —-> 室内场景

bg1

bg2

bg3

slam

vlp16雷达

启动雷达节点脚本

lidar

mobxterm

slam算法的效果对比

因为机器狗的算力限制和一些个人能力问题,

对比了几个主流的算法,最终挑选了faster-lio作为建图算法

  • 运行速度快
  • 对算力资源要求不高
  • 实时性强
  • 易于部署

fast-lio2

faster-lio

point-lio

faster_lio

建图中

faster_lio

point cloud filtering

原本的点云

四足机器人使用faster-lio算法采集的点云

运动较为剧烈

lite3_pcd

操作为 :

使用 octomap_server 来转化的

脚本如下 pcd2grid.launch 文件

<launch>
    <!-- 静态 TF:map -> base_footprint(如果你用 base_footprint) -->
    <node pkg="tf" type="static_transform_publisher" name="static_tf_map_base" args="0 0 0 0 0 0 map base_footprint 100"/>

    <node pkg="octomap_server" type="my_octomap_node" name="my_octomap" output="screen">
        <param name="file_path" value="/home/ysc/Desktop/tools/pcdFiles/lite3.pcd"/>
        <param name="filter_leaf_size_m" value="0.05"/>
        <param name="frame_id" type="string" value="map"/>
        <param name="base_frame_id" value="base_footprint"/>
        
        <param name="pointcloud_min_z" value="-1.0"/>
        <param name="pointcloud_max_z" value="2.0"/>
        <param name="occupancy_min_z" value="-0.1"/>
        <param name="occupancy_max_z" value="2.0"/>

        <param name="filter_speckles" value="false"/>
        <param name="sensor_model/max_range" value="100.0"/>
        <param name="sensor_model/min_range" value="-1.0"/>
        <param name="resolution" value="0.05"/>
        <param name="compress_map" value="true"/>
        <param name="publish_free_space" value="false"/>
        <param name="latch" value="true"/>
    
        <remap from="/projected_map" to="/map" />
    </node>

    <node name="rviz" pkg="rviz" type="rviz" args="-d $(find pcd2grid)/rviz/config.rviz" />
</launch>

pcd_grid

改良

CSF

CSF算法,全称为Cloth Simulation Filtering,是一种基于欧几里得空间中最小生成树思想的聚类算法,它可以很好地对点云数据进行分割和分类。这个算法的核心思想是把点云数据转化成一张图,然后通过逐步连接最近邻点构建出多棵生成树,再将所有生成树合并成一张图,并利用深度优先搜索、双向广搜等手段对其上点进行分割。这样就可以将点云数据快速分割成多个类别,并且获取到每个点所属的类别。

​ 在CSF算法的基础上,可以使用其地面点滤波方法。具体方法是通过使用RANSAC算法估计地面模型的法向量,然后通过计算每个点到地面模型的距离来滤除非地面点,而最后剩下的点即为地面点。同时,需要在此基础上做一些修改来提高算法的效率:首先,将待处理的点云数据进行降采样,减少计算量;其次,使用KD树快速搜索每个点的最近邻点,加速点云数据的处理过程。

​ 此外,CSF算法也可以结合其他技术进行优化,例如在数据预处理阶段使用数据降维技术,减少数据量;在生成树构建阶段使用多线程并行计算,提高计算效率;在分割阶段使用机器学习算法,提高分割精度等。

​ 总之,CSF算法是一种基于图模型的聚类算法,适用于点云数据的分割和分类任务。通过与其他技术的结合,可以实现更高效、更精确的点云数据处理。

使用的库为: https://github.com/jianboqi/CSF

import open3d as o3d
import numpy as np
import CSF  # pip install cloth-simulation-filter

'''
    算法的仓库地址为 : https://github.com/jianboqi/CSF    
    CSF 对 numpy 的版本 要求是 < 2 ,即 1几版本的 numpy
    布料结算
'''


def run_csf_filter(txt_path, ground_txt="ground.txt", non_ground_txt="non_ground.txt"):
    csf = CSF.CSF()
    csf.readPointsFromFile(txt_path)

    # 设置参数(你可根据实际需求调节)
    csf.params.bSloopSmooth = False
    csf.params.cloth_resolution = 0.5
    csf.params.class_threshold = 0.5
    csf.params.interations = 500

    ground = CSF.VecInt()
    non_ground = CSF.VecInt()
    csf.do_filtering(ground, non_ground)

    csf.savePoints(ground, ground_txt)
    csf.savePoints(non_ground, non_ground_txt)
    print("[✓] CSF 地面分离完成")


def pcd_to_csf_txt(pcd_path, txt_path):
    pcd = o3d.io.read_point_cloud(pcd_path)
    points = np.asarray(pcd.points)
    np.savetxt(txt_path, points, fmt="%.6f")
    print(f"[✓] 点云已保存为 CSF 输入格式:{txt_path}")

def csf_txt_to_pcd(txt_path, pcd_path):
    points = np.loadtxt(txt_path)
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points)
    o3d.io.write_point_cloud(pcd_path, pcd)
    print(f"[✓] 已保存为 PCD 格式:{pcd_path}")



def filter_lower_points(points_np, threshold=-0.1):
    """
    过滤掉 z 坐标低于阈值的点,默认阈值为 -0.1 米(地面以下10cm)
    """
    filtered_points_np = points_np[points_np[:, 2] >= threshold]
    print(f"过滤后点云共 {len(filtered_points_np)} 个点")
    return filtered_points_np

def downsample_pcd(points_np, voxel_size=0.05):
    """
    使用体素网格对点云下采样。
    参数:
        points_np: 原始点云的 numpy 数组,形状为 (N, 3)
        voxel_size: 下采样体素大小(单位:米)
    返回:
        下采样后的 numpy 数组
    """
    # 将 NumPy 数组转换为 PointCloud
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points_np)

    # 执行下采样
    downsampled_pcd = pcd.voxel_down_sample(voxel_size=voxel_size)

    # 转换为 NumPy 数组返回
    downsampled_points_np = np.asarray(downsampled_pcd.points)
    print(f"下采样后点云共 {len(downsampled_points_np)} 个点")
    return downsampled_points_np

def visualize_ground_non_ground(ground_points, non_ground_points):
    # 创建地面点云并上色(绿色)
    ground_pcd = o3d.geometry.PointCloud()
    ground_pcd.points = o3d.utility.Vector3dVector(ground_points)
    ground_pcd.paint_uniform_color([0, 1, 0])  # 绿色 RGB

    # 创建非地面点云并上色(红色)
    non_ground_pcd = o3d.geometry.PointCloud()
    non_ground_pcd.points = o3d.utility.Vector3dVector(non_ground_points)
    non_ground_pcd.paint_uniform_color([1, 0, 0])  # 红色 RGB

    # 可视化展示
    o3d.visualization.draw_geometries([ground_pcd, non_ground_pcd],
                                      window_name="CSF 地面去除效果",
                                      width=900, height=1500,
                                      point_show_normal=False)


def office_exp():
    csf = CSF.CSF()
    csf.readPointsFromFile('./pcd/temp_input.txt')

    csf.params.bSloopSmooth = False
    csf.params.cloth_resolution = 0.5

    ground = CSF.VecInt()  # a list to indicate the index of ground points after calculation
    non_ground = CSF.VecInt()  # a list to indicate the index of non-ground points after calculation
    csf.do_filtering(ground, non_ground)  # do actual filtering.
    csf.savePoints(ground, "ground.txt")


def main():
    # 1. 转换为 CSF 输入格式
    pcd_path = "./pcd/lite3.pcd"
    txt_input = "./pcd/temp_input.txt"
    pcd_to_csf_txt(pcd_path, txt_input)

    # 2. CSF 执行过滤
    run_csf_filter(txt_input, "ground.txt", "non_ground.txt")

    # 3. 输出转换回 PCD
    csf_txt_to_pcd("ground.txt", "./pcd/csf_ground.pcd")
    csf_txt_to_pcd("non_ground.txt", "./pcd/csf_non_ground.pcd")

if __name__ == '__main__':
    main()
    # office_exp()

过滤效果

csf 对于崎岖复杂的地形效果可能会更好,这边把桌腿也给过滤了,有点过滤过猛

  • 速度偏慢【可能是py的原因】
  • 效果还不错
  • 环境配置比较复杂,对算力要求较高
  • 有过滤过猛的现象,但是应该对于复杂地形十分地实用

image-20250405165306920

转为栅格图

csf_gridmap

RANSAC

RANSAC 是一种“用少数点猜模型、用多数点投票验证”的鲁棒拟合方法,适合在含噪声的数据中找到最优几何结构(如地面、直线、平面等)。

代码如下

import open3d as o3d
import numpy as np
# import CSF  # pyCSF 库


'''
    ransac 不能高度过滤,过滤后产生的平面会变成墙体

'''


def read_pcd(file):
    pcd = o3d.io.read_point_cloud(file)
    points_np = np.asarray(pcd.points)
    print(f"已读取点云,共 {len(points_np)} 个点")
    return points_np


def fast_ground_filtering(points_np, distance_threshold=0.1, upright_threshold=0.95):
    """
    快速地面点提取,只提取接近水平的平面作为地面。

    参数:
        points_np: 输入点云 (Nx3)
        distance_threshold: 点到平面最大距离(RANSAC容忍)
        upright_threshold: 与 z 轴夹角的 cos 值下限,越接近 1 越接近水平

    返回:
        ground_points: 只包含地面的点
        non_ground_points: 其余所有点(含墙/物体等)
    """
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points_np)

    plane_model, inliers = pcd.segment_plane(distance_threshold=distance_threshold,
                                             ransac_n=3,
                                             num_iterations=5000)
    [a, b, c, d] = plane_model
    normal = np.array([a, b, c])
    normal = normal / np.linalg.norm(normal)
    cos_angle = abs(np.dot(normal, np.array([0, 0, 1])))  # 与z轴的夹角余弦值

    print(f"平面方程: {a:.2f}x + {b:.2f}y + {c:.2f}z + {d:.2f} = 0")
    print(f"法向量: {normal},与z轴夹角cos值: {cos_angle:.3f}")

    if cos_angle < upright_threshold:
        print("[×] 拟合到的平面不是水平地面(可能是墙面),不提取")
        return np.empty((0, 3)), points_np

    ground_points = points_np[inliers]
    non_ground_points = np.delete(points_np, inliers, axis=0)

    print(f"[✓] 地面点提取成功:{len(ground_points)} 点,非地面点:{len(non_ground_points)} 点")
    return ground_points, non_ground_points

def filter_high_points(points_np, z_threshold=1.0):
    """只保留z坐标低于 z_threshold 的点(排除墙面)"""
    filtered = points_np[points_np[:, 2] <= z_threshold]
    print(f"高度过滤后剩余 {len(filtered)} 个点")
    return filtered


def visualize_ground_non_ground(ground_points, non_ground_points):
    # 创建地面点云并上色(绿色)
    ground_pcd = o3d.geometry.PointCloud()
    ground_pcd.points = o3d.utility.Vector3dVector(ground_points)
    ground_pcd.paint_uniform_color([0, 1, 0])  # 绿色 RGB

    # 创建非地面点云并上色(红色)
    non_ground_pcd = o3d.geometry.PointCloud()
    non_ground_pcd.points = o3d.utility.Vector3dVector(non_ground_points)
    non_ground_pcd.paint_uniform_color([1, 0, 0])  # 红色 RGB

    # 可视化展示
    o3d.visualization.draw_geometries([ground_pcd, non_ground_pcd],
                                      window_name="CSF 地面去除效果",
                                      width=1000, height=1500,
                                      point_show_normal=True)

def save_non_ground(non_ground_points_np,path):
    print(non_ground_points_np.shape)
    # 将 NumPy 点云数组转换为 PointCloud 对象
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(non_ground_points_np)

    # 保存为 PCD 或 PLY 文件
    o3d.io.write_point_cloud(path, pcd)
    print(f"非地面点已保存至:{path}")

def downsample_pcd(points_np, voxel_size=0.05):
    """
    使用体素网格对点云下采样。
    参数:
        points_np: 原始点云的 numpy 数组,形状为 (N, 3)
        voxel_size: 下采样体素大小(单位:米)
    返回:
        下采样后的 numpy 数组
    """
    # 将 NumPy 数组转换为 PointCloud
    pcd = o3d.geometry.PointCloud()
    pcd.points = o3d.utility.Vector3dVector(points_np)

    # 执行下采样
    downsampled_pcd = pcd.voxel_down_sample(voxel_size=voxel_size)

    # 转换为 NumPy 数组返回
    downsampled_points_np = np.asarray(downsampled_pcd.points)
    print(f"下采样后点云共 {len(downsampled_points_np)} 个点")
    return downsampled_points_np

def filter_lower_points(points_np, threshold=-0.1):
    """
    过滤掉 z 坐标低于阈值的点,默认阈值为 -0.1 米(地面以下10cm)
    """
    filtered_points_np = points_np[points_np[:, 2] >= threshold]
    print(f"过滤后点云共 {len(filtered_points_np)} 个点")
    return filtered_points_np



def main():
    file = "./pcd/lite3.pcd"
    pcd_points = read_pcd(file)
    # 高度阈值会扰乱
    # pcd_points = filter_lower_points(pcd_points)
    pcd_points = downsample_pcd(pcd_points)
    ground_points, non_ground_points = fast_ground_filtering(pcd_points)
    visualize_ground_non_ground(ground_points, non_ground_points)
    save_non_ground(non_ground_points,"./ransac_non_ground.pcd")

if __name__ == "__main__":
    main()

效果如下

效果很不错,速度很快,比较平坦的地面点云直接过滤出来了

image-20250405165449437

栅格地图

ransac_gridmap

csf和 ransac 和 原本的对比

对比

传统机器学习—>ML 【突发奇想】

我的点云没打标签,所以我优先想到的是无监督学习算法

SVM 恰好有一个无监督学习的版本

SVM 【One-Class SVM】

svm

KMEANS

从原理的角度上,KMEANS就很不适合,但想试试有多不适合

给自己蠢到了,效果很乐

kmeans

深度学习

什么年代了还在玩传统的机器学习?

来试试深度学习!

。。。。。。未完待续

pcd to gird map

启动 pcd2gird.sh 脚本

将pcd转化为2维的栅格图

文件比较大,需要一定的时间来转化

pcd2gird

栅格地图

map

导航直接 基于 move_base 的架构

move_base

启动导航脚本

nav

由于栅格地图太占据资源,故不显示在rviz中

位置初始化,位置比较好会自动初始化,如果位置太差也可以手动初始化

local_planner

A* 作为全局规划器,TEB作为局部规划器

TEB对于四足机器人平台还挺好用的

设置导航点使用 A* 规划算法和 TEB 规划器

A*

A*(A-Star)算法是一种启发式搜索算法,是图搜索中最经典、最常用的路径规划算法之一,常用于机器人导航、游戏AI、地图路径规划等场景。

A* 为每个节点 n 计算一个评估函数:

f(n)=g(n)+h(n)f(n) = g(n) + h(n)f(n)=g(n)+h(n)

符号 含义 解释
g(n) 起点到当前节点 n 的实际代价 走到当前位置花了多少代价(已知)
h(n) 当前节点到目标的启发式估计 还剩多少距离(估算值)
f(n) 总估价 当前路径的“总成本”预估
1. 将起点加入 open 列表(待探索)
2. 重复以下步骤,直到 open 列表为空或找到终点:
    a. 从 open 中选出 f(n) 最小的节点 n
    b. 若 n 是终点,结束,路径找到
    c. 将 n 加入 closed(已探索)
    d. 遍历 n 的所有邻居:
        - 若邻居在 closed,跳过
        - 若不在 open 或发现更短路径:
            - 更新 g(n), h(n), f(n)
            - 设置父节点(用于回溯路径)
            - 加入 open
3. 回溯路径:从终点依次沿父节点返回起点

ros中的 A*

move_base 默认使用 global_planner 中的 A* 算法进行全局路径规划

costmap_2d 提供的地图栅格成本用于 g(n) 评估;

启发式函数通常为目标距离;

结果是 /move_base/NavfnROS 话题输出的一条可行路径。

。。。。。。未完待续

TEB

。。。。。。未完待续

github