完善 LiDAR–双天线 RTK 手眼标定仓库:补充旧式及多传感器数据导出、small_gicp/Open3D GICP 标定、结果复核与3D可视化流程,并整理三批数据和标定结果说明。

This commit is contained in:
lichun.qu
2026-07-23 18:55:50 +08:00
parent 6b7844a977
commit f72fcb71cc
91 changed files with 39561 additions and 503 deletions
+177 -350
View File
@@ -1,399 +1,226 @@
# 双天线 RTK—3D 激光雷达严谨手眼标定
# LiDAR双天线 RTK 手眼标定
本仓库是当前建议提交和继续维护的正式版本。第二批 38 站高频 RTK 用于主要求解;第一批 38 站稀疏 RTK 只作辅助复核。Open3D GICP 和 small_gicp 分别生成雷达运动 (B),两套独立结果及跨后端共识结果均保留。
实际部署只读取 [`results/final_extrinsic_deployment.json`](results/final_extrinsic_deployment.json)。算法原始求解、残差、Bootstrap 和筛选记录保留在其他结果文件中,不应与部署文件混用。
## 标定总体流程
1. **准备静止站点数据**:每站车辆静止,保存一帧原始雷达点云和该站 RTK;程序只使用雷达传感器坐标系下的 `points_raw`
2. **由 RTK 生成车体运动 (A)**:将双天线 RTK 的位置、水平航向、杆臂和 heading offset 转为车体位姿 (T_{WB_i}),再计算
\[
A_{ij}=T_{WB_i}^{-1}T_{WB_j}.
\]
3. **由点云配准生成雷达运动 (B)**Open3D GICP 和 small_gicp 分别把第 j 站点云配准到第 i 站,得到
\[
B_{ij}=T_{L_iL_j}.
\]
4. **先验证和筛选 B,再求 X**:使用对称留出点、重叠率、留出点 RMSE、海森矩阵、正反向一致性、多初值稳定性、旋转共轭不变量和闭环误差检查 B。这个阶段不读取待求外参 X,也不使用 AX 残差筛选 B。
5. **建立跨后端共识 B**:只保留 Open3D 与 small_gicp 对同一运动对的 B 相差不超过 `0.05 m / 0.50°` 的边;最终使用 Open3D Bsmall_gicp 作为独立一致性门控。
6. **求解手眼外参 X**:固定 A 和通过筛选的 B 后,求解
\[
A_{ij}X=XB_{ij},\qquad X=T_{body\leftarrow lidar}.
\]
使用 Huber 鲁棒最小二乘、12 个初值、地面平面约束和 100 次 Bootstrap。
7. **结果复核**:第二批共识对用于主要求解和整体残差统计;第一批重新独立生成 B,只作为稀疏 RTK 辅助复核;3D 可视化分别比较模式 3 的 GICP B 与模式 4 的 (X^{-1}AX)。
8. **形成部署结果**:第二批跨后端共识结果给出完整的 `[x,y,z,roll,pitch,yaw]`;部署 JSON 直接采用该算法结果。。
### 验证逻辑
顺序固定为:
本仓库提供一套可从原始 Medulla 记录复现的静态站点标定流程,求解三维激光雷达到后轮轴中心车体系的外参
```text
先生成 B
-> 用不依赖 X 的点云与几何指标筛 B
-> 用另一个 GICP 后端复核同一 B
-> 固定共识 B
-> 最后才求 X
-> AX 残差只用于结果评价,不反过来选择 B
X = T_body_lidar
```
代码证据
- [`code/refine_pairs.py`](code/refine_pairs.py) 不读取外参文件,只使用留出点、正反向和旋转共轭不变量等指标;
- [`code/cross_backend_filter.py`](code/cross_backend_filter.py) 只比较两个后端的 B,不读取 X;
- [`code/rigorous_calibration.py`](code/rigorous_calibration.py) 在 B 集合固定后才执行 `calibrate` 求 X
- 第一批辅助复核的 B 也重新独立生成,且不使用 AX 残差筛选。
但这些检查仍不能把 B 变成绝对真值:重复结构、动态物体或错误局部最优仍可能让两个后端同时出错,所以必须结合模式 3 点云质量、留出集指标和多运动对整体统计判断。
## 最终部署外参
坐标变换定义:
\[
X=T_{body\leftarrow lidar}
\]
即将原始雷达点从雷达坐标系变换到以后轮轴中心为原点、X 前/Y 左/Z 上的车体坐标系。
部署值:
- 平移 `[x,y,z] = [1.297760, -0.000067, 0.720498] m`
- RPY `[roll,pitch,yaw] = [-0.785151, 1.202661, -0.835510] deg`
- 唯一部署文件:[`results/final_extrinsic_deployment.json`](results/final_extrinsic_deployment.json)
该值来自第二批 39 对跨后端共识、完整地面平面约束和 Bootstrap 稳定性检查。
算法在完整地面平面模型下的原始结果为:
- 平移 `[1.297760, -0.000067, 0.720498] m`
- RPY `[-0.785151, 1.202661, -0.835510] deg`
- 文件:[`results/final_extrinsic_recommended.json`](results/final_extrinsic_recommended.json)
`final_extrinsic_recommended.json` 保留完整求解、残差和 Bootstrap 记录,用于复现和审计;`final_extrinsic_deployment.json` 是唯一部署入口。
共识结果统计:
- 第二批 39 对 AX RMS`0.07985 m / 0.96118°`
- 第一批 22 对辅助复核:`0.06067 m / 1.00298°`
- 结果摘要:[`results/final_summary.json`](results/final_summary.json)
两套后端分别求得:
| 后端 | 平移 `[x,y,z]` m | RPY deg | 第二批 AX RMS |
|---|---|---|---|
| Open3D GICP | `[1.297883,-0.003100,0.721789]` | `[-0.757207,1.146042,-0.879781]` | `0.1002 m / 1.0115°` |
| small_gicp | `[1.299630,-0.003565,0.721886]` | `[-0.787156,1.141456,-0.935757]` | `0.1187 m / 1.1351°` |
两后端单独求出的 X 相差 1.81 mm、0.063°。共识求解的 100 次 Bootstrap 标准差 `[x,y,z,roll,pitch,yaw]` 为:
约定 `T_A_B` 将 B 系坐标变换到 A 系。对任意站点 i、j
```text
[0.00322 m, 0.00333 m, 0.00169 m, 0.0980°, 0.0746°, 0.1189°]
A_ij = T_W_Bi^-1 T_W_Bj # RTK 给出的车体相对运动
B_ij = T_Li_Lj # GICP 给出的雷达相对运动
A_ij X = X B_ij
```
这里的 Z Bootstrap 只描述当前地面模型条件下的内部稳定性,不代表机械高度的绝对精度
当前部署建议仍采用 [results/01_previous_two_batches/final_extrinsic_deployment.json](results/01_previous_two_batches/final_extrinsic_deployment.json)。data4 是一次独立重算,结果与部署值相差约 `1.592 cm / 0.234°`,但自身 AX 残差更高,因此只作为候选和稳定性证据,不自动替换部署值
重要限制:旋转 RMS 仍约 1°,所以这不是厘米级绝对精度认证。远距离点云仍可能看到角度错层;Bootstrap 也不包含 RTK 参考点、航向偏置或杆臂定义错误等系统误差。
### `code/` 文件职责
## 标定总流程
`code/` 中的 Python 脚本按“生成 B → 独立筛选 B → 求解 X → 汇总/诊断”的顺序组织。通常应通过 `run/` 下的 PowerShell 脚本调度,而不是手工跳过中间筛选步骤。
| 文件 | 职责 | 主要输入 → 主要输出 |
|---|---|---|
| [`rigorous_calibration.py`](code/rigorous_calibration.py) | **主求解器**,实现坐标变换、Open3D GICP/small_gicp 配准、质量计算和手眼优化。`ground``points_raw` 做 RANSAC 地面平面;`pairs` 将 RTK 车体轨迹构造成 A、将点云配准构造成 B,并输出海森矩阵、留出点、正反向和多初值质量报告;`calibrate` 用 Huber 鲁棒最小二乘、地面约束、多初值和 Bootstrap 求 `T_body_lidar``validate` 只计算给定 X 在独立 B 集上的 AX 残差。 | prepared 的 `frames_all`、车体位姿 CSV、B/地面文件 → `.npz` B 集、质量 JSON/CSV、外参 JSON、辅助复核 JSON。 |
| [`refine_pairs.py`](code/refine_pairs.py) | **第二级 B 精筛器**。按留出点重叠率/RMSE、旋转共轭不变量、正反向一致性剔除差的 B;不读取 X,也不用 AX 残差选 B。 | 初始 B `.npz` + 配准质量 JSON → `*_refined.npz` + `*.refinement.json` 审计记录。 |
| [`cross_backend_filter.py`](code/cross_backend_filter.py) | **跨后端一致性门控**。只保留 Open3D 与 small_gicp 对同一运动对的 B 差异不超过默认 `0.05 m / 0.50°` 的边;最终 B 本体采用 Open3D,small_gicp 仅作为独立门控。 | 两套 refined B `.npz``consensus/B_batch*_consensus.npz` + `*.consensus.json`。 |
| [`finalize_consensus.py`](code/finalize_consensus.py) | **发布共识解**。读取共识 B 求得的外参、第一批辅助复核和两单后端外参,写出带选择依据的 `final_extrinsic_recommended.json``final_summary.json`。 | 共识外参/复核 JSON、两后端外参 JSON → 推荐外参和最终摘要 JSON。 |
| [`summarize_results.py`](code/summarize_results.py) | **单后端阶段汇总器**。比较 Open3D 与 small_gicp 的外参差异、第二批拟合残差和第一批辅助复核,生成 `comparison_summary.json`;完整流程随后由 `finalize_consensus.py` 以共识解覆盖推荐结果。 | 两单后端外参、质量、辅助复核 JSON → 对比摘要 JSON 与阶段性推荐 JSON。 |
| [`scan_extrinsic_sensitivity.py`](code/scan_extrinsic_sensitivity.py) | **局部灵敏度诊断器**。对 X 在车体系左乘小的 roll/pitch/yaw 修正,比较某一对与全部 B 对的残差、改善/变差数量、地面指标和 Z 可观性。只生成扫描 JSON/CSV**绝不改写 X**。 | B `.npz`、外参 JSON、可选地面 CSV → `diagnostics/*_scan.json/.csv`。 |
| [`visualize_pair_3d.py`](code/visualize_pair_3d.py) | **交互式 3D 点云核查器**。蓝色为目标站 i、橙色为源站 j;按键 1/2/3/4 分别查看原始、RTK 初值 A、GICP B、外参预测 `X^-1 A X`,并打印 `B^-1(X^-1AX)` 的 cm/deg 增量。可选按键 5 显示临时 RPY 修正,修正不写入文件。 | 站点点云、B `.npz`、外参 JSON → Open3D 交互窗口与终端增量。 |
## 坐标与公式
\[
A_{ij}=T_{WB_i}^{-1}T_{WB_j},\qquad
B_{ij}=T_{L_iL_j},\qquad
A_{ij}X=XB_{ij}
\]
`B_ij` 把第 j 站雷达点变换到第 i 站雷达坐标系。程序只读取 `points_raw`,禁止使用已经变到车体坐标系的点。
RTK 轨迹使用 `body_poses_rear_gga_raw_rear_to_front.csv`,对应后天线 GGA 位置和双天线后到前方向。
地面约束采用完整平面变换:
\[
n_B=R_Xn_L,\qquad r_h=d_L-n_B^Tt_X-h_{body}
\]
不再使用旧近似式 `d_L-(t_z+h_body)`
## 详细求解流程
1. 第二批高频 RTK 生成 A;第一批稀疏 RTK 不参与主要求解。
2. 不使用手量外参作初值:生成 B 时 (X_0=I),所以 (B_0=A);求 X 也从单位变换和随机初值开始。
3. 两后端均采用三级粗到细:体素 0.30/0.15/0.08 m,对应距离 1.20/0.50/0.25 m。
4. 逐对检查对称留出点、海森矩阵、正反向一致性、多初值稳定性和 B 三角闭环。
5. 使用重叠率、留出点 RMSE 和旋转共轭不变量筛选 B;不使用 AX 残差筛选。
6. 两后端分别求 X;随后使用跨后端 B 一致性得到最终 39 对共识集合。
7. 使用 Huber 鲁棒最小二乘、12 个随机初值、完整地面约束和 100 次 Bootstrap 求算法 X。
8. 第一批重新独立生成 B,仅作辅助复核,不参与第二批求解。
9. 使用共识算法完整解形成精简部署 JSON,不覆盖算法原始结果。
## 目录
```text
code/
rigorous_calibration.py
refine_pairs.py
cross_backend_filter.py
summarize_results.py
finalize_consensus.py
scan_extrinsic_sensitivity.py
visualize_pair_3d.py
run/
run_all.ps1
run_consensus_finish.ps1
run_sensitivity_scan.ps1
view_open3d_result.ps1
view_small_gicp_result.ps1
tools/
frontlidar_dlog_export.py # 从单个原始 dlog 导出 LiDAR NPZ 与 RTK sidecar
prepare_station_dataset.py # 从逐站导出结果构建标定所需 prepared 数据集
results/
common/
open3d_gicp/
small_gicp/
consensus/
diagnostics/
final_extrinsic_deployment.json # 实际部署只读取这个文件
final_extrinsic_recommended.json # 算法原始结果及详细诊断
final_summary.json
```mermaid
flowchart LR
raw["原始站点 dlog / RTK、IMU rscap"] --> export["分别解析并统一到时间轴"]
export --> assoc["按每个 LiDAR 帧关联 RTK/IMU,导出 NPZ"]
assoc --> prep["每站选一帧,构建 RTK 车体位姿 A"]
prep --> b1["small_gicp 求 B"]
prep --> b2["Open3D GICP 求 B"]
b1 --> gate["与 X 无关的质量筛选及双后端一致性"]
b2 --> gate
gate --> solve["AX=XB + 地面约束求 X"]
solve --> check["残差、bootstrap、条件数、跨批复核和 3D 可视化"]
```
## 从原始数据到最终外参
流程有两个原始数据入口:
下面是从云盘中的**原始 dlog**到最终 LiDAR—车体外参的完整流程。第 2、3 步由本仓库的 tools/ 脚本完成;第 4 步及之后由 run/ 调度。不要把云盘中的原始数据直接传给 run_all.ps1。
- 旧式数据:LiDAR 和 `GPS-POST-Z` 位于每个站点 dlog 中,使用 `export_legacy_stations.ps1`
- 新式多传感器数据:LiDAR 位于逐站 dlogRTK 与 IMU 是独立 `.rscap`,使用 `export_multisensor_stations.ps1`。处理顺序是统一时间轴、分别解析、按 LiDAR 帧关联、导出 NPZ。
```text
原始静止站点 dlog(两批)
→ 逐站导出 LiDAR 原始点云 + RTK 旁路表(NPZ)
→ 逐站质量筛选、选取静止帧、重建 RTK 车体位姿(prepared
→ Open3D / small_gicp 分别求 B
→ X 无关的 B 精筛与跨后端共识
→ AX=XB + 地面平面约束求 X
→ 第一批辅助复核、3D 点云核查、确认部署 JSON
```
IMU 会在新式数据中原样解析并随 LiDAR 帧关联保存,但当前 LiDAR–RTK 外参求解不使用 IMU,也不做运动畸变校正,因为每一站采集点云时车辆静止。IMU 外参应使用单独的激励数据和专用标定流程求解。
### 1. 整理原始数据与标定元数据
## 三批数据的角色
每一个静止站点目录必须完整保留 LiDAR DObject 记录、对应 `.dorec` 文件和 RTK 日志。dlog 导出器要求站点目录至少具有:
| 数据 | 原始格式 | 站点 | RTK 情况 | 在本仓库中的角色 |
|---|---|---:|---|---|
| 第一批 | 逐站 dlog,内嵌 GPS-POST-Z | 38 | 约 10 秒一条,部分站仅 1–11 个有效样本 | 辅助复核,不承担主要求解 |
| 第二批 | 逐站 dlog,内嵌 GPS-POST-Z | 38 | 每站约 125–412 个有效样本,航向稳定 | 现部署外参的主要求解数据 |
| data4 | 逐站 LiDAR dlog + 独立 RTK/IMU rscap | 34 | 11,678 个 LiDAR 帧均成功关联 fixed RTK、heading 和 IMU | 独立重算与跨批比较 |
```text
<station_dlog>/
dobject/ # DObject 索引日志
dobject_recording/ # 与索引对应的 .dorec 二进制记录
GPS-POST-Z/ # RTK 文本记录;建议同时保留完整原始 N4 串口流
```
原始数据体积较大,不在 Git 仓库中。复现者应从云盘取得第一批、第二批或 data4 的原始目录,并在命令行传入路径。
两批数据要分开存放:第二批高频 RTK 用于主要求解,第一批仅用于辅助复核。与数据一同归档的标定元数据必须包括:LiDAR DObject 名称、时区、RTK 天线参考点、双天线基线方向、heading offset、天线到后轮轴中心的杆臂、车体系定义,以及 LiDAR/RTK/IMU 的时间基准。
## 环境
对于后续新录数据,RTK 原始记录应保留 `gnss_week``gnss_tow_ms`、GGA UTC、`pitch_deg`、基线长度、解算状态/类型、标准差、HDOP 和完整原文。PC 接收时间只能作为延迟诊断,不能替代 GNSS 测量时间。
- Windows PowerShell 5.1 或 PowerShell 7
- Python 3.10+
- `pip install -r requirements.txt`
- `small_gicp` 后端需要可导入 `small_gicp`Open3D 后端需要 `open3d`
### 2. 将每个原始站点 dlog 导出为 NPZ
所有脚本从自身位置推导仓库根目录。数据和输出路径均由参数传入,不依赖开发者电脑上的固定路径。
本仓库的 [tools/frontlidar_dlog_export.py](tools/frontlidar_dlog_export.py) 从每个静止站点的原始 dlog 导出 LiDAR 原始点云与 RTK sidecar。对每个站点分别执行;必须开启 RTK sidecar 与审计报告:
## 从原始数据开始复现
以下路径只表示格式,请替换为自己的目录。
### A. 第一批、第二批旧式 dlog
```powershell
$Repo = "D:\Outdoor Ackerman Cart Sensor Adaptation\LiDAR_RTK_Calibration_Rigorous_20260721"
$Exporter = "$Repo\tools\frontlidar_dlog_export.py"
$RawStation = "<某一个原始静止站点 dlog 目录>"
$StationOut = "<工作目录>\export\001"
$Repo = "D:\你的代码目录\calibration"
python $Exporter `
--dlog "$RawStation" `
--out "$StationOut" `
--object frontlidar `
--format npz `
--timezone +08:00 `
--rtk-sidecars `
--write-reports
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\export_legacy_stations.ps1" `
-DataRoot "D:\你的数据目录\batch2_raw" `
-OutputRoot "D:\你的输出目录\batch2_export"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\prepare_legacy_dataset.ps1" `
-ExportRoot "D:\你的输出目录\batch2_export" `
-Output "D:\你的输出目录\batch2_prepared" `
-HeadingOffsetDeg 21.226 `
-AntennaLever -0.320,-0.365,0.620 `
-ExpectedStations 38 `
-HeadingStdLimitDeg 0.5
```
导出后的站点目录应包含:
第一批采用同一导出方式,但应在导出命令显式添加 `-RtkMaxDtMs 15000`;因其 RTK 稀疏,准备阶段也不建议沿用 `0.5°` 的严格站内航向离散度阈值。上述 `21.226°` 和杆臂 `[-0.320,-0.365,0.620] m` 是本项目已有两批数据采用的配置,不是通用常数;换车或改变天线安装后必须重新确认。
```text
<StationOut>/
frames/ # 多帧 LiDAR NPZ;每帧保留 points_raw
rtk/ # gps_post_z.csv/jsonl/npz 等 RTK 旁路表
reports/manifest.csv # 每帧导出状态、时间及 RTK 匹配信息
reports/validation_report.json
reports/metadata.json
```
先检查每个站点的 `validation_report.json``manifest.csv``rtk/gps_post_z.csv`LiDAR payload 必须可读;候选帧必须存在;RTK 位置/航向有效;并记录 LiDAR—RTK 时间差。若新插件已开始输出 GNSS 周/周内时间与 pitch,导出器也必须同步扩展并写出这些字段;旧导出器只保留旧插件字段时,不能伪称已完成严格时间同步。
### 3. 从导出 NPZ 构建 prepared 静止站点数据
对每个批次,将所有站点导出目录汇总后执行以下预处理逻辑,生成供本仓库使用的 `prepared` 目录:
1. 每个站点只选择一帧**车辆已静止**且 RTK 质量合格的 LiDAR 帧;通常取有效候选帧中的中间帧,避免启动/停车边缘。
2. 仅保留 `position_valid=true``heading_valid=true`、Fix 为 4 或 5,且航向标准差满足项目门限的 RTK 样本;记录每站的样本数、时间跨度、航向圆标准差与被剔除原因。
3. 使用 GNSS 测量时间把位置、heading 和 pitch 配成同一时刻;将经纬高转换到同一 ENU 世界系;根据天线杆臂和双天线方向求后轮轴中心车体位姿 `T_WB`。双天线可提供 heading/pitch,但不能提供 rollroll 在后续 LiDAR—IMU 流程中由 IMU 补充。
4. 将选出的 LiDAR 帧原样复制到 `frames_all/`。标定程序从每帧的 `points_raw` 转为 LiDAR 传感器系 XYZ,不得使用已经变换到车体系的点。
5. 输出时间戳车体位姿 CSV。当前主求解器要求列为:
```text
time,x,y,z,qx,qy,qz,qw
```
最终目录必须类似:
```text
<BatchPrepared>/
frames_all/
station_01.npz
station_02.npz
...
body_poses_rear_gga_raw_rear_to_front.csv
station_summary.csv # 推荐保留:站点质量和选择原因
manifest.json # 推荐保留:参考点、杆臂、heading/pitch 定义与筛选配置
```
本仓库的 [tools/prepare_station_dataset.py](tools/prepare_station_dataset.py) 完成上述固定站点筛选、点云复制和车体位姿重建。它要求每个站点均已由上一步使用 --rtk-sidecars --write-reports 导出。下面的参数仅用于复现本仓库历史结果:lat/lon/h 被假定为后天线相位中心,raw_heading_deg 被假定为后天线指向前天线;车体系为后轮轴中心、X 前 Y 左 Z 上。
~~~powershell
$Repo = "D:\Outdoor Ackerman Cart Sensor Adaptation\LiDAR_RTK_Calibration_Rigorous_20260721"
$Prepare = "$Repo\tools\prepare_station_dataset.py"
# 第二批:主要求解数据。ExportBatch2 下为 38 个逐站导出目录。
python $Prepare --export-root "<ExportBatch2>" --output "<PreparedBatch2>" --expected-stations 38 --heading-std-limit-deg 0.5 --heading-offset-deg 21.226 --antenna-lever -0.320 -0.365 0.620 --pose-name rear_gga_raw_rear_to_front
# 第一批:辅助复核数据。若航向质量较低,不应设置过严的 heading 标准差阈值。
python $Prepare --export-root "<ExportBatch1>" --output "<PreparedBatch1>" --expected-stations 38 --heading-offset-deg 21.226 --antenna-lever -0.320 -0.365 0.620 --pose-name rear_gga_raw_rear_to_front
~~~
命令完成后检查 <PreparedBatch*>/station_summary.csv:每行均应有有效 RTK 样本,且没有超出设定的航向离散度。manifest.json 记录实际采用的杆臂、航向偏移、ENU 原点和输入站点;它应与原始数据一同归档。
不要把上述 21.226° 和 [-0.320,-0.365,0.620] m 当成通用常数:它们是本车、后天线、后天线到前天线航向定义、以及后轮轴中心车体原点的历史配置。换车、换参考天线、改变车体原点或改变 rawHeading 定义后,必须先复核并替换这些参数,再重建 A。
当前导出器可复现本仓库历史日志,但历史 RTK 插件只记录工控机接收时间,并未保存 GNSS 周/周内时间、双天线 pitch、质量标准差及完整原始串口流。因此它不能把旧数据宣称为严格 GNSS 时间同步。插件升级后,应先扩展导出器以保存新字段,再以 GNSS 测量时间重建位置/航向(及可用的 pitch)轨迹。
### 4. 安装标定依赖并运行第一阶段
### B. data4 式独立 RTK/IMU rscap
```powershell
$Repo = "D:\Outdoor Ackerman Cart Sensor Adaptation\LiDAR_RTK_Calibration_Rigorous_20260721"
python -m pip install -r "$Repo\requirements.txt"
python -c "import numpy, scipy, open3d, small_gicp; print('dependencies OK')"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\export_multisensor_stations.ps1" `
-DataRoot "D:\你的数据目录\data4_raw" `
-RtkCapture "D:\你的数据目录\captures\rtk.rscap" `
-ImuCapture "D:\你的数据目录\captures\imu.rscap" `
-OutputRoot "D:\你的输出目录\data4_export"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\prepare_multisensor_dataset.ps1" `
-CombinedRoot "D:\你的输出目录\data4_export\combined" `
-Output "D:\你的输出目录\data4_prepared" `
-HeadingOffsetDeg 21.226 `
-AntennaLever -0.320,-0.365,0.620 `
-ExpectedStations 34
```
### C. 运行标定
单批数据同时运行 small_gicp、Open3D GICP 和跨后端共识:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_single_dataset.ps1" `
-Prepared "D:\你的输出目录\data4_prepared" `
-OutputRoot "D:\你的输出目录\data4_calibration" `
-BodyHeight 0.2335
```
复现本仓库“第二批求解、第一批辅助复核”的历史流程:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_all.ps1" `
-Batch1Prepared "<第一批 prepared 目录>" `
-Batch2Prepared "<第二批 prepared 目录>"
-Batch1Prepared "D:\你的输出目录\batch1_prepared" `
-Batch2Prepared "D:\你的输出目录\batch2_prepared" `
-OutputRoot "D:\你的输出目录\two_batch_calibration"
```
`run_all.ps1` 完成:第二批地面拟合;Open3D 与 small_gicp 两套 B 生成和精筛;两套单后端 X 求解;第一批独立 B 与辅助复核;以及单后端对比摘要。第一阶段至少应生成:
`BodyHeight=0.2335 m` 是后轮轴中心离地高度,用于把雷达地面平面约束转换到车体原点;它不是雷达离地高度。换轮胎、胎压或车体载荷后应重新测量并评估不确定度。
## 如何判断结果
不能仅凭 `AX=XB` 残差或一张叠图宣称外参正确。至少同时检查:
1. B 的配准质量:收敛、有效对应点数/比例、RMSE、Hessian 信息矩阵特征值与条件数、small_gicp 与 Open3D 的相对运动一致性。
2. X 的可观性:加权雅可比条件数越接近 1 越均衡;极大值说明存在弱方向,但没有脱离尺度和数据分布的单一硬阈值。本次 data4 为 `7.44`,不是病态。
3. AX 残差分布:看 RMS、median、P90/P95、max 和逐对异常,而不是只看均值。
4. 重采样稳定性:bootstrap 的六自由度标准差及置信区间。
5. 跨批检查:同一 X 在独立批次的残差,以及两批独立求出的 X 之 SE(3) 差。
6. 3D 可视化:模式 3 的 B 与模式 4 的 `X^-1 A X` 是否都使相同墙面、杆件和地面重合。
当前没有把“增量小于某个值”当成绝对真值判据。工程筛查可先关注多数优质运动对是否约在厘米级到数厘米、亚度级;但阈值必须结合场景尺度、点云分辨率、RTK 质量和车辆运动幅度制定。模式 3 已错位时优先检查 B;模式 3 正常而模式 4 系统性错位时,再检查 A、坐标约定和 X。
## 结果摘要
| 结果 | 平移 xyz (m) | roll/pitch/yaw (deg) | AX 平移/旋转 RMS | 结论 |
|---|---|---|---|---|
| 部署值(第二批求解) | `[1.297760,-0.000067,0.720498]` | `[-0.785151,1.202661,-0.835510]` | `0.07985 m / 0.96118°`39 对 | 当前建议部署 |
| data4 独立候选 | `[1.300376,-0.001707,0.704877]` | `[-0.791892,1.393823,-0.970743]` | `0.11762 m / 1.24257°`,26 对 | 独立候选,不替换部署值 |
data4 候选相对部署值变化 `1.592 cm / 0.234°`,其中 z 低 `1.562 cm`。旧部署值作用于 data4 的残差约 `0.11953 m / 1.24836°`;data4 候选作用于历史第二批的残差约 `0.07931 m / 0.98926°`。两个 X 的跨批表现接近,当前数据不足以证明 data4 的较低 z 更接近真值。
## 仓库目录
```text
results/common/ground_planes_batch2.csv
results/open3d_gicp/B_batch2_refined.npz
results/small_gicp/B_batch2_refined.npz
results/open3d_gicp/extrinsic_batch2_refined.json
results/small_gicp/extrinsic_batch2_refined.json
results/comparison_summary.json
code/ 标定、配准筛选、共识、比较和可视化核心程序
tools/ 原始 dlog/rscap 解析、时间关联、NPZ 导出和数据准备
run/ 不含本机固定路径的 PowerShell 入口
results/ 历史两批、data4 与跨批比较三个结果目录
```
### 5. 构造跨后端共识并生成算法结果
完整复现流程和所有主要文件职责均在本 README;`run/README.md``tools/README.md` 和 [results/README.md](results/README.md) 只是目录内快速索引。
第一阶段成功后执行:
## 代码、工具和运行入口职责
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_consensus_finish.ps1"
```
根 README 是本仓库唯一的完整复现说明。`run/README.md``tools/README.md``results/README.md` 只作为进入对应目录时的快速索引,不承载另一套流程。
该步骤以 `0.05 m / 0.50°` 门限对同一运动对的 Open3D 与 small_gicp B 做一致性门控,用共识 B 重求第二批外参,并将第一批只用于辅助复核。主要输出为:
### code:标定核心
| 文件 | 职责 |
|---|---|
| `rigorous_calibration.py` | 核心 CLI。`ground` 拟合每站地面;`pairs` 用 small_gicp 或 Open3D GICP 求 B 和质量指标;`calibrate` 联合 AX=XB 与地面约束求 X`validate` 计算指定 X 的逐对残差。 |
| `refine_pairs.py` | 根据收敛、RMSE、对应关系、Hessian/信息矩阵和运动覆盖筛选 B;不读取 X,避免循环挑选。 |
| `cross_backend_filter.py` | 对齐 Open3D 与 small_gicp 的同一站点对,只保留两个后端相互一致的 B。 |
| `finalize_consensus.py` | 汇总历史两批的 consensus B、外参和第一批辅助复核。 |
| `summarize_results.py` | 汇总两个后端的外参、B 质量和跨批检查,生成推荐结果。 |
| `compare_extrinsics.py` | 在 SE(3) 上计算两套外参的严格相对平移和旋转差。 |
| `visualize_pair_3d.py` | 交互显示原始点云、RTK A、GICP B、`X^-1AX`,并打印 `B^-1(X^-1AX)` 数值增量。 |
| `scan_extrinsic_sensitivity.py` | 对 X 左乘小角度 roll/pitch/yaw 扰动,检查指定运动对的局部敏感方向。 |
### tools:原始数据到 prepared
| 文件 | 职责 |
|---|---|
| `frontlidar_dlog_export.py` | 从 Medulla dlog 导出传感器坐标系 `points_raw`;旧格式可同时匹配站内 GPS-POST-Z。 |
| `prepare_station_dataset.py` | 从旧式逐站导出中每站选择一帧,计算 yaw-only RTK 后轮轴位姿并生成 prepared。 |
| `build_multisensor_npz.py` | 将独立 LiDAR、RTK、IMU 统一到 LiDAR 帧索引并生成 combined NPZ。 |
| `prepare_multisensor_station_dataset.py` | 从 combined NPZ 选择每站静止帧,生成与旧流程相同的 prepared 接口。 |
| `rscap_v2/capture_format_v2.py` | 读取 rscap v2 文件头、原始记录块和文件尾。 |
| `rscap_v2/audit_capture_v2.py` | 审计 capture 完整性、时间范围和记录统计。 |
| `rscap_v2/parse_rtk_imu_v2.py` | 分别解析 RTK 与 IMU capture,输出 JSONL。 |
| `rscap_v2/pipeline_common*.py` | rscap 解析、时间处理和采集格式兼容的共用逻辑。 |
### run:推荐 PowerShell 入口
| 文件 | 职责 |
|---|---|
| `export_legacy_stations.ps1` | 批量导出 LiDAR 与 RTK 同在逐站 dlog 中的旧格式。 |
| `export_multisensor_stations.ps1` | 解析独立 RTK/IMU,导出逐站 LiDAR,并按 LiDAR 帧建立关联。 |
| `prepare_legacy_dataset.ps1` | 旧式导出结果转换为 prepared。 |
| `prepare_multisensor_dataset.ps1` | combined 多传感器结果转换为 prepared。 |
| `run_single_dataset.ps1` | 单批数据同时完成两个 GICP 后端、B 筛选、consensus 和 X 求解。 |
| `run_all.ps1` | 历史流程:第二批求解,第一批稀疏 RTK 数据作辅助复核。 |
| `run_consensus_finish.ps1` | 在已有两个后端 B 的基础上重新生成 consensus 和汇总结果。 |
| `run_sensitivity_scan.ps1` | 对历史运动对执行外参角度灵敏度扫描。 |
| `view_result.ps1` | 传入匹配的 frames、B 和 X,运行交互式 3D 可视化。 |
### 标定核心文件的数据关系
```text
results/consensus/B_batch2_consensus.npz
results/consensus/extrinsic_batch2_consensus.json
results/consensus/batch1_auxiliary_check.json
results/final_extrinsic_recommended.json
results/final_summary.json
原始 dlog/rscap
-> tools 导出和时间关联
-> prepared/{frames_all, body_poses_*.csv}
-> rigorous_calibration.py pairs 生成 A、B
-> refine_pairs.py 做与 X 无关的 B 筛选
-> cross_backend_filter.py 生成 consensus B
-> rigorous_calibration.py calibrate 联合 AX=XB 与地面约束求 X
-> validate / visualize_pair_3d.py 做数值和三维检查
```
### 6. 验证、可视化与部署确认
先检查 `results/final_summary.json`:当前历史数据的参考值是第二批共识 39 对、AX RMS 约 `0.07985 m / 0.96118°`。若明显偏离,应从第 2、3 步检查站点选择、RTK 时间/参考点/杆臂/heading 定义,而不是直接手调 X。
## 重要限制
再使用 3D 可视化检查:模式 3 为点云配准 B,模式 4 为外参预测 `X^-1AX`;终端打印 `B^-1(X^-1AX)` 的平移与旋转增量。只有 B 质量、全体运动对统计、地面约束和可视化均合理时,才可确认外参
- RTK 车体姿态当前是双天线 heading 构造的 yaw-only 轨迹;没有用 RTK pitch/roll 构造 A
- 新式解析器保存 IMU 与 RTK pitch 等原始字段,但当前手眼方程未融合 IMU。
- 静止站点法不估计 LiDAR–RTK 时间偏移;时间戳关联必须在导出阶段通过审计。
- 地面约束负责 roll、pitch 和 z 的补充可观性,不会独立求出另一套六自由度外参。
- 仓库归档的是结果和轻量 B 文件,不包含云盘中的原始点云数据。
`final_extrinsic_recommended.json` 是完整算法审计结果。`run_consensus_finish.ps1` 不会自动覆盖 `results/final_extrinsic_deployment.json`;部署 JSON 必须在人工确认坐标定义和高度约束后明确写入。
## prepared 阶段的快速重跑
## 专题说明
```powershell
$Repo = "D:\Outdoor Ackerman Cart Sensor Adaptation\LiDAR_RTK_Calibration_Rigorous_20260721"
python -m pip install -r "$Repo\requirements.txt"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_all.ps1"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_consensus_finish.ps1"
```
如果 prepared 数据移动了:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_all.ps1" -Batch1Prepared "你的第一批prepared目录" -Batch2Prepared "你的第二批prepared目录"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_consensus_finish.ps1"
```
重跑 `run_consensus_finish.ps1` 会重新生成算法原始结果,但不会自动生成或覆盖已经人工确认的 `final_extrinsic_deployment.json`
## 3D 可视化
Open3D 单后端结果:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\view_open3d_result.ps1" -PairIndex 0
```
small_gicp 单后端结果:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\view_small_gicp_result.ps1" -PairIndex 0
```
蓝色是目标站 i,橙色是源站 j。按键:`1` 原始;`2` RTK 初始 A`3` GICP B`4` 外参预测的 (X^{-1}AX);指定左乘 RPY 试验量后,`5` 为试验结果。
程序打印的差值为:
\[
B^{-1}(X^{-1}AX)
\]
其 xyz/RPY 分量位于站点 j 的雷达局部坐标系,不是屏幕坐标。模式 3 好而模式 4 差,说明 A/X 一致性不足;模式 3 本身差,说明该 B 不能用于判断外参。
共识运动对可直接指定部署外参:
```powershell
python "$Repo\code\visualize_pair_3d.py" --frames "C:\Users\admin\Documents\Codex\2026-07-15\wo\outputs\calibration_data1_20260720\prepared\frames_all" --pairs "$Repo\results\consensus\B_batch2_consensus.npz" --extrinsic "$Repo\results\final_extrinsic_deployment.json" --pair-index 0
```
局部灵敏度扫描和判断规则见 [`PAIR_DIAGNOSTICS.md`](PAIR_DIAGNOSTICS.md)。
## 关键质量文件
- [`results/open3d_gicp/B_batch2_quality.json`](results/open3d_gicp/B_batch2_quality.json):海森矩阵、留出点、正反向和多初值信息;
- [`results/open3d_gicp/B_batch2_refined.refinement.json`](results/open3d_gicp/B_batch2_refined.refinement.json):二级筛选原因;
- [`results/consensus/B_batch2_consensus.consensus.json`](results/consensus/B_batch2_consensus.consensus.json):跨后端一致性筛选;
- [`results/consensus/batch1_auxiliary_check.json`](results/consensus/batch1_auxiliary_check.json):第一批辅助复核;
- [`results/final_summary.json`](results/final_summary.json):算法结果摘要;
- [`results/diagnostics/`](results/diagnostics/):局部修正对单对及全体运动对的影响。
## 必须外部确认的假设
- `lat/lon/h` 确为后天线相位中心;
- `rawHeading` 确为后天线指向前天线;
- heading offset 数值和正负号正确;
任何一项变化,都必须更新相应约束;RTK 定义变化时需要重新生成 A 并重跑。
- [运动对诊断](PAIR_DIAGNOSTICS.md)
- [双后端共识筛选](CONSENSUS_SELECTION.md)
- [结果文件索引](results/README.md)