三维点云处理实战:从激光雷达原始数据到结构化场景理解

如果你刚从二维图像处理转向三维世界,第一次拿到激光雷达扫描出的那团“点云”时,大概率会感到一丝茫然。它不像图像那样规整地排列在像素网格上,而是空间中数以万计、看似无序的散点。然而,正是这团“混沌”的数据,承载着物体精确的三维几何、位置和朝向信息,是自动驾驶汽车感知环境、机器人规划路径、乃至数字孪生构建现实世界的基石。处理点云,核心任务就是将这团原始数据“降噪”、“梳理”并“解构”成有意义的部件。今天,我们就以最广泛使用的开源库PCL(Point Cloud Library)为工具,手把手带你走完激光雷达数据预处理、滤波与分割的完整流程,让你能快速获得干净、可分、可用的三维结构化信息。

1. 环境搭建与PCL初探

在深入算法之前,一个稳定且高效的开发环境是基石。PCL虽然功能强大,但其依赖复杂,安装过程曾是许多新手的“第一道坎”。如今,随着包管理工具的完善和容器化技术的普及,搭建环境已变得轻松许多。

对于大多数开发场景,我推荐使用 Ubuntu 20.04/22.04 LTS 作为开发系统,其软件源对PCL的支持最为友好。如果你使用Windows或macOS,通过 vcpkgHomebrew 安装也是可行的,但可能会遇到一些平台特有的链接库问题。为了极致的可复现性和环境隔离,我强烈建议初学者和项目团队使用 Docker

下面是一个包含PCL核心模块及常用可视化工具(如PCL Visualizer)的Dockerfile示例:

FROM ubuntu:22.04

RUN apt-get update && apt-get install -y \
    build-essential \
    cmake \
    git \
    libpcl-dev \
    pcl-tools \
    libvtk9-dev \
    libeigen3-dev \
    libboost-all-dev \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /workspace

使用 docker build 构建镜像后,你便拥有了一个纯净、一致的PCL开发环境。接下来,我们创建一个最简单的CMake项目来验证安装并理解PCL的基本数据流。

PCL的核心数据结构是 pcl::PointCloud<T>。最常用的点类型是 pcl::PointXYZ,它只包含XYZ坐标。但实际应用中,点通常携带更多信息,例如:

点类型包含字段典型应用场景
PointXYZx, y, z基本的几何处理,如滤波、配准
PointXYZIx, y, z, intensity激光雷达数据,强度可用于区分材质
PointXYZRGBx, y, z, r, g, bRGB-D相机数据,颜色信息用于分割、识别
PointNormalx, y, z, normal_x, normal_y, normal_z表面重建,需要法向量信息

一个完整的PCL处理流程通常遵循“加载 -> 预处理 -> 特征提取/分割 -> 输出/可视化”的管道模式。让我们从一个“Hello World”程序开始,它读取一个.pcd文件并打印其基本信息:

#include <iostream>
#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>

int main(int argc, char** argv) {
    if (argc != 2) {
        std::cerr << "请指定一个PCD文件路径,例如: ./read_pcd test.pcd" << std::endl;
        return -1;
    }

    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);

    if (pcl::io::loadPCDFile<pcl::PointXYZ>(argv[1], *cloud) == -1) {
        PCL_ERROR("无法读取文件 %s\n", argv[1]);
        return -1;
    }

    std::cout << "点云加载成功!" << std::endl;
    std::cout << "点数: " << cloud->points.size() << std::endl;
    std::cout << "宽度: " << cloud->width << std::endl;
    std::cout << "高度: " << cloud->height << std::endl;
    std::cout << "第一个点的坐标: (" 
              << cloud->points[0].x << ", " 
              << cloud->points[0].y << ", " 
              << cloud->points[0].z << ")" << std::endl;

    return 0;
}

提示:PCL广泛使用智能指针(如 pcl::PointCloud::Ptr)来管理点云对象的内存,这能有效避免内存泄漏,尤其是在复杂的处理流水线中。

编译并运行这个程序,如果你能看到点云的基本信息,那么恭喜你,环境已就绪,我们即将进入数据清洗的战场。

2. 点云数据预处理:滤波与降噪的艺术

直接从传感器(如激光雷达)获取的原始点云几乎总是充满“杂质”的。这些噪声可能来源于传感器本身的测量误差、环境中漂浮的尘埃、雨滴、或者远处物体反射的微弱回波。未经处理的数据会严重干扰后续的分割、识别等高级任务。因此,滤波是点云处理流水线中不可或缺的第一步。

2.1 体素栅格下采样:从海量到精炼

