1. 点云:三维世界的“像素点”,为何如此特别?

大家好,我是老张,在三维视觉和机器人领域摸爬滚打了十几年。今天想和大家聊聊一个听起来有点“硬核”,但实际上无处不在的技术——点云的特征提取与分类。简单来说,这就像是教机器看懂三维世界。我们人类看一个杯子,能立刻知道它是杯子,能装水。但机器看到的,只是一堆密密麻麻、漂浮在空间中的点(也就是点云)。如何让机器从这堆“散沙”中认出杯子、桌子、汽车,就是我们要解决的问题。

你可能觉得,图像识别都那么成熟了,三维识别不是手到擒来?还真不是。点云数据和二维图像有本质区别,这也正是它有趣和困难的地方。我刚开始接触时,也踩过不少坑。点云数据有三个“臭脾气”:无序性、稀疏性和信息有限性。

想象一下,你用手持扫描仪从不同角度扫描同一个椅子,得到的点云里,点的排列顺序是完全随机的。这种无序性,让那些擅长处理规整网格数据的传统模型直接“傻眼”。其次,点云很“稀疏”。比如自动驾驶的激光雷达,打出去的光束在远处可能几十厘米才有一个点,远远比不上相机图像的像素密度。最后,一个点通常只包含坐标(x, y, z),顶多再加个反射强度,信息量非常“干瘪”,不像图像有丰富的颜色和纹理。

正是因为这些特性,点云处理技术的发展走出了一条从“手工设计”到“自动学习”的独特路径。早期,工程师们像手工匠人一样,精心设计各种特征描述子(比如VFH、FPFH)来捕捉物体的几何形状。后来,深度学习的浪潮席卷而来,研究者们又发明了像PointNet、PointCNN这样的网络,让机器自己从数据中学习特征。这两种思路各有千秋,也各有适用的场景。接下来,我就带你从传统方法一路走到深度学习,看看我们是如何一步步教会机器“看懂”三维世界的,并分享一些我实战中的代码和心得。

2. 传统方法时代:手工打造的“特征尺”

在深度学习还没成为主流之前,处理点云全靠“手工特征”。这就像你要向一个没见过苹果的人描述苹果,你会说:“它是圆的、红色的、有个把儿”。这里的“圆”、“红”、“把儿”就是你手工提取的特征。在点云中,我们也需要设计一套数学语言来描述物体的形状。

2.1 特征的三六九等:从单点到全局

点云特征可以按不同维度来划分,理解这个分类对后续方法选择至关重要。

按物理属性分,主要是几何特征(形状、曲率、法向量)和强度特征(激光反射强度)。在自动驾驶中,强度信息能帮助区分柏油路面和草地。

按空间尺度分,这是更核心的分类方式:

  • 单点特征:每个点自带的属性,比如它的三维坐标、法向量方向、曲率值。这就像一个人的身高、体重。
  • 局部特征:描述一个点及其周围小邻域的几何特性。比如PFH(点特征直方图)和它的加速版FPFH。它们通过统计点与邻居点之间法向量的夹角关系,形成一个直方图,来描述这个局部区域的表面变化情况。这个特征对物体表面的细节很敏感。
  • 全局特征:描述整个点云对象的整体形状。最具代表性的就是VFH(视点特征直方图)。它是在FPFH的基础上,融入了物体中心到视点(扫描仪位置)的方向信息。这样一来,它不仅描述了物体本身的形状,还编码了观察视角,使得同一个物体从不同角度看,其特征表达也能保持相对稳定。

我刚开始用PCL(点云库)时,就被这些缩写搞晕过。后来发现,抓住核心思想就容易了:局部特征关注“皮肤纹理”,全局特征关注“整体轮廓”。在物体识别任务中,我们通常提取一个全局特征(如VFH)来代表整个物体,然后扔给分类器(如SVM)去学习。而在点云分割(比如把一辆车上的点分成车轮、车身、车窗)任务中,则需要为每一个点计算其局部特征,再结合上下文进行判断。

2.2 实战:用VFH+SVM搭建一个物体分类器

理论说了不少,我们来点实际的。我最早做机器人抓取项目时,需要让机械臂识别工作台上的几种工具(扳手、螺丝刀、锤子)。当时深度学习资源还不多,VFH+SVM是稳定可靠的选择。下面我带你走一遍流程,并附上关键代码。

第一步:计算VFH特征 VFH特征可以理解为一个308维的向量(由FPFH分量和视点分量组成)。使用PCL库计算非常方便。

