34 · 感測器的假數字:三個「合理但錯」的位置
把雷射從無到有接上去的過程中,錯過三次。三次的輸出都是一組合理的數字。
這是感測器這一層最麻煩的性質:它幾乎不會給你 NaN 或例外,它會給你一組穩定、一致、落在量程內的讀數——而那組讀數描述的是另一個世界。
驗證狀態:內容來自一段多車 + 多樓層的 Isaac Sim 場域前期研究(2026-07 至 08),
/scan於 2026-08-16 接上實際的 lidar。數字連同幾何條件列出。⚠ 有一項明確標了「原始紀錄沒有討論」,不代下結論。
1. 感測器埋在自己的碰撞盒裡
lidar 一開始掛在車體原點,而車體是一個實心的 Cube 碰撞盒。
結果:240 束全部回傳 0.120 m,也就是量程下限。
那個值穩定、一致、在合理範圍內——與「車貼著牆」在資料上完全一樣。
沒有任何辦法從資料本身分辨這兩件事。改成掛在車頭前緣外 2 cm。
這一條的一般形式:感測器的安裝位置是它的量測前提,而那個前提不在資料裡。任何「所有讀數都一樣」的感測器輸出,第一個要懷疑的是它在不在自己身體裡。
2. 視角在這個幾何下根本裝不下
移到車頭之後仍有 41 束是量程下限。
原因可以算出來:方位角超過
180° − atan( (車寬/2) / 淨空 )
的射線會往後打進自己的車體。2 cm 淨空、0.6 m 車寬時,這個角度約 93.5°。
±120° 的視角在這個幾何下根本裝不下。
要保住 240° 的視野只有兩條路:
- 感測器伸到車頭前 18 cm 以上(一根突出的桿子),或
- 在下游遮掉後方那個楔形(而 OmniGraph 這側沒有濾波節點)
最後改成 ±90° 前向,並讓建構函式在裝不下時直接擋——把上限算出來當成前置條件,不是等下次有人再踩一遍。
這是把一個幾何約束寫成程式碼裡的守衛。 「感測器視角」與「車體寬度 + 安裝淨空」之間有一條硬關係,而那條關係在任何設定檔裡都看不出來。
3. 正對照自己的幾何錯了
寫好的正對照是:車前方 2 m 放一個箱子,量到的最近距離要是 2 m。
它失敗了。而感測器其實正常。
兩個獨立的錯誤疊在一起:
- 箱子放在車體原點前方 2 m,但感測器在車頭、離原點 0.4 m —— 雷射看到的是 1.6 m。
- 那台車前方本來就有一面 2.1 m 的牆,箱子與牆落在同一個判定帶裡,束數一根都沒變。
兩個教訓:
距離要從感測器量,不是從車體原點。 對照的距離要從資料算(取當下最近距離的一半),不要寫死一個可能與場景碰撞的數字。
現在的判準是一個加擾動 / 去擾動的閉環:
放箱子 → 最近距離變成箱子的距離
拿掉 → 回到原本的值
實測:2.095 → 1.048 → 2.095
第三個數字回到第一個,才證明變化是箱子造成的而不是別的東西漂移。只做「放箱子」那一半,同時相容於「感測器有效」與「剛好有別的東西進了視野」。
⚠ 這一條與27 篇 §6、30 篇 §4 是同一條規則的第三次出現:驗法本身也是要被驗的東西。這次它以「正對照的幾何算錯」的形式出現。
4. 資料是對的,但你看不見:三個成因
視覺化那一層有另一組坑,而它們的症狀完全一樣(什麼都看不到),成因卻互不相干:
① 形狀建太晚。 射線曲線第一版是在迴圈裡才 Define 的,結果一條都看不到。物理那側收得到(對照用的箱子就是模擬中新建的,PhysX 認得),但 Hydra 已經把場景收下了。
症狀與「根本沒呼叫繪製函式」一模一樣,查的時候會往顏色、線寬、材質各繞一圈。
處置:在 world.reset() 之前先建好形狀,之後每幀只改點座標。
② 線寬是次像素。 線寬 1.2 cm 在四十公尺外的機位是次像素寬——畫面寬 1280 px 涵蓋約五十公尺,一個像素約 4 cm。
③ displayColor 只是提示。 RTX 這側走材質,沒綁材質的曲線在暗色地板上等於看不見。
三個都不是「資料錯」,是「呈現錯」。而在排查時它們與「資料錯」長得一模一樣——所以驗感測器要驗數值,不要靠看畫面。
5. ROS 2 那側四個會讓數字說謊的量法
這一節跟雷射沒有直接關係,但排查感測器時幾乎一定會用到:
① get_topic_names_and_types() 看得到某個 topic,不代表有人在發。 自己建的訂閱就會讓那個名字出現在列表裡。要問 publisher 用 get_publishers_info_by_topic()。
第一次量到「5 個 topic 都在,但 publisher 是 0」就是這個差別——把前者當證據會得到完全相反的結論。
② 一次 rclpy.spin_once() 只處理一個 callback。 五個訂閱共用一次 spin,收到的則數會被自己的取件速度節流(300 步只收到約 50 則),而那個數字看起來像「發佈頻率不足」。每步多轉幾次之後是 296~299 / 300。
③ 不要把它換算成 Hz。 牆鐘頻率取決於這台跑多快,與圖對不對無關;分母要用「應發幾則」(每 tick 一則 = 步數)。
(這與 29 篇 §2 是同一個病:牆鐘與模擬節拍不是同一件事,任何以牆鐘為分母的指標都在量機器負載。)
④ publisher 預設沒有訂閱者就不發(publish_without_verification 預設 false)。所以「量不到」也可能只是沒有人在聽。
四條的共同形狀:一個回空或偏低的量測,同時相容於「東西壞了」與「我的量法有洞」。
6. 材質與顏色:量素材,不要看檔名
電梯與車原本只綁物理材質(摩擦、恢復係數),沒有視覺材質——所以都是預設灰。
補視覺材質時踩到幾個:
- 電梯原本綁一張金屬板貼圖(Poly Haven,CC0),而那張圖的 diffuse 平均色量出來是
(61, 50, 28)/255——在這個場域的照度下,井道整個算成一塊黑,跟「材質壞掉」在畫面上分不出來。改綁純色(淺鋼藍灰)。 - 中間試過「不換素材,用
UsdUVTexture的inputs:scale提亮 3.5 倍」:屬性確實寫進了 USD(grep 看得到),但算出來的畫面一模一樣——這條路在這個 renderer 上行不通。 - 判準是量素材本身的亮度,不是看檔名。 一張名為
blue_floor_tiles_01的圖量出來是(149, 139, 113),也就是米色而不是藍色;地板看起來偏橄欖色不是打光的問題。 - 車改成每台一個純色。五台都是灰白盒子的話,影片裡分不出誰是誰,而「哪一台搭了電梯」「哪兩台靠得太近」正是要看的東西。真實 AMR 也是烤漆單色,所以純色比貼圖更接近實物;純色也不會有「貼圖檔不在就變全黑」的風險。
- ⚠ 材質 prim 放
/World/Looks,不要放/World/Robots底下 —— 冒煙與測試都用「/World/Robots的子節點數」當車數,多一個Looks就會數成六台(測試抓到過)。 - 環境光從 1000 加到 2500:1000 在室外夠,而這個場域有牆有樓板,從側面看進去時牆會擋掉大半環境光,轎廂內部幾乎全黑。
「屬性寫進去了但畫面沒變」那一條與 17 篇的無聲失效同源:寫得進去不代表被採用,而這次採用它的是 renderer 而不是 PhysX。
⚠ 原始紀錄沒有討論「材質與顏色會不會影響雷射測距」。 §1–§3 那三個雷射錯誤的成因分別是碰撞盒幾何、視角幾何、量測基準點,都與材質無關。但「所以雷射不受渲染影響」是一個推論而不是查證過的結論,這裡不下。要用的話自己驗。
7. 誠實標註:還沒接上的那條線
/cmd_vel收得到但動不了車。ROS2SubscribeTwist吐出速度之後,要有差速控制器 + articulation controller 才會變成輪子的動作。當時車輛是直接搬位置的,等於繞過物理驅動——這正是 23 篇講的那種捷徑,而它的下游後果在 32 篇兌現。- 第一幀有一個暫態:
ROS2PublishTransformTree第一次計算時inputs:parentFrames等陣列還是空的,於是退回另一條路並報Please specify at least one valid target prim。整輪只出現一次,之後就走連線那條。要消掉它就把備援那組也填上。
8. 檢查清單
- [ ] 感測器不在自己的碰撞體裡(所有讀數都是量程下限 → 先查這個)
- [ ] 視角上限用
180° − atan((車寬/2)/淨空)算過,而且寫成程式裡的守衛 - [ ] 距離從感測器量,不從車體原點
- [ ] 正對照的距離從當下資料算,不寫死
- [ ] 對照是閉環:加擾動 → 去擾動 → 回到原值
- [ ] 驗感測器驗數值,不看畫面(畫面有三個獨立的「看不見」成因)
- [ ] 繪製用的形狀在
world.reset()之前建好 - [ ] 用
get_publishers_info_by_topic()問誰在發,不用 topic 列表 - [ ] 收訊率的分母用「應發幾則」,不換算成 Hz
- [ ] 素材亮度用量的,不看檔名
- [ ] 材質 prim 不要放在會被當成計數依據的路徑底下
延伸閱讀:31 OmniGraph 與 ROS 2 橋接的真相、32 把車做成真的會動的車(車不看自己的雷射,是它開不完路網的第五個成因)、30 驗收探針與實驗預先登記、27 失效模式分類學。