激光雷达一帧数据动辄产生数十万个点,对每个点进行复杂运算代价高昂。体素栅格滤波(Voxel Grid Filter) 的核心思想非常直观:将三维空间划分为均匀的微小立方体(体素),然后用每个体素内所有点的重心(或中心点)来代表这个体素。这既能大幅减少点云数量(下采样),又能在一定程度上保持原始点云的几何形状。

它的关键参数只有一个:体素叶子尺寸(leaf size)。这个值决定了滤波的“粗糙度”。

#include <pcl/filters/voxel_grid.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);           // 输入原始点云
sor.setLeafSize(0.05f, 0.05f, 0.05f); // 设置体素边长,单位通常为米
sor.filter(*cloud_filtered);        // 执行滤波,输出结果

std::cout << "体素滤波后,点数从 " << cloud->size() 
          << " 减少到 " << cloud_filtered->size() << std::endl;

如何选择 leaf size?这需要在效率精度之间做权衡:

  • 0.01m ~ 0.03m:高精度,保留大量细节,适用于精细建模或近距离物体处理,但计算量较大。
  • 0.05m ~ 0.1m:中等精度,在自动驾驶中处理远处环境或进行快速预处理时常用。
  • > 0.1m:粗糙,会丢失大量结构信息,通常只用于对场景的快速概览或极端情况下的提速。

注意:体素滤波是非均匀采样。在点云密集的区域,它减少的点数更多;在稀疏区域,可能几乎不减少点数。这有时会导致特征分布不均,在后续计算点特征(如法向量)时需要注意。

2.2 统计离群点移除:剔除“孤僻”的噪声点

有些噪声点远离主点云,像“离群索居”的野点。统计离群点移除(Statistical Outlier Removal) 算法基于点邻域的统计分析来识别并移除它们。对于每个点,算法计算其到所有K个最近邻点的平均距离。假设整个点云中这些距离的分布符合高斯分布,那么那些平均距离超出均值一定标准差范围的点就被认为是离群点。

#include <pcl/filters/statistical_outlier_removal.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_sor_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setMeanK(50);                 // 为每个点考虑的邻居数量
sor.setStddevMulThresh(1.0);      // 距离均值的标准差倍数阈值
sor.filter(*cloud_sor_filtered);

// 如果你想查看被移除的离群点,可以这样做
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_noise(new pcl::PointCloud<pcl::PointXYZ>);
sor.setNegative(true);            // 设置为true,则输出被移除的点
sor.filter(*cloud_noise);
  • setMeanK(K):这个参数至关重要。K值太小,算法对局部噪声敏感;K值太大,可能会平滑掉真实的细小结构。对于室外激光雷达数据,K通常在30-100之间。
  • setStddevMulThresh(σ):阈值倍数。1.0意味着剔除所有距离大于“均值+1倍标准差”的点。增大这个值(如2.0)会保留更多点,变得更宽松;减小则更严格。

统计滤波能有效去除明显的、孤立的噪声,但对于附着在物体表面的小簇噪声(如灰尘形成的点簇)效果有限。

2.3 半径离群点移除:处理局部密度异常

另一种思路是基于空间密度。半径离群点移除(Radius Outlier Removal) 检查每个点给定半径球体内的邻居数量。如果邻居数量少于阈值,则认为该点是孤立的噪声点。

#include <pcl/filters/radius_outlier_removal.h>

pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_ror_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setInputCloud(cloud);
ror.setRadiusSearch(0.1);        // 搜索半径(米)
ror.setMinNeighborsInRadius(10); // 半径内最少邻居数
ror.filter(*cloud_ror_filtered);

这个算法非常适合处理那种“稀疏背景中的散点噪声”。例如,在室内场景中,远处墙壁上可能因为多次反射出现一些零星的点,使用半径滤波可以很好地清理掉。它的两个参数需要根据点云的平均点间距来设置。半径设置得比点间距稍大,最小邻居数则根据你希望保留的点的最小簇大小来定。

在实际项目中,我通常会采用组合滤波策略:先使用体素滤波进行下采样,提升整体处理速度;然后使用统计滤波去除明显的离群点;如果场景中有特定的稀疏噪声,再辅以半径滤波。这个流程能平衡速度和效果,为后续的分割任务打下坚实基础。

3. 点云分割:从混沌中提取结构

滤波后的点云变得“干净”了,但它仍然是一个整体。分割的目标是将这个整体按照某种标准(如几何属性、颜色、纹理)划分成不同的子集,每个子集对应一个潜在的物体或结构。对于自动驾驶和机器人导航,最常见的任务之一就是分离地面与非地面物体

