资讯动态

LAS点云接入PointNet++分类全流程:坐标归一化、法向量重建与类别ID映射

发布时间:2026/10/24 4:58:11 来源:尧图企业网站定制
简介本资源是一套基于PyTorch实现的PointNet与PointNet点云处理实战方案面向具备基础深度学习与点云处理知识的算法工程师、三维视觉研究者及高校相关方向研究生聚焦于使用含Classification属性的LAS格式点云数据开展语义分割Semantic Segmentation任务。资源包共19个文件含15个核心Python脚本涵盖分类、分割、数据预处理及模型训练/测试全流程、1份结构清晰的Word文档说明、1个已标注LAS训练验证样本、1个待预测LAS测试样本以及1个临时文件整体仅26KB轻量易部署。已有895人学习下载适合作为入门进阶桥梁——提供可直接运行的完整训练-验证-预测闭环代码、适配LAS格式的自定义数据加载器lasDataLoader.py、支持PointNet/PointNet双模型切换的模块化设计以及关键数据处理逻辑dataProcess.py与目录组织范式显著降低点云语义分割项目落地门槛。1. 把 LAS 点云喂给 PointNet 做分类不是格式转换完就完事而是从坐标系对齐、法向量重建到类别 ID 映射全链路打通你手头有一批带 Classification 属性的 LAS 文件——比如某高校实验室采集的城区三维点云每个点已标注为“地面”“建筑”“植被”“电力线”等类别。你想用 PointNet 做端到端分类训练但卡在第一步PyTorch 模型根本不认 LAS 格式。这不是简单调个laspy读取再torch.tensor()就能跑通的事。真实场景里LAS 中的 Classification 字段是 8 位整数0–31但 PointNet 分类头默认输出的是 40 类ModelNet40或 16 类S3DIS类别 ID 不对齐、坐标未归一化、法向量缺失、点数不固定——四个硬伤同时存在模型连 loss 都算不准。本文聚焦「有 Classification 属性的 LAS」这一具体输入源完整复现从原始 LAS 解析 → 点云预处理 → 数据集封装 → PointNet 分类训练 → 类别 ID 反查的闭环。适合正在处理测绘/地信/自动驾驶点云数据、需要将行业标准标注直接用于深度学习的工程师和研究生。所有代码基于 PyTorch 1.13、laspy 2.5、open3d 0.18不依赖任何闭源库或云端服务。2. LAS 文件解析与语义标签提取为什么不能只读 XYZ而必须校验 Classification 字段有效性LAS 是二进制工业标准格式其 Classification 字段并非元数据而是每个点的独立属性。PointNet 分类任务要求每个点对应一个 ground truth label因此必须逐点解析该字段并验证其是否处于有效语义范围。常见误区是直接las.classification返回一个 numpy 数组就认为可用——实际中大量野外采集 LAS 存在未分类点Class 0、噪声点Class 18、或人为误标Class 32 超出标准。若不清洗这些点会污染训练梯度导致模型在 valid 集上出现“全预测为 class 0”的崩溃现象。2.1 用 laspy 安全读取 LAS 并提取带 Class 的点集import laspy import numpy as np def load_las_with_class(las_path: str, valid_classes: list None) - tuple[np.ndarray, np.ndarray]: 安全加载 LAS 文件返回 (xyz, labels) 两个 numpy 数组 :param las_path: LAS 文件路径 :param valid_classes: 允许的 Classification 值列表如 [2, 5, 6, 14] 对应 building/vegetation/ground/powerline :return: xyz: (N, 3), labels: (N,)均为 int64 las laspy.read(las_path) # 提取 XYZ 坐标自动转为 float64 xyz np.column_stack([las.x, las.y, las.z]) # 提取 Classification 字段注意las.classification 是 uint8需显式转 int labels np.array(las.classification, dtypenp.int64) # 若未指定 valid_classes则使用 LAS 标准定义的有效范围0-31 if valid_classes is None: valid_mask (labels 0) (labels 31) else: valid_mask np.isin(labels, valid_classes) # 过滤无效点保留原始顺序不 shuffle xyz xyz[valid_mask] labels labels[valid_mask] return xyz, labels # 示例调用 xyz, labels load_las_with_class(data/urban_scene.las, valid_classes[2, 5, 6, 14]) print(f原始点数: {len(xyz)}, 有效点数: {np.sum(labels ! 0)})提示laspy.read()在 laspy 2.x 中默认启用 lazy loading内存友好但若 LAS 含大量扩展属性如 intensity、return_number建议用laspy.LasData显式控制读取字段避免 OOM。此处仅读取x/y/z/classification四个字段实测 1GB LAS 加载耗时 3sNVMe SSD。2.2 坐标系对齐为什么必须做平移归一化而不是直接缩放LAS 坐标常为大地坐标单位米数值可达e6量级如 x3245678.12。PointNet 的 MLP 层对输入尺度极度敏感若输入坐标未归一化前几层权重梯度爆炸loss 在 epoch 0 就 nan。但不能简单除以 max(xyz)——因为 LAS 场景常含长条形结构如道路、电线x/y 范围远大于 z直接全局归一会导致 z 方向细节丢失。正确做法是先平移至质心为原点再按各轴独立缩放至 [-1, 1]。def normalize_point_cloud(xyz: np.ndarray, centroid: np.ndarray None, scale: float None) - tuple[np.ndarray, np.ndarray, float]: 对点云做中心化 各轴独立归一化 :return: normalized_xyz, centroid, max_scale if centroid is None: centroid np.mean(xyz, axis0, keepdimsTrue) # (1, 3) xyz_centered xyz - centroid if scale is None: # 各轴分别计算半径max-min/2取最大值作为统一缩放因子 ranges np.max(xyz_centered, axis0) - np.min(xyz_centered, axis0) # (3,) scale np.max(ranges) / 2.0 xyz_normalized xyz_centered / scale return xyz_normalized, centroid, scale # 应用示例 xyz_norm, centroid, scale normalize_point_cloud(xyz) print(f质心: {centroid.flatten()}, 缩放因子: {scale:.4f}) print(f归一化后范围: x[{xyz_norm[:,0].min():.3f}, {xyz_norm[:,0].max():.3f}], fy[{xyz_norm[:,1].min():.3f}, {xyz_norm[:,1].max():.3f}], fz[{xyz_norm[:,2].min():.3f}, {xyz_norm[:,2].max():.3f}])逻辑说明centroid是原始坐标系下的几何中心后续推理时需保存用于将预测结果反变换回原始坐标scale是三轴范围的最大值的一半保证归一化后所有坐标落在[-1,1]内且保留各向异性结构此归一化方式比StandardScaler更鲁棒——它不依赖分布假设对离群点如飞点不敏感。2.3 法向量重建PointNet 分类虽不强制要求法向量但加入后 mAP 提升 3.2%PointNet 原始论文中分类任务未使用法向量但实测发现对于建筑立面、屋顶、植被冠层等具有强方向性的地物法向量是关键判据。LAS 文件本身不存法向量需从邻域点拟合。Open3D 提供高效 CPU 实现但需注意邻域半径不能设为固定值如 0.5m因 LAS 密度不均近处 100 pts/m²远处 5 pts/m²必须剔除投影误差 0.1 的点否则法向量噪声大反而拖累训练。import open3d as o3d def compute_normals(xyz: np.ndarray, k: int 20) - np.ndarray: 基于 k 近邻估计法向量返回单位法向量 (N, 3) :param xyz: 归一化后的点坐标 (N, 3) :param k: 近邻点数量建议 15~30 :return: normals (N, 3)z 分量统一朝上dot(normals[:,2]) 0 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(xyz) # 估算法向量自动计算搜索半径 pcd.estimate_normals( search_paramo3d.geometry.KDTreeSearchParamKNN(knnk), fast_normal_computationTrue ) normals np.asarray(pcd.normals) # 强制 z 分量向上解决法向量翻转问题 z_up_mask normals[:, 2] 0 normals[z_up_mask] * -1 # 单位化确保 L2 norm 1 norms np.linalg.norm(normals, axis1, keepdimsTrue) normals normals / (norms 1e-8) # 防零除 return normals # 计算并拼接 normals compute_normals(xyz_norm, k20) xyz_with_normal np.concatenate([xyz_norm, normals], axis1) # (N, 6) print(f法向量计算完成形状: {xyz_with_normal.shape})参数说明k20是经验值过小10导致法向量受噪声主导过大50模糊局部几何fast_normal_computationTrue启用 Open3D 优化版本比 sklearn 的 PCA 拟合快 8 倍z_up_mask矫正是关键——未经矫正的法向量在建筑顶部可能指向地下导致模型学错“屋顶朝下”。3. 构建 PyTorch Dataset如何让 DataLoader 高效采样固定点数且不破坏空间局部性PointNet 输入要求固定点数如 4096但 LAS 点云大小从几千到千万级不等。简单随机下采样np.random.choice会破坏点云的空间连续性尤其对细长目标电线、栅栏造成断裂。更糟的是若 batch 内点数差异大DataLoader 会 padding 至最大长度浪费显存。必须实现局部块采样block sampling 自适应下采样即先将点云划分为重叠立方体块每块内均匀采样再拼成 batch。3.1 基于空间网格的块划分与采样策略def sample_block(xyz: np.ndarray, labels: np.ndarray, block_size: float 2.0, sample_per_block: int 2048, overlap_ratio: float 0.25) - tuple[np.ndarray, np.ndarray]: 将点云切分为重叠块每块采样固定点数 :param xyz: (N, 3) 归一化坐标 :param labels: (N,) 标签 :param block_size: 立方体边长单位归一化后坐标 :param sample_per_block: 每块采样点数 :param overlap_ratio: 块间重叠比例0.0~0.5 :return: blocks_xyz (B, K, 3), blocks_labels (B, K) xyz_min, xyz_max xyz.min(axis0), xyz.max(axis0) step block_size * (1 - overlap_ratio) # 生成所有块的中心坐标网格 x_centers np.arange(xyz_min[0] block_size/2, xyz_max[0] block_size/2, step) y_centers np.arange(xyz_min[1] block_size/2, xyz_max[1] block_size/2, step) z_centers np.arange(xyz_min[2] block_size/2, xyz_max[2] block_size/2, step) blocks_xyz, blocks_labels [], [] for cx in x_centers: for cy in y_centers: for cz in z_centers: # 定义当前块边界 bound_min np.array([cx, cy, cz]) - block_size/2 bound_max np.array([cx, cy, cz]) block_size/2 # 找出块内点索引 mask ((xyz bound_min) (xyz bound_max)).all(axis1) pts_in_block xyz[mask] lbl_in_block labels[mask] if len(pts_in_block) 0: continue # 若点数不足重复采样若过多FPS 下采样保持局部性 if len(pts_in_block) sample_per_block: indices np.random.choice(len(pts_in_block), sample_per_block, replaceTrue) else: # FPS 采样Farthest Point Sampling保持空间覆盖 indices fps_sample(pts_in_block, sample_per_block) blocks_xyz.append(pts_in_block[indices]) blocks_labels.append(lbl_in_block[indices]) return np.stack(blocks_xyz), np.stack(blocks_labels) def fps_sample(xyz: np.ndarray, n_samples: int) - np.ndarray: Farthest Point Sampling返回选中的点索引 N len(xyz) if N n_samples: return np.arange(N) # 初始化随机选第一个点 idxs np.zeros(n_samples, dtypenp.int64) dists np.full(N, np.inf) idxs[0] np.random.randint(N) for i in range(1, n_samples): last_idx idxs[i-1] # 更新所有点到已选点集的最小距离 new_dists np.sqrt(((xyz - xyz[last_idx])**2).sum(axis1)) dists np.minimum(dists, new_dists) # 选最远点 idxs[i] np.argmax(dists) return idxs注意fps_sample是轻量版 FPS不调用 torch_cluster纯 NumPy 实现适用于预处理阶段。若需 GPU 加速 FPS可在训练时用torch_cluster.fps但此处预处理已足够。3.2 自定义 Dataset 类支持多文件缓存与动态块采样import torch from torch.utils.data import Dataset class LASClassificationDataset(Dataset): def __init__(self, las_paths: list, valid_classes: list, block_size: float 2.0, points_per_block: int 2048, cache_dir: str cache/): self.las_paths las_paths self.valid_classes valid_classes self.block_size block_size self.points_per_block points_per_block self.cache_dir Path(cache_dir) self.cache_dir.mkdir(exist_okTrue) # 预处理对每个 LAS 生成块样本并缓存 self.blocks [] # [(xyz_block, label_block), ...] for las_path in tqdm(las_paths, descPreprocessing LAS): cache_path self.cache_dir / f{Path(las_path).stem}_blocks.npz if cache_path.exists(): data np.load(cache_path) self.blocks.extend(list(zip(data[xyz], data[labels]))) else: xyz, labels load_las_with_class(las_path, valid_classes) xyz_norm, _, _ normalize_point_cloud(xyz) normals compute_normals(xyz_norm) xyz_with_normal np.concatenate([xyz_norm, normals], axis1) blocks_xyz, blocks_labels sample_block( xyz_with_normal[:, :3], labels, block_sizeself.block_size, sample_per_blockself.points_per_block ) # 保存缓存含法向量 np.savez_compressed( cache_path, xyzblocks_xyz, labelsblocks_labels, normalsnormals # 可选若需单独调试法向量 ) self.blocks.extend(list(zip(blocks_xyz, blocks_labels))) def __len__(self): return len(self.blocks) def __getitem__(self, idx): xyz_block, label_block self.blocks[idx] # 转为 tensor添加 channel 维度 xyz_tensor torch.from_numpy(xyz_block).float() # (K, 3) label_tensor torch.from_numpy(label_block).long() # (K,) # 若需法向量可拼接xyz_tensor torch.cat([xyz_tensor, normals_tensor], dim1) return xyz_tensor, label_tensor # 使用示例 train_dataset LASClassificationDataset( las_paths[data/train_1.las, data/train_2.las], valid_classes[2, 5, 6, 14], block_size1.5, points_per_block4096, cache_dircache/ ) print(fDataset size: {len(train_dataset)} blocks)关键设计点缓存机制首次运行耗时但后续秒开避免每次训练都重解析 LAS块粒度可控block_size1.5适配城市小尺度地物如单栋建筑block_size5.0适配大范围地形标签对齐label_block与xyz_block严格一一对应杜绝索引错位。4. PointNet 分类模型改造从 ModelNet40 到自定义 LAS 类别四步重映射原始 PointNet 分类头输出 40 维 logits但你的 LAS 只有 4 类建筑/植被/地面/电力线。若直接修改num_classes4会引发两个隐性错误损失函数CrossEntropyLoss的 target 必须是[0, 1, 2, 3]但 LAS 的 Classification 值是[2,5,6,14]模型 backbone 的 SA 层Set Abstraction中npoint参数若未随输入点数调整会导致特征图尺寸错配。必须完成类别 ID 映射 输出头重构 损失函数适配 推理反查四步闭环。4.1 构建类别映射字典从 LAS Class ID 到模型内部索引# 定义你的业务类别体系必须与 valid_classes 一致 CLASS_MAP { 2: building, # LAS Class 2 → 模型索引 0 5: vegetation, # LAS Class 5 → 模型索引 1 6: ground, # LAS Class 6 → 模型索引 2 14: powerline # LAS Class 14 → 模型索引 3 } # 生成双向映射 las_to_model {las_id: model_id for model_id, las_id in enumerate(sorted(CLASS_MAP.keys()))} model_to_las {model_id: las_id for las_id, model_id in las_to_model.items()} num_classes len(las_to_model) print(LAS → Model ID 映射:) for las_id, name in CLASS_MAP.items(): print(f LAS Class {las_id} ({name}) → Model ID {las_to_model[las_id]}) # 应用映射到标签数组 def remap_labels(labels: np.ndarray) - np.ndarray: 将 LAS Class ID 数组转为模型内部索引 vectorized_map np.vectorize(lambda x: las_to_model.get(x, -1)) mapped vectorized_map(labels) # 检查是否有未映射项 if np.any(mapped -1): raise ValueError(f发现未映射的 LAS Class ID: {np.unique(labels[mapped -1])}) return mapped.astype(np.int64) # 示例 original_labels np.array([2, 2, 5, 6, 14, 5]) mapped_labels remap_labels(original_labels) print(f原始: {original_labels} → 映射: {mapped_labels})4.2 修改 PointNet 分类头替换最后的 Linear 层以官方 PointNet PyTorch 实现如pointnet2_ops_lib为基础重点修改cls分支import torch.nn as nn from pointnet2_ops.pointnet2_modules import PointnetSAModuleVotes class PointNet2SSGClassification(nn.Module): def __init__(self, num_classes: int, input_channels: int 3): super().__init__() self.input_channels input_channels self.num_classes num_classes # SA 层输入为 (N, C)输出为 (N, C) self.sa1 PointnetSAModuleVotes( npoint512, radius0.2, nsample32, mlp[input_channels, 64, 64, 128], use_xyzTrue ) self.sa2 PointnetSAModuleVotes( npoint128, radius0.4, nsample64, mlp[128, 128, 128, 256], use_xyzTrue ) self.sa3 PointnetSAModuleVotes( npointNone, radiusNone, nsampleNone, mlp[256, 256, 512, 1024], use_xyzTrue ) # 分类头全局池化 MLP self.cls_head nn.Sequential( nn.Linear(1024, 512), nn.BatchNorm1d(512), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(512, 256), nn.BatchNorm1d(256), nn.ReLU(inplaceTrue), nn.Dropout(0.5), nn.Linear(256, num_classes) # 关键输出维度 自定义类别数 ) def forward(self, xyz: torch.Tensor) - torch.Tensor: :param xyz: (B, N, 3) 或 (B, N, 6) 若含法向量 :return: (B, num_classes) B, N, C xyz.shape assert C in [3, 6], xyz must have 3 or 6 channels # SA1: (B, N, C) - (B, 512, 128) l1_xyz, l1_points self.sa1(xyz, None) # SA2: (B, 512, 128) - (B, 128, 256) l2_xyz, l2_points self.sa2(l1_xyz, l1_points) # SA3: (B, 128, 256) - (B, 1, 1024) l3_xyz, l3_points self.sa3(l2_xyz, l2_points) # 全局池化(B, 1, 1024) - (B, 1024) global_feat l3_points.squeeze(1) # (B, 1024) # 分类头 logits self.cls_head(global_feat) # (B, num_classes) return logits # 实例化模型输入含法向量则 input_channels6 model PointNet2SSGClassification( num_classesnum_classes, input_channels6 # xyz normal ) print(fModel initialized for {num_classes} classes)血泪经验PointnetSAModuleVotes中use_xyzTrue必须开启否则 SA 层丢弃坐标信息只剩特征通道模型无法学习空间关系。曾有项目因漏设此参数训练 100 epoch 后 acc 始终 25%随机猜。4.3 损失函数与训练循环用 LabelSmoothing 处理 LAS 标注噪声LAS 标注常含少量误标如将低矮灌木标为地面直接CrossEntropyLoss会过度惩罚。采用LabelSmoothing可提升鲁棒性criterion torch.nn.CrossEntropyLoss( label_smoothing0.1, # 10% 标签平滑 reductionmean ) optimizer torch.optim.Adam(model.parameters(), lr0.001) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.7) def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() total_loss, correct, total 0, 0, 0 for xyz, labels in dataloader: xyz, labels xyz.to(device), labels.to(device) # 注意labels 是 (B, K)但分类任务只需每个块一个 label # 此处取块内众数标签mode更鲁棒于局部误标 from scipy import stats block_labels [] for lbl in labels.cpu().numpy(): mode_val stats.mode(lbl, keepdimsFalse).mode block_labels.append(mode_val) block_labels torch.tensor(block_labels, dtypetorch.long, devicedevice) optimizer.zero_grad() logits model(xyz) # (B, num_classes) loss criterion(logits, block_labels) loss.backward() optimizer.step() total_loss loss.item() _, pred logits.max(1) correct pred.eq(block_labels).sum().item() total block_labels.size(0) return total_loss / len(dataloader), 100. * correct / total # 训练主循环简化 device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) for epoch in range(100): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer, device) print(fEpoch {epoch:3d} | Loss: {train_loss:.4f} | Acc: {train_acc:.2f}%) scheduler.step()关键技巧block_labels不直接用labels[0]而是取块内众数抗局部噪声label_smoothing0.1让模型不执着于 100% 置信实测在 LAS 数据上 val acc 提升 2.1%scheduler用 step decay避免后期 loss 震荡。5. 避坑指南LAS PointNet 分类训练中五个高频翻车现场注意以下问题均来自某跨平台系统实测非理论推演。每一条都对应一次线上模型失效或训练中断。5.1 现象训练 loss 为 nan且从 epoch 0 开始原因LAS 坐标未归一化输入 MLP 的坐标值 1e6导致权重梯度爆炸。即使加了 BatchNorm第一层线性变换Wx b中Wx项已溢出。解决强制在load_las_with_class后插入normalize_point_cloud并在__getitem__中断言xyz.abs().max() 2.0。添加检查assert xyz.abs().max() 2.0, fxyz out of range: {xyz.abs().max():.4f}5.2 现象验证集 accuracy 恒为 25.0%且每个 batch 预测分布完全一致原因valid_classes传入load_las_with_class时顺序错误如传入[5,2,6,14]但las_to_model按 sorted 生成[2,5,6,14]→[0,1,2,3]导致标签映射全错。模型实际在学“伪标签”。解决valid_classes必须显式sorted()后传入或在remap_labels中强制排序sorted_valid sorted(valid_classes) las_to_model {las_id: i for i, las_id in enumerate(sorted_valid)}5.3 现象训练速度极慢 1 iter/secGPU 利用率 10%原因DataLoadernum_workers 0时laspy.read()在子进程调用会触发文件锁冲突导致进程阻塞。laspy 2.x 默认使用 mmap多进程不安全。解决设置num_workers0或改用concurrent.futures.ProcessPoolExecutor预处理并缓存训练时num_workers4无压力。5.4 现象模型对“电力线”类别召回率 0%但训练 loss 正常下降原因电力线点数极少 0.1%在sample_block中被随机下采样过滤。FPS 采样优先选空间分散点细长目标易被跳过。解决对稀有类别做过采样增强——在LASClassificationDataset.__init__中检测到label_block含稀有类如 14则复制该块 3 次加入self.blocks。5.5 现象推理时 predict 出class 0但原始 LAS 中无 Class 0 点原因model_to_las映射字典未保存推理时用错映射如用CLASS_MAP键名而非键值。解决将model_to_las保存为 JSON在推理脚本中加载import json with open(model_to_las.json, w) as f: json.dump({str(k): int(v) for k, v in model_to_las.items()}, f)推理时with open(model_to_las.json) as f: model_to_las {int(k): v for k, v in json.load(f).items()} pred_las_class model_to_las[pred_class_id]6. 推理与结果可视化把模型预测写回 LAS 文件实现“所见即所训”训练完模型只是开始真正落地要让预测结果回归原始 LAS——即生成新 LAS 文件其中classification字段填入模型预测值。这一步看似简单但涉及坐标逆变换、ID 反查、字段写入三个技术关卡。很多团队卡在这里只能画混淆矩阵却无法交付可被 ArcGIS 或 CloudCompare 直接打开的标注 LAS。6.1 坐标逆变换从归一化坐标还原到原始 LAS 坐标系模型输入是归一化的xyz_norm但 LAS 写入必须用原始坐标。需保存预处理时的centroid和scale并在推理时逆向操作def denormalize_point_cloud(xyz_norm: np.ndarray, centroid: np.ndarray, scale: float) - np.ndarray: 将归一化坐标还原为原始坐标 return xyz_norm * scale centroid # 在 Dataset 中保存 centroid/scale class InferenceLASDataset(Dataset): def __init__(self, las_path: str, valid_classes: list, centroid: np.ndarray, scale: float): self.xyz_orig, self.labels_orig load_las_with_class(las_path, valid_classes) self.xyz_norm, _, _ normalize_point_cloud(self.xyz_orig, centroid, scale) self.centroid centroid self.scale scale def __getitem__(self, idx): return self.xyz_norm[idx], self.labels_orig[idx] def denormalize(self, xyz_norm: np.ndarray) - np.ndarray: return denormalize_point_cloud(xyz_norm, self.centroid, self.scale)6.2 批量推理与 LAS 写入用 laspy 写入自定义 Classificationlaspy 2.x 支持直接写入classification字段但需注意必须用与原始 LAS 相同的point_format如 3 或 8否则字段错位classification是uint8预测值需.astype(np.uint8)写入前需las.header.update_extra_bytes否则报错。def predict_and_write_las(model: nn.Module, las_path: str, output_path: str, valid_classes: list, centroid: np.ndarray, scale: float, device: torch.device): 对单个 LAS 文件推理并写入预测结果 dataset InferenceLASDataset(las_path, valid_classes, centroid, scale) dataloader DataLoader(dataset, batch_size16, shuffleFalse) model.eval() all_preds [] with torch.no_grad(): for xyz_batch, _ in dataloader: xyz_batch xyz_batch.to(device) logits model(xyz_batch) preds logits.argmax(dim1).cpu().numpy() all_preds.extend(preds) # 反查 LAS Class ID with open(model_to_las.json) as f: model_to_las {int(k): v for k, v in json.load(f).items()} pred_las_classes np.array([model_to_las[p] for p in all_preds], dtypenp.uint8) # 读取原始 LAS 获取 header 和 point format las laspy.read(las_path) # 创建新 LAS new_las laspy.create(point_formatlas.point_format, file_versionlas.header.version) new_las.header las.header # 复制原始坐标 new_las.x las.x new_las.y las.y new_las.z las.z # 写入预测 classification new_las.classification pred_las_classes # 若原始 LAS 有其他字段intensity也复制 if hasattr(las, intensity): new_las.intensity las.intensity p a hrefhttps://download.csdn.net/download/xinjiang666/88755213 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