#include <pcl/point_types.h>
#include <pcl/features/vfh.h>
#include <pcl/features/normal_3d.h>
#include <pcl/io/pcd_io.h>

pcl::PointCloud<pcl::VFHSignature308>::Ptr computeVFH(pcl::PointCloud<pcl::PointXYZ>::Ptr cloud) {
    // 1. 估计法线(VFH的基础是法线信息)
    pcl::NormalEstimation<pcl::PointXYZ, pcl::Normal> ne;
    pcl::PointCloud<pcl::Normal>::Ptr normals(new pcl::PointCloud<pcl::Normal>());
    pcl::search::KdTree<pcl::PointXYZ>::Ptr tree(new pcl::search::KdTree<pcl::PointXYZ>());
    ne.setInputCloud(cloud);
    ne.setSearchMethod(tree);
    ne.setKSearch(50); // 使用最近的50个点来估计法线
    ne.compute(*normals);

    // 2. 计算VFH
    pcl::VFHEstimation<pcl::PointXYZ, pcl::Normal, pcl::VFHSignature308> vfh;
    pcl::PointCloud<pcl::VFHSignature308>::Ptr vfh_features(new pcl::PointCloud<pcl::VFHSignature308>());
    vfh.setInputCloud(cloud);
    vfh.setInputNormals(normals);
    vfh.setSearchMethod(tree);
    vfh.compute(*vfh_features);

    return vfh_features;
}

这段代码是核心。注意,计算法线时setKSearch的参数很重要,太小会受噪声影响,太大会平滑掉细节。根据我的经验,对于室内桌面物体,点云数量在几万级别时,K取30-50是个不错的起点。

第二步:准备训练数据 我们需要收集每个类别(扳手、螺丝刀等)的多个点云样本,为每个样本计算VFH特征,并打上标签。

// 假设我们遍历一个文件夹,里面按类别存放了.pcd文件
std::vector<cv::Mat> training_data;
std::vector<float> training_labels;

for (每个类别) {
    for (该类别的每个点云文件) {
        pcl::PointCloud<pcl::PointXYZ>::Ptr cloud = loadPCDFile(文件名);
        // 可选:进行下采样滤波,减少计算量
        auto vfh = computeVFH(cloud);
        // 将308维特征向量转换成OpenCV的Mat格式
        cv::Mat descriptor(1, 308, CV_32FC1);
        for (int i=0; i<308; ++i) {
            descriptor.at<float>(0, i) = vfh->points[0].histogram[i];
        }
        training_data.push_back(descriptor);
        training_labels.push_back(类别ID);
    }
}

第三步:训练SVM分类器 这里我用OpenCV的SVM模块,它接口简单,够用。

#include <opencv2/ml.hpp>

void trainSVM(const std::vector<cv::Mat>& data, const std::vector<float>& labels) {
    // 准备OpenCV SVM需要的训练数据格式
    cv::Mat trainDataMat(data.size(), 308, CV_32FC1);
    cv::Mat labelsMat(labels.size(), 1, CV_32SC1);
    for (size_t i=0; i<data.size(); ++i) {
        data[i].copyTo(trainDataMat.row(i));
        labelsMat.at<int>(i, 0) = labels[i];
    }

    // 设置SVM参数(这里是最费功夫调参的地方)
    cv::Ptr<cv::ml::SVM> svm = cv::ml::SVM::create();
    svm->setType(cv::ml::SVM::C_SVC); // 用于多分类
    svm->setKernel(cv::ml::SVM::RBF); // 径向基核函数,通常比线性核效果好
    svm->setC(10.0);  // 惩罚系数,越大越不允许分错,但可能过拟合
    svm->setGamma(0.01); // RBF核的参数,影响单个样本的影响范围

    // 自动训练寻找最优参数(可选,但很耗时)
    // svm->trainAuto(trainDataMat, cv::ml::ROW_SAMPLE, labelsMat);

    // 手动训练
    svm->train(trainDataMat, cv::ml::ROW_SAMPLE, labelsMat);

    // 保存模型
    svm->save("tool_classifier_svm.xml");
    std::cout << "模型训练并保存完成!" << std::endl;
}

调参心得:C和Gamma是SVM(RBF核)的灵魂。我的经验是,先用trainAuto跑一遍,得到一个基准参数,然后在这个基准附近做网格搜索。比如C在[0.1, 1, 10, 100]里试,Gamma在[0.001, 0.01, 0.1]里试。虽然麻烦,但效果提升明显。