3.1 RANSAC平面分割:快速找到最大平面

RANSAC(Random Sample Consensus) 是一种强大的模型参数估计方法,特别适用于数据中包含大量外点(不符合模型的点)的情况。在点云中,我们可以用它来拟合诸如平面、圆柱、球体等几何模型。寻找地面,本质上就是寻找一个最大的平面模型。

#include <pcl/ModelCoefficients.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>

// 创建分割对象并设置参数
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);
pcl::SACSegmentation<pcl::PointXYZ> seg;
seg.setOptimizeCoefficients(true);      // 优化模型系数
seg.setModelType(pcl::SACMODEL_PLANE);  // 分割模型:平面
seg.setMethodType(pcl::SAC_RANSAC);     // 方法:RANSAC
seg.setDistanceThreshold(0.03);         // 距离阈值:点到平面的距离小于此值则视为内点
seg.setMaxIterations(1000);             // RANSAC最大迭代次数

seg.setInputCloud(cloud_filtered);
seg.segment(*inliers, *coefficients);   // 执行分割

if (inliers->indices.size() == 0) {
    PCL_ERROR("未能估计出平面模型。\n");
    return -1;
}

// 提取地面点云(内点)
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_ground(new pcl::PointCloud<pcl::PointXYZ>);
pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud_filtered);
extract.setIndices(inliers);
extract.setNegative(false); // false 表示提取内点(地面)
extract.filter(*cloud_ground);

// 提取非地面点云(外点)
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_obstacles(new pcl::PointCloud<pcl::PointXYZ>);
extract.setNegative(true); // true 表示提取外点(非地面)
extract.filter(*cloud_obstacles);

std::cout << "地面点数量: " << cloud_ground->size() << std::endl;
std::cout << "非地面点数量: " << cloud_obstacles->size() << std::endl;
std::cout << "平面方程系数 (ax+by+cz+d=0): " 
          << coefficients->values[0] << ", " 
          << coefficients->values[1] << ", "
          << coefficients->values[2] << ", "
          << coefficients->values[3] << std::endl;
  • setDistanceThreshold():这是RANSAC平面分割中最敏感的参数。它定义了“一个点多近才算在平面上”。对于平整的室内地面,可以设置较小(如0.02m);对于粗糙的室外路面,则需要设置得大一些(如0.05m-0.1m)。
  • setMaxIterations():迭代次数越多,找到正确模型的概率越高,但计算时间也越长。通常1000次对于大多数场景已足够。
  • 系数解读:输出的平面方程 ax+by+cz+d=0 中,(a, b, c) 是平面的法向量。在自动驾驶坐标系(前x,左y,上z)中,地面的法向量应接近 (0, 0, 1)

RANSAC的优点是简单、快速,能抵抗大量外点干扰。但它一次只能提取一个模型(最大的平面)。在实际道路场景中,地面可能不是单一的平面,或者有斜坡,这时就需要更复杂的方法。

3.2 欧几里得聚类分割:区分不同的障碍物

分离出地面后,剩下的“非地面点云”可能包含车辆、行人、树木、建筑物等多个物体。我们需要进一步将它们区分开。欧几里得聚类分割(Euclidean Cluster Extraction) 基于一个朴素的假设:属于同一个物体的点,在空间中的距离应该比较近;而不同物体之间的点,距离则相对较远。

这本质上是一个基于距离的聚类问题。PCL中通过 pcl::EuclideanClusterExtraction 来实现:

#include <pcl/kdtree/kdtree.h>
#include <pcl/segmentation/extract_clusters.h>

// 为搜索创建KD-Tree结构,加速近邻搜索
pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
tree->setInputCloud(cloud_obstacles); // 对非地面点云进行聚类

std::vector<pcl::PointIndices> cluster_indices;
pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
ec.setClusterTolerance(0.25);     // 聚类距离容差(米)。两点距离小于此值视为同一簇。
ec.setMinClusterSize(50);         // 一个簇最少包含的点数
ec.setMaxClusterSize(25000);      // 一个簇最多包含的点数
ec.setSearchMethod(tree);
ec.setInputCloud(cloud_obstacles);
ec.extract(cluster_indices);      // 执行聚类,结果保存在cluster_indices中

std::cout << "共发现 " << cluster_indices.size() << " 个聚类。" << std::endl;

