修正安装Z离地先验,并改进旋转可视化模式4避免坏IMU位移误导。
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -4,19 +4,23 @@ vehicle:
|
|||||||
vehicle_id: "outdoor_usable_20260808"
|
vehicle_id: "outdoor_usable_20260808"
|
||||||
body_frame:
|
body_frame:
|
||||||
name: "base_link"
|
name: "base_link"
|
||||||
# CAD / 后轮轴中心测量系(与安装图 dX/dY/dZ 一致)
|
# 车体约定:后轮轴中心在地面投影为原点附近参考;X 前 / Y 左 / Z 上
|
||||||
|
# translation_m 的 Z 使用「离地高度」;后轮轴中心离地 294 mm
|
||||||
axes: "X forward, Y left, Z up"
|
axes: "X forward, Y left, Z up"
|
||||||
unit: m
|
unit: m
|
||||||
reference_point: "rear_axle_center"
|
reference_point: "rear_axle_center_xy__z_above_ground"
|
||||||
|
rear_axle_height_above_ground_m: 0.294
|
||||||
|
|
||||||
installation:
|
installation:
|
||||||
installation_id: "20260808_priority_windows"
|
installation_id: "20260808_priority_windows"
|
||||||
installed_at: "2026-08-08"
|
installed_at: "2026-08-08"
|
||||||
notes: >
|
notes: >
|
||||||
HI13R4 + H32 DLogCapture. CAD mounts are origins vs rear axle center
|
HI13R4 + H32 DLogCapture. Body +X forward: LiDAR and IMU at positive X.
|
||||||
(translation only). LiDAR phase-center Z = CAD dZ + 63.5 mm.
|
CAD sheet may draw +X rearward; numbers below are body-frame.
|
||||||
|
Z is height above ground = CAD height at axle + 0.294 m (axle AGL).
|
||||||
|
LiDAR phase-center AGL is the measured 1.9165 m (not CAD dZ+63.5).
|
||||||
IMU axes: HI13R4 manual §2.4 RFU (X right, Y forward, Z up).
|
IMU axes: HI13R4 manual §2.4 RFU (X right, Y forward, Z up).
|
||||||
LiDAR Cartesian assumed body-aligned.
|
LiDAR Cartesian in NPZ assumed body-aligned (X forward).
|
||||||
|
|
||||||
sensors:
|
sensors:
|
||||||
imu:
|
imu:
|
||||||
@@ -26,14 +30,13 @@ sensors:
|
|||||||
axes: "X right, Y forward, Z up (RFU)"
|
axes: "X right, Y forward, Z up (RFU)"
|
||||||
driver_axis_remapped: false
|
driver_axis_remapped: false
|
||||||
mount_in_body:
|
mount_in_body:
|
||||||
# CAD 图二:后轮轴中心 → IMU,单位 m(mm/1000)
|
# X/Y:后轮轴中心 → IMU;Z:离地 = CAD 0.8925 + 0.294
|
||||||
# dX=2574.126255, dY=36.5, dZ=892.5
|
translation_m: [2.574126255, 0.0365, 1.1865]
|
||||||
translation_m: [2.574126255, 0.0365, 0.8925]
|
|
||||||
# body <- imu : p_body = R_body_imu * p_imu
|
# body <- imu : p_body = R_body_imu * p_imu
|
||||||
# R_body_imu = [[0,1,0],[-1,0,0],[0,0,1]] (fwd=imu_y, left=-imu_x, up=imu_z)
|
# R_body_imu = [[0,1,0],[-1,0,0],[0,0,1]] (fwd=imu_y, left=-imu_x, up=imu_z)
|
||||||
rotation_matrix_body_imu: [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
|
rotation_matrix_body_imu: [[0.0, 1.0, 0.0], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
|
||||||
rotation_quaternion_xyzw: null
|
rotation_quaternion_xyzw: null
|
||||||
source: "CAD dX/dY/dZ + HI13R4 manual RFU"
|
source: "CAD X/Y in body (+X forward); Z = CAD axle-height + 294mm AGL + HI13R4 RFU"
|
||||||
|
|
||||||
lidar:
|
lidar:
|
||||||
model: "RSLidarH32"
|
model: "RSLidarH32"
|
||||||
@@ -42,13 +45,11 @@ sensors:
|
|||||||
axes: "X forward, Y left, Z up (Cartesian metres in NPZ points)"
|
axes: "X forward, Y left, Z up (Cartesian metres in NPZ points)"
|
||||||
driver_axis_remapped: false
|
driver_axis_remapped: false
|
||||||
mount_in_body:
|
mount_in_body:
|
||||||
# CAD 图一:后轮轴中心 → 雷达安装点,再加相位中心 +63.5 mm(仅 Z)
|
# X/Y:后轮轴中心 → 雷达;Z:相位中心真实离地 1.9165 m
|
||||||
# dX=2522.276859, dY=0.020526, dZ=1637.499879+63.5=1700.999879
|
translation_m: [2.522276859, 0.000020526, 1.9165]
|
||||||
translation_m: [2.522276859, 0.000020526, 1.700999879]
|
|
||||||
# 假设雷达系与车体 CAD 轴一致(导出 XYZ 已按此约定)
|
|
||||||
rotation_matrix_body_lidar: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
|
rotation_matrix_body_lidar: [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
|
||||||
rotation_quaternion_xyzw: null
|
rotation_quaternion_xyzw: null
|
||||||
source: "CAD dX/dY/dZ + phase-center +63.5mm on Z; attitude assumed = body"
|
source: "CAD X/Y in body (+X forward); Z = measured phase-center AGL 1.9165 m; attitude = body"
|
||||||
|
|
||||||
rtk:
|
rtk:
|
||||||
frame_definition: ""
|
frame_definition: ""
|
||||||
@@ -66,18 +67,17 @@ time:
|
|||||||
# t_IMU_lidar = R_IMU_body * t_body, R_IMU_lidar = R_IMU_body * R_body_lidar
|
# t_IMU_lidar = R_IMU_body * t_body, R_IMU_lidar = R_IMU_body * R_body_lidar
|
||||||
derived_T_IMU_lidar_prior:
|
derived_T_IMU_lidar_prior:
|
||||||
R_IMU_lidar: [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
|
R_IMU_lidar: [[0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0]]
|
||||||
t_IMU_lidar_m: [0.036479474, -0.051849396, 0.808499879]
|
t_IMU_lidar_m: [0.036479474, -0.051849396, 0.730]
|
||||||
t_lidar_from_imu_in_body_m: [-0.051849396, -0.036479474, 0.808499879]
|
t_lidar_from_imu_in_body_m: [-0.051849396, -0.036479474, 0.730]
|
||||||
notes: >
|
notes: >
|
||||||
Rotation prior is ~90 deg yaw between body/lidar (X-fwd) and IMU RFU (Y-fwd).
|
Rotation prior ~90 deg yaw (body/lidar X-fwd vs IMU Y-fwd).
|
||||||
Translation prior from CAD + LiDAR phase-center offset; use for full_se3 /
|
Relative Z = 1.9165 - 1.1865 = 0.730 m (was 0.8085 m with old CAD+63.5mm Z).
|
||||||
sanity, not as hard lock for rotation_only.
|
|
||||||
|
|
||||||
initialization:
|
initialization:
|
||||||
translation_prior:
|
translation_prior:
|
||||||
enabled: true
|
enabled: true
|
||||||
sigma_m: [0.05, 0.05, 0.05]
|
sigma_m: [0.05, 0.05, 0.05]
|
||||||
t_IMU_lidar_m: [0.036479474, -0.051849396, 0.808499879]
|
t_IMU_lidar_m: [0.036479474, -0.051849396, 0.730]
|
||||||
rotation_prior:
|
rotation_prior:
|
||||||
enabled: true
|
enabled: true
|
||||||
sigma_deg: 15.0
|
sigma_deg: 15.0
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
# 20260808 HI13 + H32:LiDAR–IMU 标定现状与问题
|
||||||
|
|
||||||
|
> 数据:`D:\data\calibration_usable_20260808`
|
||||||
|
> 可用会话:`sessions_v1_host_aligned`(三优先窗)
|
||||||
|
> 当前结果目录:各窗 `out_fixed_dt0/`
|
||||||
|
> 清单:`sessions_v1_host_aligned/calibration_manifest_fixed_dt0.json`
|
||||||
|
> 车辆配置:`config/vehicle_hi13_h32_20260808.yaml`
|
||||||
|
> 约定外参:`p_IMU = T_IMU_lidar · p_lidar`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 一句话结论
|
||||||
|
|
||||||
|
**旋转 + 主机桥接时间对齐可以冻结;平移(full_se3)尚不可正式交付。**
|
||||||
|
三窗 `rotation_only`(δt=0)结果跨窗一致,**不必因平移先验 Z 修正而重跑旋转**。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 当前可用结果(`out_fixed_dt0`)
|
||||||
|
|
||||||
|
| 窗 | 状态 | δt | yaw (°) | 手眼 RMS (°) | 手眼对数 | vs CAD prior |
|
||||||
|
|----|------|----|---------|--------------|----------|--------------|
|
||||||
|
| `priority_174005_174515` | `rotation_only_accepted` | 0 | ≈90.00 | 0.62 | 1374 | ≈0.41° |
|
||||||
|
| `priority_174905_175450` | 同上 | 0 | ≈90.00 | 0.29 | 1182 | ≈0.47° |
|
||||||
|
| `priority_175910_180530` | 同上 | 0 | ≈90.01 | 0.79 | 789 | ≈0.38° |
|
||||||
|
|
||||||
|
- 跨窗旋转互差约 **0.15°–0.47°**(相对三窗均值 ≤0.26°)。
|
||||||
|
- 交付矩阵中 **平移为 0**(`translation_accepted=false`)。
|
||||||
|
- CAD/安装平移先验只用于后续 SE3 / 校验,不写入本轮交付 `T`。
|
||||||
|
|
||||||
|
### 相对历史失败轮次
|
||||||
|
|
||||||
|
| 轮次 | 问题 | 结果 |
|
||||||
|
|------|------|------|
|
||||||
|
| `sessions_v1_aligned` | 首帧强行对齐设备钟 | 三窗手眼失败,RMS ~9°–12° |
|
||||||
|
| 自由估 δt + signed refine | 窗3 δt 漂到 −0.48 s;窗2 yaw≈19° | 跨窗 yaw 矛盾(81°/19°/93°) |
|
||||||
|
| **本轮 fixed δt=0** | 主机桥接后冻结时间 | 三窗 yaw≈90°,可互证 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 已澄清并写入配置的坐标系 / 先验
|
||||||
|
|
||||||
|
### 3.1 车体与传感器
|
||||||
|
|
||||||
|
- 车体:X 前 / Y 左 / Z 上;雷达与 IMU 安装在 **X 正方向**(后轮轴前方)。
|
||||||
|
- CAD 图纸可能画成 +X 朝后,那只是读图坐标系,**不是**车体真实轴。
|
||||||
|
- IMU:HI13 RFU(X 右 / Y 前 / Z 上),原始数据不做轴向重映射。
|
||||||
|
- 雷达 NPZ:假定与车体一致(X 前 / Y 左 / Z 上)。
|
||||||
|
|
||||||
|
### 3.2 安装量(`translation_m`)
|
||||||
|
|
||||||
|
| 传感器 | X / Y(后轮轴中心) | Z(离地) |
|
||||||
|
|--------|---------------------|-----------|
|
||||||
|
| IMU | 2.574 / 0.0365 m | 0.8925 + 0.294 = **1.1865 m** |
|
||||||
|
| 雷达 | 2.522 / 0.00002 m | 相位中心实测 **1.9165 m** |
|
||||||
|
|
||||||
|
- 后轮轴中心离地:**294 mm**(Z 用离地高时加在 CAD 轴心高上)。
|
||||||
|
- 雷达相位中心以实测 1.9165 m 为准(不再用「CAD dZ + 63.5 mm」作为最终离地高)。
|
||||||
|
|
||||||
|
### 3.3 导出外参先验
|
||||||
|
|
||||||
|
- `R_IMU_lidar` ≈ yaw 90°:`[[0,-1,0],[1,0,0],[0,0,1]]`(软约束 σ=15°)。
|
||||||
|
- `t_IMU_lidar` ≈ **`[0.0365, -0.0518, 0.730]` m**(相对 Z = 1.9165 − 1.1865)。
|
||||||
|
- **旋转先验未因 Z 修正改变**;仅平移先验 Z 从旧值 0.8085 改为 0.730。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 现存问题清单
|
||||||
|
|
||||||
|
### P1. IMU 预积分平移 `Δp` 不可用(阻塞正式平移)
|
||||||
|
|
||||||
|
- 现象:可视化模式 4 若用完整 `X⁻¹ A X`,橙/蓝点云常呈**上下错层**(Z 差米级~几十米)。
|
||||||
|
- 根因:加速度预积分缺少可靠重力/零偏处理,`t_A` 尤其 Z 发散;**不是旋转外参错了**。
|
||||||
|
- 旁证:相对 GICP 的旋转残差中位约 0.16°;`|t_A|` 中位却常 >1 m。
|
||||||
|
- 影响:`full_se3` / 依赖 IMU 位移的平移估计不可信。
|
||||||
|
- 缓解(已做):`visualize_pair_3d.py` 对 `rotation_only` 默认模式 4 = **R 共轭 + GICP 的 t_B**(`--mode4-translation gicp|imu|auto`)。
|
||||||
|
|
||||||
|
### P2. 平面运动导致竖直平移弱可观
|
||||||
|
|
||||||
|
- 三优先窗以水平转弯为主,缺少缓坡/俯仰激励。
|
||||||
|
- 流水线门控已给出 `translation_accepted=false`。
|
||||||
|
- 即使打开平移先验(σ≈5 cm),弱激励下结果易变成**先验回显**,不宜当标定成功。
|
||||||
|
|
||||||
|
### P3. 时间偏移若再自由估计会被带偏(已规避,需保持)
|
||||||
|
|
||||||
|
- 主机 UTC 桥接(MSOP/IMU `HostReceiveUtc`)后,两路已在同一时间轴,残差通常几十毫秒量级。
|
||||||
|
- 若再做有符号 δt 精修,会与错误/未收敛的 R 耦合,窗3 曾从约 −0.12 s 走到 **−0.48 s**。
|
||||||
|
- **现行做法**:桥接会话使用 `--fixed-time-offset-s 0 --no-signed-time-refine`。
|
||||||
|
|
||||||
|
### P4. 单窗低残差 ≠ 外参正确(历史教训)
|
||||||
|
|
||||||
|
- 自由 δt 轮次中,窗2 手眼 RMS 最低(~0.3°)但 yaw≈19°,与 CAD/其他窗差 60°+。
|
||||||
|
- 平面运动下 yaw 外参可出现多个能拟合 `R_A R_X ≈ R_X R_B` 的解。
|
||||||
|
- **必须**做跨窗一致性 + 可视化叠点,不能只看单窗 RMS。
|
||||||
|
|
||||||
|
### P5. 旋转软先验尚未做无先验对照
|
||||||
|
|
||||||
|
- 当前 σ=15°;笔记显示 Tsai 初值本身已接近(约 0.3°–1.1° RMS),不像纯先验硬拽。
|
||||||
|
- 仍缺一次:关闭先验或放大 `sigma_deg` 的对照,以排除「只是被拉到 90°」的疑虑。
|
||||||
|
|
||||||
|
### P6. 环境/导入陷阱(工程)
|
||||||
|
|
||||||
|
- 本机若存在指向其他仓库的 editable 安装(如 `Lidar-IMU`),`python tools\...` 可能导入错误包。
|
||||||
|
- `visualize_pair_3d.py` 已插入仓库根到 `sys.path`;长期仍建议在本仓库 `pip install -e .`。
|
||||||
|
|
||||||
|
### P7. 文档与操作约定未完全同步(工程)
|
||||||
|
|
||||||
|
- README 需明确写清:host-bridge 后固定 δt=0、禁用 signed refine、rotation_only 可视化用法。
|
||||||
|
- 交付物目前缺一版「冻结的联合/中位 R + 使用说明」JSON/报告(旋转可交,平移明确不交)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 不该做 / 可以做
|
||||||
|
|
||||||
|
| 动作 | 建议 |
|
||||||
|
|------|------|
|
||||||
|
| 因 Z 先验修正重跑三窗 rotation_only | **不必**(R 未依赖新 t) |
|
||||||
|
| 正式交付 6-DOF / 信赖当前 `Δp` 估 t | **不要** |
|
||||||
|
| 试验性 `full_se3`(固定 R、δt=0、新 t 先验) | 可做,结果标「实验」 |
|
||||||
|
| 可视化验收模式 3 vs 4(gicp 平移) | **建议做** |
|
||||||
|
| 无先验 / 大 σ 旋转对照 | **建议做** |
|
||||||
|
| 冻结交付 `R` + `δt=0` 说明 | **建议做** |
|
||||||
|
| 补采缓坡或加强垂直尺寸约束后再估 t | 正式平移前需要 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 建议下一步顺序
|
||||||
|
|
||||||
|
1. **验收旋转**:三窗抽转弯运动对,模式 3/4 叠点;可选无先验对照。
|
||||||
|
2. **定稿旋转**:三窗中位或联合手眼 → 交付 `R_IMU_lidar` +「δt=0(主机桥接)」说明;**明确不交 t**。
|
||||||
|
3. **工程收尾**:README 主机桥接配方;需要时再整理联合标定脚本入口。
|
||||||
|
4. **平移(靠后)**:改善 IMU 位移模型或改用更可靠的位移观测 + 竖直激励后,再用新 `t` 先验跑 SE3。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 常用路径与命令
|
||||||
|
|
||||||
|
```text
|
||||||
|
数据根:
|
||||||
|
D:\data\calibration_usable_20260808\sessions_v1_host_aligned\
|
||||||
|
|
||||||
|
结果:
|
||||||
|
...\priority_XXXX\out_fixed_dt0\summary.json
|
||||||
|
...\priority_XXXX\out_fixed_dt0\motion_pairs.json
|
||||||
|
...\calibration_manifest_fixed_dt0.json
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 可视化(rotation_only 默认模式4用 GICP 平移)
|
||||||
|
python tools\visualize_pair_3d.py `
|
||||||
|
--lidar D:\data\calibration_usable_20260808\sessions_v1_host_aligned\priority_174005_174515\lidar `
|
||||||
|
--summary D:\data\calibration_usable_20260808\sessions_v1_host_aligned\priority_174005_174515\out_fixed_dt0\summary.json `
|
||||||
|
--pair-index 0
|
||||||
|
|
||||||
|
# 若要看「坏 Δp」导致的错层效果:
|
||||||
|
# --mode4-translation imu
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 问题优先级(跟踪用)
|
||||||
|
|
||||||
|
| ID | 严重度 | 状态 | 标题 |
|
||||||
|
|----|--------|------|------|
|
||||||
|
| P1 | 高 | 未解决 | IMU `Δp` 不可用,阻塞正式平移 |
|
||||||
|
| P2 | 高 | 未解决 | 平面运动,竖直 t 弱可观 |
|
||||||
|
| P3 | 高 | 已规避 | 自由 δt / signed refine 带偏(需保持冻结) |
|
||||||
|
| P4 | 中 | 已吸收教训 | 单窗低残差不可单独验收 |
|
||||||
|
| P5 | 中 | 待做 | 无旋转先验对照 |
|
||||||
|
| P6 | 低 | 部分修复 | 错误 `imu_lidar` 包导入 |
|
||||||
|
| P7 | 低 | 待做 | README/交付物同步 |
|
||||||
+107
-15
@@ -9,6 +9,8 @@ Modes (keyboard):
|
|||||||
2 IMU prediction with X=I (B_pred = A)
|
2 IMU prediction with X=I (B_pred = A)
|
||||||
3 LiDAR registration B (reference)
|
3 LiDAR registration B (reference)
|
||||||
4 calibrated prediction B_pred = X^{-1} A X
|
4 calibrated prediction B_pred = X^{-1} A X
|
||||||
|
(rotation_only runs default to R conjug + t_B so bad IMU Δp
|
||||||
|
does not dominate the overlay)
|
||||||
N / ] next motion pair
|
N / ] next motion pair
|
||||||
P / [ previous motion pair
|
P / [ previous motion pair
|
||||||
Q / Esc exit
|
Q / Esc exit
|
||||||
@@ -20,10 +22,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
from imu_lidar.geometry import (
|
from imu_lidar.geometry import (
|
||||||
inverse_transform,
|
inverse_transform,
|
||||||
make_transform,
|
make_transform,
|
||||||
@@ -80,24 +88,45 @@ MODE_NAMES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _load_extrinsic(summary_path: Path) -> tuple[np.ndarray, float, np.ndarray]:
|
def _load_extrinsic(summary_path: Path) -> tuple[np.ndarray, float, np.ndarray, dict[str, Any]]:
|
||||||
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
summary = json.loads(summary_path.read_text(encoding="utf-8"))
|
||||||
t_block = summary.get("T_IMU_lidar")
|
t_block = summary.get("T_IMU_lidar")
|
||||||
|
meta: dict[str, Any] = {
|
||||||
|
"rotation_only": False,
|
||||||
|
"translation_accepted": False,
|
||||||
|
"status": str(summary.get("status") or ""),
|
||||||
|
}
|
||||||
if t_block is None:
|
if t_block is None:
|
||||||
matrix = summary.get("matrix")
|
matrix = summary.get("matrix")
|
||||||
if matrix is not None:
|
if matrix is not None:
|
||||||
return np.asarray(matrix, dtype=float), 0.0, np.zeros(3)
|
return np.asarray(matrix, dtype=float), 0.0, np.zeros(3), meta
|
||||||
raise ValueError(f"no T_IMU_lidar in {summary_path}")
|
raise ValueError(f"no T_IMU_lidar in {summary_path}")
|
||||||
t_mat = np.asarray(t_block["matrix"], dtype=float)
|
t_mat = np.asarray(t_block["matrix"], dtype=float)
|
||||||
dt = float(summary.get("time_offset_s") or 0.0)
|
dt = float(summary.get("time_offset_s") or 0.0)
|
||||||
session = (summary.get("details") or {}).get("sessions", [{}])[0]
|
session = (summary.get("details") or {}).get("sessions", [{}])[0]
|
||||||
|
joint = session.get("joint") or {}
|
||||||
bias = np.asarray(
|
bias = np.asarray(
|
||||||
(session.get("imu_audit") or {}).get("gyro_bias_rad_s")
|
(session.get("imu_audit") or {}).get("gyro_bias_rad_s")
|
||||||
or (session.get("joint") or {}).get("gyro_bias_rad_s")
|
or joint.get("gyro_bias_rad_s")
|
||||||
or [0.0, 0.0, 0.0],
|
or [0.0, 0.0, 0.0],
|
||||||
dtype=float,
|
dtype=float,
|
||||||
).reshape(3)
|
).reshape(3)
|
||||||
return t_mat, dt, bias
|
status = str(summary.get("status") or "")
|
||||||
|
translation_accepted = bool(
|
||||||
|
joint.get("translation_accepted")
|
||||||
|
or (summary.get("details") or {}).get("joint", {}).get("translation_accepted")
|
||||||
|
)
|
||||||
|
rotation_only = ("rotation_only" in status) or (
|
||||||
|
not translation_accepted and float(np.linalg.norm(t_mat[:3, 3])) < 1e-9
|
||||||
|
)
|
||||||
|
meta.update(
|
||||||
|
{
|
||||||
|
"rotation_only": rotation_only,
|
||||||
|
"translation_accepted": translation_accepted,
|
||||||
|
"status": status,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return t_mat, dt, bias, meta
|
||||||
|
|
||||||
|
|
||||||
def _delta_components(reference: np.ndarray, candidate: np.ndarray) -> dict:
|
def _delta_components(reference: np.ndarray, candidate: np.ndarray) -> dict:
|
||||||
@@ -196,16 +225,42 @@ def _pair_from_indices(
|
|||||||
return frame_i, frame_j, a, reg.transform
|
return frame_i, frame_j, a, reg.transform
|
||||||
|
|
||||||
|
|
||||||
def _transforms_for_pair(x: np.ndarray, a_ij: np.ndarray, b_gicp: np.ndarray) -> dict[str, np.ndarray]:
|
def _transforms_for_pair(
|
||||||
|
x: np.ndarray,
|
||||||
|
a_ij: np.ndarray,
|
||||||
|
b_gicp: np.ndarray,
|
||||||
|
*,
|
||||||
|
mode4_translation: str = "imu",
|
||||||
|
) -> dict[str, np.ndarray]:
|
||||||
|
"""Build overlay transforms.
|
||||||
|
|
||||||
|
``mode4_translation``:
|
||||||
|
- ``imu``: full SE3 conjug ``X^{-1} A X`` (needs trustworthy IMU Δp)
|
||||||
|
- ``gicp``: rotation conjug only; translation taken from LiDAR B
|
||||||
|
(correct check for rotation_only calibrations)
|
||||||
|
"""
|
||||||
|
|
||||||
|
calibrated = inverse_transform(x) @ a_ij @ x
|
||||||
|
if mode4_translation == "gicp":
|
||||||
|
calibrated = make_transform(b_gicp[:3, 3], calibrated[:3, :3])
|
||||||
|
elif mode4_translation != "imu":
|
||||||
|
raise ValueError(f"unknown mode4_translation={mode4_translation!r}")
|
||||||
return {
|
return {
|
||||||
MODE_NAMES[0]: np.eye(4),
|
MODE_NAMES[0]: np.eye(4),
|
||||||
MODE_NAMES[1]: a_ij.copy(),
|
MODE_NAMES[1]: a_ij.copy(),
|
||||||
MODE_NAMES[2]: b_gicp.copy(),
|
MODE_NAMES[2]: b_gicp.copy(),
|
||||||
MODE_NAMES[3]: inverse_transform(x) @ a_ij @ x,
|
MODE_NAMES[3]: calibrated,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _resolve_pair(frames, pairs, pair_index: int, x: np.ndarray):
|
def _resolve_pair(
|
||||||
|
frames,
|
||||||
|
pairs,
|
||||||
|
pair_index: int,
|
||||||
|
x: np.ndarray,
|
||||||
|
*,
|
||||||
|
mode4_translation: str = "imu",
|
||||||
|
):
|
||||||
pair = pairs[pair_index]
|
pair = pairs[pair_index]
|
||||||
frame_i = frames[pair.i]
|
frame_i = frames[pair.i]
|
||||||
frame_j = frames[pair.j]
|
frame_j = frames[pair.j]
|
||||||
@@ -217,7 +272,9 @@ def _resolve_pair(frames, pairs, pair_index: int, x: np.ndarray):
|
|||||||
pair.t_B_m if pair.t_B_m is not None else np.zeros(3),
|
pair.t_B_m if pair.t_B_m is not None else np.zeros(3),
|
||||||
pair.R_B,
|
pair.R_B,
|
||||||
)
|
)
|
||||||
transforms = _transforms_for_pair(x, a_ij, b_gicp)
|
transforms = _transforms_for_pair(
|
||||||
|
x, a_ij, b_gicp, mode4_translation=mode4_translation
|
||||||
|
)
|
||||||
label = (
|
label = (
|
||||||
f"pair {pair_index + 1}/{len(pairs)} "
|
f"pair {pair_index + 1}/{len(pairs)} "
|
||||||
f"frames {pair.i} <- {pair.j} "
|
f"frames {pair.i} <- {pair.j} "
|
||||||
@@ -282,12 +339,15 @@ def _run_gui(
|
|||||||
start_index: int,
|
start_index: int,
|
||||||
voxel: float,
|
voxel: float,
|
||||||
fixed_single_pair: tuple | None,
|
fixed_single_pair: tuple | None,
|
||||||
|
mode4_translation: str = "imu",
|
||||||
) -> None:
|
) -> None:
|
||||||
import open3d as o3d
|
import open3d as o3d
|
||||||
|
|
||||||
if fixed_single_pair is not None:
|
if fixed_single_pair is not None:
|
||||||
frame_i, frame_j, a_ij, b_gicp = fixed_single_pair
|
frame_i, frame_j, a_ij, b_gicp = fixed_single_pair
|
||||||
transforms = _transforms_for_pair(x, a_ij, b_gicp)
|
transforms = _transforms_for_pair(
|
||||||
|
x, a_ij, b_gicp, mode4_translation=mode4_translation
|
||||||
|
)
|
||||||
label = f"fixed frames (no pair switching)"
|
label = f"fixed frames (no pair switching)"
|
||||||
pair_index = 0
|
pair_index = 0
|
||||||
n_pairs = 1
|
n_pairs = 1
|
||||||
@@ -297,7 +357,7 @@ def _run_gui(
|
|||||||
n_pairs = len(pairs)
|
n_pairs = len(pairs)
|
||||||
use_list = True
|
use_list = True
|
||||||
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
||||||
frames, pairs, pair_index, x
|
frames, pairs, pair_index, x, mode4_translation=mode4_translation
|
||||||
)
|
)
|
||||||
|
|
||||||
viewer = o3d.visualization.VisualizerWithKeyCallback()
|
viewer = o3d.visualization.VisualizerWithKeyCallback()
|
||||||
@@ -338,7 +398,7 @@ def _run_gui(
|
|||||||
return
|
return
|
||||||
new_index = int(new_index) % n_pairs
|
new_index = int(new_index) % n_pairs
|
||||||
frame_i, frame_j, _a, b_gicp, transforms, label = _resolve_pair(
|
frame_i, frame_j, _a, b_gicp, transforms, label = _resolve_pair(
|
||||||
frames, pairs, new_index, x
|
frames, pairs, new_index, x, mode4_translation=mode4_translation
|
||||||
)
|
)
|
||||||
state["pair_index"] = new_index
|
state["pair_index"] = new_index
|
||||||
state["transforms"] = transforms
|
state["transforms"] = transforms
|
||||||
@@ -429,9 +489,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
action="store_true",
|
action="store_true",
|
||||||
help="Skip Open3D window (use with --save-png)",
|
help="Skip Open3D window (use with --save-png)",
|
||||||
)
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mode4-translation",
|
||||||
|
choices=("auto", "gicp", "imu"),
|
||||||
|
default="auto",
|
||||||
|
help=(
|
||||||
|
"Mode-4 translation source: gicp=R conjug + t_B (rotation check); "
|
||||||
|
"imu=full X^-1 A X; auto=gicp for rotation_only summaries"
|
||||||
|
),
|
||||||
|
)
|
||||||
args = parser.parse_args(argv)
|
args = parser.parse_args(argv)
|
||||||
|
|
||||||
x, delta_t_s, gyro_bias = _load_extrinsic(args.summary)
|
x, delta_t_s, gyro_bias, extr_meta = _load_extrinsic(args.summary)
|
||||||
|
if args.mode4_translation == "auto":
|
||||||
|
mode4_translation = "gicp" if extr_meta.get("rotation_only") else "imu"
|
||||||
|
else:
|
||||||
|
mode4_translation = args.mode4_translation
|
||||||
cache_path = args.motion_pairs or resolve_motion_pairs_path(args.summary)
|
cache_path = args.motion_pairs or resolve_motion_pairs_path(args.summary)
|
||||||
use_cache = (not args.rebuild_pairs) and cache_path is not None and args.frame_i is None
|
use_cache = (not args.rebuild_pairs) and cache_path is not None and args.frame_i is None
|
||||||
|
|
||||||
@@ -456,7 +529,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
frames = _LazyFrameStore(args.lidar)
|
frames = _LazyFrameStore(args.lidar)
|
||||||
pairs = tuple(pair_list)
|
pairs = tuple(pair_list)
|
||||||
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
||||||
frames, pairs, args.pair_index, x
|
frames, pairs, args.pair_index, x, mode4_translation=mode4_translation
|
||||||
)
|
)
|
||||||
print(f"loaded {len(pairs)} cached pairs from {cache_path}")
|
print(f"loaded {len(pairs)} cached pairs from {cache_path}")
|
||||||
else:
|
else:
|
||||||
@@ -479,7 +552,9 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
delta_t_s=delta_t_s,
|
delta_t_s=delta_t_s,
|
||||||
gyro_bias=gyro_bias,
|
gyro_bias=gyro_bias,
|
||||||
)
|
)
|
||||||
transforms = _transforms_for_pair(x, a_ij, b_gicp)
|
transforms = _transforms_for_pair(
|
||||||
|
x, a_ij, b_gicp, mode4_translation=mode4_translation
|
||||||
|
)
|
||||||
label = f"frames {args.frame_i} <- {args.frame_j}"
|
label = f"frames {args.frame_i} <- {args.frame_j}"
|
||||||
fixed_single_pair = (frame_i, frame_j, a_ij, b_gicp)
|
fixed_single_pair = (frame_i, frame_j, a_ij, b_gicp)
|
||||||
pairs = ()
|
pairs = ()
|
||||||
@@ -493,10 +568,26 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
)
|
)
|
||||||
pairs = pair_set.pairs
|
pairs = pair_set.pairs
|
||||||
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
frame_i, frame_j, a_ij, b_gicp, transforms, label = _resolve_pair(
|
||||||
frames, pairs, args.pair_index, x
|
frames, pairs, args.pair_index, x, mode4_translation=mode4_translation
|
||||||
)
|
)
|
||||||
print(f"rebuilt {len(pairs)} pairs from {len(keyframes.indices)} keyframes")
|
print(f"rebuilt {len(pairs)} pairs from {len(keyframes.indices)} keyframes")
|
||||||
|
|
||||||
|
print(
|
||||||
|
f"mode4 translation={mode4_translation} "
|
||||||
|
f"(status={extr_meta.get('status') or 'n/a'}, "
|
||||||
|
f"rotation_only={bool(extr_meta.get('rotation_only'))})"
|
||||||
|
)
|
||||||
|
if mode4_translation == "gicp":
|
||||||
|
print(
|
||||||
|
"note: mode4 uses R conjug + t_B; IMU Δp is ignored "
|
||||||
|
"(typical for rotation_only — raw Δp often has large Z drift)."
|
||||||
|
)
|
||||||
|
if mode4_translation == "imu":
|
||||||
|
print(
|
||||||
|
"note: mode4 uses full X^-1 A X. If clouds stack vertically, "
|
||||||
|
"IMU Δp is likely bad; retry with --mode4-translation gicp."
|
||||||
|
)
|
||||||
|
|
||||||
if args.save_png is not None:
|
if args.save_png is not None:
|
||||||
_print_pair_header(label, b_gicp, transforms)
|
_print_pair_header(label, b_gicp, transforms)
|
||||||
_save_topdown_png(args.save_png, frame_i.points_xyz, frame_j.points_xyz, transforms)
|
_save_topdown_png(args.save_png, frame_i.points_xyz, frame_j.points_xyz, transforms)
|
||||||
@@ -520,6 +611,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
start_index=args.pair_index,
|
start_index=args.pair_index,
|
||||||
voxel=args.voxel,
|
voxel=args.voxel,
|
||||||
fixed_single_pair=fixed_single_pair,
|
fixed_single_pair=fixed_single_pair,
|
||||||
|
mode4_translation=mode4_translation,
|
||||||
)
|
)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user