2.3 传统方法的优势与天花板

这套VFH+SVM的组合拳,在我早期的项目中立下了汗马功劳。它的优点非常突出:原理清晰,可解释性强。你可以直观地理解VFH直方图的每一个bin代表什么几何意义。对训练数据量要求不高,几十到几百个样本就能得到一个不错的模型。计算速度相对较快,在资源受限的嵌入式设备(如早期的机器人控制器)上也能跑。

但是,它的天花板也很低。首先,手工特征的设计极度依赖先验知识和领域经验。VFH对整体形状规则的物体(如杯子、盒子)效果好,但对于结构复杂、有大量内部细节或严重遮挡的物体,就力不从心了。其次,特征与分类器是分离的,特征提取过程没有针对最终的分类目标进行优化,可能存在信息损失。最后,对点云质量敏感,噪声、密度不均、遮挡都会严重影响VFH等特征的稳定性。

我遇到过一个问题:识别不同型号的螺丝刀。它们的全局形状非常相似,但手柄纹理不同。VFH这种全局几何特征完全无法区分,项目一度卡壳。这让我深刻意识到,需要一种能自动学习并捕捉细微差别的能力。这自然就引向了深度学习。

3. 深度学习革命:让网络自己学习“特征”

时间来到2015年前后,深度学习在图像领域大杀四方。但点云的无序性像一堵高墙,让标准的CNN(卷积神经网络)无法直接应用。想象一下,你把点云随机打乱顺序,它代表的物体并没有变,但输入到网络的数据数组却完全变了,这对于需要固定输入结构的网络来说是灾难。直到2017年,Qi等人提出的PointNet,才真正打开了点云深度学习的潘多拉魔盒。

3.1 PointNet:开创性的直接处理点云网络

PointNet的核心思想非常巧妙,它解决了无序性问题。它的网络结构可以概括为:为每个点独立学习一个特征(通过共享权重的多层感知机MLP),然后通过一个对称函数(比如最大池化max pooling)将所有点的特征聚合起来,得到一个全局特征。

这个最大池化是关键!无论点的顺序如何变化,取所有点特征在每一个维度上的最大值,得到的结果都是一样的。这就保证了网络的置换不变性。PointNet还引入了T-Net(一个微型网络)来学习一个变换矩阵,对输入点云进行对齐(旋转、平移归一化),增强了旋转不变性。

我用PyTorch实现过一个简化的PointNet分类核心部分,可以帮助你理解:

import torch
import torch.nn as nn
import torch.nn.functional as F

class PointNetCls(nn.Module):
    def __init__(self, num_classes=10):
        super().__init__()
        # 为每个点独立提取特征
        self.conv1 = nn.Conv1d(3, 64, 1) # 输入通道3 (x,y,z), 输出64
        self.conv2 = nn.Conv1d(64, 128, 1)
        self.conv3 = nn.Conv1d(128, 1024, 1) # 最终每个点得到一个1024维特征
        self.bn1 = nn.BatchNorm1d(64)
        self.bn2 = nn.BatchNorm1d(128)
        self.bn3 = nn.BatchNorm1d(1024)

        # 分类头
        self.fc1 = nn.Linear(1024, 512)
        self.fc2 = nn.Linear(512, 256)
        self.fc3 = nn.Linear(256, num_classes)
        self.dropout = nn.Dropout(p=0.3)

    def forward(self, x):
        # x shape: [B, 3, N]  B是批大小,N是点数
        x = F.relu(self.bn1(self.conv1(x)))
        x = F.relu(self.bn2(self.conv2(x)))
        x = self.bn3(self.conv3(x)) # [B, 1024, N]

        # 对称函数:最大池化,聚合全局特征
        x = torch.max(x, 2, keepdim=True)[0] # [B, 1024, 1]
        x = x.view(-1, 1024) # [B, 1024]

        # 通过全连接层分类
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        x = F.relu(self.fc2(x))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

你可以看到,网络输入就是原始的(x,y,z)坐标,不需要任何手工特征。Conv1d在这里的作用等价于共享权重的MLP,对每个点进行特征变换。torch.max就是那个关键的对称聚合函数。

PointNet的优缺点:它简单、高效,在ModelNet40这样的标准数据集上取得了当时最好的效果。但它也有明显缺点:局部特征提取能力弱。最大池化会丢失很多点的空间分布信息,网络对点的局部邻域关系不敏感。这就像只看一个人的最高分和最低分来评价他,忽略了中间的大量信息。