// 为每个聚类分配随机颜色并可视化(或保存)
int j = 0;
for (const auto& cluster : cluster_indices) {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster(new pcl::PointCloud<pcl::PointXYZ>);
    for (const auto& idx : cluster.indices) {
        cloud_cluster->points.push_back(cloud_obstacles->points[idx]);
    }
    cloud_cluster->width = cloud_cluster->points.size();
    cloud_cluster->height = 1;
    cloud_cluster->is_dense = true;

    std::cout << "聚类 " << j << " 包含 " << cloud_cluster->size() << " 个点。" << std::endl;
    // 此处可以保存 cloud_cluster 或进行进一步处理(如边界框拟合、分类等)
    j++;
}
  • setClusterTolerance():这是聚类的“分辨率”。设置太小,一个物体会被拆分成多个小簇;设置太大,多个靠近的物体会被合并成一个簇。对于车载激光雷达,0.2m到0.5m是一个常见的起始尝试范围。
  • setMinClusterSize():过滤掉过小的点簇,它们很可能是噪声或无关紧要的小物体(如路边的碎石)。
  • setMaxClusterSize():防止将整个场景错误地聚为一类(例如当容差设置过大时)。

欧几里得聚类简单高效,是障碍物检测中非常实用的第一步。但它只考虑了空间距离,对于互相接触或部分遮挡的物体(如两辆并排停靠的汽车)可能无法正确分割。这时就需要引入更高级的特征,如颜色、法向量,甚至深度学习模型。

4. 实战整合与性能优化

现在,让我们将上述模块串联起来,构建一个完整的、面向自动驾驶场景的激光雷达点云处理流水线。这个流水线将从原始的.pcd文件开始,输出分割好的地面点和各个障碍物聚类。

4.1 完整处理流水线示例

下面的代码展示了如何将读取、滤波、分割、聚类和输出整合到一个程序中。为了便于管理,我们使用PCL的 PassThrough 滤波器先进行一个粗略的Z轴范围过滤,只保留我们感兴趣的高度范围内的点(例如,去掉天空和地下过深的点)。

#include <pcl/io/pcd_io.h>
#include <pcl/filters/voxel_grid.h>
#include <pcl/filters/statistical_outlier_removal.h>
#include <pcl/filters/passthrough.h>
#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/segmentation/extract_clusters.h>
#include <pcl/visualization/pcl_visualizer.h>
#include <iostream>
#include <vector>

