
簡介這是面向點云處理學習與研究者的PointNet與PointNet實現源碼包適用于3D物體識別、場景分割、自動駕駛等典型應用也適合希望深入理解無序點云深度學習建模方法的讀者。資源包共包含33個文件其中22個Python腳本覆蓋模型定義、訓練流程、數據預處理、評估與推理等核心環節另有5個txt說明、2個md文檔、2個shell腳本、1個license及1張結構預覽圖整體壓縮包僅495KB輕量且模塊化便于按需查閱。目前已有274人學習下載可作為點云方向入門與進階的實用參考。通過研讀源碼可以掌握PointNet全局最大池化與PointNet分層采樣、局部上下文聚合的關鍵實現并借助訓練腳本和數據處理工具快速復現分類、分割任務同時還能針對brushomd等自定義點云數據進行二次開發與參數調優更好地服務于實際項目。1. 從無序點云到端到端推理解讀 PointNet 源碼包的核心價值拿到激光掃描儀或者深度相機輸出的原始點云時第一反應往往是先做網格化、體素化再送進 3D CNN。但分辨率稍高一點顯存就會迅速見底而且體素化本身會丟失幾何細節。PointNet 走了一條完全不同的路直接把 N×3 的坐標序列喂進網絡通過共享 MLP 對每個點獨立提特征再用全局最大池化把無序點集壓成一個全局描述子。這套 pointnet-master.zip 源碼包就是 PointNet 官方 TensorFlow 實現并且附帶分類、部件分割、室內場景語義分割三套完整訓練與評估流程。對于想理解點云深度學習的底層邏輯或者準備在自己數據集上復現點云分割、3D 分類任務的工程師這套代碼是目前最值得逐行讀的參考實現之一。接下來按數據管道、模型結構、訓練評估、擴展遷移的順序把它拆開。2. 數據管道拆解download_data.sh、provider.py 與 h5 預處理鏈路2.1 下載腳本與 h5 數據格式先將壓縮包解壓進入倉庫根目錄unzip pointnet-master.zip -d pointnet cd pointnet倉庫根目錄的download_data.sh負責拉取公開數據集并整理為 h5 格式。以 ModelNet40 分類任務為例腳本執行后會在data/modelnet40_ply_hdf5_2048/下生成多個.h5文件。打開一個 h5 文件內部結構如下h5 keyshape說明data(N, 2048, 3)每個樣本固定 2048 個點的 xyz 坐標label(N,)類別 ID對應shape_names.txtmask(N, 2048)有效點掩碼用于指示哪些點是實際采樣點之所以統一到 2048 點是因為 PointNet 理論上能處理變長輸入但 TensorFlow 的 batch 訓練需要維度對齊所以預處理階段統一做最遠點采樣Farthest Point Sampling, FPS。h5 格式在這里的優勢很明顯讀入一個文件就能拿到整個數據集的數組IO 壓力和文件碎片都比零散存放的 PLY 文件小得多。之前有人把數據換成散裝.npy再喂給tf.data每個 epoch 的文件打開次數直接漲了一個數量級訓練速度明顯下降所以我一般不建議改掉 h5 這條鏈路。2.2 provider.py采樣、打亂與數據增強provider.py是數據增強的核心模塊。其中rotate_point_cloud繞 Y 軸做隨機旋轉模擬不同朝向的點云def rotate_point_cloud(batch_data): rotated_data np.zeros(batch_data.shape, dtypenp.float32) for k in range(batch_data.shape[0]): rotation_angle np.random.uniform() * 2 * np.pi cosval, sinval np.cos(rotation_angle), np.sin(rotation_angle) rotation_matrix np.array([[cosval, 0, sinval], [0, 1, 0], [-sinval, 0, cosval]]) rotated_data[k, ...] np.dot(batch_data[k, ...], rotation_matrix) return rotated_data這里的旋轉矩陣采用右乘形式即對每個點的行向量做變換。需要注意如果在做姿態估計或者配準類任務旋轉矩陣必須保存下來否則 augmentation 出來的數據沒有對應的真值標簽模型學到的分布會和你期望的不一致。provider.py里還有jitter_point_cloud它對每個點的坐標加上服從高斯分布的小擾動模擬傳感器噪聲random_point_dropout則隨機丟棄部分點增強模型對遮擋的魯棒性。這套組合拳是 PointNet 在 ModelNet40 上拿到不錯泛化效果的重要因素建議在自己的數據集上完整保留。2.3 pc_util.pyPLY 解析與幾何工具utils/pc_util.py提供 PLY 文件讀取和點云幾何處理的工具函數。日常調試中最常用的路徑是把自有 PLY 點云加載進來經過 FPS 采樣后組裝成訓練數據from utils.pc_util import load_ply, farthest_point_sampling # 讀取 PLY 文件返回 (points, faces) points, _ load_ply(scan.ply) # 最遠點采樣到統一點數 2048 sampled farthest_point_sampling(points, 2048)直接隨機采樣的問題在于如果點云分布不均勻密集區域的點會反復被選中稀疏區域直接被忽略。FPS 每次選取離已選集合最遠的點保證采樣結果的覆蓋范圍盡量鋪滿整個表面。在自制數據集時我會先用 CloudCompare 把原始掃描數據做一次粗略的噪點過濾再用load_ply讀取并統一點數最后落到 h5 文件里。順帶一提pc_util.py里還有 PCA 求法線、計算包圍盒等函數寫分割任務的預處理腳本時可以直接復用。3. 模型結構分析T-Net、全局最大池化與逐點分割的實現細節3.1 transform_nets.py輸入變換與特征對齊models/transform_nets.py實現了 PointNet 的輸入變換網絡 T-Net。它的本質是一個小型的 PointNet輸出一個 K×K 的變換矩陣對輸入坐標或者特征做對齊操作def input_transform_net(point_cloud, is_training, K3): num_point point_cloud.get_shape()[1].value input_image tf.expand_dims(point_cloud, -1) net tf_util.conv2d(input_image, 64, [1, 1], paddingVALID, scopetconv1, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 128, [1, 1], paddingVALID, scopetconv2, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 1024, [1, 1], paddingVALID, scopetconv3, bnFalse, is_trainingis_training) net tf_util.max_pool2d(net, [num_point, 1], paddingVALID, scopetmaxpool) net tf.reshape(net, [batch_size, -1]) net tf_util.fully_connected(net, 512, scopetfc1, bnTrue, is_trainingis_training) net tf_util.fully_connected(net, 256, scopetfc2, bnTrue, is_trainingis_training) with tf.variable_scope(transform_XYZ): weights tf.get_variable(weights, [256, K * K], initializertf.zeros_initializer()) biases tf.get_variable(bias, [K * K], initializertf.constant_initializer(0.0)) biases tf.constant(np.eye(K).flatten(), dtypetf.float32) transform tf.matmul(net, weights) transform tf.reshape(transform, [batch_size, K, K]) biases return transform關鍵點在最后幾行偏置初始化為單位矩陣保證訓練初期變換近似恒等不會一開始就破壞原始幾何結構。第一個 T-Net 輸出 3×3 矩陣作用在原始坐標上第二個 T-Net 輸出 64×64 矩陣作用在高維特征空間。如果不加 64×64 特征變換分類精度大約會掉兩到三個百分點這屬于看著不起眼、實際很關鍵的模塊。文件里對應的feature_transform_regularizer會把變換矩陣和單位陣的差引入損失系數通常取 0.001目的是約束特征變換不要偏離恒等映射太遠。3.2 pointnet_cls.py分類網絡的分層設計models/pointnet_cls.py定義了完整的分類主網絡核心邏輯如下with tf.variable_scope(transform_net1): transform input_transform_net(point_cloud, is_training, K3) point_cloud_transformed tf.matmul(point_cloud, transform) net tf_util.conv2d(point_cloud_transformed, 64, [1, 1], paddingVALID, scopeconv1, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 64, [1, 1], paddingVALID, scopeconv2, bnTrue, is_trainingis_training) with tf.variable_scope(transform_net2): transform_feat feature_transform_net(net, is_training, K64) net tf.matmul(net, transform_feat) net tf_util.conv2d(net, 128, [1, 1], paddingVALID, scopeconv3, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 1024, [1, 1], paddingVALID, scopeconv4, bnTrue, is_trainingis_training) net tf_util.max_pool2d(net, [num_point, 1], paddingVALID, scopemaxpool)每個[1, 1]卷積實際作用在單點上等價于對每個點獨立做 MLP。經過兩層 64 維特征提取后再升到 1024 維最后用max_pool2d在點維度上做全局最大池化。各層輸出維度如下層名操作輸出維度conv1Conv1d(3, 64) BN ReLU(B, 64, N)conv2Conv1d(64, 64) BN ReLU(B, 64, N)conv3Conv1d(64, 128) BN ReLU(B, 128, N)conv4Conv1d(128, 1024) BN ReLU(B, 1024, N)maxpoolMaxPool(N, 1)(B, 1024, 1)全局最大池化是 PointNet 處理無序性的核心操作無論點的順序怎么打亂每個維度上的最大值不變輸出特征對輸入排列保持置換不變。池化之后接全連接層 MLP(512, 256, num_class)并在第一個全連接后加 dropout概率默認 0.3。這個 dropout 放在分類頭的位置比較講究——前面的卷積層都帶 BN不需要 dropout 輔助分類頭是最后的全局特征匯聚dropout 能有效抑制過擬合。3.3 pointnet_seg.py從全局特征到逐點分割分割網絡models/pointnet_seg.py與分類網絡的差別在于全局最大池化之后要把 1024 維全局特征拼回每個點的 64 維局部特征上再通過卷積逐點輸出分類分數net tf_util.conv2d(point_cloud_transformed, 64, [1, 1], paddingVALID, scopeconv1, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 64, [1, 1], paddingVALID, scopeconv2, bnTrue, is_trainingis_training) # 全局特征與局部特征拼接 net tf.concat([net, net_global], axis2) # (B, N, 64) (B, N, 1024) net tf_util.conv2d(net, 512, [1, 1], paddingVALID, scopeconv3, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 256, [1, 1], paddingVALID, scopeconv4, bnTrue, is_trainingis_training) net tf_util.conv2d(net, 128, [1, 1], paddingVALID, scopeconv5, bnTrue, is_trainingis_training) net tf_util.conv2d(net, seg_classes, [1, 1], paddingVALID, scopeconv6, bnFalse, is_trainingis_training)這里的net_global是全局池化后的特征復制到每個點上得到的。為什么全局特征能幫助逐點分割因為很多語義標簽依賴上下文一個點本身可能是白色平板單獨看無法判斷是桌面還是墻面只有知道它屬于整個場景的哪個位置、周圍是什么結構才能做出正確分類。全局特征恰好提供了這種上下文信息。倉庫的part_seg/目錄對應 ShapeNet 部件分割任務sem_seg/目錄對應 S3DIS 室內場景語義分割任務兩者訓練腳本獨立但網絡骨架都遵循這個局部特征 全局特征拼接的設計模式。4. 訓練與評估train.py 調參、IoU 計算與語義分割驗證4.1 train.py 的運行方式與啟動參數倉庫里有多個train.py注意區分職責根目錄下的是 ModelNet40 分類訓練腳本part_seg/下的是 ShapeNet 部件分割訓練腳本sem_seg/下的是 S3DIS 語義分割訓練腳本。啟動分類訓練的命令python train.py \ --log_dir cls_log \ --num_point 2048 \ --num_class 40 \ --batch_size 16 \ --max_epoch 250關鍵訓練參數如下參數默認值說明learning_rate0.001Adam 優化器初始學習率decay_step200000學習率衰減步長decay_rate0.7每經過 decay_step學習率乘 0.7batch_size16分類任務常用 16分割任務受顯存限制降到 8max_epoch250總訓練輪數momentum0.9BN 層滑動平均動量訓練過程中日志里同時輸出 loss、classification accuracy 和 point accuracy。判斷收斂是否正常主要看 train loss 是否平滑下降以及 val acc 是否同步上升。如果在第二個 T-Net 特征變換沒有加正則項的情況下train acc 在 80% 附近反復震蕩先檢查 loss 里是否包含feature_transformation_regularizer那一項這是最容易被誤刪的模塊。4.2 eval_iou_accuracy.py 的 IoU 計算口徑sem_seg/eval_iou_accuracy.py用于 S3DIS 語義分割的評估。它讀取測試集的預測結果和真值標簽按類別分別統計預測為正的像素數、真實為正的像素數和交疊數n_pred np.zeros(gt_classes) n_gt np.zeros(gt_classes) n_intersect np.zeros(gt_classes) for i in range(num_points): gt gt_label[i] pred pred_label[i] n_gt[gt] 1 n_pred[pred] 1 if gt pred: n_intersect[gt] 1 iou_per_class n_intersect / (n_pred n_gt - n_intersect 1e-6) miou np.mean(iou_per_class)這里算的是逐類別 IoU 后取平均即 mIoU每個類別權重相同不受類別樣本數影響。使用這個腳本時注意S3DIS 數據集里不同房間的類別分布差異很大比如走廊里墻和地板占比高會議室里桌椅和黑板占比高如果直接跑完不做逐場景評估mIoU 很容易被大類別帶偏。官方實現里按 Area 劃分訓練集和測試集我一般會在評估時同時輸出 per-class IoU 和 overall accuracy 兩列前者看少數類別有沒有崩后者看整體表現。4.3 調參經驗學習率、批量大小和正則化在這套源碼上調參優先級最高的幾個點分別是學習率衰減策略、BN 開關和 dropout 位置。學習率從 0.001 起步200k 步衰減到原來的 0.7 倍這個策略在分類和分割任務上都表現穩定。不要一上來就用 cosine annealingPointNet 這種輕量網絡用階梯式衰減更容易鎖住收斂點。批量大小直接受 GPU 顯存約束。分類任務 2048 點批量 16 大約占用 4~5 GB分割任務由于要拼接全局特征并輸出逐點分數顯存占用會明顯上漲批量 8 起步比較穩妥。另外測試時is_training必須設為 False否則 BN 會繼續更新全局統計量導致驗證集精度和訓練過程看到的精度不一致。這個坑在源碼里用placeholder傳入很多改動過的版本容易在固化模型時把這個參數寫死最終推理結果與訓練時偏差很大。5. PointNet 的局部聚合改進與 TF 1.x 復現避坑5.1 set abstraction 的核心改進這套pointnet-master.zip本身不包含 PointNet但理解 PointNet 的邊界后PointNet 的改進點就變得非常清晰。PointNet 的全局池化把所有點壓成一個向量局部幾何細節在這個過程里被平均掉了。PointNet 引入 set abstraction 層每一層先做最遠點采樣選出中心點再以球鄰域為單位做分組對每個組內點集跑一個 PointNet得到局部特征逐級聚合。對應到 S3DIS 室內場景語義分割場景PointNet 對桌椅這類局部幾何特征明顯的物體mIoU 通常比 PointNet 高 7~10 個百分點。如果新項目的核心訴求是精細分割而不是快速驗證直接上 PointNet 是更合理的選擇。5.2 從 TF 1.x 遷移的注意事項這套代碼基于 TensorFlow 1.x在 TF 2.x 環境下運行會直接報module tensorflow has no attribute placeholder之類的錯誤。常見做法是搭一個tf.compat.v1兼容層但更推薦的遷移路線是直接基于 PyTorch 復現 pointnet 結構模型定義短、反向傳播邏輯透明后續換網絡也更方便class PointNetCls(nn.Module): def __init__(self, num_classes): super().__init__() self.mlp1 nn.Sequential(nn.Conv1d(3, 64, 1), nn.BatchNorm1d(64), nn.ReLU(), nn.Conv1d(64, 64, 1), nn.BatchNorm1d(64), nn.ReLU()) self.mlp2 nn.Sequential(nn.Conv1d(64, 128, 1), nn.BatchNorm1d(128), nn.ReLU(), nn.Conv1d(128, 1024, 1), nn.BatchNorm1d(1024), nn.ReLU()) self.fc nn.Sequential(nn.Linear(1024, 512), nn.ReLU(), nn.Dropout(0.3), nn.Linear(512, 256), nn.ReLU(), nn.Linear(256, num_classes)) def forward(self, x): # x: (B, N, 3) - permute 到 (B, 3, N) x x.permute(0, 2, 1) local self.mlp1(x) global_feat self.mlp2(local).max(dim2)[0] return self.fc(global_feat)遷移時最容易丟的兩個東西是 T-Net 和特征變換正則項。原始分類網絡有 3×3 和 64×64 兩個變換網絡PyTorch 復刻時即使實現了也很容易忘記在 loss 里加正則項導致精度下降。如果模型規模或數據量不大可以先省略 T-Net 觀察 baseline再逐步加回來這樣定位問題更容易。5.3 用 CloudCompare 驗證分割輸出跑完分割推理后直接看數字不直觀我習慣把預測結果可視化驗證。PointNet 分割輸出每個點的類別 ID保存成.npy后用 CloudCompare 打開原始點云再通過命令行把 label 作為 scalar field 附加上去cloudcompare.CloudCompare -SILENT \ -O original_cloud.ply \ -ADD_HEADER 0 \ -MERGE_CLOUDS \ -SAVE_CLOUDS更常見的做法是直接在 Python 里寫一份 PLY 文件把 label 寫到red、green、blue三個屬性里然后拖進 CloudCompare 按顏色觀察。注意 CloudCompare 默認顯示的點尺寸較小分割結果邊界處的混色點往往意味著網絡對類別邊界的預測不穩定這時回頭檢查訓練數據里對應區域的類別標注是否干凈——很多分割精度問題不是網絡結構造成的而是標注噪聲被網絡學進去了。點云俠社區里關于 CloudCompare 的點云轉三維模型和標注重建的教程很多處理分割結果驗證時值得參考。本文還有配套的精品資源點擊獲取