3.2 进阶网络:从局部感受到图卷积

为了克服PointNet的不足,后续研究集中在如何更好地建模点之间的局部几何关系。这催生了两大类主流方法:

1. 基于卷积的方法:代表是PointCNN。它的思路是,虽然点云无序,但我可以学习一个“排序”或“权重”。PointCNN通过一个X-Conv操作,先对局部点进行加权和排序,将其变换到一个规范的潜在空间中,然后再应用标准的卷积。这相当于让网络自己学会如何组织无序的点,以便进行卷积运算。这类方法效果很好,但计算量相对较大。

2. 基于图的方法:代表是DGCNN(动态图卷积网络)。这是我最喜欢也最常用的架构之一。它的思想非常直观:把点云看作一个图,每个点是一个节点,通过寻找每个点的K近邻来动态地构建边。然后在图上进行卷积操作(图卷积),来聚合邻居点的信息。

DGCNN的巧妙之处在于“动态”二字。网络在每一层都会根据当前点的特征,重新计算K近邻来构建图。这意味着,随着网络层数加深,特征相似的点(即使空间上不近)也会被连接起来,从而捕捉更高层次的语义相似性。我把它比作“物以类聚”——网络自己学会了把属于同一部件(比如车轮上的点)的点归到一起。

# 一个简化的DGCNN图卷积层示意
def edge_conv(x, k=20):
    """
    x: 输入点特征 [B, C, N]
    k: 近邻数量
    """
    batch_size, num_dims, num_points = x.size()
    # 1. 计算K近邻 (这里简化,实际使用特征空间的距离)
    idx = knn(x, k=k) # 返回索引 [B, N, k]
    # 2. 构建局部邻域特征
    neighbors = get_graph_feature(x, k=k, idx=idx) # [B, 2*C, N, k]
    # 3. 应用共享MLP(等价于1x1卷积)和图池化
    x = some_mlp(neighbors) # [B, C', N, k]
    x = torch.max(x, dim=-1)[0] # 在邻居维度上池化 [B, C', N]
    return x

在实际项目中,比如做室内场景的物体分割(区分墙、地板、桌子、椅子),DGCNN的表现通常比PointNet好不少,因为它能更好地利用局部上下文信息。不过,动态建图也带来了额外的计算开销。

3.3 实战:用PyTorch和DGCNN训练一个分类模型

理论再好,不如跑通代码。我们以在ModelNet40数据集上训练一个分类模型为例。现在网上有很多开源代码,我建议从官方实现或流行的开源库(如torch-geometric)开始。

第一步:数据准备 ModelNet40数据集包含40个类别的CAD模型。我们需要将其采样成点云。通常使用1024个点。

import torch
from torch.utils.data import Dataset, DataLoader
import os
import numpy as np

class ModelNet40(Dataset):
    def __init__(self, root, split='train', num_points=1024):
        self.root = root
        self.split = split
        self.num_points = num_points
        self.catfile = os.path.join(root, 'modelnet40_shape_names.txt')
        self.cat = [line.rstrip() for line in open(self.catfile)]
        self.classes = dict(zip(self.cat, range(len(self.cat))))
        
        # 加载数据路径和标签
        shape_ids = {}
        shape_ids['train'] = [line.rstrip() for line in open(os.path.join(root, 'modelnet40_train.txt'))]
        shape_ids['test'] = [line.rstrip() for line in open(os.path.join(root, 'modelnet40_test.txt'))]
        
        self.datapath = []
        for name in shape_ids[split]:
            shape_name = name.split('_')[0]
            self.datapath.append([shape_name, os.path.join(root, shape_name, name + '.txt')])
            
    def __getitem__(self, index):
        fn = self.datapath[index]
        cls = self.classes[fn[0]]
        point_set = np.loadtxt(fn[1], delimiter=',').astype(np.float32)
        # 采样固定点数
        choice = np.random.choice(point_set.shape[0], self.num_points, replace=True)
        point_set = point_set[choice, :3] # 只取xyz坐标
        # 归一化到单位球内
        point_set = point_set - np.expand_dims(np.mean(point_set, axis=0), 0)
        dist = np.max(np.sqrt(np.sum(point_set ** 2, axis=1)))
        point_set = point_set / dist
        return torch.from_numpy(point_set).float(), torch.tensor(cls).long()
    
    def __len__(self):
        return len(self.datapath)