int main() {
    // 1. 加载点云
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    if (pcl::io::loadPCDFile<pcl::PointXYZ>("street_scene.pcd", *cloud) == -1) {
        PCL_ERROR("文件读取失败。\n");
        return -1;
    }
    std::cout << "原始点云点数: " << cloud->size() << std::endl;

    // 2. 直通滤波,限制Z轴范围(例如 -2m 到 5m)
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_passthrough(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PassThrough<pcl::PointXYZ> pass;
    pass.setInputCloud(cloud);
    pass.setFilterFieldName("z");
    pass.setFilterLimits(-2.0, 5.0);
    pass.filter(*cloud_passthrough);

    // 3. 体素下采样
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_voxel(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::VoxelGrid<pcl::PointXYZ> voxel;
    voxel.setInputCloud(cloud_passthrough);
    voxel.setLeafSize(0.05f, 0.05f, 0.05f);
    voxel.filter(*cloud_voxel);
    std::cout << "体素滤波后点数: " << cloud_voxel->size() << std::endl;

    // 4. 统计离群点移除
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_clean(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
    sor.setInputCloud(cloud_voxel);
    sor.setMeanK(50);
    sor.setStddevMulThresh(1.0);
    sor.filter(*cloud_clean);
    std::cout << "统计滤波后点数: " << cloud_clean->size() << std::endl;

    // 5. RANSAC平面分割(提取地面)
    pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
    pcl::PointIndices::Ptr ground_inliers(new pcl::PointIndices);
    pcl::SACSegmentation<pcl::PointXYZ> seg;
    seg.setOptimizeCoefficients(true);
    seg.setModelType(pcl::SACMODEL_PLANE);
    seg.setMethodType(pcl::SAC_RANSAC);
    seg.setDistanceThreshold(0.1); // 室外路面,阈值设大一些
    seg.setInputCloud(cloud_clean);
    seg.segment(*ground_inliers, *coefficients);

    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_ground(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_obstacles(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::ExtractIndices<pcl::PointXYZ> extract;
    extract.setInputCloud(cloud_clean);
    extract.setIndices(ground_inliers);
    extract.setNegative(false);
    extract.filter(*cloud_ground);
    extract.setNegative(true);
    extract.filter(*cloud_obstacles);
    std::cout << "地面点: " << cloud_ground->size() << ", 障碍物点: " << cloud_obstacles->size() << std::endl;

    // 6. 欧几里得聚类(分割障碍物)
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>);
    tree->setInputCloud(cloud_obstacles);
    std::vector<pcl::PointIndices> cluster_indices;
    pcl::EuclideanClusterExtraction<pcl::PointXYZ> ec;
    ec.setClusterTolerance(0.3);
    ec.setMinClusterSize(80);
    ec.setMaxClusterSize(5000);
    ec.setSearchMethod(tree);
    ec.setInputCloud(cloud_obstacles);
    ec.extract(cluster_indices);

    std::cout << "发现障碍物聚类数量: " << cluster_indices.size() << std::endl;

    // 7. 可视化结果(可选)
    pcl::visualization::PCLVisualizer::Ptr viewer(new pcl::visualization::PCLVisualizer("3D Viewer"));
    viewer->setBackgroundColor(0, 0, 0);
    // 用灰色显示地面
    viewer->addPointCloud<pcl::PointXYZ>(cloud_ground, "ground");
    viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR, 0.5, 0.5, 0.5, "ground");
    // 用不同颜色显示每个聚类
    int cluster_id = 0;
    for (const auto& indices : cluster_indices) {
        pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_cluster(new pcl::PointCloud<pcl::PointXYZ>);
        for (auto index : indices.indices)
            cloud_cluster->points.push_back(cloud_obstacles->points[index]);
        cloud_cluster->width = cloud_cluster->points.size();
        cloud_cluster->height = 1;
        std::stringstream ss;
        ss << "cluster_" << cluster_id++;
        viewer->addPointCloud<pcl::PointXYZ>(cloud_cluster, ss.str());
        // 分配随机颜色
        viewer->setPointCloudRenderingProperties(pcl::visualization::PCL_VISUALIZER_COLOR,
                                                 rand() / (RAND_MAX + 1.0),
                                                 rand() / (RAND_MAX + 1.0),
                                                 rand() / (RAND_MAX + 1.0),
                                                 ss.str());
    }
    while (!viewer->wasStopped()) {
        viewer->spinOnce(100);
    }

    return 0;
}

4.2 参数调优与常见陷阱

在实际部署中,参数绝不是一成不变的。你需要根据具体的传感器、场景和环境动态调整。这里有一些经验性的指导原则:

  1. 体素叶子尺寸:起始值可以设为传感器在10米处的点间距的2-3倍。你可以通过计算点云中最近邻点的平均距离来估算。
  2. RANSAC距离阈值:在平整地面上,可以先设一个较小的值(如0.03),然后逐渐增大,直到地面点被完整地分割出来。观察平面法向量是否接近垂直向上,可以验证分割的正确性。
  3. 聚类容差:这个值应该略大于同一物体上点与点之间的最大预期间隙。对于64线激光雷达,在20米内的物体,0.2m到0.4m通常效果不错。可以先从一个较小的值开始,如果物体被分割得过碎,再适当调大。
  4. 最小聚类点数:这个值需要根据体素滤波后的点云密度和你想检测的最小物体尺寸来定。一个经验公式是:最小点数 ≈ (物体最小体积 / (体素叶子尺寸^3)) * 填充因子。填充因子是一个小于1的值,考虑到物体不是实心的。

注意:滤波和分割的顺序有时会影响结果。例如,先做统计滤波再去地面,与先去地面再做统计滤波,得到的非地面点云可能不同,因为地面点本身可能影响其上方点的邻域统计。需要根据实际情况试验。

4.3 性能考量与进阶方向

当处理高频激光雷达数据(如10Hz)时,性能至关重要。体素滤波是最大的提速环节。此外,将PCL与多线程库(如Intel TBB)一起编译,可以自动并行化许多算法。

对于更复杂的地面(如斜坡、起伏路面),单一的RANSAC平面模型就不够了。可以考虑:

  • 分段平面拟合:将点云按区域划分,分别拟合平面。
  • 基于法向量的地面分割:先计算点云法向量,然后将法向量接近垂直向上的点归类为地面。
  • 使用深度学习模型:如RangeNet++、Cylinder3D等,可以直接对点云进行逐点语义分割,准确区分地面、车辆、行人等多种类别,这是目前前沿的研究和应用方向。

处理完分割后的点云,你可以为每个聚类计算最小包围盒(Bounding Box)质心体积等特征,进而实现障碍物的跟踪、分类和行为预测,这才是整个感知系统价值闭环的关键。从一团混沌的原始点云,到清晰分明的结构化物体,这个过程本身就像是在数据中雕刻出世界的形状。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