與驗證)
簡介這個項目圍繞激光雷達點云分割中的場景視點偏移優(yōu)化展開面向自動駕駛與機器人導航領域的開發(fā)者以及需要完成畢業(yè)設計或課程設計的計算機視覺專業(yè)學生。采用Python實現(xiàn)涉及點云數(shù)據預處理、矩陣變換模擬多視角觀察、特征提取以及基于密度或聚類的分割算法。資源包共135個文件壓縮后約15.68MB包含73個py源碼文件、28個pyc編譯文件、16個txt說明文檔及5個npy點云數(shù)據文件另有模型檢查點文件index/meta/data與訓練日志便于快速加載預訓練權重并繼續(xù)實驗。已有148人下載學習。通過閱讀源碼可掌握點云濾波、地面分割、視點偏移矩陣構造、特征提取與分割后處理等完整流程配套的md/txt文檔和多組KITTI數(shù)據集npy樣例為復現(xiàn)與二次開發(fā)提供了直接可用的數(shù)據輸入和代碼骨架。1. 場景視點偏移改善點云分割這個項目到底改進了什么第一次看到場景視點偏移這幾個字我以為只是給點云數(shù)據加個隨機抖動結果看完源碼才發(fā)現(xiàn)完全不是一回事。激光雷達點云分割一直是自動駕駛感知里的硬骨頭點云稀疏、遮擋嚴重、物體尺度差異大傳統(tǒng)的固定視角分割很容易把相鄰物體黏在一起。視點偏移的思路很直接把同一幀點云繞不同軸做輕微的旋轉平移變換讓算法從多個虛擬視角觀察同一個場景相當于一次推理里塞進了多幀的上下文信息。這個項目打包了完整的 Python 源碼、訓練好的 TensorFlow checkpoint 模型和 KITTI 數(shù)據集預處理后的 .npy 點云幀適合做畢業(yè)設計、課程設計也適合剛入門的開發(fā)者拆著玩——既能看清數(shù)據流入模型的完整鏈路又能直接拿現(xiàn)成權重跑推理。2. 數(shù)據怎么進來.npy 點云幀與預處理鏈路2.1 從文件結構反推數(shù)據格式拿到項目先把壓縮包解了里面除了model.ckpt-23000開頭的四個模型文件就是一堆.npy文件。2011_09_26_0051_0000000011.npy、2011_09_26_0061_0000000400.npy這種命名方式很有特征——前半段是日期后半段是序列號這是從 KITTI 原始數(shù)據里按時間戳抽出來的點云幀。000000.npy和000000_r_05.npy則有意思得多_r_05大概率是 rotated 0.5也就是原始幀做了 0.5 弧度約 28.6 度旋轉變換后的結果正好對應視點偏移的實現(xiàn)邏輯。讀取.npy文件用 NumPy 就夠了不需要引入額外的點云庫先做一次解析import numpy as np # 讀取原始點云幀 cloud np.load(2011_09_26_0051_0000000011.npy) print(shape:, cloud.shape) print(dtype:, cloud.dtype) print(前5行:\n, cloud[:5])這段代碼首先用np.load直接加載.npy文件然后打印點云的形狀、數(shù)據類型和前五行內容。KITTI 的原始點云一般是(N, 4)的數(shù)組四列分別代表 x、y、z 坐標和反射強度值。如果打印出來的shape是(N, 3)說明數(shù)據已經被預處理過了只保留了空間坐標如果看到的是(N, 4)后續(xù)操作就要考慮到第四列的強度信息。dtype通常是float32或float64這會影響內存占用和計算速度建議用float32精度足夠而且顯存占用減半。# 檢查數(shù)據統(tǒng)計特征 print(點數(shù):, cloud.shape[0]) print(坐標范圍 x:, cloud[:, 0].min(), ~, cloud[:, 0].max()) print(坐標范圍 y:, cloud[:, 1].min(), ~, cloud[:, 1].max()) print(坐標范圍 z:, cloud[:, 2].min(), ~, cloud[:, 2].max())點云數(shù)據的坐標范圍是最容易忽略的信息。如果 z 軸的數(shù)值范圍在 0 到 3 米之間說明這套數(shù)據大概率是做過地面分割或者已經經過了坐標變換如果 z 軸出現(xiàn)負值說明雷達的安裝高度已經通過外參校準到了全局坐標系里。拿到數(shù)據先看統(tǒng)計信息能避免后面做視點偏移時對旋轉中心產生誤判。預處理鏈路一般是這幾步體素降采樣減點數(shù)保留空間輪廓去掉冗余點、統(tǒng)計濾波去掉離群噪點、地面分割把地面點和非地面點分開。這套流程在自動駕駛數(shù)據集里幾乎是標配因為地面點數(shù)量太多直接喂給分割網絡會嚴重干擾前景物體的特征提取。2.2 視點偏移的數(shù)學本質旋轉與平移的組合視點偏移的核心就是剛體變換。剛體變換保證點與點之間的相對距離不變只是整體換個位姿觀察。數(shù)學表達式寫成齊次形式P R * P T其中 P 是原始點坐標(x, y, z, 1)R 是 3x3 旋轉矩陣T 是 3x1 平移向量P 是變換后的點坐標。繞 z 軸旋轉 theta 角度的旋轉矩陣就是R_z(theta) [[cos(theta), -sin(theta), 0], [sin(theta), cos(theta), 0], [0, 0, 1]]對應到代碼實現(xiàn)用 SciPy 的Rotation模塊生成旋轉矩陣最省事import numpy as np from scipy.spatial.transform import Rotation def apply_viewpoint_offset(cloud, yawnp.radians(10), pitch0.0, roll0.0, translationNone): 對點云做視點偏移變換 :param cloud: 輸入的 (N, 3) 或 (N, 4) 點云數(shù)組 :param yaw: 繞 z 軸旋轉角度弧度制 :param pitch: 繞 y 軸旋轉角度 :param roll: 繞 x 軸旋轉角度 :param translation: 平移向量默認 None 表示不平移 :return: 變換后的點云與輸入同形狀 # 分離坐標和強度信息 if cloud.shape[1] 4: coords cloud[:, :3] intensity cloud[:, 3:4] else: coords cloud[:, :3] intensity None # 生成旋轉矩陣 rotation Rotation.from_euler(xyz, [roll, pitch, yaw]).as_matrix() # 旋轉點坐標 rotated coords rotation.T # 平移 if translation is not None: rotated rotated np.array(translation) if intensity is not None: return np.hstack([rotated, intensity]) return rotated # 測試加載原始幀后做偏移 orig np.load(2011_09_26_0051_0000000011.npy) offset_cloud apply_viewpoint_offset(orig, yawnp.radians(15)) print(原始范圍:, orig[:, :3].min(axis0), orig[:, :3].max(axis0)) print(偏移后范圍:, offset_cloud[:, :3].min(axis0), offset_cloud[:, :3].max(axis0))這段代碼里有幾個細節(jié)值得展開。Rotation.from_euler(xyz, [roll, pitch, yaw])里的xyz指的是旋轉順序先繞 x 軸、再繞 y 軸、最后繞 z 軸。旋轉順序不同結果完全不同這是最容易出錯的參數(shù)。coords rotation.T里為什么要轉置因為 SciPy 的旋轉矩陣把列向量當作點的表示點坐標是行向量時就要右乘矩陣的轉置。如果寫成rotation coords.T然后再轉置回來效果一樣但直接 rotation.T更簡潔且不容易搞混。旋轉角度建議用弧度制。很多第一次用的人習慣寫 30、45 這種角度制數(shù)值但np.radians和Rotation內部都是弧度制——傳 30 進去等于轉了 30 弧度結果直接把點云旋到面目全非這就是視點偏移參數(shù)設置的第一大坑。2.3 多個視角怎么組合從單幀到多幀融合視點偏移不是只做一次旋轉而是把同一幀點云通過多個角度變換后拼接起來形成多視角的超級幀。常見做法是取{-15°, -7.5°, 0°, 7.5°, 15°}這樣一組小角度分別變換后再合并。為什么要小角度而不是大角度旋轉因為視點偏移的目的不是生成一個新場景而是讓分割算法在推理時能通過多視角看到同一個物體的不同側面。角度太大物體的朝向變了語義標簽就失去了意義角度太小變換前后的點云幾乎重合等于重復計算。5~15度的偏移量是實踐中最常用的區(qū)間。import numpy as np def multi_view_fusion(cloud, angles_deg(-15, -7.5, 0, 7.5, 15)): 多視點偏移融合對同一幀點云應用多個旋轉角拼接結果 :param cloud: 輸入點云 (N, 3) 或 (N, 4) :param angles_deg: 旋轉角度列表單位度 :return: 融合后的點云數(shù)組 fused [] for angle in angles_deg: transformed apply_viewpoint_offset(cloud, yawnp.radians(angle)) fused.append(transformed) # 沿第一個維度拼接 result np.vstack(fused) return result cloud np.load(2011_09_26_0051_0000000011.npy) multi_cloud multi_view_fusion(cloud) print(融合前點數(shù):, cloud.shape[0]) print(融合后點數(shù):, multi_cloud.shape[0])融合后的點云點數(shù)會變成原來的 5 倍這是正常的。但要注意多視角融合后的點云會引入一個隱患同一個物理點在不同視角下的坐標有微小偏差這會在物體邊緣產生重影。所以融合之后一般還要做一次體素降采樣把距離過近的點合并掉。體素大小選 0.05 米到 0.1 米比較合適——太小起不到去重效果太大又會把物體的精細結構磨平。這里還需要考慮類別標簽的問題。做監(jiān)督學習訓練時視點偏移之后的點云標簽也要跟著做同樣的變換不能只變換輸入不變換標簽。好在旋轉變化不改變點的語義類別只需要對每個點應用相同的旋轉矩陣就行。3. 分割模型怎么跑checkpoint 恢復與推理3.1 checkpoint 文件結構與 TensorFlow 恢復項目里那組model.ckpt-23000.data-00000-of-00001、model.ckpt-23000.index、model.ckpt-23000.meta文件從命名規(guī)則看是 TensorFlow 1.x 的 checkpoint 產物。meta文件存的是計算圖結構index文件存的是張量索引表data文件存的是實際的權重數(shù)值。23000 是訓練步數(shù)說明模型訓練了 23000 步之后保存的。如果是做推理而不是繼續(xù)訓練直接恢復這張圖最省事import tensorflow as tf # 恢復計算圖 saver tf.train.import_meta_graph(model.ckpt-23000.meta) with tf.Session() as sess: # 恢復權重 saver.restore(sess, model.ckpt-23000) # 從圖中取出輸入輸出節(jié)點 graph tf.get_default_graph() input_cloud graph.get_tensor_by_name(input_cloud:0) output_seg graph.get_tensor_by_name(output_seg:0) # 加載點云并送入模型 cloud np.load(2011_09_26_0051_0000000011.npy) feed_cloud np.expand_dims(cloud[:, :3], axis0) # 加 batch 維 result sess.run(output_seg, feed_dict{input_cloud: feed_cloud}) print(分割結果 shape:, result.shape)這段代碼首先通過import_meta_graph把圖結構加載進來然后用saver.restore恢復權重。get_tensor_by_name是獲取輸入輸出節(jié)點的常用方式但前提是你知道節(jié)點名字。怎么知道節(jié)點名打開readme.md看網絡定義或者用graph.get_operations()列出所有操作找到最后一個輸出操作的名字。這是第一個隱藏的坑tf.train.import_meta_graph對 TensorFlow 版本敏感。TF 1.x 的.meta文件無法直接在 TF 2.x 里加載如果環(huán)境是 TF 2.x需要開啟兼容模式。項目 README 里如果沒有明確標注版本建議先裝tensorflow-gpu1.15這個版本兼容性最好NVIDIA 驅動要求也不算苛刻。3.2 模型輸入與輸出維度、歸一化和解碼從模型定義的角度看輸入的點云一般會做一次體素化或者最近鄰采樣把數(shù)量不定的原始點云變成固定尺寸的張量。點云分割的常見輸出格式有兩種逐點分類標簽每個點一個類別 ID和逐點掩碼二值分割。推理結果出來之后后處理是繞不開的一步。import numpy as np # 假設模型輸出是逐點分類的 logitsshape 為 (1, N, num_classes) def decode_segmentation(logits): 將模型輸出的 logits 轉換為類別標簽 :param logits: (1, N, num_classes) 的得分 :return: (N,) 的整數(shù)標簽 # 去掉 batch 維 logits logits[0] # 每個點取得分最高的類別 labels np.argmax(logits, axis-1) return labels labels decode_segmentation(result) print(標簽分布:, {cls: int((labels cls).sum()) for cls in np.unique(labels)}) # 提取某個類別的點比如車假設類別 2 是車 car_points cloud[labels 2] print(識別為車的點數(shù):, car_points.shape[0])np.argmax(logits, axis-1)是逐點分割最常見的解碼方式直接取每個位置上得分最高的類別。但要注意如果模型最后一層用了 softmax得到的 logits 就已經是概率分布了如果沒加 softmax那argmax的結果也是一樣的因為 softmax 是嚴格單調遞增的。只有在需要做多標簽分類或者不確定性評估時才要保留完整的概率向量。標簽分布統(tǒng)計這一步看似簡單實際價值很大。如果某個類別一個點都沒有分出來大概率不是模型問題而是預處理時把這類點給濾掉了。比如地面分割太激進就會把矮小的物體路沿、錐桶連帶著清掉。3.3 預處理與特征提取的落地順序源碼里大概率有這樣一段流程濾波 → 降采樣 → 坐標變換 → 特征提取 → 分割。這個流程順序不能亂。如果先做視點偏移再做降采樣會讓降采樣在變換后的空間里重新采樣等于對多視角融合的結果做均勻劃分效果反而是對的。如果反過來先降采樣再偏移那多個視角共用的采樣點會產生重復計算。常用的特征包括法線方向、曲率、局部密度、高度差。法線估計用 Open3D 一句話就能搞定import open3d as o3d import numpy as np def estimate_features(cloud): 用 Open3D 估計法線和曲率 :param cloud: (N, 3) 或 (N, 4) 點云 :return: 法線數(shù)組和曲率數(shù)組 # 轉換為 Open3D 點云對象 pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(cloud[:, :3]) # 估計法線knn 設置為 20 pcd.estimate_normals(search_paramo3d.geometry.KDTreeSearchParamKNN(knn20)) # 提取法線數(shù)組 normals np.asarray(pcd.normals) # Open3D 不直接提供曲率通常用鄰域擬合平面的殘差近似 tree o3d.geometry.KDTreeFlann(pcd) curvatures [] for i in range(len(pcd.points)): _, idx, _ tree.search_knn_vector_3d(pcd.points[i], 20) neighbors np.asarray(pcd.points)[idx, :] # 計算鄰域點到中心點法線的投影距離作為曲率近似 centroid neighbors.mean(axis0) delta neighbors - centroid # 用協(xié)方差矩陣的最小特征值近似曲率 cov np.cov(delta.T) eigenvalues np.linalg.eigvalsh(cov) curvatures.append(eigenvalues[0] / (eigenvalues.sum() 1e-12)) return normals, np.array(curvatures) normals, curvatures estimate_features(cloud) print(法線 shape:, normals.shape) print(曲率范圍:, curvatures.min(), ~, curvatures.max())法線估計的knn20是個經驗值。點數(shù)密集的城區(qū)場景k 值取 20 到 30 比較合適稀疏的野外場景k 值可以降到 10 左右。k 值太大法線會被大尺度結構主導損失物體邊緣的細節(jié)k 值太小法線噪聲大分割邊界會很毛糙。曲率近似用的是協(xié)方差矩陣最小特征值占比數(shù)值越小說明鄰域點越共面數(shù)值越大說明局部彎曲越劇烈——這個特征對區(qū)分平面結構墻面、地面和曲面結構車輛、行人非常有效。4. 實戰(zhàn)避坑源碼落地時我踩過的五個坑4.1 坑一np.load 報錯ValueError: Object arrays cannot be loaded when allow_pickleFalse現(xiàn)象運行np.load(xxx.npy)直接崩錯誤提示指向allow_pickle。原因.npy文件是用 Python 2 的 pickle 協(xié)議保存的或者文件里混入了非數(shù)值類型。解決先檢查文件類型如果確屬 Python 2 時代的數(shù)據python -c import numpy as np; data np.load(2011_09_26_0051_0000000011.npy, allow_pickleTrue); print(data.shape)能打印出 shape 就沒問題。但allow_pickleTrue只是權宜之計——如果你的環(huán)境是 Python 3.8pickle 的版本兼容性還可能繼續(xù)報錯。常見的解決方案是把數(shù)據先轉成文本或者重新保存成新版.npy格式一勞永逸。4.2 坑二分割結果整體偏移了幾個像素現(xiàn)象模型訓練時指標正常但實際推理時分割結果與原始點云對不齊整個結果像被平移過。原因訓練前做了視點偏移但測試時忘了對輸入做同樣的坐標變換。模型在偏移后的坐標系里學到的是物體在 A 位置推理時喂進去的卻是原始坐標系的數(shù)據特征空間對不上。解決把訓練時的變換參數(shù)原封不動地應用到推理流水線上cloud np.load(...npy) # 訓練時用了 yaw0.087 rad (5度)推理時必須同樣旋轉 cloud_offset apply_viewpoint_offset(cloud, yaw0.087)這里最容易出現(xiàn)的問題是訓練腳本和推理腳本分別維護了一份變換參數(shù)某次調參忘了同步結果模型的表現(xiàn)波動很大但找不到原因。從那以后我都是把變換參數(shù)寫進配置文件統(tǒng)一管理每次跑實驗前先自動核對訓練和推理的參數(shù)是否一致。4.3 坑三地面分割把車輛底部點一起濾掉了現(xiàn)象分割結果里車輛區(qū)域大量缺失尤其是車底和車輪部分。原因地面分割算法用的是高度閾值車輛底盤離地面很近閾值設得太低把車底點當成了地面。解決改用基于法線的地面分割而不是純高度import open3d as o3d import numpy as np def segment_ground_by_normal(pcd, z_threshold0.3, normal_threshold0.15): 基于法線和高度聯(lián)合判斷地面 :param pcd: Open3D 點云對象 :param z_threshold: 高度閾值 :param normal_threshold: 法線方向與 z 軸的夾角余弦閾值 points np.asarray(pcd.points) normals np.asarray(pcd.normals) # 法線接近垂直向上的點認為是地面候選 vertical_mask np.abs(normals[:, 2]) normal_threshold # 高度接近最低點的候選 low_mask points[:, 2] (points[:, 2].min() z_threshold) ground_mask vertical_mask low_mask non_ground points[~ground_mask] return non_ground這個方案的核心是雙重判斷法線方向先排除墻面、坡道等非水平面高度閾值再排除凸起物。只用法線或者只用高度都會誤殺。4.4 坑四模型恢復時Restoring from checkpoint failed現(xiàn)象TensorFlow 提示 checkpoint 里某些張量名字不匹配或者直接報NotFoundError。原因模型代碼改了網絡結構加了層或者刪了層checkpoint 里的權重與當前計算圖對不上。解決不要指望老的 checkpoint 能直接匹配新結構。先查看 checkpoint 里存了哪些變量和當前模型定義的變量做對比import tensorflow as tf # 列出 checkpoint 里的所有變量 reader tf.train.NewCheckpointReader(model.ckpt-23000) var_to_shape reader.get_variable_to_shape_map() for key, shape in var_to_shape.items(): print(key, shape)如果差異只存在于最后一層分類頭的數(shù)量可以只恢復前面共享層的權重最后一層重新初始化。4.5 坑五點云文件數(shù)量巨大導致內存溢出現(xiàn)象多幀點云同時載入內存直接打滿程序被系統(tǒng)殺掉。原因點云一幀少說幾十萬點float64存儲單幀就占幾百 MB多幀疊加必然爆內存。解決先降精度、再分批處理python -c import numpy as np cloud np.load(2011_09_26_0051_0000000011.npy) cloud cloud.astype(np.float32) # 降精度 np.save(2011_09_26_0051_0000000011_f32.npy, cloud) 降精度之后內存占用直接減半。如果還是不夠就分批加載、分批處理不要圖省事一次性把所有點云堆進內存。5. 怎么驗證結果不是玄學一份對比流程分割模型跑完直接肉眼看一下彩色可視化就下結論這在正式的畢設答辯或項目評審里站不住腳。驗證的核心思路是拿固定視角和視點偏移視角做同條件對比用定量的指標說明偏移帶來了多少提升。一個可復現(xiàn)的對比方案是加載同一幀點云生成兩份輸入——一份原圖一份做了 15 度視點偏移后的融合圖分別讓模型推理把兩份分割結果映射回原始坐標系逐點計算預測標簽與真實標簽的 IoU。IoU 的代碼很短import numpy as np def compute_iou(pred_labels, gt_labels, num_classes): 逐類別計算 IoU 并返回均值 :param pred_labels: (N,) 預測標簽 :param gt_labels: (N,) 真實標簽 :param num_classes: 類別數(shù) ious [] for cls in range(num_classes): pred_mask (pred_labels cls) gt_mask (gt_labels cls) intersection np.logical_and(pred_mask, gt_mask).sum() union np.logical_or(pred_mask, gt_mask).sum() if union 0: continue ious.append(intersection / union) return np.mean(ious), ious # 示例加載兩個版本的推理結果 pred_viewpoint np.load(pred_viewpoint.npy) # 視點偏移后的預測 pred_original np.load(pred_original.npy) # 固定視角的預測 gt_cloud np.load(gt_labels.npy) iou_op, per_cls_op compute_iou(pred_viewpoint, gt_cloud, num_classes8) iou_orig, per_cls_orig compute_iou(pred_original, gt_cloud, num_classes8) print(f視點偏移 mIoU: {iou_op:.4f}) print(f固定視角 mIoU: {iou_orig:.4f}) print(f提升: {(iou_op - iou_orig) * 100:.2f}%)如果提升為負先不要急著否定視點偏移——排查順序是先確認偏移角度是否合適角度太大導致語義漂移再確認是否做了多視角融合還是只做了單次偏移最后確認標簽是否跟著點云一起做了同步變換。這三個地方錯了任何數(shù)據增強策略都救不回來??梢暬炞C方面Open3D 的可視化接口比 matplotlib 的三維散點圖靠譜得多import open3d as o3d import numpy as np def visualize_segmentation(cloud, labels, colormap): 將分割結果按顏色可視化 :param cloud: (N, 3) 點云 :param labels: (N,) 類別標簽 :param colormap: 類別到 RGB 的映射字典 colors np.zeros((cloud.shape[0], 3)) for cls, rgb in colormap.items(): mask labels cls colors[mask] rgb pcd o3d.geometry.PointCloud() pcd.points o3d.utility.Vector3dVector(cloud) pcd.colors o3d.utility.Vector3dVector(colors) o3d.visualization.draw_geometries([pcd], window_nameSegmentation Result) colormap { 0: [0.5, 0.5, 0.5], # 背景-灰色 1: [0.9, 0.1, 0.1], # 車輛-紅色 2: [0.1, 0.8, 0.1], # 行人-綠色 # 其他類別繼續(xù)追加 } visualize_segmentation(cloud, labels, colormap)Open3D 可視化最大的優(yōu)點是交互式旋轉縮放可以近距離檢查物體邊界上的分割質量——車輛邊緣的鋸齒、行人點云的粘連拉近了一眼就能看出來。我習慣在寫完分割管線后先把三組結果對比看一遍固定視角、小角度偏移5度、大角度偏移15度用顏色區(qū)分類別直接觀察物體邊界的平滑度差異。從那以后每次拿到新的點云數(shù)據我都會強制走一遍加載 → 統(tǒng)計 → 視點偏移 → 推理 → 可視化這個閉環(huán)參數(shù)調沒調對、預處理鏈路有沒有斷跑一遍全暴露了。希望這份拆解能幫你在自己的項目里少走幾步彎路點云分割這條路值得認真走一遍。本文還有配套的精品資源點擊獲取