第二步:定义DGCNN模型 我们可以使用torch_geometric库,它封装了图神经网络的各种层,非常方便。

import torch.nn as nn
import torch.nn.functional as F
from torch_geometric.nn import DynamicEdgeConv, global_max_pool
from torch_geometric.data import Data, Batch

class DGCNN(nn.Module):
    def __init__(self, num_classes=40, k=20):
        super(DGCNN, self).__init__()
        self.k = k
        # 第一个EdgeConv层
        self.conv1 = DynamicEdgeConv(nn=nn.Sequential(
            nn.Linear(3*2, 64),
            nn.ReLU(),
            nn.Linear(64, 64)
        ), k=k)
        # 第二个EdgeConv层
        self.conv2 = DynamicEdgeConv(nn=nn.Sequential(
            nn.Linear(64*2, 128),
            nn.ReLU(),
            nn.Linear(128, 128)
        ), k=k)
        # 第三个EdgeConv层
        self.conv3 = DynamicEdgeConv(nn=nn.Sequential(
            nn.Linear(128*2, 256),
            nn.ReLU(),
            nn.Linear(256, 256)
        ), k=k)
        
        # 分类头
        self.lin1 = nn.Linear(64+128+256, 1024) # 拼接各层特征
        self.lin2 = nn.Linear(1024, 512)
        self.lin3 = nn.Linear(512, 256)
        self.lin4 = nn.Linear(256, num_classes)
        self.drop = nn.Dropout(0.5)
        
    def forward(self, data):
        # data.x: [num_points_in_batch, 3]
        # data.batch: 指示每个点属于哪个样本
        x, batch = data.x, data.batch
        
        x1 = self.conv1(x, batch)
        x2 = self.conv2(x1, batch)
        x3 = self.conv3(x2, batch)
        
        # 将每一层学到的特征拼接起来
        out = torch.cat([x1, x2, x3], dim=1)
        
        # 全局池化得到每个样本的全局特征
        out = global_max_pool(out, batch)
        
        # 通过全连接层
        out = F.relu(self.lin1(out))
        out = self.drop(out)
        out = F.relu(self.lin2(out))
        out = self.drop(out)
        out = F.relu(self.lin3(out))
        out = self.drop(out)
        out = self.lin4(out)
        return out

第三步:训练循环 训练深度学习模型需要GPU,流程是标准的。

def train(model, train_loader, optimizer, criterion, epoch):
    model.train()
    total_loss = 0
    for data in train_loader:
        data = data.to(device) # 将数据移到GPU
        optimizer.zero_grad()
        out = model(data)
        loss = criterion(out, data.y)
        loss.backward()
        optimizer.step()
        total_loss += loss.item() * data.num_graphs
    return total_loss / len(train_loader.dataset)

# 主程序
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = DGCNN().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)
scheduler = torch.optim.lr_scheduler.StepLR(optimizer, step_size=20, gamma=0.5)
criterion = nn.CrossEntropyLoss()

train_dataset = ModelNet40(root='./modelnet40_normal_resampled/', split='train')
train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4)

for epoch in range(1, 251):
    loss = train(model, train_loader, optimizer, criterion, epoch)
    scheduler.step()
    if epoch % 10 == 0:
        print(f'Epoch {epoch:03d}, Loss: {loss:.4f}')
        # 这里可以加上在测试集上验证的代码

训练小贴士:点云网络通常需要训练较长时间(200-300个epoch)。学习率调度(StepLR)和权重衰减(weight_decay)对于防止过拟合很重要。Batch Size不宜过大,因为点云数据本身内存占用就高,32或64是比较常见的选择。

4. 技术对比与选型指南:何时用传统,何时上深度?

走完了从传统VFH到深度DGCNN的旅程,你可能会问:在实际项目中,我到底该选哪个?这里没有银弹,只有最适合场景的锤子。我根据多年经验,总结了一个对比表格,帮你快速决策。

特性维度传统方法 (如 VFH+SVM)深度学习方法 (如 PointNet/DGCNN)
核心思想人工设计特征描述子,捕捉几何特性。端到端学习,网络自动提取分层特征。
数据需求少样本即可工作,几十到几百样本可训练。需要大量标注数据,通常成千上万。
计算效率推理速度快,特征提取和SVM分类计算量小。训练和推理慢,尤其是复杂模型,依赖GPU。
可解释性高,特征直方图有明确的几何意义。低,黑盒模型,难以理解决策依据。
鲁棒性对噪声、遮挡、密度变化敏感。更强,通过数据增强和网络结构能学习到一定鲁棒性。
特征表达能力有限,只能表达预设的几何模式。强大,能学习复杂、高维的语义特征。
适用场景1. 类别少、形状差异明显的物体识别。
2. 嵌入式设备等计算资源受限环境。
3. 对模型可解释性有要求的场景(如工业质检)。
1. 复杂场景下的精细分类与分割(如自动驾驶街景)。
2. 类别多、形状相似的物体区分。
3. 有充足数据和计算资源的研发阶段。

我的实战选型经验:

  • 机器人抓取:如果只是在固定光照、固定背景的工位上识别几种已知的规则零件,VFH+SVM足够了。它稳定、快速,部署到工控机上毫无压力。我曾经用这个方法在不到一周内就上线了一个螺丝分类系统。
  • 自动驾驶感知:面对复杂的街道环境,车辆、行人、自行车、障碍物形态各异,遮挡严重,点云稀疏。这时候必须上深度学习。我们团队用的是基于PointPillars或SECOND这类将点云体素化后再用3D CNN的方法,它们在KITTI等公开榜单上效果拔群。DGCNN这类直接处理点云的网络则更多用于车内手势识别或驾驶员状态监控等对细节要求高的任务。
  • 文化遗产数字化:扫描一个古建筑,点云数据量巨大(上亿点),但只需要进行粗分类(如墙体、屋顶、柱子)。传统方法可能因为计算全局特征太慢而卡住,而轻量级的PointNet却能较快地给出不错的结果。这是一个折中的选择。

给新手的建议:不要盲目追求SOTA(最先进技术)。先从传统方法开始,理解点云数据的特性和你任务的核心难点。如果传统方法效果已经满足需求,它就是最好的方法。当遇到瓶颈,比如识别率上不去、对遮挡无能为力时,再考虑引入深度学习。同时,一定要考虑工程落地的成本:有没有标注数据?部署平台的算力如何?模型的实时性要求是多少?把这些想清楚,技术选型就不会迷茫。

5. 未来展望与实战避坑指南

点云技术还在快速发展,Transformer架构、自监督学习、多模态融合(点云+图像)是当前的热点方向。比如Point Transformer,将注意力机制引入点云,能更好地建模长距离依赖。但对于大多数工程师来说,掌握好经典的传统方法和主流的深度学习网络(PointNet++, DGCNN),已经能解决80%的实际问题了。

最后,分享几个我踩过的“坑”,希望能帮你少走弯路:

  1. 数据预处理是生命线:无论是传统还是深度方法,点云归一化至关重要。一定要将点云中心平移到原点,并缩放到一个固定范围(如单位球内)。否则,SVM的核函数和深度学习网络的梯度都会出问题。
  2. 法线估计的陷阱:传统方法极度依赖准确的法线。PCL中setKSearch或setRadiusSearch的参数需要仔细调。法线方向不一致(朝向问题)可以通过flipNormalTowardsViewpoint函数来统一朝向视点。
  3. 深度学习中的数据增强:除了常见的随机旋转、平移、抖动噪声,对点云特别有效的是随机丢弃点(模拟遮挡)和随机缩放。这能极大地提升模型的鲁棒性。
  4. 类别不平衡问题:在真实场景中,有些物体(如背景)的点云数量远多于目标物体。在训练深度学习模型时,务必使用加权交叉熵损失或Focal Loss,否则模型会倾向于预测多数类。
  5. 部署时的性能考量:如果要将模型部署到Jetson等边缘设备,一定要考虑模型压缩。知识蒸馏(用大模型教小模型)和量化(将FP32权重转为INT8)是常用的手段。我曾将一个DGCNN模型量化后,推理速度提升了3倍,精度只掉了不到1%。

点云的世界充满挑战,也充满乐趣。从手工设计特征的“匠人时代”,到深度学习自动学习的“智能时代”,技术的演进让我们有能力解决越来越复杂的三维感知问题。无论你选择哪条路径,理解数据本质、紧密结合应用场景,才是做出好产品的关键。希望我的这些经验,能成为你探索三维视觉世界的一块有用的垫脚石。如果在实践中遇到具体问题,不妨多看看开源代码,多复现几篇经典论文,动手做一遍,理解会深刻得多。

Logo

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

更多推荐