新增雷达到RTK直接手眼标定流程

This commit is contained in:
lichun.qu
2026-07-24 00:06:50 +08:00
parent f72fcb71cc
commit d2aae6177e
43 changed files with 9113 additions and 0 deletions
+24
View File
@@ -0,0 +1,24 @@
# Python
__pycache__/
*.py[cod]
.pytest_cache/
.venv/
venv/
# IDE / OS
.idea/
.vscode/
.DS_Store
Thumbs.db
# Raw data and generated outputs
data/raw/
work/
outputs/
*.rscap
*.dorec
*.log
# Large generated point clouds outside the archived reference result
**/frames/
**/frames_all/
+185
View File
@@ -0,0 +1,185 @@
# 双天线RTK—3D LiDAR直接手眼标定
本仓库从静态站点原始数据复现 `T_RTK_lidar`:把原始雷达坐标转换到RTK导航坐标系。它**不是** `base_link` 车体外参,也不会在求解阶段使用车体航向偏置或RTK到后轮轴的XY杆臂。
## 1. 输出坐标约定
统一约定 `T_A_B` 把B系点变换到A系:
```text
p_RTK = T_RTK_lidar · p_lidar
```
RTK导航系在本仓库中定义为:
- 原点:GGA位置参考点(通常为ANT1相位中心,必须结合接收机配置确认);
- X轴:`rawHeading`所表示的双天线基线在水平面的投影;
- Y轴:左;
- Z轴:上;
- ENU航向:`yaw = 90° - rawHeading`
- roll、pitch:当前轨迹中固定为0。
如果下游需要 `T_body_lidar`,必须另有经过确认的 `T_body_rtk`
```text
T_body_lidar = T_body_rtk · T_RTK_lidar
```
## 2. 算法流程
```text
逐站LiDAR dlog + RTK.rscap + IMU.rscap
→ 分别解析并保留原始字段
→ 以LiDAR帧时间为索引关联RTK/IMU,生成combined NPZ
→ 每站选择一帧静态点云,GGA转局部ENUrawHeading构造yaw-only RTK pose
→ Open3D GICP和small_gicp分别求 B_ij = T_Li_Lj
→ 留出点、Hessian、正反向、多初值和旋转共轭不变量筛选
→ 两后端共同认可的边形成consensus B
→ A_ij X = X B_ij + 地面法向/高度约束求 X = T_RTK_lidar
→ bootstrap、双后端差异、逐对残差和3D可视化检查
```
代码实际使用:
```text
A_ij = inv(T_W_Ri) · T_W_Rj = T_Ri_Rj
B_ij = T_Li_Lj # 将站点j点云变换到站点i
A_ij · X = X · B_ij
X = T_RTK_lidar
```
## 3. 原始数据目录
大体积数据不提交Git。`DataRoot`下每个站点必须是一个独立dlog目录,至少包含:
```text
raw_dataset/
├── stations/
│ ├── 001/
│ │ ├── dobject/
│ │ └── dobject_recording/
│ ├── 002/
│ └── ...
└── captures/
├── rtk.rscap
└── imu.rscap
```
每个站点应在车辆完全静止后记录点云;建议不少于30站,并包含充足的直行、左转、右转和大角度转向姿态变化。
## 4. 环境安装
已验证环境为Windows、PowerShell、Python 3.11。安装依赖:
```powershell
python -m pip install -r requirements.txt
```
依赖包括NumPy、SciPy、Open3D和small_gicp。若small_gicp没有对应Windows wheel,可在WSL2中安装后运行Python核心命令,或先只运行Open3D后端;完整共识流程需要两个后端都可用。
## 5. 从原始数据一键复现
在仓库根目录执行,路径由使用者通过参数传入,脚本内没有本机绝对路径:
```powershell
$Repo = (Resolve-Path ".").Path
$Raw = "E:\calibration_data\data4"
$Out = "E:\calibration_output\rtk_lidar"
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_full_pipeline.ps1" `
-DataRoot "$Raw\stations" `
-RtkCapture "$Raw\captures\rtk.rscap" `
-ImuCapture "$Raw\captures\imu.rscap" `
-OutputRoot $Out `
-RtkReferenceHeightAboveGroundM 0.8535 `
-ExpectedStations 34
```
主要输出:
```text
$Out/
├── exported/
│ ├── export/ # 各站LiDAR逐帧NPZ
│ ├── parsed/ # RTK/IMU JSONL
│ └── combined/ # 按LiDAR帧关联后的多传感器NPZ
├── prepared_rtk_direct/
│ ├── frames_all/ # 每站选中的静态帧
│ └── reference_poses_rtk_gga_raw_heading.csv
└── calibration/
├── open3d_gicp/
├── small_gicp/
├── consensus/
├── summary.json
└── final_T_RTK_lidar.json
```
若已经有`combined/`,可跳过原始导出:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\run_direct_rtk_lidar.ps1" `
-CombinedRoot "E:\calibration_output\exported\combined" `
-WorkRoot "E:\calibration_output\prepared_rtk_direct" `
-OutputRoot "E:\calibration_output\calibration" `
-RtkReferenceHeightAboveGroundM 0.8535 `
-ExpectedStations 34
```
## 6. 3D可视化
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$Repo\run\view_result.ps1" `
-Frames "$Out\prepared_rtk_direct\frames_all" `
-Pairs "$Out\calibration\consensus\B_consensus.npz" `
-Extrinsic "$Out\calibration\final_T_RTK_lidar.json" `
-PairIndex 0
```
窗口中:
- 蓝色:目标站点i;橙色:站点j
- `1`:原始点云;
- `2`RTK运动A直接作为初值;
- `3`GICP测得的B
- `4`:最终外参预测的 `X^-1 A X`
- `Q/Esc`:退出。
模式3和4应让同一墙面、立柱、路缘和地面尽量重合。终端同时打印 `B^-1(X^-1AX)` 的平移和旋转增量。应查看多对,不能只挑视觉效果最好的一对。
## 7. data4参考结果
仓库保留了精简参考产物,见[`results/reference_data4`](results/reference_data4/README.md)
```text
translation_m = [1.638179350, -0.240844799, 0.084481236]
RPY_deg_xyz = [-0.817167459, 1.323288119, -22.104163318]
站点:34
共识运动对:25
AX Translation RMS0.100207 m
AX Rotation RMS1.252794°
Weighted Jacobian condition7.739413
Open3D vs small_gicp0.003889 m / 0.188431°
```
唯一建议下游读取的参考结果是[`final_T_RTK_lidar.json`](results/reference_data4/final_T_RTK_lidar.json)。
## 8. z与精度限制
平面阿克曼运动不能独立观测z。参考结果使用34站地面平面和RTK参考点离地`0.8535 m`约束z;该高度必须量到实际GGA参考点/天线相位中心。更改参考高度后必须重新求解。
AX残差、Hessian/Jacobian条件数、bootstrap和双后端一致性只证明内部一致性,不能单独证明逐帧GT达到±3 cm。当前关联仍以LiDAR和串口主机接收时间为主;GNSS周/周内时间和IMU设备时间被保留,但没有联合估计时钟偏移与漂移。用于连续GT pose前,应补做严格设备时间同步和独立轨迹验证。
此外,代码无法单独证明GGA对应哪根物理天线、`rawHeading`是ANT1→ANT2还是ANT2→ANT1;必须用接收机配置、接线和现场运动实验确认。方向错误会导致RTK坐标系yaw相差约180°。
## 9. 仓库目录
| 目录 | 职责 |
|---|---|
| [`code/`](code/) | GICP、运动对质量评价、AX=XB求解、结果封装和3D可视化 |
| [`tools/`](tools/) | 原始dlog/rscap解析、按LiDAR帧关联及静态站点prepared生成 |
| [`run/`](run/) | PowerShell入口;所有数据和输出路径都通过参数传入 |
| [`results/reference_data4/`](results/reference_data4/) | 可提交Git的精简参考结果,不包含点云和本机过程目录 |
| `work/``outputs/` | 本地运行生成物,已由`.gitignore`排除 |
各代码文件职责见[`code/README.md`](code/README.md),命令索引见[`run/README.md`](run/README.md),工具说明见[`tools/README.md`](tools/README.md)。
@@ -0,0 +1,12 @@
# code目录
| 文件 | 职责 |
|---|---|
| `rigorous_calibration.py` | 核心CLI:读取静态点云/RTK位姿,Open3D或small_gicp求B,拟合地面,求解/验证AX=XB |
| `refine_pairs.py` | 不使用最终X,按留出点重叠率、RMSE、旋转共轭不变量和正反向一致性精筛运动对 |
| `cross_backend_filter.py` | 保留Open3D与small_gicp共同认可且变换接近的边;共识B数值取Open3D结果 |
| `finalize_direct_rtk_lidar.py` | 将三路求解结果封装为明确方向的`T_RTK_lidar`,选择consensus为最终结果 |
| `visualize_pair_3d.py` | 交互显示原始、RTK初值、GICP B和`X^-1AX`,并打印增量 |
| `compare_extrinsics.py` | 计算两套外参的SE(3)平移/旋转差异 |
核心约定:`A=T_Ri_Rj``B=T_Li_Lj``X=T_RTK_lidar`,满足`A X = X B`。点云配准以i为target、j为sourceB将j帧点云变换到i帧。
@@ -0,0 +1,43 @@
#!/usr/bin/env python3
"""Compare two homogeneous-extrinsic JSON files in parameter space and on SE(3)."""
import argparse
import json
from pathlib import Path
import numpy as np
from scipy.spatial.transform import Rotation
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--reference", type=Path, required=True)
parser.add_argument("--candidate", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
reference = json.loads(args.reference.read_text(encoding="utf-8-sig"))
candidate = json.loads(args.candidate.read_text(encoding="utf-8-sig"))
a = np.asarray(reference["matrix_4x4"], dtype=float)
b = np.asarray(candidate["matrix_4x4"], dtype=float)
delta = np.linalg.inv(a) @ b
result = {
"convention": "delta = inverse(reference) @ candidate",
"reference": str(args.reference.resolve()),
"candidate": str(args.candidate.resolve()),
"candidate_minus_reference_translation_xyz_m": (b[:3, 3] - a[:3, 3]).tolist(),
"candidate_minus_reference_rpy_xyz_deg": (
np.asarray(candidate["rotation_rpy_deg_xyz"], float)
- np.asarray(reference["rotation_rpy_deg_xyz"], float)
).tolist(),
"relative_translation_norm_m": float(np.linalg.norm(delta[:3, 3])),
"relative_rotation_deg": float(np.degrees(Rotation.from_matrix(delta[:3, :3]).magnitude())),
"relative_matrix_4x4": delta.tolist(),
}
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(result, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Keep common A/B edges on which Open3D and small_gicp agree, without using X."""
import argparse
import json
from pathlib import Path
import numpy as np
from scipy.spatial.transform import Rotation
def key(meta):
return int(meta[0]), int(meta[1])
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--open3d-pairs", required=True)
parser.add_argument("--small-pairs", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--audit")
parser.add_argument("--max-translation", type=float, default=0.05)
parser.add_argument("--max-rotation", type=float, default=0.50)
parser.add_argument("--min-pairs", type=int, default=25)
args = parser.parse_args()
with np.load(args.open3d_pairs, allow_pickle=False) as source:
open_a = np.asarray(source["A"], float)
open_b = np.asarray(source["B"], float)
open_meta = np.asarray(source["meta"], float)
station_times = np.asarray(source["station_times"])
rtk_dt = np.asarray(source["rtk_nearest_dt_s"])
with np.load(args.small_pairs, allow_pickle=False) as source:
small = {key(meta): np.asarray(b, float)
for meta, b in zip(source["meta"], source["B"])}
keep, audit = [], []
for meta, b_open in zip(open_meta, open_b):
edge = key(meta)
if edge not in small:
audit.append({"i": edge[0], "j": edge[1], "accepted": False,
"reason": "not_in_small_gicp_refined"})
keep.append(False)
continue
delta = np.linalg.inv(b_open) @ small[edge]
translation = float(np.linalg.norm(delta[:3, 3]))
rotation = float(np.rad2deg(Rotation.from_matrix(delta[:3, :3]).magnitude()))
accepted = translation <= args.max_translation and rotation <= args.max_rotation
keep.append(accepted)
audit.append({
"i": edge[0], "j": edge[1],
"open3d_small_translation_m": translation,
"open3d_small_rotation_deg": rotation,
"accepted": accepted,
"reason": "" if accepted else "backend_disagreement",
})
keep = np.asarray(keep, bool)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(
output, A=open_a[keep], B=open_b[keep], meta=open_meta[keep],
station_times=station_times, rtk_nearest_dt_s=rtk_dt,
backend=np.asarray("open3d_gicp_cross_backend_consensus"),
)
audit_path = Path(args.audit or output.with_suffix(".consensus.json"))
audit_path.write_text(json.dumps({
"selection_is_X_independent": True,
"B_source": "Open3D; small_gicp is used only as an agreement gate",
"max_translation_m": args.max_translation,
"max_rotation_deg": args.max_rotation,
"input_open3d_pairs": len(open_b),
"accepted_pairs": int(np.count_nonzero(keep)),
"pairs": audit,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if np.count_nonzero(keep) < args.min_pairs:
raise RuntimeError(f"only {np.count_nonzero(keep)} consensus pairs")
print(json.dumps({"accepted_pairs": int(np.count_nonzero(keep)),
"output": str(output.resolve()), "audit": str(audit_path.resolve())}, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,128 @@
from __future__ import annotations
import argparse
import json
import math
from pathlib import Path
import numpy as np
from scipy.spatial.transform import Rotation
def load(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8-sig"))
def write(path: Path, document: dict) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(document, ensure_ascii=False, indent=2), encoding="utf-8")
def inverse(t: np.ndarray) -> np.ndarray:
result = np.eye(4)
result[:3, :3] = t[:3, :3].T
result[:3, 3] = -result[:3, :3] @ t[:3, 3]
return result
def delta(a: np.ndarray, b: np.ndarray) -> dict:
d = inverse(a) @ b
return {
"translation_m": float(np.linalg.norm(d[:3, 3])),
"rotation_deg": float(np.linalg.norm(Rotation.from_matrix(d[:3, :3]).as_rotvec()) * 180.0 / math.pi),
"delta_matrix_4x4": d.tolist(),
}
def corrected(raw: dict, backend: str, reference_height: float) -> dict:
return {
"schema_version": 1,
"success": bool(raw["success"]),
"convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
"equation": "A_RTK_ij X = X B_LiDAR_ij",
"frames": {
"RTK": {
"origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
"x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
"y_axis": "left",
"z_axis": "up",
"yaw_enu_deg": "90 - rawHeadingDeg",
},
"LiDAR": "raw LiDAR sensor frame",
},
"backend": backend,
"measured_lidar_extrinsic_used_as_initial": False,
"body_heading_offset_used": False,
"body_antenna_lever_xy_used": False,
"translation_m": raw["translation_m"],
"rotation_rpy_deg_xyz": raw["rotation_rpy_deg_xyz"],
"quaternion_xyzw": raw["quaternion_xyzw"],
"matrix_4x4": raw["matrix_4x4"],
"quality": {
"stations": raw["estimation"]["stations"],
"pairs": raw["estimation"]["pairs"],
"residuals": raw["estimation"]["residuals"],
"weighted_jacobian_condition_number": raw["weighted_jacobian_condition_number"],
"linearized_one_sigma": raw["linearized_one_sigma"],
"bootstrap": raw["bootstrap"],
},
"z_constraint": {
"observable_from_planar_AX_XB": False,
"method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
"rtk_reference_height_above_ground_m": reference_height,
"warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion",
},
"important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification",
}
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--result-root", type=Path, required=True)
parser.add_argument("--reference-height", type=float, required=True)
args = parser.parse_args()
def solver_output(directory: str) -> Path:
raw = args.result_root / directory / "extrinsic_raw.json"
standard = args.result_root / directory / "extrinsic.json"
return raw if raw.exists() else standard
paths = {
"open3d_gicp": solver_output("open3d_gicp"),
"small_gicp": solver_output("small_gicp"),
"consensus": solver_output("consensus"),
}
docs = {}
for backend, path in paths.items():
document = corrected(load(path), backend, args.reference_height)
write(path.with_name("extrinsic_rtk_lidar.json"), document)
docs[backend] = document
open_t = np.asarray(docs["open3d_gicp"]["matrix_4x4"], float)
small_t = np.asarray(docs["small_gicp"]["matrix_4x4"], float)
final = dict(docs["consensus"])
final["selection"] = {
"recommended": True,
"reason": "Uses only motion pairs accepted independently by both Open3D GICP and small_gicp",
"open3d_vs_small_gicp": delta(open_t, small_t),
}
write(args.result_root / "final_T_RTK_lidar.json", final)
summary = {
"final": {
"translation_m": final["translation_m"],
"rotation_rpy_deg_xyz": final["rotation_rpy_deg_xyz"],
"pairs": final["quality"]["pairs"],
"translation_rms_m": final["quality"]["residuals"]["translation_m"]["rms"],
"rotation_rms_deg": final["quality"]["residuals"]["rotation_deg"]["rms"],
"condition_number": final["quality"]["weighted_jacobian_condition_number"],
},
"backend_difference": delta(open_t, small_t),
}
write(args.result_root / "summary.json", summary)
print(json.dumps(summary, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""X-independent second-stage filter for stationary A/B pairs."""
import argparse
import json
from pathlib import Path
import numpy as np
from rigorous_calibration import read_pairs, rotation_angle_deg
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--pairs", required=True)
parser.add_argument("--quality-json", required=True)
parser.add_argument("--output", required=True)
parser.add_argument("--audit")
parser.add_argument("--min-pairs", type=int, default=25)
parser.add_argument("--min-inlier-ratio", type=float, default=0.70)
parser.add_argument("--max-inlier-rmse", type=float, default=0.13)
parser.add_argument("--max-rotation-invariant-error", type=float, default=0.75)
parser.add_argument("--reverse-translation-tolerance", type=float, default=0.05)
parser.add_argument("--reverse-rotation-tolerance", type=float, default=0.50)
args = parser.parse_args()
a_array, b_array, meta, _ = read_pairs(args.pairs)
quality = json.loads(Path(args.quality_json).read_text(encoding="utf-8-sig"))
reports = {(int(item["i"]), int(item["j"])): item for item in quality["pairs"]}
keep, audit = [], []
for a_ij, b_ij, item_meta in zip(a_array, b_array, meta):
key = (int(item_meta[0]), int(item_meta[1]))
report = reports[key]
heldout = report["heldout_symmetric"]
reverse = report["forward_reverse"]
invariant = abs(rotation_angle_deg(a_ij[:3, :3]) - rotation_angle_deg(b_ij[:3, :3]))
reasons = []
if heldout["inlier_ratio"] < args.min_inlier_ratio:
reasons.append("overlap_ratio")
if heldout["inlier_rmse_m"] is None or heldout["inlier_rmse_m"] > args.max_inlier_rmse:
reasons.append("heldout_rmse")
if invariant > args.max_rotation_invariant_error:
reasons.append("rotation_conjugacy_invariant")
if reverse["translation_m"] > args.reverse_translation_tolerance:
reasons.append("forward_reverse_translation")
if reverse["rotation_deg"] > args.reverse_rotation_tolerance:
reasons.append("forward_reverse_rotation")
accepted = not reasons
keep.append(accepted)
audit.append({
"i": key[0], "j": key[1], "heldout_inlier_ratio": heldout["inlier_ratio"],
"heldout_inlier_rmse_m": heldout["inlier_rmse_m"],
"rotation_invariant_error_deg": invariant,
"reverse_translation_m": reverse["translation_m"],
"reverse_rotation_deg": reverse["rotation_deg"],
"accepted": accepted, "rejection_reasons": reasons,
})
keep = np.asarray(keep, bool)
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
with np.load(args.pairs, allow_pickle=False) as source:
np.savez_compressed(
output, A=a_array[keep], B=b_array[keep], meta=meta[keep],
station_times=np.asarray(source["station_times"]),
rtk_nearest_dt_s=np.asarray(source["rtk_nearest_dt_s"]),
backend=np.asarray(source["backend"]),
)
audit_path = Path(args.audit or output.with_suffix(".refinement.json"))
audit_path.write_text(json.dumps({
"selection_is_X_independent": True,
"criteria": {
"min_inlier_ratio": args.min_inlier_ratio,
"max_inlier_rmse_m": args.max_inlier_rmse,
"max_rotation_invariant_error_deg": args.max_rotation_invariant_error,
"reverse_translation_tolerance_m": args.reverse_translation_tolerance,
"reverse_rotation_tolerance_deg": args.reverse_rotation_tolerance,
},
"input_pairs": len(keep), "accepted_pairs": int(np.count_nonzero(keep)),
"pairs": audit,
}, ensure_ascii=False, indent=2), encoding="utf-8")
if np.count_nonzero(keep) < args.min_pairs:
raise RuntimeError(f"only {np.count_nonzero(keep)} refined pairs; need {args.min_pairs}")
print(json.dumps({"input_pairs": len(keep), "accepted_pairs": int(np.count_nonzero(keep)),
"output": str(output.resolve()), "audit": str(audit_path.resolve())}, indent=2))
if __name__ == "__main__":
main()
@@ -0,0 +1,771 @@
#!/usr/bin/env python3
"""Rigorous stationary LiDAR / reference-trajectory hand-eye calibration.
Convention: T_A_B maps points from frame B into frame A.
For this repository the reference frame is the RTK navigation frame.
X = T_RTK_lidar, A_ij = T_W_Ri^-1 T_W_Rj, B_ij = T_Li_Lj,
therefore A_ij X = X B_ij. Raw sensor-frame points_raw are used.
"""
from __future__ import annotations
import argparse
import csv
import json
import math
import time
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from scipy.optimize import least_squares
from scipy.spatial import cKDTree
def skew(v):
x, y, z = v
return np.array([[0.0, -z, y], [z, 0.0, -x], [-y, x, 0.0]])
def so3_exp(v):
angle = float(np.linalg.norm(v))
if angle < 1e-12:
return np.eye(3) + skew(v)
k = skew(np.asarray(v, float) / angle)
return np.eye(3) + math.sin(angle) * k + (1.0 - math.cos(angle)) * k @ k
def so3_log(rotation):
cosine = float(np.clip((np.trace(rotation) - 1.0) / 2.0, -1.0, 1.0))
angle = math.acos(cosine)
vee = np.array([
rotation[2, 1] - rotation[1, 2],
rotation[0, 2] - rotation[2, 0],
rotation[1, 0] - rotation[0, 1],
])
if angle < 1e-9:
return vee / 2.0
if abs(math.pi - angle) < 1e-5:
values, vectors = np.linalg.eigh((rotation + np.eye(3)) / 2.0)
return vectors[:, int(np.argmax(values))] * angle
return vee * angle / (2.0 * math.sin(angle))
def quat_to_rotation(q):
x, y, z, w = np.asarray(q, float) / np.linalg.norm(q)
return np.array([
[1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w)],
[2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w)],
[2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)],
])
def rotation_to_quat(rotation):
from scipy.spatial.transform import Rotation
return Rotation.from_matrix(rotation).as_quat()
def rpy_deg(rotation):
from scipy.spatial.transform import Rotation
return Rotation.from_matrix(rotation).as_euler("xyz", degrees=True).tolist()
def make_transform(translation, rotation):
transform = np.eye(4)
transform[:3, :3] = rotation
transform[:3, 3] = translation
return transform
def params_transform(params):
return make_transform(params[:3], so3_exp(params[3:]))
def inverse_transform(transform):
answer = np.eye(4)
answer[:3, :3] = transform[:3, :3].T
answer[:3, 3] = -answer[:3, :3] @ transform[:3, 3]
return answer
def transform_points(points, transform):
return points @ transform[:3, :3].T + transform[:3, 3]
def rotation_angle_deg(rotation):
return math.degrees(np.linalg.norm(so3_log(rotation)))
@dataclass
class PoseSeries:
time: np.ndarray
transforms: np.ndarray
def read_poses(path):
timestamps, transforms = [], []
with Path(path).open(encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
required = ("time", "x", "y", "z", "qx", "qy", "qz", "qw")
missing = [key for key in required if key not in (reader.fieldnames or [])]
if missing:
raise ValueError(f"{path}: missing pose fields {missing}")
for row in reader:
timestamps.append(float(row["time"]))
translation = np.array([float(row[k]) for k in ("x", "y", "z")])
quaternion = np.array([float(row[k]) for k in ("qx", "qy", "qz", "qw")])
transforms.append(make_transform(translation, quat_to_rotation(quaternion)))
order = np.argsort(timestamps)
return PoseSeries(np.asarray(timestamps)[order], np.asarray(transforms)[order])
def nearest_pose(series, timestamp):
index = int(np.argmin(np.abs(series.time - timestamp)))
return series.transforms[index], float(abs(series.time[index] - timestamp))
def npz_files(root):
files = sorted(Path(root).rglob("*.npz"))
if not files:
raise FileNotFoundError(f"no NPZ files under {root}")
return files
def load_npz_xyz(path, min_range=1.0, max_range=50.0):
with np.load(path, allow_pickle=False) as data:
if "points_raw" not in data:
raise ValueError(f"{path}: points_raw is required; cart-frame points are forbidden")
raw = np.asarray(data["points_raw"], dtype=np.float64)
timestamp = float(np.ravel(data["unix_time_ns"])[0]) / 1e9
counter = int(np.ravel(data["frame_counter"])[0])
distance = raw[:, 0] * 0.001
azimuth = np.deg2rad(raw[:, 1])
altitude = np.deg2rad(raw[:, 2])
valid = (
np.isfinite(distance + azimuth + altitude)
& (distance >= min_range)
& (distance <= max_range)
)
distance, azimuth, altitude = distance[valid], azimuth[valid], altitude[valid]
xyz = np.column_stack((
distance * np.cos(altitude) * np.cos(azimuth),
distance * np.cos(altitude) * np.sin(azimuth),
distance * np.sin(altitude),
))
return timestamp, counter, xyz
def load_stations(root, min_range, max_range, z_min=None, z_max=None):
stations = []
for path in npz_files(root):
timestamp, counter, xyz = load_npz_xyz(path, min_range, max_range)
if z_min is not None:
xyz = xyz[(xyz[:, 2] >= z_min) & (xyz[:, 2] <= z_max)]
stations.append((timestamp, counter, path, xyz))
stations.sort(key=lambda item: item[0])
return stations
def split_holdout(points, fraction, phase):
stride = max(int(round(1.0 / fraction)), 2)
index = np.arange(len(points))
holdout = ((index + phase) % stride) == 0
return points[~holdout], points[holdout]
def make_o3d_cloud(points, voxel):
import open3d as o3d
cloud = o3d.geometry.PointCloud()
cloud.points = o3d.utility.Vector3dVector(np.asarray(points, float))
return cloud.voxel_down_sample(voxel)
def align_open3d(target, source, initial, voxels, correspondences, iterations):
import open3d as o3d
registration = o3d.pipelines.registration
estimate = registration.TransformationEstimationForGeneralizedICP()
criteria = registration.ICPConvergenceCriteria(max_iteration=iterations)
transform, stages = np.asarray(initial, float), []
final_target = final_source = final_answer = None
started = time.perf_counter()
for voxel, correspondence in zip(voxels, correspondences):
target_cloud = make_o3d_cloud(target, voxel)
source_cloud = make_o3d_cloud(source, voxel)
answer = registration.registration_generalized_icp(
source_cloud, target_cloud, correspondence, transform, estimate, criteria
)
transform = np.asarray(answer.transformation, float)
stages.append({
"voxel_m": voxel,
"max_correspondence_m": correspondence,
"fitness": float(answer.fitness),
"inlier_rmse_m": float(answer.inlier_rmse),
"target_points": len(target_cloud.points),
"source_points": len(source_cloud.points),
})
final_target, final_source, final_answer = target_cloud, source_cloud, answer
information = registration.get_information_matrix_from_point_clouds(
final_source, final_target, correspondences[-1], transform
)
inliers = int(round(float(final_answer.fitness) * len(final_source.points)))
return {
"transform": transform,
"hessian": np.asarray(information, float),
"converged": None,
"iterations": None,
"num_inliers": inliers,
"objective": float(final_answer.inlier_rmse ** 2 * max(inliers, 1)),
"elapsed_sec": time.perf_counter() - started,
"stages": stages,
}
def align_small_gicp(target, source, initial, voxels, correspondences, iterations, threads):
import small_gicp
transform, stages, result = np.asarray(initial, float), [], None
started = time.perf_counter()
for voxel, correspondence in zip(voxels, correspondences):
result = small_gicp.align(
np.ascontiguousarray(target),
np.ascontiguousarray(source),
transform,
registration_type="GICP",
downsampling_resolution=voxel,
max_correspondence_distance=correspondence,
num_threads=threads,
max_iterations=iterations,
rotation_epsilon=math.radians(0.005),
translation_epsilon=0.0005,
verbose=False,
)
transform = np.asarray(result.T_target_source, float)
stages.append({
"voxel_m": voxel,
"max_correspondence_m": correspondence,
"converged": bool(result.converged),
"iterations": int(result.iterations),
"num_inliers": int(result.num_inliers),
"objective": float(result.error),
})
return {
"transform": transform,
"hessian": np.asarray(result.H, float),
"converged": bool(result.converged),
"iterations": int(result.iterations),
"num_inliers": int(result.num_inliers),
"objective": float(result.error),
"elapsed_sec": time.perf_counter() - started,
"stages": stages,
}
def align_backend(backend, target, source, initial, args):
if backend == "open3d":
return align_open3d(
target, source, initial, args.voxels, args.correspondences, args.iterations
)
return align_small_gicp(
target, source, initial, args.voxels, args.correspondences,
args.iterations, args.threads
)
def symmetric_heldout_metrics(target_fit, target_holdout, source_fit, source_holdout,
transform, threshold):
transformed_source_fit = transform_points(source_fit, transform)
transformed_source_holdout = transform_points(source_holdout, transform)
forward = cKDTree(target_fit).query(transformed_source_holdout, workers=-1)[0]
reverse = cKDTree(transformed_source_fit).query(target_holdout, workers=-1)[0]
distances = np.concatenate((forward, reverse))
inliers = distances[distances <= threshold]
return {
"evaluated": int(len(distances)),
"inliers": int(len(inliers)),
"inlier_ratio": float(len(inliers) / max(len(distances), 1)),
"inlier_rmse_m": float(np.sqrt(np.mean(inliers**2))) if len(inliers) else None,
"median_m": float(np.median(distances)),
"p90_m": float(np.quantile(distances, 0.90)),
"p95_m": float(np.quantile(distances, 0.95)),
}
def hessian_metrics(hessian, characteristic_length=10.0):
hessian = 0.5 * (np.asarray(hessian, float) + np.asarray(hessian, float).T)
scale = np.diag([1.0 / characteristic_length] * 3 + [1.0] * 3)
scaled = scale.T @ hessian @ scale
values, vectors = np.linalg.eigh(scaled)
largest = max(float(np.max(np.abs(values))), np.finfo(float).eps)
positive = values[values > largest * 1e-9]
condition = float(positive[-1] / positive[0]) if len(positive) else float("inf")
return {
"native_order": ["rx_rad", "ry_rad", "rz_rad", "tx_m", "ty_m", "tz_m"],
"scaled_eigenvalues": values.tolist(),
"effective_rank": int(len(positive)),
"scaled_condition_number": condition,
"weakest_scaled_direction": vectors[:, int(np.argmin(values))].tolist(),
}
def transform_difference(reference, candidate):
delta = inverse_transform(reference) @ candidate
return {
"translation_m": float(np.linalg.norm(delta[:3, 3])),
"rotation_deg": rotation_angle_deg(delta[:3, :3]),
}
def loop_metrics(transforms):
loops = []
for (i, j), b_ij in transforms.items():
for (j2, k), b_jk in transforms.items():
if j2 != j or (i, k) not in transforms:
continue
loops.append(transform_difference(transforms[(i, k)], b_ij @ b_jk))
if not loops:
return {"count": 0}
translation = np.array([item["translation_m"] for item in loops])
rotation = np.array([item["rotation_deg"] for item in loops])
return {
"count": len(loops),
"translation_rms_m": float(np.sqrt(np.mean(translation**2))),
"translation_p95_m": float(np.quantile(translation, 0.95)),
"rotation_rms_deg": float(np.sqrt(np.mean(rotation**2))),
"rotation_p95_deg": float(np.quantile(rotation, 0.95)),
}
def cmd_ground(args):
stations = load_stations(args.frames, args.min_range, args.max_range)
rows = []
for timestamp, counter, _, xyz in stations:
roi = xyz[(xyz[:, 2] >= args.z_min) & (xyz[:, 2] <= args.z_max)]
if len(roi) < args.min_inliers:
continue
cloud = make_o3d_cloud(roi, args.voxel)
plane, indexes = cloud.segment_plane(
args.distance_threshold, 3, args.ransac_iterations
)
normal = np.asarray(plane[:3], float)
norm = np.linalg.norm(normal)
normal, distance = normal / norm, float(plane[3] / norm)
if distance < 0:
normal, distance = -normal, -distance
points = np.asarray(cloud.points)[indexes]
rms = float(np.sqrt(np.mean((points @ normal + distance) ** 2)))
if len(indexes) >= args.min_inliers and rms <= args.max_rms:
rows.append([timestamp, *normal, distance, len(indexes), rms, counter])
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
with output.open("w", encoding="utf-8", newline="") as stream:
writer = csv.writer(stream)
writer.writerow(["time", "nx", "ny", "nz", "d", "inliers", "rms_m", "frame_counter"])
writer.writerows(rows)
print(json.dumps({"planes": len(rows), "output": str(output.resolve())}, indent=2))
def cmd_pairs(args):
if len(args.voxels) != len(args.correspondences):
raise ValueError("--voxels and --correspondences must have equal lengths")
stations = load_stations(
args.frames, args.min_range, args.max_range, args.z_min, args.z_max
)
reference = read_poses(args.reference_poses)
if len(stations) < args.min_stations:
raise ValueError(f"need at least {args.min_stations} stations, got {len(stations)}")
reference_poses, reference_dt = [], []
for timestamp, _, _, xyz in stations:
if len(xyz) < args.min_roi_points:
raise ValueError(f"station at {timestamp} has only {len(xyz)} ROI points")
pose, dt = nearest_pose(reference, timestamp + args.time_offset)
reference_poses.append(pose)
reference_dt.append(dt)
reference_poses = np.asarray(reference_poses)
split = [split_holdout(station[3], args.holdout_fraction, i)
for i, station in enumerate(stations)]
rng = np.random.default_rng(args.seed)
accepted_a, accepted_b, accepted_meta, reports = [], [], [], []
accepted_transforms = {}
for i in range(len(stations)):
for j in range(i + args.min_gap, min(len(stations), i + args.max_gap + 1)):
a_ij = inverse_transform(reference_poses[i]) @ reference_poses[j]
translation = float(np.linalg.norm(a_ij[:2, 3]))
rotation = rotation_angle_deg(a_ij[:3, :3])
if translation < args.min_translation and rotation < args.min_rotation:
continue
initial_b = a_ij.copy() # X0=I; no measured extrinsic.
target_fit, target_holdout = split[i]
source_fit, source_holdout = split[j]
forward = align_backend(args.backend, target_fit, source_fit, initial_b, args)
heldout = symmetric_heldout_metrics(
target_fit, target_holdout, source_fit, source_holdout,
forward["transform"], args.evaluation_distance
)
hessian = hessian_metrics(forward["hessian"])
reverse_answer = align_backend(
args.backend, source_fit, target_fit, inverse_transform(initial_b), args
)
reverse = transform_difference(
forward["transform"], inverse_transform(reverse_answer["transform"])
)
multistart = []
for _ in range(args.multistart):
perturb = np.r_[
rng.normal(0.0, args.multistart_translation_sigma, 3),
np.deg2rad(rng.normal(0.0, args.multistart_rotation_sigma, 3)),
]
candidate = align_backend(
args.backend, target_fit, source_fit,
params_transform(perturb) @ initial_b, args
)
multistart.append(transform_difference(forward["transform"], candidate["transform"]))
stable = [
item["translation_m"] <= args.multistart_translation_tolerance
and item["rotation_deg"] <= args.multistart_rotation_tolerance
for item in multistart
]
success_rate = float(np.mean(stable)) if stable else 1.0
reasons = []
if forward["converged"] is False:
reasons.append("backend_not_converged")
if heldout["inlier_ratio"] < args.min_inlier_ratio:
reasons.append("heldout_inlier_ratio")
if heldout["inlier_rmse_m"] is None or heldout["inlier_rmse_m"] > args.max_inlier_rmse:
reasons.append("heldout_inlier_rmse")
if hessian["effective_rank"] < 6:
reasons.append("hessian_rank")
if hessian["scaled_condition_number"] > args.max_hessian_condition:
reasons.append("hessian_condition")
if reverse["translation_m"] > args.reverse_translation_tolerance:
reasons.append("forward_reverse_translation")
if reverse["rotation_deg"] > args.reverse_rotation_tolerance:
reasons.append("forward_reverse_rotation")
if success_rate < args.min_multistart_success:
reasons.append("multistart_instability")
accepted = not reasons
report = {
"i": i, "j": j,
"lidar_time_i": stations[i][0], "lidar_time_j": stations[j][0],
"frame_counter_i": stations[i][1], "frame_counter_j": stations[j][1],
"rtk_translation_m": translation, "rtk_rotation_deg": rotation,
"nearest_rtk_dt_i_s": reference_dt[i], "nearest_rtk_dt_j_s": reference_dt[j],
"initial_B_source": "X0=identity; B0=A (no measured extrinsic)",
"B_ij_4x4": forward["transform"].tolist(),
"backend": args.backend, "backend_converged": forward["converged"],
"backend_iterations": forward["iterations"],
"backend_num_inliers": forward["num_inliers"],
"backend_objective": forward["objective"],
"backend_elapsed_sec": forward["elapsed_sec"],
"multiscale_stages": forward["stages"],
"heldout_symmetric": heldout, "hessian": hessian,
"forward_reverse": reverse,
"multistart": {"runs": len(multistart), "success_rate": success_rate,
"deltas": multistart},
"accepted": accepted, "rejection_reasons": reasons,
}
reports.append(report)
print(f"{args.backend} {i:02d}->{j:02d} rmse={heldout['inlier_rmse_m']} "
f"ratio={heldout['inlier_ratio']:.3f} accepted={accepted}")
if accepted:
accepted_a.append(a_ij)
accepted_b.append(forward["transform"])
accepted_meta.append([i, j, stations[i][0], stations[j][0]])
accepted_transforms[(i, j)] = forward["transform"]
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
np.savez_compressed(
output, A=np.asarray(accepted_a), B=np.asarray(accepted_b),
meta=np.asarray(accepted_meta),
station_times=np.asarray([item[0] for item in stations]),
rtk_nearest_dt_s=np.asarray(reference_dt), backend=np.asarray(args.backend),
)
quality = {
"schema_version": 2,
"backend": args.backend,
"transform_convention": "B_ij=T_Li_Lj maps station j points into station i",
"raw_point_field": "points_raw",
"measured_extrinsic_used_as_initial": False,
"stations": len(stations), "candidate_pairs": len(reports),
"accepted_pairs": len(accepted_a),
"parameters": vars(args),
"accepted_loop_closure": loop_metrics(accepted_transforms),
"pairs": reports,
}
quality["parameters"].pop("func", None)
quality_path = Path(args.quality_json or output.with_suffix(".quality.json"))
quality_path.write_text(json.dumps(quality, ensure_ascii=False, indent=2), encoding="utf-8")
csv_path = Path(args.quality_csv or output.with_suffix(".quality.csv"))
with csv_path.open("w", encoding="utf-8", newline="") as stream:
fields = ["i", "j", "rtk_translation_m", "rtk_rotation_deg",
"heldout_inlier_ratio", "heldout_inlier_rmse_m",
"hessian_rank", "hessian_condition", "reverse_translation_m",
"reverse_rotation_deg", "multistart_success_rate", "accepted",
"rejection_reasons"]
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
for item in reports:
writer.writerow({
"i": item["i"], "j": item["j"],
"rtk_translation_m": item["rtk_translation_m"],
"rtk_rotation_deg": item["rtk_rotation_deg"],
"heldout_inlier_ratio": item["heldout_symmetric"]["inlier_ratio"],
"heldout_inlier_rmse_m": item["heldout_symmetric"]["inlier_rmse_m"],
"hessian_rank": item["hessian"]["effective_rank"],
"hessian_condition": item["hessian"]["scaled_condition_number"],
"reverse_translation_m": item["forward_reverse"]["translation_m"],
"reverse_rotation_deg": item["forward_reverse"]["rotation_deg"],
"multistart_success_rate": item["multistart"]["success_rate"],
"accepted": item["accepted"],
"rejection_reasons": ";".join(item["rejection_reasons"]),
})
if len(accepted_a) < args.min_pairs:
raise RuntimeError(f"only {len(accepted_a)} accepted pairs; need {args.min_pairs}")
print(json.dumps({
"backend": args.backend, "stations": len(stations),
"candidate_pairs": len(reports), "accepted_pairs": len(accepted_a),
"output": str(output.resolve()), "quality_json": str(quality_path.resolve()),
"loop": quality["accepted_loop_closure"],
}, indent=2))
def read_planes(path):
planes = []
with Path(path).open(encoding="utf-8-sig", newline="") as stream:
for row in csv.DictReader(stream):
normal = np.array([float(row[k]) for k in ("nx", "ny", "nz")])
norm = np.linalg.norm(normal)
normal, distance = normal / norm, float(row["d"]) / norm
if distance < 0:
normal, distance = -normal, -distance
planes.append([*normal, distance])
return np.asarray(planes)
def read_pairs(path):
with np.load(path, allow_pickle=False) as data:
return (np.asarray(data["A"], float), np.asarray(data["B"], float),
np.asarray(data["meta"], float), len(data["station_times"]))
def calibration_residual(params, a_array, b_array, planes, args):
x = params_transform(params)
values = []
for a_ij, b_ij in zip(a_array, b_array):
error = inverse_transform(a_ij @ x) @ x @ b_ij
values.extend((error[:3, 3] / args.translation_sigma).tolist())
values.extend((so3_log(error[:3, :3]) / math.radians(args.rotation_sigma)).tolist())
body_up = np.array([0.0, 0.0, 1.0])
for plane in planes:
normal_body = x[:3, :3] @ plane[:3]
values.extend((np.cross(normal_body, body_up) / args.plane_normal_sigma).tolist())
body_distance = plane[3] - float(normal_body @ x[:3, 3])
values.append((body_distance - args.reference_height) / args.plane_height_sigma)
return np.asarray(values)
def pair_metrics(a_array, b_array, x):
translation, rotation, rows = [], [], []
for index, (a_ij, b_ij) in enumerate(zip(a_array, b_array)):
predicted = inverse_transform(x) @ a_ij @ x
delta = inverse_transform(b_ij) @ predicted
t = float(np.linalg.norm(delta[:3, 3]))
r = rotation_angle_deg(delta[:3, :3])
translation.append(t); rotation.append(r)
rows.append({"pair_index": index, "translation_m": t, "rotation_deg": r})
translation, rotation = np.asarray(translation), np.asarray(rotation)
def stats(values):
return {
"rms": float(np.sqrt(np.mean(values**2))),
"median": float(np.median(values)),
"p90": float(np.quantile(values, 0.90)),
"p95": float(np.quantile(values, 0.95)),
"max": float(np.max(values)),
}
return {"pairs": len(rows), "translation_m": stats(translation),
"rotation_deg": stats(rotation), "per_pair": rows}
def solve_extrinsic(a_array, b_array, planes, args):
rng = np.random.default_rng(args.seed)
starts = [np.zeros(6)]
for _ in range(args.solver_multistart - 1):
starts.append(np.r_[
rng.normal(0.0, args.start_translation_sigma, 3),
np.deg2rad(rng.normal(0.0, args.start_rotation_sigma, 3)),
])
candidates = []
lower = np.r_[[-5.0] * 3, [-math.pi] * 3]
upper = np.r_[[5.0] * 3, [math.pi] * 3]
for start in starts:
answer = least_squares(
calibration_residual, np.clip(start, lower, upper),
args=(a_array, b_array, planes, args),
bounds=(lower, upper), loss="huber", f_scale=1.5,
x_scale="jac", max_nfev=args.max_nfev,
)
candidates.append(answer)
best = min(candidates, key=lambda item: item.cost)
return best, candidates
def cmd_calibrate(args):
a_array, b_array, meta, stations = read_pairs(args.pairs)
planes = read_planes(args.ground_planes)
best, candidates = solve_extrinsic(a_array, b_array, planes, args)
x = params_transform(best.x)
residual = calibration_residual(best.x, a_array, b_array, planes, args)
absolute = np.abs(residual)
weights = np.ones_like(residual)
weights[absolute > 1.5] = 1.5 / absolute[absolute > 1.5]
weighted_jacobian = best.jac * np.sqrt(weights)[:, None]
singular = np.linalg.svd(weighted_jacobian, compute_uv=False)
condition = float(singular[0] / max(singular[-1], 1e-15))
dof = max(len(residual) - 6, 1)
covariance = np.linalg.pinv(weighted_jacobian.T @ weighted_jacobian) * float(
np.sum(weights * residual**2) / dof
)
sigma = np.sqrt(np.maximum(np.diag(covariance), 0.0))
candidate_summary = []
for item in candidates:
candidate_x = params_transform(item.x)
candidate_summary.append({
"cost": float(item.cost), "success": bool(item.success),
**transform_difference(x, candidate_x),
})
bootstrap = []
rng = np.random.default_rng(args.seed + 1)
for _ in range(args.bootstrap):
indexes = rng.integers(0, len(a_array), len(a_array))
answer = least_squares(
calibration_residual, best.x,
args=(a_array[indexes], b_array[indexes], planes, args),
loss="huber", f_scale=1.5, x_scale="jac", max_nfev=args.max_nfev,
)
bootstrap.append(np.r_[answer.x[:3], rpy_deg(so3_exp(answer.x[3:]))])
bootstrap = np.asarray(bootstrap)
result = {
"schema_version": 2,
"success": bool(best.success),
"message": best.message,
"convention": "T_reference_lidar maps raw LiDAR points into the supplied reference frame",
"equation": "A_ij X = X B_ij",
"measured_extrinsic_used_as_initial": False,
"translation_m": x[:3, 3].tolist(),
"rotation_rpy_deg_xyz": rpy_deg(x[:3, :3]),
"quaternion_xyzw": rotation_to_quat(x[:3, :3]).tolist(),
"matrix_4x4": x.tolist(),
"estimation": {"stations": stations, "pairs": len(a_array),
"residuals": pair_metrics(a_array, b_array, x)},
"ground": {
"planes": len(planes),
"reference_origin_height_above_ground_m": args.reference_height,
"formula": "d_lidar - (R_X n_lidar)^T t_X - reference_height",
},
"linearized_one_sigma": {
"translation_m": sigma[:3].tolist(),
"rotation_deg": np.rad2deg(sigma[3:]).tolist(),
"warning": "conditional local estimate; bootstrap is the primary stability check",
},
"weighted_jacobian_condition_number": condition,
"solver_multistart": {
"runs": len(candidates), "candidates_relative_to_best": candidate_summary,
},
"bootstrap": {
"runs": len(bootstrap),
"order": ["x_m", "y_m", "z_m", "roll_deg", "pitch_deg", "yaw_deg"],
"std": np.std(bootstrap, axis=0, ddof=1).tolist() if len(bootstrap) > 1 else None,
"p025": np.quantile(bootstrap, 0.025, axis=0).tolist() if len(bootstrap) else None,
"p975": np.quantile(bootstrap, 0.975, axis=0).tolist() if len(bootstrap) else None,
},
}
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(result, ensure_ascii=False, indent=2))
def cmd_validate(args):
result = json.loads(Path(args.extrinsic).read_text(encoding="utf-8-sig"))
x = np.asarray(result["matrix_4x4"], float)
a_array, b_array, meta, stations = read_pairs(args.pairs)
metrics = pair_metrics(a_array, b_array, x)
for row, pair_meta in zip(metrics["per_pair"], meta):
row.update({"i": int(pair_meta[0]), "j": int(pair_meta[1])})
report = {
"role": "auxiliary check only; first-batch RTK is sparse",
"blind_with_respect_to_X": True,
"note": "No AX residual was used to select these pairs",
"stations": stations, "metrics": metrics,
}
output = Path(args.output)
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(report, ensure_ascii=False, indent=2))
def build_parser():
parser = argparse.ArgumentParser(description=__doc__)
commands = parser.add_subparsers(dest="command", required=True)
ground = commands.add_parser("ground")
ground.add_argument("--frames", required=True); ground.add_argument("--output", required=True)
ground.add_argument("--min-range", type=float, default=1.0); ground.add_argument("--max-range", type=float, default=30.0)
ground.add_argument("--z-min", type=float, default=-1.4); ground.add_argument("--z-max", type=float, default=-0.4)
ground.add_argument("--voxel", type=float, default=0.08); ground.add_argument("--distance-threshold", type=float, default=0.025)
ground.add_argument("--ransac-iterations", type=int, default=500); ground.add_argument("--min-inliers", type=int, default=500)
ground.add_argument("--max-rms", type=float, default=0.025); ground.set_defaults(func=cmd_ground)
pairs = commands.add_parser("pairs")
pairs.add_argument("--backend", choices=["open3d", "small_gicp"], required=True)
pairs.add_argument("--frames", required=True)
pairs.add_argument("--reference-poses", "--body", dest="reference_poses", required=True)
pairs.add_argument("--output", required=True); pairs.add_argument("--quality-json"); pairs.add_argument("--quality-csv")
pairs.add_argument("--time-offset", type=float, default=0.0)
pairs.add_argument("--min-stations", type=int, default=30); pairs.add_argument("--min-pairs", type=int, default=25)
pairs.add_argument("--min-gap", type=int, default=1); pairs.add_argument("--max-gap", type=int, default=5)
pairs.add_argument("--min-translation", type=float, default=0.5); pairs.add_argument("--min-rotation", type=float, default=3.0)
pairs.add_argument("--min-range", type=float, default=2.0); pairs.add_argument("--max-range", type=float, default=50.0)
pairs.add_argument("--z-min", type=float, default=-0.60); pairs.add_argument("--z-max", type=float, default=5.0)
pairs.add_argument("--min-roi-points", type=int, default=1000)
pairs.add_argument("--holdout-fraction", type=float, default=0.20)
pairs.add_argument("--voxels", nargs="+", type=float, default=[0.30, 0.15, 0.08])
pairs.add_argument("--correspondences", nargs="+", type=float, default=[1.20, 0.50, 0.25])
pairs.add_argument("--iterations", type=int, default=60); pairs.add_argument("--threads", type=int, default=8)
pairs.add_argument("--evaluation-distance", type=float, default=0.25)
pairs.add_argument("--min-inlier-ratio", type=float, default=0.35); pairs.add_argument("--max-inlier-rmse", type=float, default=0.16)
pairs.add_argument("--max-hessian-condition", type=float, default=1e8)
pairs.add_argument("--reverse-translation-tolerance", type=float, default=0.08)
pairs.add_argument("--reverse-rotation-tolerance", type=float, default=0.50)
pairs.add_argument("--multistart", type=int, default=2)
pairs.add_argument("--multistart-translation-sigma", type=float, default=0.30)
pairs.add_argument("--multistart-rotation-sigma", type=float, default=3.0)
pairs.add_argument("--multistart-translation-tolerance", type=float, default=0.08)
pairs.add_argument("--multistart-rotation-tolerance", type=float, default=0.50)
pairs.add_argument("--min-multistart-success", type=float, default=0.50)
pairs.add_argument("--seed", type=int, default=20260721); pairs.set_defaults(func=cmd_pairs)
calibrate = commands.add_parser("calibrate")
calibrate.add_argument("--pairs", required=True); calibrate.add_argument("--ground-planes", required=True)
calibrate.add_argument("--output", required=True)
calibrate.add_argument("--translation-sigma", type=float, default=0.05)
calibrate.add_argument("--rotation-sigma", type=float, default=0.5)
calibrate.add_argument("--plane-normal-sigma", type=float, default=0.02)
calibrate.add_argument("--plane-height-sigma", type=float, default=0.03)
calibrate.add_argument("--reference-height", "--body-height", dest="reference_height", type=float, default=0.8535)
calibrate.add_argument("--solver-multistart", type=int, default=12)
calibrate.add_argument("--start-translation-sigma", type=float, default=1.0)
calibrate.add_argument("--start-rotation-sigma", type=float, default=20.0)
calibrate.add_argument("--bootstrap", type=int, default=100)
calibrate.add_argument("--max-nfev", type=int, default=1000)
calibrate.add_argument("--seed", type=int, default=20260721); calibrate.set_defaults(func=cmd_calibrate)
validate = commands.add_parser("validate")
validate.add_argument("--pairs", required=True); validate.add_argument("--extrinsic", required=True)
validate.add_argument("--output", required=True); validate.set_defaults(func=cmd_validate)
return parser
def main():
args = build_parser().parse_args()
args.func(args)
if __name__ == "__main__":
main()
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""Interactive 3D comparison of raw, RTK, GICP and hand-eye-predicted motion."""
import argparse
import json
import numpy as np
from scipy.spatial.transform import Rotation
from rigorous_calibration import (
inverse_transform, load_stations, rotation_angle_deg, rpy_deg, transform_points,
)
COLORS = {
"target": [0.10, 0.65, 1.00],
"source": [1.00, 0.35, 0.05],
}
def cloud(o3d, points, color, voxel):
item = o3d.geometry.PointCloud()
item.points = o3d.utility.Vector3dVector(points)
item = item.voxel_down_sample(voxel)
item.paint_uniform_color(color)
return item
def delta_components(reference, candidate):
"""Components of reference^-1*candidate, plus coordinate-invariant norms."""
delta = inverse_transform(reference) @ candidate
translation = np.asarray(delta[:3, 3], float)
return {
"translation_xyz_cm": (translation * 100.0).tolist(),
"translation_norm_cm": float(np.linalg.norm(translation) * 100.0),
"rotation_rpy_deg_xyz": rpy_deg(delta[:3, :3]),
"rotation_angle_deg": rotation_angle_deg(delta[:3, :3]),
}
def body_left_rpy(x, rpy_correction_deg):
correction = np.eye(4)
correction[:3, :3] = Rotation.from_euler(
"xyz", np.asarray(rpy_correction_deg, float), degrees=True
).as_matrix()
return correction @ x
def print_delta(name, reference, candidate):
item = delta_components(reference, candidate)
tx, ty, tz = item["translation_xyz_cm"]
roll, pitch, yaw = item["rotation_rpy_deg_xyz"]
print(
f"{name}: B^-1*motion translation xyz = "
f"[{tx:+.4f}, {ty:+.4f}, {tz:+.4f}] cm; "
f"rpy xyz = [{roll:+.4f}, {pitch:+.4f}, {yaw:+.4f}] deg; "
f"norm = {item['translation_norm_cm']:.4f} cm / "
f"{item['rotation_angle_deg']:.6f} deg"
)
return item
def main():
import open3d as o3d
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--frames", required=True)
parser.add_argument("--pairs", required=True)
parser.add_argument("--extrinsic", required=True)
parser.add_argument("--pair-index", type=int, default=0)
parser.add_argument("--voxel", type=float, default=0.10)
parser.add_argument(
"--left-rpy-deg", nargs=3, type=float, default=[0.0, 0.0, 0.0],
metavar=("ROLL", "PITCH", "YAW"),
help="optional body-frame left correction applied as DeltaR_body * X",
)
args = parser.parse_args()
stations = load_stations(args.frames, 1.0, 60.0)
with np.load(args.pairs, allow_pickle=False) as data:
if len(stations) != len(data["station_times"]):
raise ValueError(
f"frames contain {len(stations)} stations but pair file records "
f"{len(data['station_times'])}"
)
if not 0 <= args.pair_index < len(data["A"]):
raise IndexError(
f"pair-index {args.pair_index} outside [0,{len(data['A']) - 1}]"
)
a_ij = np.asarray(data["A"][args.pair_index], float)
b_gicp = np.asarray(data["B"][args.pair_index], float)
i, j = np.asarray(data["meta"][args.pair_index, :2], int)
with open(args.extrinsic, encoding="utf-8-sig") as stream:
result = json.load(stream)
x = np.asarray(result["matrix_4x4"], float)
b_calibrated = inverse_transform(x) @ a_ij @ x
transforms = {
"1 raw": np.eye(4),
"2 RTK initial (X0=I)": a_ij,
"3 GICP B": b_gicp,
"4 calibrated X^-1 A X": b_calibrated,
}
correction = np.asarray(args.left_rpy_deg, float)
if np.any(np.abs(correction) > 0.0):
x_test = body_left_rpy(x, correction)
transforms[
f"5 test body-left RPY {correction.tolist()} deg"
] = inverse_transform(x_test) @ a_ij @ x_test
target = stations[i][3]
source = stations[j][3]
print(f"pair_index={args.pair_index}, station {i} <- {j}")
print("blue = target station i; orange = source station j after selected transform")
print("keys: 1 raw | 2 RTK initial | 3 GICP | 4 calibrated | 5 test correction | Q/Esc exit")
print(
"IMPORTANT: delta xyz/rpy are components of B^-1*(X^-1*A*X), expressed "
"in station-j LiDAR coordinates; screen-left/right depends on the 3D camera view."
)
baseline = print_delta("mode 4 minus mode 3", b_gicp, b_calibrated)
roll, pitch, yaw = np.abs(baseline["rotation_rpy_deg_xyz"])
if max(roll, pitch) > max(0.10, 2.0 * yaw):
print("diagnosis: roll/pitch components dominate yaw; do not prioritize yaw tuning for this pair.")
tx, ty, tz = np.abs(baseline["translation_xyz_cm"])
if tz > max(tx, ty):
print("diagnosis: the largest translation component is relative Z, not lateral XY.")
body_up = np.array([0.0, 0.0, 1.0])
if np.linalg.norm(a_ij[:3, :3] @ body_up - body_up) < 1e-8:
print(
"observability: this A preserves the body Z axis, so body-left X.z "
"translation is unobservable from this pair; use ground/external height constraints."
)
if "5 test body-left RPY " + str(correction.tolist()) + " deg" in transforms:
print_delta("mode 5 minus mode 3", b_gicp, list(transforms.values())[-1])
viewer = o3d.visualization.VisualizerWithKeyCallback()
viewer.create_window("Rigorous LiDAR registration inspection - 3D", 1400, 900)
target_cloud = cloud(o3d, target, COLORS["target"], args.voxel)
source_cloud = cloud(o3d, source, COLORS["source"], args.voxel)
viewer.add_geometry(target_cloud)
viewer.add_geometry(source_cloud)
axes = o3d.geometry.TriangleMesh.create_coordinate_frame(size=1.0)
viewer.add_geometry(axes)
current = np.eye(4)
def select(name):
def callback(vis):
nonlocal current
desired = transforms[name]
source_cloud.transform(desired @ inverse_transform(current))
current = desired
vis.update_geometry(source_cloud)
if name == "3 GICP B":
print(f"{name}: reference registration B; delta = 0")
else:
print_delta(name + " minus mode 3", b_gicp, desired)
return False
return callback
for key, name in zip((ord("1"), ord("2"), ord("3"), ord("4"), ord("5")), transforms):
viewer.register_key_callback(key, select(name))
viewer.get_render_option().background_color = np.array([0.02, 0.02, 0.02])
viewer.get_render_option().point_size = 2.0
viewer.run()
viewer.destroy_window()
if __name__ == "__main__":
main()
@@ -0,0 +1,5 @@
# 数据说明
原始LiDAR dlog、RTK/IMU rscap、逐帧NPZ和prepared点云体积较大,不进入Git。请从项目云盘取得数据,并按根README中的目录示例放置;实际路径通过命令参数传入。
公开数据包应同时提供:采集日期、车辆/传感器安装版本、站点数量、ANT1/ANT2接线、rawHeading方向、RTK参考点离地高度及其测量方法。
@@ -0,0 +1,4 @@
numpy>=1.26
scipy>=1.11
open3d>=0.18
small-gicp
@@ -0,0 +1,5 @@
# results目录
`reference_data4/`是本仓库附带的精简参考结果。新的运行结果应写到仓库外目录或`outputs/`,不要覆盖参考结果。
参考结果保留最终矩阵、共识B、两后端精筛B、逐对CSV/筛选审计和地面平面;未保留原始点云、逐帧combined数据、冗长的初筛JSON和带本机绝对路径的过程文件。
@@ -0,0 +1,23 @@
# data4参考结果
推荐下游只读取`final_T_RTK_lidar.json`,其方向为:
```text
p_RTK = T_RTK_lidar · p_lidar
```
```text
translation_m = [1.638179350, -0.240844799, 0.084481236]
RPY_deg_xyz = [-0.817167459, 1.323288119, -22.104163318]
```
| 路径 | 内容 |
|---|---|
| `final_T_RTK_lidar.json` | 唯一推荐使用的最终外参 |
| `summary.json` | 最终残差、条件数和两后端差异摘要 |
| `common/ground_planes.csv` | 34站地面RANSAC平面 |
| `open3d_gicp/` | Open3D精筛B、精筛审计、逐对质量CSV和独立X |
| `small_gicp/` | small_gicp对应产物 |
| `consensus/` | 两后端共同认可的25对B、共识审计和最终X原始求解记录 |
`z=0.084481 m`依赖RTK参考点离地`0.8535 m`,不是平面AX=XB独立观测值。当前AX RMS约`0.100207 m / 1.252794°`,结果适合算法联调和继续验证,不应据此单独宣称逐帧GT达到±3 cm。
@@ -0,0 +1,35 @@
time,nx,ny,nz,d,inliers,rms_m,frame_counter
1784783825.357129,-0.0071009788712051635,-0.01614775434066578,0.9998444009588814,0.9449714797885561,2216,0.01353681235386204,382
1784783905.353819,0.0037577953662433442,-0.00645689216229629,0.999972093369405,0.9404093678203617,1992,0.01182484680356077,1182
1784783971.0503054,-0.021571557237587136,-0.004187980739348835,0.9997585352152151,0.9464426916222599,1922,0.01241302113439391,1839
1784784059.7468228,-0.02328381206588687,0.004386003115594592,0.999719274132669,0.9506398743758987,1825,0.013207042662497559,2726
1784784149.2434597,-0.03034651356298797,8.407602826756324e-05,0.9995394349628197,0.9287260086761917,1631,0.012431422856007433,3621
1784784224.2408776,-0.02735108660242708,0.010336463557273514,0.9995724463903534,0.8697042041945898,1745,0.012010699567274814,4371
1784784301.6372502,-0.008085199791897242,-0.01261499998640764,0.9998877393586082,0.9555128377061561,2057,0.011385954851802133,5145
1784784387.733771,-0.007167201772462397,-0.008280272080485561,0.9999400323584541,0.9272403036781977,2190,0.012588169113362788,6006
1784784474.9314597,-0.015033673776128801,-0.05115510005163043,0.9985775605287256,0.9072885818985176,1852,0.011643717831315507,6878
1784784549.4274275,-0.03189811408163763,0.004490776571011993,0.999481036960594,0.9463330979345341,1638,0.013973864979699106,7623
1784784614.7244046,-0.027669126419476022,-0.013618714393779237,0.999524361914928,0.9348654978703125,1794,0.011102769699524444,8276
1784784682.921899,-0.022151829941254066,-0.026156017173182243,0.9994124069651578,0.9227652769218206,1638,0.01266204532799032,8958
1784784758.8187964,-0.022601789108727538,-0.030665670278286643,0.999274124450077,0.9305425654631599,1795,0.013340493954266352,9717
1784784836.0155501,-0.017598498968453644,-0.02647415434199472,0.999494578267403,0.961769336191532,2015,0.01382548272951725,10489
1784784921.1126208,-0.021874728045639568,-0.019005922983034846,0.9995800474021539,0.9186945373736978,1808,0.01319739563436461,11340
1784784992.709947,-0.019622211270580107,-0.02528822634559132,0.9994876059427386,0.9414876680920201,2158,0.013280256398986076,12056
1784785067.6067727,-0.031060743296391496,-0.010214597374617485,0.9994653031628212,0.9402571806911639,1758,0.013392641298608525,12805
1784785215.9006598,-0.023673459396853343,-0.027330465291762113,0.9993460926961797,0.9143984233881569,2134,0.012198902426752438,14288
1784785296.4990919,-0.013579953767407775,-0.025158411654770292,0.9995912360453568,0.929929365058787,2445,0.012790980094197171,15094
1784785363.1952267,-0.0316919344947604,-0.008177115456525313,0.9994642345130668,0.9579125765731408,1861,0.012743464679231025,15761
1784785434.592462,-0.022678088603046032,0.004435617789851151,0.9997329791459992,0.9537383917106543,1853,0.013464094518301148,16475
1784785506.389296,-0.0273694753756875,-0.01771941378886425,0.9994683257575693,0.9327361226688458,1749,0.011502338837958854,17193
1784785587.5863533,-0.034617559115875766,0.0015843237885758451,0.999399376885433,0.9191959537091571,1744,0.013401267879037225,18005
1784785681.9825997,-0.033298152875437845,-0.018946770164947627,0.9992658569747096,0.9446967789786688,2037,0.012804059875312601,18949
1784785815.4779446,-0.006681441650905292,-0.023720354219030532,0.9996963054514052,0.964084392350492,2080,0.013159652224503205,20284
1784785891.9768085,-0.026044255070688936,0.004021590005713901,0.9996527014876911,0.934003424141447,1610,0.012619787010493816,21049
1784785967.8726046,-0.026983516555153,-0.01791249491380091,0.9994753785663161,0.9339463871372334,1482,0.013831424226303278,21808
1784786031.5701303,-0.028810092762610772,-0.015551490666087386,0.9994639211562729,0.938325903591574,1592,0.013415706854842701,22445
1784786087.9670725,-0.026424364888569394,-0.014575043111831797,0.9995445568150146,0.9345909622822591,1466,0.013090809334738121,23009
1784786160.8647907,-0.032665212336793446,0.0597663130328534,0.9976777895340014,1.0611447790248285,1511,0.012251618222434443,23738
1784786252.6621523,-0.03732336904542465,-0.020702346452411244,0.9990887743211128,0.9478808317179221,1719,0.012520822005912273,24656
1784786319.6581354,-0.025461816426307887,-0.02602901616210242,0.9993368732424047,0.9466783627035876,1589,0.013468160971926036,25326
1784786396.7558627,-0.025295174442520576,-0.02343833470627969,0.9994052224278793,0.9710592140122102,1321,0.012895976784738191,26097
1784786557.4492514,-0.014426534652625146,-0.00926546906583815,0.9998530022862894,0.935013905231254,1251,0.012477706451163199,27704
1 time nx ny nz d inliers rms_m frame_counter
2 1784783825.357129 -0.0071009788712051635 -0.01614775434066578 0.9998444009588814 0.9449714797885561 2216 0.01353681235386204 382
3 1784783905.353819 0.0037577953662433442 -0.00645689216229629 0.999972093369405 0.9404093678203617 1992 0.01182484680356077 1182
4 1784783971.0503054 -0.021571557237587136 -0.004187980739348835 0.9997585352152151 0.9464426916222599 1922 0.01241302113439391 1839
5 1784784059.7468228 -0.02328381206588687 0.004386003115594592 0.999719274132669 0.9506398743758987 1825 0.013207042662497559 2726
6 1784784149.2434597 -0.03034651356298797 8.407602826756324e-05 0.9995394349628197 0.9287260086761917 1631 0.012431422856007433 3621
7 1784784224.2408776 -0.02735108660242708 0.010336463557273514 0.9995724463903534 0.8697042041945898 1745 0.012010699567274814 4371
8 1784784301.6372502 -0.008085199791897242 -0.01261499998640764 0.9998877393586082 0.9555128377061561 2057 0.011385954851802133 5145
9 1784784387.733771 -0.007167201772462397 -0.008280272080485561 0.9999400323584541 0.9272403036781977 2190 0.012588169113362788 6006
10 1784784474.9314597 -0.015033673776128801 -0.05115510005163043 0.9985775605287256 0.9072885818985176 1852 0.011643717831315507 6878
11 1784784549.4274275 -0.03189811408163763 0.004490776571011993 0.999481036960594 0.9463330979345341 1638 0.013973864979699106 7623
12 1784784614.7244046 -0.027669126419476022 -0.013618714393779237 0.999524361914928 0.9348654978703125 1794 0.011102769699524444 8276
13 1784784682.921899 -0.022151829941254066 -0.026156017173182243 0.9994124069651578 0.9227652769218206 1638 0.01266204532799032 8958
14 1784784758.8187964 -0.022601789108727538 -0.030665670278286643 0.999274124450077 0.9305425654631599 1795 0.013340493954266352 9717
15 1784784836.0155501 -0.017598498968453644 -0.02647415434199472 0.999494578267403 0.961769336191532 2015 0.01382548272951725 10489
16 1784784921.1126208 -0.021874728045639568 -0.019005922983034846 0.9995800474021539 0.9186945373736978 1808 0.01319739563436461 11340
17 1784784992.709947 -0.019622211270580107 -0.02528822634559132 0.9994876059427386 0.9414876680920201 2158 0.013280256398986076 12056
18 1784785067.6067727 -0.031060743296391496 -0.010214597374617485 0.9994653031628212 0.9402571806911639 1758 0.013392641298608525 12805
19 1784785215.9006598 -0.023673459396853343 -0.027330465291762113 0.9993460926961797 0.9143984233881569 2134 0.012198902426752438 14288
20 1784785296.4990919 -0.013579953767407775 -0.025158411654770292 0.9995912360453568 0.929929365058787 2445 0.012790980094197171 15094
21 1784785363.1952267 -0.0316919344947604 -0.008177115456525313 0.9994642345130668 0.9579125765731408 1861 0.012743464679231025 15761
22 1784785434.592462 -0.022678088603046032 0.004435617789851151 0.9997329791459992 0.9537383917106543 1853 0.013464094518301148 16475
23 1784785506.389296 -0.0273694753756875 -0.01771941378886425 0.9994683257575693 0.9327361226688458 1749 0.011502338837958854 17193
24 1784785587.5863533 -0.034617559115875766 0.0015843237885758451 0.999399376885433 0.9191959537091571 1744 0.013401267879037225 18005
25 1784785681.9825997 -0.033298152875437845 -0.018946770164947627 0.9992658569747096 0.9446967789786688 2037 0.012804059875312601 18949
26 1784785815.4779446 -0.006681441650905292 -0.023720354219030532 0.9996963054514052 0.964084392350492 2080 0.013159652224503205 20284
27 1784785891.9768085 -0.026044255070688936 0.004021590005713901 0.9996527014876911 0.934003424141447 1610 0.012619787010493816 21049
28 1784785967.8726046 -0.026983516555153 -0.01791249491380091 0.9994753785663161 0.9339463871372334 1482 0.013831424226303278 21808
29 1784786031.5701303 -0.028810092762610772 -0.015551490666087386 0.9994639211562729 0.938325903591574 1592 0.013415706854842701 22445
30 1784786087.9670725 -0.026424364888569394 -0.014575043111831797 0.9995445568150146 0.9345909622822591 1466 0.013090809334738121 23009
31 1784786160.8647907 -0.032665212336793446 0.0597663130328534 0.9976777895340014 1.0611447790248285 1511 0.012251618222434443 23738
32 1784786252.6621523 -0.03732336904542465 -0.020702346452411244 0.9990887743211128 0.9478808317179221 1719 0.012520822005912273 24656
33 1784786319.6581354 -0.025461816426307887 -0.02602901616210242 0.9993368732424047 0.9466783627035876 1589 0.013468160971926036 25326
34 1784786396.7558627 -0.025295174442520576 -0.02343833470627969 0.9994052224278793 0.9710592140122102 1321 0.012895976784738191 26097
35 1784786557.4492514 -0.014426534652625146 -0.00926546906583815 0.9998530022862894 0.935013905231254 1251 0.012477706451163199 27704
@@ -0,0 +1,332 @@
{
"selection_is_X_independent": true,
"B_source": "Open3D; small_gicp is used only as an agreement gate",
"max_translation_m": 0.05,
"max_rotation_deg": 0.5,
"input_open3d_pairs": 41,
"accepted_pairs": 25,
"pairs": [
{
"i": 0,
"j": 1,
"open3d_small_translation_m": 0.014276780914058016,
"open3d_small_rotation_deg": 0.6136066194510507,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 0,
"j": 2,
"open3d_small_translation_m": 0.019952450418738,
"open3d_small_rotation_deg": 0.14609270025586996,
"accepted": true,
"reason": ""
},
{
"i": 1,
"j": 2,
"accepted": false,
"reason": "not_in_small_gicp_refined"
},
{
"i": 2,
"j": 3,
"open3d_small_translation_m": 0.022407292900448784,
"open3d_small_rotation_deg": 0.1674801169908669,
"accepted": true,
"reason": ""
},
{
"i": 2,
"j": 5,
"open3d_small_translation_m": 0.006161707315193906,
"open3d_small_rotation_deg": 0.13760639079130288,
"accepted": true,
"reason": ""
},
{
"i": 3,
"j": 5,
"open3d_small_translation_m": 0.039798937040509075,
"open3d_small_rotation_deg": 0.21317157512260662,
"accepted": true,
"reason": ""
},
{
"i": 3,
"j": 6,
"open3d_small_translation_m": 0.012411893826145848,
"open3d_small_rotation_deg": 0.6409039547122766,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 6,
"j": 7,
"open3d_small_translation_m": 0.008481323658875535,
"open3d_small_rotation_deg": 0.21102463812659789,
"accepted": true,
"reason": ""
},
{
"i": 6,
"j": 8,
"open3d_small_translation_m": 0.0392892670228761,
"open3d_small_rotation_deg": 0.3638880951335145,
"accepted": true,
"reason": ""
},
{
"i": 7,
"j": 8,
"open3d_small_translation_m": 0.008121615288997118,
"open3d_small_rotation_deg": 0.6269514061788293,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 10,
"j": 11,
"open3d_small_translation_m": 0.021311366483595485,
"open3d_small_rotation_deg": 0.5895827931090589,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 12,
"j": 14,
"open3d_small_translation_m": 0.02159358315642783,
"open3d_small_rotation_deg": 0.2828054398005336,
"accepted": true,
"reason": ""
},
{
"i": 12,
"j": 15,
"open3d_small_translation_m": 0.036801934207227605,
"open3d_small_rotation_deg": 0.5608491395108458,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 13,
"j": 15,
"open3d_small_translation_m": 0.02340982602704092,
"open3d_small_rotation_deg": 0.5560478634176542,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 13,
"j": 16,
"open3d_small_translation_m": 0.027602744462629097,
"open3d_small_rotation_deg": 0.2529072067309812,
"accepted": true,
"reason": ""
},
{
"i": 15,
"j": 16,
"open3d_small_translation_m": 0.02090824470779122,
"open3d_small_rotation_deg": 0.034004621265071464,
"accepted": true,
"reason": ""
},
{
"i": 15,
"j": 17,
"open3d_small_translation_m": 0.04186146133502706,
"open3d_small_rotation_deg": 0.13358624916951445,
"accepted": true,
"reason": ""
},
{
"i": 15,
"j": 18,
"open3d_small_translation_m": 0.01461017711105615,
"open3d_small_rotation_deg": 0.2537851025085552,
"accepted": true,
"reason": ""
},
{
"i": 16,
"j": 19,
"open3d_small_translation_m": 0.012703728170379172,
"open3d_small_rotation_deg": 0.36607447371235324,
"accepted": true,
"reason": ""
},
{
"i": 17,
"j": 18,
"open3d_small_translation_m": 0.030547382773647488,
"open3d_small_rotation_deg": 0.6293030402470645,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 21,
"j": 22,
"open3d_small_translation_m": 0.008438605953756149,
"open3d_small_rotation_deg": 0.15351222882817242,
"accepted": true,
"reason": ""
},
{
"i": 21,
"j": 23,
"open3d_small_translation_m": 0.0184790436482599,
"open3d_small_rotation_deg": 0.5781132894399331,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 21,
"j": 24,
"open3d_small_translation_m": 0.03128464242771278,
"open3d_small_rotation_deg": 0.3921882672789548,
"accepted": true,
"reason": ""
},
{
"i": 22,
"j": 23,
"open3d_small_translation_m": 0.01156826970365892,
"open3d_small_rotation_deg": 0.061357836393243825,
"accepted": true,
"reason": ""
},
{
"i": 22,
"j": 25,
"open3d_small_translation_m": 0.04101882834929893,
"open3d_small_rotation_deg": 0.420339642399271,
"accepted": true,
"reason": ""
},
{
"i": 23,
"j": 24,
"open3d_small_translation_m": 0.009191025382465435,
"open3d_small_rotation_deg": 0.5691342384946491,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 25,
"j": 26,
"open3d_small_translation_m": 0.02091454005252894,
"open3d_small_rotation_deg": 0.4875072590330582,
"accepted": true,
"reason": ""
},
{
"i": 25,
"j": 27,
"open3d_small_translation_m": 0.09632206662156642,
"open3d_small_rotation_deg": 0.3699938120965201,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 25,
"j": 28,
"open3d_small_translation_m": 0.0301381128127144,
"open3d_small_rotation_deg": 0.46587172800425497,
"accepted": true,
"reason": ""
},
{
"i": 26,
"j": 27,
"open3d_small_translation_m": 0.01010757207206556,
"open3d_small_rotation_deg": 0.4201365669869023,
"accepted": true,
"reason": ""
},
{
"i": 26,
"j": 28,
"open3d_small_translation_m": 0.006243998078352694,
"open3d_small_rotation_deg": 1.3246136424341457,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 26,
"j": 29,
"open3d_small_translation_m": 0.03617825286340023,
"open3d_small_rotation_deg": 0.27823666091247784,
"accepted": true,
"reason": ""
},
{
"i": 27,
"j": 28,
"open3d_small_translation_m": 0.008540949499580083,
"open3d_small_rotation_deg": 0.3676810414714511,
"accepted": true,
"reason": ""
},
{
"i": 27,
"j": 29,
"open3d_small_translation_m": 0.01399120240175791,
"open3d_small_rotation_deg": 0.437290577300882,
"accepted": true,
"reason": ""
},
{
"i": 28,
"j": 29,
"open3d_small_translation_m": 0.12711262123542025,
"open3d_small_rotation_deg": 0.9231252582756669,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 28,
"j": 30,
"open3d_small_translation_m": 0.030113115024945805,
"open3d_small_rotation_deg": 0.33608547109429165,
"accepted": true,
"reason": ""
},
{
"i": 28,
"j": 31,
"accepted": false,
"reason": "not_in_small_gicp_refined"
},
{
"i": 30,
"j": 31,
"open3d_small_translation_m": 0.056474627862098614,
"open3d_small_rotation_deg": 0.6666837905088073,
"accepted": false,
"reason": "backend_disagreement"
},
{
"i": 30,
"j": 32,
"open3d_small_translation_m": 0.0152913302124184,
"open3d_small_rotation_deg": 0.3092824993876474,
"accepted": true,
"reason": ""
},
{
"i": 31,
"j": 32,
"accepted": false,
"reason": "not_in_small_gicp_refined"
},
{
"i": 31,
"j": 33,
"open3d_small_translation_m": 0.013286737983556427,
"open3d_small_rotation_deg": 0.37286489690414826,
"accepted": true,
"reason": ""
}
]
}
@@ -0,0 +1,266 @@
{
"schema_version": 1,
"success": true,
"convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
"equation": "A_RTK_ij X = X B_LiDAR_ij",
"frames": {
"RTK": {
"origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
"x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
"y_axis": "left",
"z_axis": "up",
"yaw_enu_deg": "90 - rawHeadingDeg"
},
"LiDAR": "raw LiDAR sensor frame"
},
"backend": "consensus",
"measured_lidar_extrinsic_used_as_initial": false,
"body_heading_offset_used": false,
"body_antenna_lever_xy_used": false,
"translation_m": [
1.6381793500373911,
-0.24084479868828831,
0.08448123595331278
],
"rotation_rpy_deg_xyz": [
-0.8171674587248069,
1.323288118779805,
-22.104163317857477
],
"quaternion_xyzw": [
-0.004784711987091957,
0.012700096588977744,
-0.19160273666049008,
0.9813787267829084
],
"matrix_4x4": [
[
0.926254197701683,
0.37594816689521215,
0.026760737062740007,
1.6381793500373911
],
[
-0.3761912321127582,
0.926530995670823,
0.004524482591229066,
-0.24084479868828831
],
[
-0.02309368141930373,
-0.014257975640431828,
0.9996316281556624,
0.08448123595331278
],
[
0.0,
0.0,
0.0,
1.0
]
],
"quality": {
"stations": 34,
"pairs": 25,
"residuals": {
"pairs": 25,
"translation_m": {
"rms": 0.10020667268070801,
"median": 0.06249704966098745,
"p90": 0.1210855900297508,
"p95": 0.12306740757559503,
"max": 0.35253378022021187
},
"rotation_deg": {
"rms": 1.2527941187072538,
"median": 0.7469173100535645,
"p90": 1.8215127226094046,
"p95": 1.973738680622839,
"max": 4.325671952515919
},
"per_pair": [
{
"pair_index": 0,
"translation_m": 0.04167906865173614,
"rotation_deg": 0.7044680397451787
},
{
"pair_index": 1,
"translation_m": 0.05434432733915097,
"rotation_deg": 0.3949090486960162
},
{
"pair_index": 2,
"translation_m": 0.10468985225969502,
"rotation_deg": 0.2966790038612149
},
{
"pair_index": 3,
"translation_m": 0.064702341381345,
"rotation_deg": 0.5538785486937366
},
{
"pair_index": 4,
"translation_m": 0.06403624975051968,
"rotation_deg": 0.7469173100535645
},
{
"pair_index": 5,
"translation_m": 0.019589736031301614,
"rotation_deg": 0.7060778312055135
},
{
"pair_index": 6,
"translation_m": 0.04563086081489213,
"rotation_deg": 0.8701331718844921
},
{
"pair_index": 7,
"translation_m": 0.05643960299219297,
"rotation_deg": 1.8737737246943913
},
{
"pair_index": 8,
"translation_m": 0.1234266519889254,
"rotation_deg": 1.9987299196049513
},
{
"pair_index": 9,
"translation_m": 0.06753558655646176,
"rotation_deg": 0.2674085857580904
},
{
"pair_index": 10,
"translation_m": 0.057937366822662255,
"rotation_deg": 0.339735283375759
},
{
"pair_index": 11,
"translation_m": 0.03457457850935045,
"rotation_deg": 0.5158041285368985
},
{
"pair_index": 12,
"translation_m": 0.06294442058649205,
"rotation_deg": 0.6210132605220049
},
{
"pair_index": 13,
"translation_m": 0.04800822576662163,
"rotation_deg": 1.7431212194819241
},
{
"pair_index": 14,
"translation_m": 0.06249704966098745,
"rotation_deg": 0.8651715760380532
},
{
"pair_index": 15,
"translation_m": 0.12026833019096655,
"rotation_deg": 4.325671952515919
},
{
"pair_index": 16,
"translation_m": 0.04916024916487916,
"rotation_deg": 0.9331159567430728
},
{
"pair_index": 17,
"translation_m": 0.02802786916783814,
"rotation_deg": 0.8755591753060336
},
{
"pair_index": 18,
"translation_m": 0.054001671602238746,
"rotation_deg": 0.48777111889991587
},
{
"pair_index": 19,
"translation_m": 0.10122861599246656,
"rotation_deg": 0.9367994244405716
},
{
"pair_index": 20,
"translation_m": 0.023164324685695653,
"rotation_deg": 0.2827650802657415
},
{
"pair_index": 21,
"translation_m": 0.06461070522327692,
"rotation_deg": 1.1522606437088527
},
{
"pair_index": 22,
"translation_m": 0.35253378022021187,
"rotation_deg": 0.7805430662091233
},
{
"pair_index": 23,
"translation_m": 0.11095170748867093,
"rotation_deg": 0.8561868088481429
},
{
"pair_index": 24,
"translation_m": 0.12163042992227363,
"rotation_deg": 0.17586872890763125
}
]
},
"weighted_jacobian_condition_number": 7.739413195936781,
"linearized_one_sigma": {
"translation_m": [
0.008936804232414386,
0.009199590403303341,
0.004827611183429192
],
"rotation_deg": [
0.08042474849565617,
0.07825800357808266,
0.1694976028248396
],
"warning": "conditional local estimate; bootstrap is the primary stability check"
},
"bootstrap": {
"runs": 200,
"order": [
"x_m",
"y_m",
"z_m",
"roll_deg",
"pitch_deg",
"yaw_deg"
],
"std": [
0.004007472116212317,
0.00447369016783699,
0.003161844455893403,
0.10272261218646783,
0.09618742156951016,
0.14509879375374413
],
"p025": [
1.6309949623104336,
-0.2470615833911814,
0.07875765037498306,
-0.9881676010783537,
1.1135179371917805,
-22.3851229846878
],
"p975": [
1.6456999445482652,
-0.22784916633142768,
0.09114084095723367,
-0.5808877804701225,
1.4821798187931374,
-21.82296828752347
]
}
},
"z_constraint": {
"observable_from_planar_AX_XB": false,
"method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
"rtk_reference_height_above_ground_m": 0.8535,
"warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion"
},
"important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification"
}
@@ -0,0 +1,300 @@
{
"schema_version": 1,
"success": true,
"convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
"equation": "A_RTK_ij X = X B_LiDAR_ij",
"frames": {
"RTK": {
"origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
"x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
"y_axis": "left",
"z_axis": "up",
"yaw_enu_deg": "90 - rawHeadingDeg"
},
"LiDAR": "raw LiDAR sensor frame"
},
"backend": "consensus",
"measured_lidar_extrinsic_used_as_initial": false,
"body_heading_offset_used": false,
"body_antenna_lever_xy_used": false,
"translation_m": [
1.6381793500373911,
-0.24084479868828831,
0.08448123595331278
],
"rotation_rpy_deg_xyz": [
-0.8171674587248069,
1.323288118779805,
-22.104163317857477
],
"quaternion_xyzw": [
-0.004784711987091957,
0.012700096588977744,
-0.19160273666049008,
0.9813787267829084
],
"matrix_4x4": [
[
0.926254197701683,
0.37594816689521215,
0.026760737062740007,
1.6381793500373911
],
[
-0.3761912321127582,
0.926530995670823,
0.004524482591229066,
-0.24084479868828831
],
[
-0.02309368141930373,
-0.014257975640431828,
0.9996316281556624,
0.08448123595331278
],
[
0.0,
0.0,
0.0,
1.0
]
],
"quality": {
"stations": 34,
"pairs": 25,
"residuals": {
"pairs": 25,
"translation_m": {
"rms": 0.10020667268070801,
"median": 0.06249704966098745,
"p90": 0.1210855900297508,
"p95": 0.12306740757559503,
"max": 0.35253378022021187
},
"rotation_deg": {
"rms": 1.2527941187072538,
"median": 0.7469173100535645,
"p90": 1.8215127226094046,
"p95": 1.973738680622839,
"max": 4.325671952515919
},
"per_pair": [
{
"pair_index": 0,
"translation_m": 0.04167906865173614,
"rotation_deg": 0.7044680397451787
},
{
"pair_index": 1,
"translation_m": 0.05434432733915097,
"rotation_deg": 0.3949090486960162
},
{
"pair_index": 2,
"translation_m": 0.10468985225969502,
"rotation_deg": 0.2966790038612149
},
{
"pair_index": 3,
"translation_m": 0.064702341381345,
"rotation_deg": 0.5538785486937366
},
{
"pair_index": 4,
"translation_m": 0.06403624975051968,
"rotation_deg": 0.7469173100535645
},
{
"pair_index": 5,
"translation_m": 0.019589736031301614,
"rotation_deg": 0.7060778312055135
},
{
"pair_index": 6,
"translation_m": 0.04563086081489213,
"rotation_deg": 0.8701331718844921
},
{
"pair_index": 7,
"translation_m": 0.05643960299219297,
"rotation_deg": 1.8737737246943913
},
{
"pair_index": 8,
"translation_m": 0.1234266519889254,
"rotation_deg": 1.9987299196049513
},
{
"pair_index": 9,
"translation_m": 0.06753558655646176,
"rotation_deg": 0.2674085857580904
},
{
"pair_index": 10,
"translation_m": 0.057937366822662255,
"rotation_deg": 0.339735283375759
},
{
"pair_index": 11,
"translation_m": 0.03457457850935045,
"rotation_deg": 0.5158041285368985
},
{
"pair_index": 12,
"translation_m": 0.06294442058649205,
"rotation_deg": 0.6210132605220049
},
{
"pair_index": 13,
"translation_m": 0.04800822576662163,
"rotation_deg": 1.7431212194819241
},
{
"pair_index": 14,
"translation_m": 0.06249704966098745,
"rotation_deg": 0.8651715760380532
},
{
"pair_index": 15,
"translation_m": 0.12026833019096655,
"rotation_deg": 4.325671952515919
},
{
"pair_index": 16,
"translation_m": 0.04916024916487916,
"rotation_deg": 0.9331159567430728
},
{
"pair_index": 17,
"translation_m": 0.02802786916783814,
"rotation_deg": 0.8755591753060336
},
{
"pair_index": 18,
"translation_m": 0.054001671602238746,
"rotation_deg": 0.48777111889991587
},
{
"pair_index": 19,
"translation_m": 0.10122861599246656,
"rotation_deg": 0.9367994244405716
},
{
"pair_index": 20,
"translation_m": 0.023164324685695653,
"rotation_deg": 0.2827650802657415
},
{
"pair_index": 21,
"translation_m": 0.06461070522327692,
"rotation_deg": 1.1522606437088527
},
{
"pair_index": 22,
"translation_m": 0.35253378022021187,
"rotation_deg": 0.7805430662091233
},
{
"pair_index": 23,
"translation_m": 0.11095170748867093,
"rotation_deg": 0.8561868088481429
},
{
"pair_index": 24,
"translation_m": 0.12163042992227363,
"rotation_deg": 0.17586872890763125
}
]
},
"weighted_jacobian_condition_number": 7.739413195936781,
"linearized_one_sigma": {
"translation_m": [
0.008936804232414386,
0.009199590403303341,
0.004827611183429192
],
"rotation_deg": [
0.08042474849565617,
0.07825800357808266,
0.1694976028248396
],
"warning": "conditional local estimate; bootstrap is the primary stability check"
},
"bootstrap": {
"runs": 200,
"order": [
"x_m",
"y_m",
"z_m",
"roll_deg",
"pitch_deg",
"yaw_deg"
],
"std": [
0.004007472116212317,
0.00447369016783699,
0.003161844455893403,
0.10272261218646783,
0.09618742156951016,
0.14509879375374413
],
"p025": [
1.6309949623104336,
-0.2470615833911814,
0.07875765037498306,
-0.9881676010783537,
1.1135179371917805,
-22.3851229846878
],
"p975": [
1.6456999445482652,
-0.22784916633142768,
0.09114084095723367,
-0.5808877804701225,
1.4821798187931374,
-21.82296828752347
]
}
},
"z_constraint": {
"observable_from_planar_AX_XB": false,
"method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
"rtk_reference_height_above_ground_m": 0.8535,
"warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion"
},
"important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification",
"selection": {
"recommended": true,
"reason": "Uses only motion pairs accepted independently by both Open3D GICP and small_gicp",
"open3d_vs_small_gicp": {
"translation_m": 0.003889255293759414,
"rotation_deg": 0.1884307130161592,
"delta_matrix_4x4": [
[
0.9999968771376496,
0.0024968749848742764,
-0.00010644368722136346,
0.0008526003522697501
],
[
-0.0024970968314049877,
0.9999945974960792,
-0.0021376356258303525,
-0.003658904827736509
],
[
0.00010110570323801577,
0.0021378947504826257,
0.9999977095892133,
0.0010058801324768218
],
[
0.0,
0.0,
0.0,
1.0
]
]
}
}
}
@@ -0,0 +1,97 @@
i,j,rtk_translation_m,rtk_rotation_deg,heldout_inlier_ratio,heldout_inlier_rmse_m,hessian_rank,hessian_condition,reverse_translation_m,reverse_rotation_deg,multistart_success_rate,accepted,rejection_reasons
0,1,1.6721594124489447,24.171297449440814,0.8061657032755298,0.10961296014103396,6,2.7038608113687213,0.004225163540003575,0.1530449067720668,1.0,True,
0,2,2.0412175279332088,80.09074797031303,0.7489394523717702,0.116305716193008,6,3.0720058333957327,0.02073003109723684,0.12418344306819311,1.0,True,
0,3,6.30529961936688,79.9158388329924,0.6310283235519265,0.12470979645173097,6,5.235632990817998,0.02154775870989652,0.25399044979598373,1.0,True,
1,2,1.2843386040405174,55.9194505208722,0.7867383512544803,0.11079021393934946,6,3.282529873989144,0.00768232673944143,0.0508043300468843,1.0,True,
1,3,5.433888607887495,55.744541383551606,0.6794562317367552,0.11907169138096609,6,4.183386002322132,0.024832409186708038,0.29364088503444125,1.0,True,
1,4,1.5299424710613851,106.08652205569952,0.6786112833230006,0.1125501582315264,6,3.2941312581877567,0.0077657602443488094,0.07274574571529673,1.0,True,
2,3,4.340252832276203,0.1749091373206093,0.7586776859504132,0.11541610277862546,6,3.730803369806122,0.007879904085790266,0.11659483159927261,1.0,True,
2,4,0.2520257253555564,50.1670715348273,0.7854572527608884,0.1098649389241602,6,2.641287751481567,0.017042953274276868,0.09383247792992644,1.0,True,
2,5,5.8286926576588955,8.735318060700322,0.7074574574574575,0.11912871456224486,6,3.8527093190832513,0.016640250279170064,0.24773491621516538,1.0,True,
3,4,4.1070657333447205,50.34198067214791,0.6974624291697462,0.11727630888949149,6,3.5010457716923216,0.0121487212194689,0.20793086843827258,1.0,True,
3,5,2.2886212019715484,8.910227198020936,0.7962985964476462,0.10646082199215787,6,3.037745853837991,0.011008298723512349,0.0821707761041294,1.0,True,
3,6,2.618668779147775,47.63555775102663,0.8376509054325956,0.10956583416752531,6,3.1930663579156175,0.001222821038315667,0.092075215117626,1.0,True,
4,5,5.5767898078953735,41.431753474126985,0.6652516676773802,0.12322094568315027,6,4.985227704290953,0.005399786589871835,0.13893507775898076,0.0,False,multistart_instability
4,6,6.68811709459501,97.97753842317455,0.04910385465259023,0.15664415071522125,6,4.835889397197473,2.574198069897065,12.647326063758534,0.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
4,7,6.153247042777442,123.28910472998353,0.020756115641215715,0.16997351387143192,6,17.054474046975617,5.52282870653845,6.186988956437115,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
5,6,2.0562758992403367,56.545784949047565,0.7526921648718901,0.10924852668609375,6,3.289802074469562,0.012374747586500019,0.14461856068328793,1.0,True,
5,7,0.5812996709001959,81.85735125585653,0.7833561729164071,0.11683079277948678,6,2.8807765869032624,0.013689874812447942,0.20006562776472953,1.0,True,
5,8,2.6887856969568644,172.47951556359513,0.3979730564825114,0.13180114830799514,6,4.4867592164383865,2.902311115345869,2.1073169352638135,1.0,False,forward_reverse_translation;forward_reverse_rotation
6,7,1.9723544820714844,25.311566306808967,0.8497729566094854,0.10415909908074775,6,3.3759361297847534,0.008986805974950147,0.13099479506412062,1.0,True,
6,8,0.7288530799256238,115.93373061454484,0.7678928928928929,0.1116672507978127,6,3.260920573387359,0.010085391730567652,0.1175283699615622,1.0,True,
6,9,7.898758206937296,103.90638727582184,0.6071384156199477,0.12594282886521943,6,6.882498483502542,2.7065494720876333,9.167174886516003,1.0,False,forward_reverse_translation;forward_reverse_rotation
7,8,2.6747845281541447,90.62216430773587,0.8299748110831234,0.10748525688830211,6,3.0071179226782405,0.00710646197885058,0.1069872847368705,1.0,True,
7,9,8.06955581661561,78.59482096901287,0.6188509200150206,0.12713205078393502,6,6.716979637303372,6.104542218165768,6.596313811797356,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
7,10,8.088675434881791,134.6003195530505,0.04729478766868887,0.16509079956796627,6,8.903210909129099,5.698690438550839,24.43813017288333,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
8,9,7.73033999229505,12.027343338722995,0.6326834719980131,0.12084265385763364,6,5.53968988049318,2.4349995045990127,17.158034002952295,1.0,False,forward_reverse_translation;forward_reverse_rotation
8,10,8.378411682322204,43.97815524531458,0.6163861933423412,0.1293602846396598,6,4.789005902863083,0.011327540721531722,0.17059480981566605,0.0,False,multistart_instability
8,11,11.214115106391473,22.60670813095403,0.035782503501846426,0.17440177580299876,6,9.744579276034784,1.861364410474017,5.099464012971126,1.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
9,10,1.9264207261313253,56.00549858403757,0.6543345543345543,0.10729272360686744,6,3.3788563349731624,1.6360232143180424,5.55466982065639,1.0,False,forward_reverse_translation;forward_reverse_rotation
9,11,4.747620352849464,34.63405146967702,0.5818780055682106,0.1171768781510077,6,3.555986532172078,0.00981302583568424,0.06035460198954135,1.0,True,
9,12,7.208566899019793,16.356227217122623,0.5379123584441162,0.12645695585785705,6,4.882558096197047,0.008862930161051686,0.11566409877698092,1.0,True,
10,11,3.0731501091601263,21.371447114360553,0.7118898623279099,0.1192865608074921,6,3.220203471557625,0.0029529340190141227,0.004176908093440082,1.0,True,
10,12,6.011368280631378,39.649271366914945,0.6120311738918656,0.12525652128126136,6,4.691686416856208,0.005686910809247203,0.10204044727299268,1.0,True,
10,13,9.76425648692991,72.41150002956132,0.516551290119572,0.1354903305714048,6,7.346543684414896,0.013565042721589003,0.32552304340992394,1.0,True,
11,12,3.3258193587172027,18.277824252554396,0.650555275113579,0.12104882943540898,6,4.612247786063477,0.003583199374507776,0.03300207707174117,1.0,True,
11,13,7.195203402213438,51.04005291520078,0.5698054068172914,0.1285866274322162,6,7.539783468898048,0.016001435752898144,0.11059625579950419,1.0,True,
11,14,3.634158560526847,20.548768889074672,0.6420881321982974,0.12414062948335593,6,4.952265047266808,0.012369101516230316,0.01870506040605898,1.0,True,
12,13,3.8697507070400543,32.762228662646386,0.6972966112450819,0.1168089621311632,6,4.28224799374136,0.01141023876783206,0.04779713993428384,1.0,True,
12,14,0.9871080784265185,2.2709446365202766,0.8749086479902558,0.09521299965540625,6,3.309695139564422,0.006159472215773367,0.014929948455569534,1.0,True,
12,15,4.171948395051693,25.86371030092923,0.7022030893897189,0.11797995277580106,6,4.277232786772136,0.008495008365046321,0.10278299144703042,1.0,True,
13,14,3.7992314627329202,30.491284026126113,0.6955810147299509,0.11455956122705321,6,3.350289886810734,0.01022866463344607,0.03515966054944392,1.0,True,
13,15,0.9105848166450461,6.898518361717151,0.868300353819945,0.1045421356808481,6,3.2437357133954023,0.002375025382773949,0.01072504764419793,1.0,True,
13,16,3.4957081323467,18.94489979461447,0.7265456392027422,0.1096252966406241,6,3.5227514244456217,0.008958927594996346,0.03041438512426757,1.0,True,
14,15,3.8816793634199405,23.592765664408958,0.7120070334086913,0.11868441290330703,6,4.62059246950246,0.002571958018980028,0.055069197511519646,1.0,True,
14,16,7.29101265002769,49.43618382074057,0.5918615984405458,0.12229328437386515,6,7.149509813179227,0.014273957859025563,0.25325650727957555,1.0,True,
14,17,5.915950814087913,0.8461207481731609,0.6694009445687298,0.12443900216431925,6,5.1577414296960065,0.0178952010004604,0.10920228290609924,1.0,True,
15,16,3.579287497246505,25.843418156331627,0.702887537993921,0.11495230769293859,6,3.5402899763525375,0.013545291843396808,0.03346625178333291,1.0,True,
15,17,2.2400625117908257,24.438886412582114,0.7429531936901991,0.11679524427533863,6,3.5266643942801554,0.009894110027911674,0.07786907370565768,1.0,True,
15,18,4.6956742726068,3.452521908779405,0.7209645010046886,0.11716134583909138,6,4.125231895423439,0.01165472931184747,0.13683586564190106,1.0,True,
16,17,2.956793995513645,50.28230456891372,0.618922305764411,0.11254196340940027,6,4.068632188828398,0.031047860213503222,0.10375098145236057,1.0,True,
16,18,3.369822545690391,22.390896247552213,0.6890156918687589,0.11084024896736888,6,4.421167108842175,0.01556225520373068,0.02495881796885156,1.0,True,
16,19,2.313038703742191,30.035090485266096,0.8685060899826,0.10135543575024479,6,2.9200722330018793,0.0029522513838272538,0.02753369989558306,1.0,True,
17,18,2.517968959880004,27.89140832136152,0.7697708305735859,0.10648049893472189,6,3.792822356453168,0.010582276181446961,0.03959905194910384,1.0,True,
17,19,3.518310045065406,80.31739505417983,0.6293759512937596,0.10954497717502036,6,3.648306929393189,0.012240937390578075,0.06030618467886912,1.0,True,
17,20,3.4199679241812992,152.98392843416642,0.6014520938674964,0.12300562605352787,6,4.719447385686111,0.03231013197809958,0.12856862709639913,0.0,False,multistart_instability
18,19,2.0416624616211574,52.42598673281832,0.6827314510833881,0.10834806615100022,6,3.5073857685652805,0.012418496053909043,0.11905018881087433,1.0,True,
18,20,5.836864764777489,125.0925201128049,0.5756313809779688,0.1265759478434111,6,5.389095141151799,0.012917057356044254,0.2404237301134517,0.0,False,multistart_instability
18,21,10.84206419743439,175.70238585457497,0.2352252017703723,0.15182541744787395,6,9.65894418804011,0.2039286327273425,0.13690658217533308,0.0,False,heldout_inlier_ratio;forward_reverse_translation;multistart_instability
19,20,6.120105723142265,72.6665333799865,0.6234734541714874,0.1277530580405749,6,5.958603794025071,0.009237066767190974,0.22540488888103255,1.0,True,
19,21,11.201089261967727,123.27639912174082,0.3788200074840963,0.1468780339612994,6,10.170069582593085,4.607025297377655,2.696881779819613,1.0,False,forward_reverse_translation;forward_reverse_rotation
19,22,13.962230939038237,128.85294276465584,0.17798277982779828,0.15985830060250797,6,11.806596815871064,2.332292389085354,2.4044448240559984,0.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
20,21,5.091648376409225,50.60986574175432,0.6596992097884272,0.12147955429086157,6,4.082032735955382,0.014725537493639118,0.19884224722867958,1.0,True,
20,22,7.842914217245693,56.186409384669375,0.5739414499308958,0.13255415786946775,6,5.3284863413522086,0.006040617548523686,0.19706632354686843,1.0,True,
20,23,4.953146569484805,70.79178325235414,0.6456945156330087,0.12075756038041646,6,3.9642702950866346,0.02294901101348451,0.19112109479244785,1.0,True,
21,22,2.836151129858731,5.576543642915048,0.7716237647919971,0.1163227135854156,6,2.7962015303291894,0.009963578798274064,0.1533289219532281,1.0,True,
21,23,1.4635995041292513,20.181917510599828,0.8576224819696593,0.09865676260631218,6,3.036795069384516,0.00392257332509571,0.00898487649366286,1.0,True,
21,24,2.7001854883863183,54.59467208208592,0.7715940569126165,0.11361504553552869,6,2.7681582493333527,0.007475673605592584,0.04227769001294981,1.0,True,
22,23,3.806551883906871,14.605373867684776,0.7396689147762109,0.11702962848624102,6,3.414804725088913,0.018140159434305195,0.1184732181610567,1.0,True,
22,24,4.999470711928798,49.01812843917085,0.6983240223463687,0.11898873157361631,6,3.633465593686572,1.9214516862996405,12.118772315210817,1.0,False,forward_reverse_translation;forward_reverse_rotation
22,25,2.281002791409386,12.391903814042275,0.736861094407697,0.1158639471011007,6,2.383007054117406,0.01855955252477569,0.06452602797902846,1.0,True,
23,24,1.2663665558774873,34.41275457148608,0.7853164556962026,0.11287254423109305,6,2.4099218288471635,0.002195759849349955,0.03211295915781923,1.0,True,
23,25,5.050865141821003,2.213470053642503,0.6881127450980392,0.12303206700403985,6,2.936372721803798,0.004939188019097873,0.12964064637099942,1.0,True,
23,26,5.595299803053147,40.730927532824325,0.6852618757612667,0.12040544972155913,6,3.059006509397719,0.006032445250251169,0.14039335223022864,1.0,True,
24,25,6.316196707646632,36.626224625128586,0.677667493796526,0.1234631580581415,6,3.6286524357748364,0.023479226891170584,0.16273859629355136,1.0,True,
24,26,6.840027061556236,6.318172961338242,0.6530209617755857,0.12710147612984235,6,3.5331859775372023,0.013311199903813952,0.18193579850150715,1.0,True,
24,27,7.477875812552711,31.60254826458195,0.6649014778325123,0.12481159614427853,6,4.10006355979974,0.01787282120544869,0.1680556511623414,1.0,True,
25,26,1.433673687807028,42.944397586466835,0.9127837514934289,0.09581429165893823,6,2.9017658682436958,0.0035341488401767693,0.018440169300164816,1.0,True,
25,27,1.973107678535245,68.22877288971054,0.8510739856801909,0.10418150333394123,6,2.3682534170899983,0.006103232696003387,0.13366555345654282,1.0,True,
25,28,2.578633669986193,88.09106909546726,0.8853518429870751,0.10761251229651997,6,2.6372860976794636,0.0034832316659127254,0.03697744201993421,1.0,True,
26,27,0.6711664435459649,25.284375303243706,0.9183867141162515,0.09074909570650827,6,2.8142315359282533,0.00040199505815399084,0.011554314408123986,1.0,True,
26,28,1.202247282991071,45.146671509000434,0.8853200095170116,0.1060418493965508,6,2.65640202778952,0.007320240476184076,0.12930335868606002,1.0,True,
26,29,0.8313560685788559,87.41573662125148,0.7880466815984911,0.10871274240898261,6,3.111957466886598,0.006395243061058376,0.039517035902872893,1.0,True,
27,28,0.6147316450225401,19.862296205756735,0.9289448669201521,0.08505095515326752,6,2.9632528684698194,0.005157655135593023,0.02266680787731125,1.0,True,
27,29,0.7540837235696874,62.13136131800778,0.7872365477452019,0.1045551346483567,6,2.9621627623005304,0.011650639607012715,0.16247191340775555,1.0,True,
27,30,5.07252652550922,152.0939255621477,0.03871268656716418,0.16956979514477974,6,145.85449900829832,4.5477385995336626,37.97511303619642,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
28,29,1.3242039147826599,42.26906511225104,0.8000944621560987,0.10865475473581254,6,3.146622410005858,0.001139416496730335,0.02119627748722763,1.0,True,
28,30,5.091487440029177,132.23162935639098,0.7721000935453695,0.10815845248211022,6,2.292141242686861,0.004462522533166182,0.03583195432667579,1.0,True,
28,31,6.538757407908195,158.84212980112872,0.7465330381074466,0.10669666534392452,6,2.40259836682247,0.0063576490595817345,0.04001661521442668,1.0,True,
29,30,4.767831371266539,89.96256424413991,0.7248812145092132,0.10870831469083345,6,2.947122380473709,0.004656385377159087,0.12465763189228557,0.0,False,multistart_instability
29,31,5.715598450796842,116.57306468887764,0.7243012243012243,0.11138751941762652,6,2.7645021322697088,0.0042639063218924715,0.04663739608639213,0.0,False,multistart_instability
29,32,5.281749147864957,159.39493219688646,0.32491640724086246,0.14745892800230104,6,2.8081088899077167,4.8118414680497805,1.8382424276135385,1.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
30,31,2.604154101624297,26.610500444737717,0.8185562292643862,0.09615795732951272,6,2.899558208444641,0.0049991634555749285,0.021384795873435915,1.0,True,
30,32,1.69105635082049,69.43236795274656,0.8041343079031521,0.09953343153684206,6,2.614616874224757,0.0031713764951448154,0.023767377748422268,1.0,True,
30,33,3.2411277385703925,160.7145717128097,0.05469213429825602,0.15997514255683,6,6.323554164510002,5.32763716297551,13.705863888332022,0.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
31,32,0.9137201114724802,42.82186750800884,0.8157085941946499,0.09925944770258255,6,3.048541140076824,0.004719596234885736,0.06146741991683861,1.0,True,
31,33,1.4965271484681508,134.10407126807198,0.760345235280208,0.09698073659477859,6,2.550150787416455,0.0019366659831763946,0.026538404789073603,1.0,True,
32,33,1.9169426499676907,91.28220376006315,0.7569928006609229,0.10008740880870787,6,3.6611044282667184,2.9881933505092046,2.6964434945984053,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
1 i j rtk_translation_m rtk_rotation_deg heldout_inlier_ratio heldout_inlier_rmse_m hessian_rank hessian_condition reverse_translation_m reverse_rotation_deg multistart_success_rate accepted rejection_reasons
2 0 1 1.6721594124489447 24.171297449440814 0.8061657032755298 0.10961296014103396 6 2.7038608113687213 0.004225163540003575 0.1530449067720668 1.0 True
3 0 2 2.0412175279332088 80.09074797031303 0.7489394523717702 0.116305716193008 6 3.0720058333957327 0.02073003109723684 0.12418344306819311 1.0 True
4 0 3 6.30529961936688 79.9158388329924 0.6310283235519265 0.12470979645173097 6 5.235632990817998 0.02154775870989652 0.25399044979598373 1.0 True
5 1 2 1.2843386040405174 55.9194505208722 0.7867383512544803 0.11079021393934946 6 3.282529873989144 0.00768232673944143 0.0508043300468843 1.0 True
6 1 3 5.433888607887495 55.744541383551606 0.6794562317367552 0.11907169138096609 6 4.183386002322132 0.024832409186708038 0.29364088503444125 1.0 True
7 1 4 1.5299424710613851 106.08652205569952 0.6786112833230006 0.1125501582315264 6 3.2941312581877567 0.0077657602443488094 0.07274574571529673 1.0 True
8 2 3 4.340252832276203 0.1749091373206093 0.7586776859504132 0.11541610277862546 6 3.730803369806122 0.007879904085790266 0.11659483159927261 1.0 True
9 2 4 0.2520257253555564 50.1670715348273 0.7854572527608884 0.1098649389241602 6 2.641287751481567 0.017042953274276868 0.09383247792992644 1.0 True
10 2 5 5.8286926576588955 8.735318060700322 0.7074574574574575 0.11912871456224486 6 3.8527093190832513 0.016640250279170064 0.24773491621516538 1.0 True
11 3 4 4.1070657333447205 50.34198067214791 0.6974624291697462 0.11727630888949149 6 3.5010457716923216 0.0121487212194689 0.20793086843827258 1.0 True
12 3 5 2.2886212019715484 8.910227198020936 0.7962985964476462 0.10646082199215787 6 3.037745853837991 0.011008298723512349 0.0821707761041294 1.0 True
13 3 6 2.618668779147775 47.63555775102663 0.8376509054325956 0.10956583416752531 6 3.1930663579156175 0.001222821038315667 0.092075215117626 1.0 True
14 4 5 5.5767898078953735 41.431753474126985 0.6652516676773802 0.12322094568315027 6 4.985227704290953 0.005399786589871835 0.13893507775898076 0.0 False multistart_instability
15 4 6 6.68811709459501 97.97753842317455 0.04910385465259023 0.15664415071522125 6 4.835889397197473 2.574198069897065 12.647326063758534 0.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
16 4 7 6.153247042777442 123.28910472998353 0.020756115641215715 0.16997351387143192 6 17.054474046975617 5.52282870653845 6.186988956437115 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
17 5 6 2.0562758992403367 56.545784949047565 0.7526921648718901 0.10924852668609375 6 3.289802074469562 0.012374747586500019 0.14461856068328793 1.0 True
18 5 7 0.5812996709001959 81.85735125585653 0.7833561729164071 0.11683079277948678 6 2.8807765869032624 0.013689874812447942 0.20006562776472953 1.0 True
19 5 8 2.6887856969568644 172.47951556359513 0.3979730564825114 0.13180114830799514 6 4.4867592164383865 2.902311115345869 2.1073169352638135 1.0 False forward_reverse_translation;forward_reverse_rotation
20 6 7 1.9723544820714844 25.311566306808967 0.8497729566094854 0.10415909908074775 6 3.3759361297847534 0.008986805974950147 0.13099479506412062 1.0 True
21 6 8 0.7288530799256238 115.93373061454484 0.7678928928928929 0.1116672507978127 6 3.260920573387359 0.010085391730567652 0.1175283699615622 1.0 True
22 6 9 7.898758206937296 103.90638727582184 0.6071384156199477 0.12594282886521943 6 6.882498483502542 2.7065494720876333 9.167174886516003 1.0 False forward_reverse_translation;forward_reverse_rotation
23 7 8 2.6747845281541447 90.62216430773587 0.8299748110831234 0.10748525688830211 6 3.0071179226782405 0.00710646197885058 0.1069872847368705 1.0 True
24 7 9 8.06955581661561 78.59482096901287 0.6188509200150206 0.12713205078393502 6 6.716979637303372 6.104542218165768 6.596313811797356 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
25 7 10 8.088675434881791 134.6003195530505 0.04729478766868887 0.16509079956796627 6 8.903210909129099 5.698690438550839 24.43813017288333 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
26 8 9 7.73033999229505 12.027343338722995 0.6326834719980131 0.12084265385763364 6 5.53968988049318 2.4349995045990127 17.158034002952295 1.0 False forward_reverse_translation;forward_reverse_rotation
27 8 10 8.378411682322204 43.97815524531458 0.6163861933423412 0.1293602846396598 6 4.789005902863083 0.011327540721531722 0.17059480981566605 0.0 False multistart_instability
28 8 11 11.214115106391473 22.60670813095403 0.035782503501846426 0.17440177580299876 6 9.744579276034784 1.861364410474017 5.099464012971126 1.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
29 9 10 1.9264207261313253 56.00549858403757 0.6543345543345543 0.10729272360686744 6 3.3788563349731624 1.6360232143180424 5.55466982065639 1.0 False forward_reverse_translation;forward_reverse_rotation
30 9 11 4.747620352849464 34.63405146967702 0.5818780055682106 0.1171768781510077 6 3.555986532172078 0.00981302583568424 0.06035460198954135 1.0 True
31 9 12 7.208566899019793 16.356227217122623 0.5379123584441162 0.12645695585785705 6 4.882558096197047 0.008862930161051686 0.11566409877698092 1.0 True
32 10 11 3.0731501091601263 21.371447114360553 0.7118898623279099 0.1192865608074921 6 3.220203471557625 0.0029529340190141227 0.004176908093440082 1.0 True
33 10 12 6.011368280631378 39.649271366914945 0.6120311738918656 0.12525652128126136 6 4.691686416856208 0.005686910809247203 0.10204044727299268 1.0 True
34 10 13 9.76425648692991 72.41150002956132 0.516551290119572 0.1354903305714048 6 7.346543684414896 0.013565042721589003 0.32552304340992394 1.0 True
35 11 12 3.3258193587172027 18.277824252554396 0.650555275113579 0.12104882943540898 6 4.612247786063477 0.003583199374507776 0.03300207707174117 1.0 True
36 11 13 7.195203402213438 51.04005291520078 0.5698054068172914 0.1285866274322162 6 7.539783468898048 0.016001435752898144 0.11059625579950419 1.0 True
37 11 14 3.634158560526847 20.548768889074672 0.6420881321982974 0.12414062948335593 6 4.952265047266808 0.012369101516230316 0.01870506040605898 1.0 True
38 12 13 3.8697507070400543 32.762228662646386 0.6972966112450819 0.1168089621311632 6 4.28224799374136 0.01141023876783206 0.04779713993428384 1.0 True
39 12 14 0.9871080784265185 2.2709446365202766 0.8749086479902558 0.09521299965540625 6 3.309695139564422 0.006159472215773367 0.014929948455569534 1.0 True
40 12 15 4.171948395051693 25.86371030092923 0.7022030893897189 0.11797995277580106 6 4.277232786772136 0.008495008365046321 0.10278299144703042 1.0 True
41 13 14 3.7992314627329202 30.491284026126113 0.6955810147299509 0.11455956122705321 6 3.350289886810734 0.01022866463344607 0.03515966054944392 1.0 True
42 13 15 0.9105848166450461 6.898518361717151 0.868300353819945 0.1045421356808481 6 3.2437357133954023 0.002375025382773949 0.01072504764419793 1.0 True
43 13 16 3.4957081323467 18.94489979461447 0.7265456392027422 0.1096252966406241 6 3.5227514244456217 0.008958927594996346 0.03041438512426757 1.0 True
44 14 15 3.8816793634199405 23.592765664408958 0.7120070334086913 0.11868441290330703 6 4.62059246950246 0.002571958018980028 0.055069197511519646 1.0 True
45 14 16 7.29101265002769 49.43618382074057 0.5918615984405458 0.12229328437386515 6 7.149509813179227 0.014273957859025563 0.25325650727957555 1.0 True
46 14 17 5.915950814087913 0.8461207481731609 0.6694009445687298 0.12443900216431925 6 5.1577414296960065 0.0178952010004604 0.10920228290609924 1.0 True
47 15 16 3.579287497246505 25.843418156331627 0.702887537993921 0.11495230769293859 6 3.5402899763525375 0.013545291843396808 0.03346625178333291 1.0 True
48 15 17 2.2400625117908257 24.438886412582114 0.7429531936901991 0.11679524427533863 6 3.5266643942801554 0.009894110027911674 0.07786907370565768 1.0 True
49 15 18 4.6956742726068 3.452521908779405 0.7209645010046886 0.11716134583909138 6 4.125231895423439 0.01165472931184747 0.13683586564190106 1.0 True
50 16 17 2.956793995513645 50.28230456891372 0.618922305764411 0.11254196340940027 6 4.068632188828398 0.031047860213503222 0.10375098145236057 1.0 True
51 16 18 3.369822545690391 22.390896247552213 0.6890156918687589 0.11084024896736888 6 4.421167108842175 0.01556225520373068 0.02495881796885156 1.0 True
52 16 19 2.313038703742191 30.035090485266096 0.8685060899826 0.10135543575024479 6 2.9200722330018793 0.0029522513838272538 0.02753369989558306 1.0 True
53 17 18 2.517968959880004 27.89140832136152 0.7697708305735859 0.10648049893472189 6 3.792822356453168 0.010582276181446961 0.03959905194910384 1.0 True
54 17 19 3.518310045065406 80.31739505417983 0.6293759512937596 0.10954497717502036 6 3.648306929393189 0.012240937390578075 0.06030618467886912 1.0 True
55 17 20 3.4199679241812992 152.98392843416642 0.6014520938674964 0.12300562605352787 6 4.719447385686111 0.03231013197809958 0.12856862709639913 0.0 False multistart_instability
56 18 19 2.0416624616211574 52.42598673281832 0.6827314510833881 0.10834806615100022 6 3.5073857685652805 0.012418496053909043 0.11905018881087433 1.0 True
57 18 20 5.836864764777489 125.0925201128049 0.5756313809779688 0.1265759478434111 6 5.389095141151799 0.012917057356044254 0.2404237301134517 0.0 False multistart_instability
58 18 21 10.84206419743439 175.70238585457497 0.2352252017703723 0.15182541744787395 6 9.65894418804011 0.2039286327273425 0.13690658217533308 0.0 False heldout_inlier_ratio;forward_reverse_translation;multistart_instability
59 19 20 6.120105723142265 72.6665333799865 0.6234734541714874 0.1277530580405749 6 5.958603794025071 0.009237066767190974 0.22540488888103255 1.0 True
60 19 21 11.201089261967727 123.27639912174082 0.3788200074840963 0.1468780339612994 6 10.170069582593085 4.607025297377655 2.696881779819613 1.0 False forward_reverse_translation;forward_reverse_rotation
61 19 22 13.962230939038237 128.85294276465584 0.17798277982779828 0.15985830060250797 6 11.806596815871064 2.332292389085354 2.4044448240559984 0.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
62 20 21 5.091648376409225 50.60986574175432 0.6596992097884272 0.12147955429086157 6 4.082032735955382 0.014725537493639118 0.19884224722867958 1.0 True
63 20 22 7.842914217245693 56.186409384669375 0.5739414499308958 0.13255415786946775 6 5.3284863413522086 0.006040617548523686 0.19706632354686843 1.0 True
64 20 23 4.953146569484805 70.79178325235414 0.6456945156330087 0.12075756038041646 6 3.9642702950866346 0.02294901101348451 0.19112109479244785 1.0 True
65 21 22 2.836151129858731 5.576543642915048 0.7716237647919971 0.1163227135854156 6 2.7962015303291894 0.009963578798274064 0.1533289219532281 1.0 True
66 21 23 1.4635995041292513 20.181917510599828 0.8576224819696593 0.09865676260631218 6 3.036795069384516 0.00392257332509571 0.00898487649366286 1.0 True
67 21 24 2.7001854883863183 54.59467208208592 0.7715940569126165 0.11361504553552869 6 2.7681582493333527 0.007475673605592584 0.04227769001294981 1.0 True
68 22 23 3.806551883906871 14.605373867684776 0.7396689147762109 0.11702962848624102 6 3.414804725088913 0.018140159434305195 0.1184732181610567 1.0 True
69 22 24 4.999470711928798 49.01812843917085 0.6983240223463687 0.11898873157361631 6 3.633465593686572 1.9214516862996405 12.118772315210817 1.0 False forward_reverse_translation;forward_reverse_rotation
70 22 25 2.281002791409386 12.391903814042275 0.736861094407697 0.1158639471011007 6 2.383007054117406 0.01855955252477569 0.06452602797902846 1.0 True
71 23 24 1.2663665558774873 34.41275457148608 0.7853164556962026 0.11287254423109305 6 2.4099218288471635 0.002195759849349955 0.03211295915781923 1.0 True
72 23 25 5.050865141821003 2.213470053642503 0.6881127450980392 0.12303206700403985 6 2.936372721803798 0.004939188019097873 0.12964064637099942 1.0 True
73 23 26 5.595299803053147 40.730927532824325 0.6852618757612667 0.12040544972155913 6 3.059006509397719 0.006032445250251169 0.14039335223022864 1.0 True
74 24 25 6.316196707646632 36.626224625128586 0.677667493796526 0.1234631580581415 6 3.6286524357748364 0.023479226891170584 0.16273859629355136 1.0 True
75 24 26 6.840027061556236 6.318172961338242 0.6530209617755857 0.12710147612984235 6 3.5331859775372023 0.013311199903813952 0.18193579850150715 1.0 True
76 24 27 7.477875812552711 31.60254826458195 0.6649014778325123 0.12481159614427853 6 4.10006355979974 0.01787282120544869 0.1680556511623414 1.0 True
77 25 26 1.433673687807028 42.944397586466835 0.9127837514934289 0.09581429165893823 6 2.9017658682436958 0.0035341488401767693 0.018440169300164816 1.0 True
78 25 27 1.973107678535245 68.22877288971054 0.8510739856801909 0.10418150333394123 6 2.3682534170899983 0.006103232696003387 0.13366555345654282 1.0 True
79 25 28 2.578633669986193 88.09106909546726 0.8853518429870751 0.10761251229651997 6 2.6372860976794636 0.0034832316659127254 0.03697744201993421 1.0 True
80 26 27 0.6711664435459649 25.284375303243706 0.9183867141162515 0.09074909570650827 6 2.8142315359282533 0.00040199505815399084 0.011554314408123986 1.0 True
81 26 28 1.202247282991071 45.146671509000434 0.8853200095170116 0.1060418493965508 6 2.65640202778952 0.007320240476184076 0.12930335868606002 1.0 True
82 26 29 0.8313560685788559 87.41573662125148 0.7880466815984911 0.10871274240898261 6 3.111957466886598 0.006395243061058376 0.039517035902872893 1.0 True
83 27 28 0.6147316450225401 19.862296205756735 0.9289448669201521 0.08505095515326752 6 2.9632528684698194 0.005157655135593023 0.02266680787731125 1.0 True
84 27 29 0.7540837235696874 62.13136131800778 0.7872365477452019 0.1045551346483567 6 2.9621627623005304 0.011650639607012715 0.16247191340775555 1.0 True
85 27 30 5.07252652550922 152.0939255621477 0.03871268656716418 0.16956979514477974 6 145.85449900829832 4.5477385995336626 37.97511303619642 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
86 28 29 1.3242039147826599 42.26906511225104 0.8000944621560987 0.10865475473581254 6 3.146622410005858 0.001139416496730335 0.02119627748722763 1.0 True
87 28 30 5.091487440029177 132.23162935639098 0.7721000935453695 0.10815845248211022 6 2.292141242686861 0.004462522533166182 0.03583195432667579 1.0 True
88 28 31 6.538757407908195 158.84212980112872 0.7465330381074466 0.10669666534392452 6 2.40259836682247 0.0063576490595817345 0.04001661521442668 1.0 True
89 29 30 4.767831371266539 89.96256424413991 0.7248812145092132 0.10870831469083345 6 2.947122380473709 0.004656385377159087 0.12465763189228557 0.0 False multistart_instability
90 29 31 5.715598450796842 116.57306468887764 0.7243012243012243 0.11138751941762652 6 2.7645021322697088 0.0042639063218924715 0.04663739608639213 0.0 False multistart_instability
91 29 32 5.281749147864957 159.39493219688646 0.32491640724086246 0.14745892800230104 6 2.8081088899077167 4.8118414680497805 1.8382424276135385 1.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
92 30 31 2.604154101624297 26.610500444737717 0.8185562292643862 0.09615795732951272 6 2.899558208444641 0.0049991634555749285 0.021384795873435915 1.0 True
93 30 32 1.69105635082049 69.43236795274656 0.8041343079031521 0.09953343153684206 6 2.614616874224757 0.0031713764951448154 0.023767377748422268 1.0 True
94 30 33 3.2411277385703925 160.7145717128097 0.05469213429825602 0.15997514255683 6 6.323554164510002 5.32763716297551 13.705863888332022 0.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
95 31 32 0.9137201114724802 42.82186750800884 0.8157085941946499 0.09925944770258255 6 3.048541140076824 0.004719596234885736 0.06146741991683861 1.0 True
96 31 33 1.4965271484681508 134.10407126807198 0.760345235280208 0.09698073659477859 6 2.550150787416455 0.0019366659831763946 0.026538404789073603 1.0 True
97 32 33 1.9169426499676907 91.28220376006315 0.7569928006609229 0.10008740880870787 6 3.6611044282667184 2.9881933505092046 2.6964434945984053 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
@@ -0,0 +1,889 @@
{
"selection_is_X_independent": true,
"criteria": {
"min_inlier_ratio": 0.7,
"max_inlier_rmse_m": 0.13,
"max_rotation_invariant_error_deg": 0.75,
"reverse_translation_tolerance_m": 0.05,
"reverse_rotation_tolerance_deg": 0.5
},
"input_pairs": 73,
"accepted_pairs": 41,
"pairs": [
{
"i": 0,
"j": 1,
"heldout_inlier_ratio": 0.8061657032755298,
"heldout_inlier_rmse_m": 0.10961296014103396,
"rotation_invariant_error_deg": 0.021953694767642418,
"reverse_translation_m": 0.004225163540003575,
"reverse_rotation_deg": 0.1530449067720668,
"accepted": true,
"rejection_reasons": []
},
{
"i": 0,
"j": 2,
"heldout_inlier_ratio": 0.7489394523717702,
"heldout_inlier_rmse_m": 0.116305716193008,
"rotation_invariant_error_deg": 0.672251363597141,
"reverse_translation_m": 0.02073003109723684,
"reverse_rotation_deg": 0.12418344306819311,
"accepted": true,
"rejection_reasons": []
},
{
"i": 0,
"j": 3,
"heldout_inlier_ratio": 0.6310283235519265,
"heldout_inlier_rmse_m": 0.12470979645173097,
"rotation_invariant_error_deg": 0.3195502906704917,
"reverse_translation_m": 0.02154775870989652,
"reverse_rotation_deg": 0.25399044979598373,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 1,
"j": 2,
"heldout_inlier_ratio": 0.7867383512544803,
"heldout_inlier_rmse_m": 0.11079021393934946,
"rotation_invariant_error_deg": 0.6532823847336218,
"reverse_translation_m": 0.00768232673944143,
"reverse_rotation_deg": 0.0508043300468843,
"accepted": true,
"rejection_reasons": []
},
{
"i": 1,
"j": 3,
"heldout_inlier_ratio": 0.6794562317367552,
"heldout_inlier_rmse_m": 0.11907169138096609,
"rotation_invariant_error_deg": 0.28968149037613955,
"reverse_translation_m": 0.024832409186708038,
"reverse_rotation_deg": 0.29364088503444125,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 1,
"j": 4,
"heldout_inlier_ratio": 0.6786112833230006,
"heldout_inlier_rmse_m": 0.1125501582315264,
"rotation_invariant_error_deg": 0.23509206217087808,
"reverse_translation_m": 0.0077657602443488094,
"reverse_rotation_deg": 0.07274574571529673,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 2,
"j": 3,
"heldout_inlier_ratio": 0.7586776859504132,
"heldout_inlier_rmse_m": 0.11541610277862546,
"rotation_invariant_error_deg": 0.06385020919199427,
"reverse_translation_m": 0.007879904085790266,
"reverse_rotation_deg": 0.11659483159927261,
"accepted": true,
"rejection_reasons": []
},
{
"i": 2,
"j": 4,
"heldout_inlier_ratio": 0.7854572527608884,
"heldout_inlier_rmse_m": 0.1098649389241602,
"rotation_invariant_error_deg": 0.8714733340459802,
"reverse_translation_m": 0.017042953274276868,
"reverse_rotation_deg": 0.09383247792992644,
"accepted": false,
"rejection_reasons": [
"rotation_conjugacy_invariant"
]
},
{
"i": 2,
"j": 5,
"heldout_inlier_ratio": 0.7074574574574575,
"heldout_inlier_rmse_m": 0.11912871456224486,
"rotation_invariant_error_deg": 0.18237928888532906,
"reverse_translation_m": 0.016640250279170064,
"reverse_rotation_deg": 0.24773491621516538,
"accepted": true,
"rejection_reasons": []
},
{
"i": 3,
"j": 4,
"heldout_inlier_ratio": 0.6974624291697462,
"heldout_inlier_rmse_m": 0.11727630888949149,
"rotation_invariant_error_deg": 0.4902436735823201,
"reverse_translation_m": 0.0121487212194689,
"reverse_rotation_deg": 0.20793086843827258,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 3,
"j": 5,
"heldout_inlier_ratio": 0.7962985964476462,
"heldout_inlier_rmse_m": 0.10646082199215787,
"rotation_invariant_error_deg": 0.5522737825084345,
"reverse_translation_m": 0.011008298723512349,
"reverse_rotation_deg": 0.0821707761041294,
"accepted": true,
"rejection_reasons": []
},
{
"i": 3,
"j": 6,
"heldout_inlier_ratio": 0.8376509054325956,
"heldout_inlier_rmse_m": 0.10956583416752531,
"rotation_invariant_error_deg": 0.24026261153986894,
"reverse_translation_m": 0.001222821038315667,
"reverse_rotation_deg": 0.092075215117626,
"accepted": true,
"rejection_reasons": []
},
{
"i": 5,
"j": 6,
"heldout_inlier_ratio": 0.7526921648718901,
"heldout_inlier_rmse_m": 0.10924852668609375,
"rotation_invariant_error_deg": 0.8049600839827491,
"reverse_translation_m": 0.012374747586500019,
"reverse_rotation_deg": 0.14461856068328793,
"accepted": false,
"rejection_reasons": [
"rotation_conjugacy_invariant"
]
},
{
"i": 5,
"j": 7,
"heldout_inlier_ratio": 0.7833561729164071,
"heldout_inlier_rmse_m": 0.11683079277948678,
"rotation_invariant_error_deg": 0.7937418989476015,
"reverse_translation_m": 0.013689874812447942,
"reverse_rotation_deg": 0.20006562776472953,
"accepted": false,
"rejection_reasons": [
"rotation_conjugacy_invariant"
]
},
{
"i": 6,
"j": 7,
"heldout_inlier_ratio": 0.8497729566094854,
"heldout_inlier_rmse_m": 0.10415909908074775,
"rotation_invariant_error_deg": 0.026617733421641532,
"reverse_translation_m": 0.008986805974950147,
"reverse_rotation_deg": 0.13099479506412062,
"accepted": true,
"rejection_reasons": []
},
{
"i": 6,
"j": 8,
"heldout_inlier_ratio": 0.7678928928928929,
"heldout_inlier_rmse_m": 0.1116672507978127,
"rotation_invariant_error_deg": 0.32815442351946444,
"reverse_translation_m": 0.010085391730567652,
"reverse_rotation_deg": 0.1175283699615622,
"accepted": true,
"rejection_reasons": []
},
{
"i": 7,
"j": 8,
"heldout_inlier_ratio": 0.8299748110831234,
"heldout_inlier_rmse_m": 0.10748525688830211,
"rotation_invariant_error_deg": 0.29848794137558343,
"reverse_translation_m": 0.00710646197885058,
"reverse_rotation_deg": 0.1069872847368705,
"accepted": true,
"rejection_reasons": []
},
{
"i": 9,
"j": 11,
"heldout_inlier_ratio": 0.5818780055682106,
"heldout_inlier_rmse_m": 0.1171768781510077,
"rotation_invariant_error_deg": 0.2635280280577561,
"reverse_translation_m": 0.00981302583568424,
"reverse_rotation_deg": 0.06035460198954135,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 9,
"j": 12,
"heldout_inlier_ratio": 0.5379123584441162,
"heldout_inlier_rmse_m": 0.12645695585785705,
"rotation_invariant_error_deg": 0.5448949372769505,
"reverse_translation_m": 0.008862930161051686,
"reverse_rotation_deg": 0.11566409877698092,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 10,
"j": 11,
"heldout_inlier_ratio": 0.7118898623279099,
"heldout_inlier_rmse_m": 0.1192865608074921,
"rotation_invariant_error_deg": 0.037876295004018345,
"reverse_translation_m": 0.0029529340190141227,
"reverse_rotation_deg": 0.004176908093440082,
"accepted": true,
"rejection_reasons": []
},
{
"i": 10,
"j": 12,
"heldout_inlier_ratio": 0.6120311738918656,
"heldout_inlier_rmse_m": 0.12525652128126136,
"rotation_invariant_error_deg": 0.22694248875488654,
"reverse_translation_m": 0.005686910809247203,
"reverse_rotation_deg": 0.10204044727299268,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 10,
"j": 13,
"heldout_inlier_ratio": 0.516551290119572,
"heldout_inlier_rmse_m": 0.1354903305714048,
"rotation_invariant_error_deg": 0.7336960316225287,
"reverse_translation_m": 0.013565042721589003,
"reverse_rotation_deg": 0.32552304340992394,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"heldout_rmse"
]
},
{
"i": 11,
"j": 12,
"heldout_inlier_ratio": 0.650555275113579,
"heldout_inlier_rmse_m": 0.12104882943540898,
"rotation_invariant_error_deg": 0.16828520325654495,
"reverse_translation_m": 0.003583199374507776,
"reverse_rotation_deg": 0.03300207707174117,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 11,
"j": 13,
"heldout_inlier_ratio": 0.5698054068172914,
"heldout_inlier_rmse_m": 0.1285866274322162,
"rotation_invariant_error_deg": 0.7985328447719269,
"reverse_translation_m": 0.016001435752898144,
"reverse_rotation_deg": 0.11059625579950419,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 11,
"j": 14,
"heldout_inlier_ratio": 0.6420881321982974,
"heldout_inlier_rmse_m": 0.12414062948335593,
"rotation_invariant_error_deg": 0.9364144322988963,
"reverse_translation_m": 0.012369101516230316,
"reverse_rotation_deg": 0.01870506040605898,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 12,
"j": 13,
"heldout_inlier_ratio": 0.6972966112450819,
"heldout_inlier_rmse_m": 0.1168089621311632,
"rotation_invariant_error_deg": 0.9873386907551662,
"reverse_translation_m": 0.01141023876783206,
"reverse_rotation_deg": 0.04779713993428384,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 12,
"j": 14,
"heldout_inlier_ratio": 0.8749086479902558,
"heldout_inlier_rmse_m": 0.09521299965540625,
"rotation_invariant_error_deg": 0.7228372421049902,
"reverse_translation_m": 0.006159472215773367,
"reverse_rotation_deg": 0.014929948455569534,
"accepted": true,
"rejection_reasons": []
},
{
"i": 12,
"j": 15,
"heldout_inlier_ratio": 0.7022030893897189,
"heldout_inlier_rmse_m": 0.11797995277580106,
"rotation_invariant_error_deg": 0.34088274652411243,
"reverse_translation_m": 0.008495008365046321,
"reverse_rotation_deg": 0.10278299144703042,
"accepted": true,
"rejection_reasons": []
},
{
"i": 13,
"j": 14,
"heldout_inlier_ratio": 0.6955810147299509,
"heldout_inlier_rmse_m": 0.11455956122705321,
"rotation_invariant_error_deg": 1.7562842773469676,
"reverse_translation_m": 0.01022866463344607,
"reverse_rotation_deg": 0.03515966054944392,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 13,
"j": 15,
"heldout_inlier_ratio": 0.868300353819945,
"heldout_inlier_rmse_m": 0.1045421356808481,
"rotation_invariant_error_deg": 0.6514660625063202,
"reverse_translation_m": 0.002375025382773949,
"reverse_rotation_deg": 0.01072504764419793,
"accepted": true,
"rejection_reasons": []
},
{
"i": 13,
"j": 16,
"heldout_inlier_ratio": 0.7265456392027422,
"heldout_inlier_rmse_m": 0.1096252966406241,
"rotation_invariant_error_deg": 0.48470120712195097,
"reverse_translation_m": 0.008958927594996346,
"reverse_rotation_deg": 0.03041438512426757,
"accepted": true,
"rejection_reasons": []
},
{
"i": 14,
"j": 15,
"heldout_inlier_ratio": 0.7120070334086913,
"heldout_inlier_rmse_m": 0.11868441290330703,
"rotation_invariant_error_deg": 1.1046128571505562,
"reverse_translation_m": 0.002571958018980028,
"reverse_rotation_deg": 0.055069197511519646,
"accepted": false,
"rejection_reasons": [
"rotation_conjugacy_invariant"
]
},
{
"i": 14,
"j": 16,
"heldout_inlier_ratio": 0.5918615984405458,
"heldout_inlier_rmse_m": 0.12229328437386515,
"rotation_invariant_error_deg": 1.240484202449963,
"reverse_translation_m": 0.014273957859025563,
"reverse_rotation_deg": 0.25325650727957555,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 14,
"j": 17,
"heldout_inlier_ratio": 0.6694009445687298,
"heldout_inlier_rmse_m": 0.12443900216431925,
"rotation_invariant_error_deg": 0.4822313558358869,
"reverse_translation_m": 0.0178952010004604,
"reverse_rotation_deg": 0.10920228290609924,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 15,
"j": 16,
"heldout_inlier_ratio": 0.702887537993921,
"heldout_inlier_rmse_m": 0.11495230769293859,
"rotation_invariant_error_deg": 0.16340661916523302,
"reverse_translation_m": 0.013545291843396808,
"reverse_rotation_deg": 0.03346625178333291,
"accepted": true,
"rejection_reasons": []
},
{
"i": 15,
"j": 17,
"heldout_inlier_ratio": 0.7429531936901991,
"heldout_inlier_rmse_m": 0.11679524427533863,
"rotation_invariant_error_deg": 0.050851974711708436,
"reverse_translation_m": 0.009894110027911674,
"reverse_rotation_deg": 0.07786907370565768,
"accepted": true,
"rejection_reasons": []
},
{
"i": 15,
"j": 18,
"heldout_inlier_ratio": 0.7209645010046886,
"heldout_inlier_rmse_m": 0.11716134583909138,
"rotation_invariant_error_deg": 0.26477152026833295,
"reverse_translation_m": 0.01165472931184747,
"reverse_rotation_deg": 0.13683586564190106,
"accepted": true,
"rejection_reasons": []
},
{
"i": 16,
"j": 17,
"heldout_inlier_ratio": 0.618922305764411,
"heldout_inlier_rmse_m": 0.11254196340940027,
"rotation_invariant_error_deg": 0.05549924909794868,
"reverse_translation_m": 0.031047860213503222,
"reverse_rotation_deg": 0.10375098145236057,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 16,
"j": 18,
"heldout_inlier_ratio": 0.6890156918687589,
"heldout_inlier_rmse_m": 0.11084024896736888,
"rotation_invariant_error_deg": 0.06628387310017914,
"reverse_translation_m": 0.01556225520373068,
"reverse_rotation_deg": 0.02495881796885156,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 16,
"j": 19,
"heldout_inlier_ratio": 0.8685060899826,
"heldout_inlier_rmse_m": 0.10135543575024479,
"rotation_invariant_error_deg": 0.3152201090321931,
"reverse_translation_m": 0.0029522513838272538,
"reverse_rotation_deg": 0.02753369989558306,
"accepted": true,
"rejection_reasons": []
},
{
"i": 17,
"j": 18,
"heldout_inlier_ratio": 0.7697708305735859,
"heldout_inlier_rmse_m": 0.10648049893472189,
"rotation_invariant_error_deg": 0.2007356143443495,
"reverse_translation_m": 0.010582276181446961,
"reverse_rotation_deg": 0.03959905194910384,
"accepted": true,
"rejection_reasons": []
},
{
"i": 17,
"j": 19,
"heldout_inlier_ratio": 0.6293759512937596,
"heldout_inlier_rmse_m": 0.10954497717502036,
"rotation_invariant_error_deg": 0.25455213438726787,
"reverse_translation_m": 0.012240937390578075,
"reverse_rotation_deg": 0.06030618467886912,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 18,
"j": 19,
"heldout_inlier_ratio": 0.6827314510833881,
"heldout_inlier_rmse_m": 0.10834806615100022,
"rotation_invariant_error_deg": 0.4543752098998439,
"reverse_translation_m": 0.012418496053909043,
"reverse_rotation_deg": 0.11905018881087433,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 19,
"j": 20,
"heldout_inlier_ratio": 0.6234734541714874,
"heldout_inlier_rmse_m": 0.1277530580405749,
"rotation_invariant_error_deg": 0.038058886117710244,
"reverse_translation_m": 0.009237066767190974,
"reverse_rotation_deg": 0.22540488888103255,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 20,
"j": 21,
"heldout_inlier_ratio": 0.6596992097884272,
"heldout_inlier_rmse_m": 0.12147955429086157,
"rotation_invariant_error_deg": 0.33308699073501913,
"reverse_translation_m": 0.014725537493639118,
"reverse_rotation_deg": 0.19884224722867958,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 20,
"j": 22,
"heldout_inlier_ratio": 0.5739414499308958,
"heldout_inlier_rmse_m": 0.13255415786946775,
"rotation_invariant_error_deg": 0.49226728584466883,
"reverse_translation_m": 0.006040617548523686,
"reverse_rotation_deg": 0.19706632354686843,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"heldout_rmse"
]
},
{
"i": 20,
"j": 23,
"heldout_inlier_ratio": 0.6456945156330087,
"heldout_inlier_rmse_m": 0.12075756038041646,
"rotation_invariant_error_deg": 0.4097857444953803,
"reverse_translation_m": 0.02294901101348451,
"reverse_rotation_deg": 0.19112109479244785,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 21,
"j": 22,
"heldout_inlier_ratio": 0.7716237647919971,
"heldout_inlier_rmse_m": 0.1163227135854156,
"rotation_invariant_error_deg": 0.20299476740399935,
"reverse_translation_m": 0.009963578798274064,
"reverse_rotation_deg": 0.1533289219532281,
"accepted": true,
"rejection_reasons": []
},
{
"i": 21,
"j": 23,
"heldout_inlier_ratio": 0.8576224819696593,
"heldout_inlier_rmse_m": 0.09865676260631218,
"rotation_invariant_error_deg": 0.08196486868098773,
"reverse_translation_m": 0.00392257332509571,
"reverse_rotation_deg": 0.00898487649366286,
"accepted": true,
"rejection_reasons": []
},
{
"i": 21,
"j": 24,
"heldout_inlier_ratio": 0.7715940569126165,
"heldout_inlier_rmse_m": 0.11361504553552869,
"rotation_invariant_error_deg": 0.4941752812908575,
"reverse_translation_m": 0.007475673605592584,
"reverse_rotation_deg": 0.04227769001294981,
"accepted": true,
"rejection_reasons": []
},
{
"i": 22,
"j": 23,
"heldout_inlier_ratio": 0.7396689147762109,
"heldout_inlier_rmse_m": 0.11702962848624102,
"rotation_invariant_error_deg": 0.07221906147914936,
"reverse_translation_m": 0.018140159434305195,
"reverse_rotation_deg": 0.1184732181610567,
"accepted": true,
"rejection_reasons": []
},
{
"i": 22,
"j": 25,
"heldout_inlier_ratio": 0.736861094407697,
"heldout_inlier_rmse_m": 0.1158639471011007,
"rotation_invariant_error_deg": 0.19913456977445776,
"reverse_translation_m": 0.01855955252477569,
"reverse_rotation_deg": 0.06452602797902846,
"accepted": true,
"rejection_reasons": []
},
{
"i": 23,
"j": 24,
"heldout_inlier_ratio": 0.7853164556962026,
"heldout_inlier_rmse_m": 0.11287254423109305,
"rotation_invariant_error_deg": 0.5709657470221714,
"reverse_translation_m": 0.002195759849349955,
"reverse_rotation_deg": 0.03211295915781923,
"accepted": true,
"rejection_reasons": []
},
{
"i": 23,
"j": 25,
"heldout_inlier_ratio": 0.6881127450980392,
"heldout_inlier_rmse_m": 0.12303206700403985,
"rotation_invariant_error_deg": 2.3734594630006685,
"reverse_translation_m": 0.004939188019097873,
"reverse_rotation_deg": 0.12964064637099942,
"accepted": false,
"rejection_reasons": [
"overlap_ratio",
"rotation_conjugacy_invariant"
]
},
{
"i": 23,
"j": 26,
"heldout_inlier_ratio": 0.6852618757612667,
"heldout_inlier_rmse_m": 0.12040544972155913,
"rotation_invariant_error_deg": 0.15748268923838538,
"reverse_translation_m": 0.006032445250251169,
"reverse_rotation_deg": 0.14039335223022864,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 24,
"j": 25,
"heldout_inlier_ratio": 0.677667493796526,
"heldout_inlier_rmse_m": 0.1234631580581415,
"rotation_invariant_error_deg": 0.10001483417664048,
"reverse_translation_m": 0.023479226891170584,
"reverse_rotation_deg": 0.16273859629355136,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 24,
"j": 26,
"heldout_inlier_ratio": 0.6530209617755857,
"heldout_inlier_rmse_m": 0.12710147612984235,
"rotation_invariant_error_deg": 0.7149536911782421,
"reverse_translation_m": 0.013311199903813952,
"reverse_rotation_deg": 0.18193579850150715,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 24,
"j": 27,
"heldout_inlier_ratio": 0.6649014778325123,
"heldout_inlier_rmse_m": 0.12481159614427853,
"rotation_invariant_error_deg": 0.5126580224867077,
"reverse_translation_m": 0.01787282120544869,
"reverse_rotation_deg": 0.1680556511623414,
"accepted": false,
"rejection_reasons": [
"overlap_ratio"
]
},
{
"i": 25,
"j": 26,
"heldout_inlier_ratio": 0.9127837514934289,
"heldout_inlier_rmse_m": 0.09581429165893823,
"rotation_invariant_error_deg": 0.0540123863768045,
"reverse_translation_m": 0.0035341488401767693,
"reverse_rotation_deg": 0.018440169300164816,
"accepted": true,
"rejection_reasons": []
},
{
"i": 25,
"j": 27,
"heldout_inlier_ratio": 0.8510739856801909,
"heldout_inlier_rmse_m": 0.10418150333394123,
"rotation_invariant_error_deg": 0.24124343828050598,
"reverse_translation_m": 0.006103232696003387,
"reverse_rotation_deg": 0.13366555345654282,
"accepted": true,
"rejection_reasons": []
},
{
"i": 25,
"j": 28,
"heldout_inlier_ratio": 0.8853518429870751,
"heldout_inlier_rmse_m": 0.10761251229651997,
"rotation_invariant_error_deg": 0.31032271091986274,
"reverse_translation_m": 0.0034832316659127254,
"reverse_rotation_deg": 0.03697744201993421,
"accepted": true,
"rejection_reasons": []
},
{
"i": 26,
"j": 27,
"heldout_inlier_ratio": 0.9183867141162515,
"heldout_inlier_rmse_m": 0.09074909570650827,
"rotation_invariant_error_deg": 0.1549242409220426,
"reverse_translation_m": 0.00040199505815399084,
"reverse_rotation_deg": 0.011554314408123986,
"accepted": true,
"rejection_reasons": []
},
{
"i": 26,
"j": 28,
"heldout_inlier_ratio": 0.8853200095170116,
"heldout_inlier_rmse_m": 0.1060418493965508,
"rotation_invariant_error_deg": 0.2535175622408232,
"reverse_translation_m": 0.007320240476184076,
"reverse_rotation_deg": 0.12930335868606002,
"accepted": true,
"rejection_reasons": []
},
{
"i": 26,
"j": 29,
"heldout_inlier_ratio": 0.7880466815984911,
"heldout_inlier_rmse_m": 0.10871274240898261,
"rotation_invariant_error_deg": 0.6273264323501451,
"reverse_translation_m": 0.006395243061058376,
"reverse_rotation_deg": 0.039517035902872893,
"accepted": true,
"rejection_reasons": []
},
{
"i": 27,
"j": 28,
"heldout_inlier_ratio": 0.9289448669201521,
"heldout_inlier_rmse_m": 0.08505095515326752,
"rotation_invariant_error_deg": 0.06642597183531862,
"reverse_translation_m": 0.005157655135593023,
"reverse_rotation_deg": 0.02266680787731125,
"accepted": true,
"rejection_reasons": []
},
{
"i": 27,
"j": 29,
"heldout_inlier_ratio": 0.7872365477452019,
"heldout_inlier_rmse_m": 0.1045551346483567,
"rotation_invariant_error_deg": 0.46617350279205994,
"reverse_translation_m": 0.011650639607012715,
"reverse_rotation_deg": 0.16247191340775555,
"accepted": true,
"rejection_reasons": []
},
{
"i": 28,
"j": 29,
"heldout_inlier_ratio": 0.8000944621560987,
"heldout_inlier_rmse_m": 0.10865475473581254,
"rotation_invariant_error_deg": 0.3806425437120282,
"reverse_translation_m": 0.001139416496730335,
"reverse_rotation_deg": 0.02119627748722763,
"accepted": true,
"rejection_reasons": []
},
{
"i": 28,
"j": 30,
"heldout_inlier_ratio": 0.7721000935453695,
"heldout_inlier_rmse_m": 0.10815845248211022,
"rotation_invariant_error_deg": 0.37744105649966286,
"reverse_translation_m": 0.004462522533166182,
"reverse_rotation_deg": 0.03583195432667579,
"accepted": true,
"rejection_reasons": []
},
{
"i": 28,
"j": 31,
"heldout_inlier_ratio": 0.7465330381074466,
"heldout_inlier_rmse_m": 0.10669666534392452,
"rotation_invariant_error_deg": 0.22580828656680296,
"reverse_translation_m": 0.0063576490595817345,
"reverse_rotation_deg": 0.04001661521442668,
"accepted": true,
"rejection_reasons": []
},
{
"i": 30,
"j": 31,
"heldout_inlier_ratio": 0.8185562292643862,
"heldout_inlier_rmse_m": 0.09615795732951272,
"rotation_invariant_error_deg": 0.608920703223724,
"reverse_translation_m": 0.0049991634555749285,
"reverse_rotation_deg": 0.021384795873435915,
"accepted": true,
"rejection_reasons": []
},
{
"i": 30,
"j": 32,
"heldout_inlier_ratio": 0.8041343079031521,
"heldout_inlier_rmse_m": 0.09953343153684206,
"rotation_invariant_error_deg": 0.7410181887106262,
"reverse_translation_m": 0.0031713764951448154,
"reverse_rotation_deg": 0.023767377748422268,
"accepted": true,
"rejection_reasons": []
},
{
"i": 31,
"j": 32,
"heldout_inlier_ratio": 0.8157085941946499,
"heldout_inlier_rmse_m": 0.09925944770258255,
"rotation_invariant_error_deg": 0.1261576421594981,
"reverse_translation_m": 0.004719596234885736,
"reverse_rotation_deg": 0.06146741991683861,
"accepted": true,
"rejection_reasons": []
},
{
"i": 31,
"j": 33,
"heldout_inlier_ratio": 0.760345235280208,
"heldout_inlier_rmse_m": 0.09698073659477859,
"rotation_invariant_error_deg": 0.11718235773108177,
"reverse_translation_m": 0.0019366659831763946,
"reverse_rotation_deg": 0.026538404789073603,
"accepted": true,
"rejection_reasons": []
}
]
}
@@ -0,0 +1,346 @@
{
"schema_version": 1,
"success": true,
"convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
"equation": "A_RTK_ij X = X B_LiDAR_ij",
"frames": {
"RTK": {
"origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
"x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
"y_axis": "left",
"z_axis": "up",
"yaw_enu_deg": "90 - rawHeadingDeg"
},
"LiDAR": "raw LiDAR sensor frame"
},
"backend": "open3d_gicp",
"measured_lidar_extrinsic_used_as_initial": false,
"body_heading_offset_used": false,
"body_antenna_lever_xy_used": false,
"translation_m": [
1.6368478986289665,
-0.2424214446279586,
0.08408840390370595
],
"rotation_rpy_deg_xyz": [
-0.8529068506201997,
1.3285763243955087,
-22.150129507458946
],
"quaternion_xyzw": [
-0.005076802391609619,
0.012807178841486907,
-0.19199197181185435,
0.9812997936448346
],
"matrix_4x4": [
[
0.9259501178598366,
0.3766733256085306,
0.027084774511975725,
1.6368478986289665
],
[
-0.3769334036732196,
0.9262266176745453,
0.005045979240275979,
-0.2424214446279586
],
[
-0.023185953305318648,
-0.014881481316772506,
0.9996204044951974,
0.08408840390370595
],
[
0.0,
0.0,
0.0,
1.0
]
],
"quality": {
"stations": 34,
"pairs": 41,
"residuals": {
"pairs": 41,
"translation_m": {
"rms": 0.11160362192890017,
"median": 0.06165368731012592,
"p90": 0.12167778728549836,
"p95": 0.12315870952465978,
"max": 0.45797693508387505
},
"rotation_deg": {
"rms": 1.082945057501201,
"median": 0.744916302248719,
"p90": 1.523342607477641,
"p95": 1.864505235125524,
"max": 4.332008025818138
},
"per_pair": [
{
"pair_index": 0,
"translation_m": 0.03696060713715898,
"rotation_deg": 0.7820683861381595
},
{
"pair_index": 1,
"translation_m": 0.039028552931951836,
"rotation_deg": 0.6949685743890126
},
{
"pair_index": 2,
"translation_m": 0.01732676228513485,
"rotation_deg": 1.0438060827136844
},
{
"pair_index": 3,
"translation_m": 0.0574978038017053,
"rotation_deg": 0.394946532754576
},
{
"pair_index": 4,
"translation_m": 0.10801668349596734,
"rotation_deg": 0.29336064351197005
},
{
"pair_index": 5,
"translation_m": 0.06577881887454434,
"rotation_deg": 0.5542732613092561
},
{
"pair_index": 6,
"translation_m": 0.06363940755916121,
"rotation_deg": 0.5890545170693674
},
{
"pair_index": 7,
"translation_m": 0.0640679503495012,
"rotation_deg": 0.746120063911557
},
{
"pair_index": 8,
"translation_m": 0.021009631897429052,
"rotation_deg": 0.6663196620233134
},
{
"pair_index": 9,
"translation_m": 0.08609573827254784,
"rotation_deg": 0.3687772861174946
},
{
"pair_index": 10,
"translation_m": 0.02732304440983888,
"rotation_deg": 0.5222061823280917
},
{
"pair_index": 11,
"translation_m": 0.04489317903985948,
"rotation_deg": 0.8700136243621963
},
{
"pair_index": 12,
"translation_m": 0.07260882213085644,
"rotation_deg": 0.463182802330913
},
{
"pair_index": 13,
"translation_m": 0.08831772542234817,
"rotation_deg": 0.659009021415606
},
{
"pair_index": 14,
"translation_m": 0.053804778572191785,
"rotation_deg": 1.864505235125524
},
{
"pair_index": 15,
"translation_m": 0.12315870952465978,
"rotation_deg": 1.9856861904193648
},
{
"pair_index": 16,
"translation_m": 0.06880647181572622,
"rotation_deg": 0.27331219092746517
},
{
"pair_index": 17,
"translation_m": 0.059582300078982915,
"rotation_deg": 0.3410200031868246
},
{
"pair_index": 18,
"translation_m": 0.034895300030885576,
"rotation_deg": 0.5283410202190479
},
{
"pair_index": 19,
"translation_m": 0.002366788176226253,
"rotation_deg": 0.3852038090000981
},
{
"pair_index": 20,
"translation_m": 0.06233663305653708,
"rotation_deg": 0.6208000110952433
},
{
"pair_index": 21,
"translation_m": 0.012626372544105475,
"rotation_deg": 0.38292733656142575
},
{
"pair_index": 22,
"translation_m": 0.047494831014178666,
"rotation_deg": 1.7712490998072288
},
{
"pair_index": 23,
"translation_m": 0.06165368731012592,
"rotation_deg": 0.8670376638275039
},
{
"pair_index": 24,
"translation_m": 0.12172160687143056,
"rotation_deg": 4.332008025818138
},
{
"pair_index": 25,
"translation_m": 0.06185808030991236,
"rotation_deg": 1.523342607477641
},
{
"pair_index": 26,
"translation_m": 0.049450564075379046,
"rotation_deg": 0.9499016191505171
},
{
"pair_index": 27,
"translation_m": 0.016599648000378425,
"rotation_deg": 0.7685476074882036
},
{
"pair_index": 28,
"translation_m": 0.028501035078080036,
"rotation_deg": 0.9107236146504971
},
{
"pair_index": 29,
"translation_m": 0.05418062512497841,
"rotation_deg": 0.49604339332524483
},
{
"pair_index": 30,
"translation_m": 0.02534272431328254,
"rotation_deg": 0.2798196345939902
},
{
"pair_index": 31,
"translation_m": 0.1006162178003358,
"rotation_deg": 0.9066113005741254
},
{
"pair_index": 32,
"translation_m": 0.023348232889945957,
"rotation_deg": 0.29316028754024664
},
{
"pair_index": 33,
"translation_m": 0.06352851658442724,
"rotation_deg": 1.123276625138155
},
{
"pair_index": 34,
"translation_m": 0.06685972566531029,
"rotation_deg": 0.7607492800980359
},
{
"pair_index": 35,
"translation_m": 0.3534873096430149,
"rotation_deg": 0.744916302248719
},
{
"pair_index": 36,
"translation_m": 0.45797693508387505,
"rotation_deg": 0.7684054290628693
},
{
"pair_index": 37,
"translation_m": 0.10414608850682983,
"rotation_deg": 0.8325040370564112
},
{
"pair_index": 38,
"translation_m": 0.11058068261532474,
"rotation_deg": 0.8695456609442757
},
{
"pair_index": 39,
"translation_m": 0.03405996353456899,
"rotation_deg": 0.7913171839743647
},
{
"pair_index": 40,
"translation_m": 0.12167778728549836,
"rotation_deg": 0.16247416554150054
}
]
},
"weighted_jacobian_condition_number": 7.006062559882809,
"linearized_one_sigma": {
"translation_m": [
0.007129000573825617,
0.007213998960440989,
0.0046717381822970125
],
"rotation_deg": [
0.06695429523124989,
0.06503891988826449,
0.14115095959507304
],
"warning": "conditional local estimate; bootstrap is the primary stability check"
},
"bootstrap": {
"runs": 100,
"order": [
"x_m",
"y_m",
"z_m",
"roll_deg",
"pitch_deg",
"yaw_deg"
],
"std": [
0.002608526149658222,
0.0028276716521100863,
0.0023368456176917846,
0.0795227773764157,
0.07300189168975164,
0.10947642324382982
],
"p025": [
1.6327841703535608,
-0.24653273132861378,
0.07991981050403614,
-0.9872072161381827,
1.1864367848117605,
-22.364052444549895
],
"p975": [
1.6431149050874008,
-0.2355422823589496,
0.08894657837563841,
-0.6759368885907463,
1.4669998309298398,
-21.953029139092944
]
}
},
"z_constraint": {
"observable_from_planar_AX_XB": false,
"method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
"rtk_reference_height_above_ground_m": 0.8535,
"warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion"
},
"important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification"
}
@@ -0,0 +1,156 @@
i,j,rtk_translation_m,rtk_rotation_deg,heldout_inlier_ratio,heldout_inlier_rmse_m,hessian_rank,hessian_condition,reverse_translation_m,reverse_rotation_deg,multistart_success_rate,accepted,rejection_reasons
0,1,1.6721594124489447,24.171297449440814,0.8193962748876044,0.11049675306366954,6,14.013392649694936,0.026679665410762447,0.12399936190197167,1.0,True,
0,2,2.0412175279332088,80.09074797031303,0.7525388867463684,0.11492533799491883,6,14.962108606929117,0.0018464756299783867,0.03093241101186373,1.0,True,
0,3,6.30529961936688,79.9158388329924,0.628093901505486,0.12365050970311502,6,23.490589415548122,0.010524333328230958,0.23898233567764737,0.5,True,
0,4,2.238422471863255,130.25781950514033,0.693351593625498,0.11485738628517483,6,16.305124521706706,0.003627491377787396,0.0495829610363469,1.0,True,
0,5,7.5269579262553155,88.82606603101335,0.6015065913370998,0.12959859333012305,6,26.27293180169831,0.013710015050868782,0.26025123892797375,1.0,True,
1,2,1.2843386040405174,55.9194505208722,0.7981310803891449,0.11167434332282765,6,13.484382276710306,0.0507423684848027,0.517099810570953,1.0,False,forward_reverse_rotation
1,3,5.433888607887495,55.744541383551606,0.678820988438572,0.12109867185657658,6,27.478714016210855,0.008324315958294127,0.2568985217684905,1.0,True,
1,4,1.5299424710613851,106.08652205569952,0.6772473651580905,0.1115749218038809,6,13.250326799303036,3.109603769112268,6.665689673243039,1.0,False,forward_reverse_translation;forward_reverse_rotation
1,5,7.072560501394692,64.65476858157254,0.618779694923731,0.1278696355679149,6,21.376356908960656,0.012044684321696756,0.5185219918090542,1.0,False,forward_reverse_rotation
1,6,8.049825623399226,8.10898363252498,0.02911760982402836,0.16814699881028172,6,51.15739724954147,2.3526085660101135,12.54864815902537,0.0,False,backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
2,3,4.340252832276203,0.1749091373206093,0.7609663064208518,0.11583878636902087,6,13.299472485717942,0.007842434748069874,0.017111535671962216,1.0,True,
2,4,0.2520257253555564,50.1670715348273,0.796748976299789,0.1148434235904791,6,12.041666425070192,0.011210942709506708,0.11462542679279858,1.0,True,
2,5,5.8286926576588955,8.735318060700322,0.7112112112112112,0.12001318292058727,6,15.618628103418056,0.011299298862678088,0.02158353743310138,1.0,True,
2,6,6.928094074716812,47.810466888347236,0.058659571772456606,0.1689308471089219,6,22.346184538451386,2.376212271528816,4.191297741726165,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
2,7,6.405228405426323,73.1220331951562,0.05777324320877439,0.16763553481687163,6,11.228331216247241,2.3764975144091216,1.7142235592052535,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
3,4,4.1070657333447205,50.34198067214791,0.6957378664695738,0.11619002437046365,6,17.76967737811838,3.222808271854619,15.590678196925943,1.0,False,forward_reverse_translation;forward_reverse_rotation
3,5,2.2886212019715484,8.910227198020936,0.7989069680784996,0.10775905778143813,6,12.151555874812614,0.013532537604982006,0.06485728954506689,1.0,True,
3,6,2.618668779147775,47.63555775102663,0.8435613682092555,0.11222309863990189,6,13.914901775514537,0.00951276519885504,0.09743636874086448,1.0,True,
3,7,2.7963089245830335,72.9471240578356,0.8651898734177215,0.11184568072686091,6,12.975777248765162,0.010827690226297664,0.12520280777371842,1.0,True,
3,8,2.6983089912812726,163.5692883655715,0.825590155700653,0.1078798726707026,6,12.959410142765158,0.005608807919239624,0.021536601402144962,1.0,True,
4,5,5.5767898078953735,41.431753474126985,0.6652516676773802,0.12444359189600171,6,17.028714587649738,0.0031552835887398907,0.05824661275042354,1.0,True,
4,6,6.68811709459501,97.97753842317455,0.03891480481217775,0.16130442983218543,6,53.12725754116488,5.386834276571879,3.248443974085464,1.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
4,7,6.153247042777442,123.28910472998353,0.01717321472695824,0.16597934102353687,6,197.50040966862915,1.8946664283973234,13.38395621317346,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
4,8,6.802787549686365,146.08873096228075,0.7129198332924737,0.11349036082807593,6,18.616925126379055,4.094017521116411,1.0439112418969816,1.0,False,forward_reverse_translation;forward_reverse_rotation
4,9,3.018117089902297,158.11607430100386,0.3323991714390155,0.13218238008205907,6,82.91588951856485,0.17404817666776692,0.7491904391974432,1.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
5,6,2.0562758992403367,56.545784949047565,0.7604901596732269,0.11101745020162715,6,16.790595774247244,0.01028831511886355,0.031120237309440944,1.0,True,
5,7,0.5812996709001959,81.85735125585653,0.8092687180764918,0.10777871969807898,6,15.203386410549202,0.010579733674272045,0.033334626234333836,1.0,True,
5,8,2.6887856969568644,172.47951556359513,0.40909652700531457,0.13021927164196687,6,88.03642906190348,2.282274682790372,1.1841285568948536,1.0,False,forward_reverse_translation;forward_reverse_rotation
5,9,7.501939677383991,160.45217222486954,0.3361179361179361,0.13367797847230906,6,83.15270070161475,5.255898884183195,7.177742907146106,0.5,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
5,10,7.507648391453363,143.54232919109307,0.5442391832766165,0.13173165083201846,6,26.337222871823244,4.175333583746659,14.054946454556381,1.0,False,forward_reverse_translation;forward_reverse_rotation
6,7,1.9723544820714844,25.311566306808967,0.8539354187689203,0.10462253085152279,6,12.50291076661203,0.0028433142784691904,0.028424505435071433,1.0,True,
6,8,0.7288530799256238,115.93373061454484,0.7757757757757757,0.11316400028465075,6,15.521747346102574,0.007224674181692249,0.10395594559619384,1.0,True,
6,9,7.898758206937296,103.90638727582184,0.6009202835468226,0.1259448851291812,6,28.795189977892598,4.270523553799638,1.1342776748452013,0.5,False,forward_reverse_translation;forward_reverse_rotation
6,10,8.382165572419371,159.91188585985944,0.048837495386886455,0.16976022756819517,6,33.03889916791793,8.842801960353667,9.008466157678757,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
6,11,11.12084001821623,138.54043874549885,0.026144624410151765,0.1725705028585339,6,71.57462790292871,2.4153309235424008,10.273490365819672,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
7,8,2.6747845281541447,90.62216430773587,0.8340050377833753,0.11132245152738934,6,16.17035077702852,0.004806949653405576,0.020340398656013788,1.0,True,
7,9,8.06955581661561,78.59482096901287,0.6160971335586432,0.12637042722943573,6,24.178664977065605,4.367245914434154,12.796681469446304,1.0,False,forward_reverse_translation;forward_reverse_rotation
7,10,8.088675434881791,134.6003195530505,0.04890429614956048,0.17569950505457485,6,17.12714594442953,9.4426170895353,17.952544168886714,0.0,False,backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
7,11,10.488793105910844,113.2288724386899,0.03723199383746309,0.16334679446297104,6,88.61274561425562,7.5280305092863244,55.909263646297305,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
7,12,13.79542539595065,94.95104818613552,0.023342903507676944,0.1706723033660903,6,71.17833596146563,5.060797550499477,27.755426788516992,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
8,9,7.73033999229505,12.027343338722995,0.6407549981373402,0.12189583104066957,6,28.06713348388492,2.3076573667891433,3.487290408239611,1.0,False,forward_reverse_translation;forward_reverse_rotation
8,10,8.378411682322204,43.97815524531458,0.6075420709986488,0.12933255488441212,6,20.429633989572718,4.865121579871581,2.927465091074779,1.0,False,forward_reverse_translation;forward_reverse_rotation
8,11,11.214115106391473,22.60670813095403,0.03922067999490641,0.16246238376196148,6,54.700772476792416,2.594876837596059,7.9735312612345846,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
8,12,14.373410961212507,4.328883878399634,0.023529411764705882,0.1707990278738782,6,65.92814703722017,0.7170106443431138,1.1046762967347212,0.5,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
8,13,18.141079267476005,28.43334478424675,0.020491803278688523,0.1759614106055459,6,128.98300559552638,1.9608884223852157,1.569223825596656,0.0,False,backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
9,10,1.9264207261313253,56.00549858403757,0.6576312576312576,0.1069384415347781,6,15.95767235456931,1.6379095873833018,4.808769928108965,1.0,False,forward_reverse_translation;forward_reverse_rotation
9,11,4.747620352849464,34.63405146967702,0.5883320678309288,0.11815838246331258,6,21.526672921842795,2.059598786777497,11.82652375960602,1.0,False,forward_reverse_translation;forward_reverse_rotation
9,12,7.208566899019793,16.356227217122623,0.5413589364844904,0.12469816852190199,6,38.03547459177405,1.1412129496099401,4.794166723137469,1.0,False,forward_reverse_translation;forward_reverse_rotation
9,13,10.706998136562337,16.406001445523756,0.015145729922362225,0.16400783300033744,6,309.6688821904962,3.999785287368469,12.657822343539058,0.5,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
9,14,7.911071381405571,14.085282580602351,0.5416463116756228,0.12859551377809345,6,26.721469573230685,0.011377143482079926,0.04588334980819398,1.0,True,
10,11,3.0731501091601263,21.371447114360553,0.7131414267834794,0.12095106901516853,6,13.404202373771934,0.006897671932216771,0.18540056788520015,1.0,True,
10,12,6.011368280631378,39.649271366914945,0.6117876278616659,0.1250022412120491,6,19.806559061723252,0.006509808900810373,0.0674238161099294,1.0,True,
10,13,9.76425648692991,72.41150002956132,0.5244808055380743,0.1368926377190841,6,36.182352720806286,0.004651842966001154,0.17931102925270942,1.0,True,
10,14,6.554187411792988,41.92021600343522,0.5996858385693572,0.12955691635611982,6,17.916357473627126,0.009593578345611885,0.0575378323241282,1.0,True,
10,15,10.1706917218607,65.51298166784419,0.5048970366649924,0.13966622273060353,6,30.173625507677606,0.006074999657627903,0.048675594115713976,1.0,True,
11,12,3.3258193587172027,18.277824252554396,0.6508076728924785,0.12017753597340275,6,15.211760062254436,0.016302173123952872,0.05141023051579196,1.0,True,
11,13,7.195203402213438,51.04005291520078,0.5666710199817161,0.12966061077144855,6,26.1420797484443,0.010250004424021028,0.06307533415790957,1.0,True,
11,14,3.634158560526847,20.548768889074672,0.64271407110666,0.12109534664165013,6,14.045896850674039,0.0076106764286992265,0.053501024445426364,1.0,True,
11,15,7.446972831184529,44.14153455348363,0.570479416362689,0.12836105648415896,6,18.857231663145765,0.006898635427401595,0.028532809464236104,1.0,True,
11,16,10.651790397923806,69.98495270981525,0.47336531178995206,0.13436934972977357,6,42.57049752296035,0.03569532774909474,0.4066994385898772,1.0,True,
12,13,3.8697507070400543,32.762228662646386,0.7056733087955325,0.1168985924651875,6,14.509757354686162,0.0020816591736723326,0.08889045468170857,1.0,True,
12,14,0.9871080784265185,2.2709446365202766,0.8745432399512789,0.09766070609034913,6,11.857755748379178,0.0024736851957500175,0.01806957610594206,1.0,True,
12,15,4.171948395051693,25.86371030092923,0.7033426183844012,0.1210390118956012,6,11.859397045728281,0.02278023379659275,0.04927694758545221,1.0,True,
12,16,7.331699780686257,51.70712845726085,0.5820235756385069,0.1245813395619955,6,32.351864267271324,0.009898398498331785,0.03808519306435069,1.0,True,
12,17,6.344245780495681,1.4248238883471156,0.6542219994988725,0.12658997017247905,6,11.229930872030522,0.019742666743374927,0.07899239993213694,1.0,True,
13,14,3.7992314627329202,30.491284026126113,0.6984766461034874,0.11406275275916469,6,13.924716870947337,0.012533601308866885,0.10861598809330086,1.0,True,
13,15,0.9105848166450461,6.898518361717151,0.8794391298650243,0.0990320912382964,6,10.514819201987361,0.006638809913640662,0.04266354358349535,1.0,True,
13,16,3.4957081323467,18.94489979461447,0.7273073505141552,0.10958532316684444,6,19.405056504086563,0.005441055528599314,0.12519365495729431,1.0,True,
13,17,2.9366461487378266,31.337404774299262,0.7130265716137395,0.11779895987026272,6,12.816390530620648,0.013263327701592529,0.16221305155705523,1.0,True,
13,18,5.248032013425982,3.445996452937746,0.7019876443728176,0.11940768524727288,6,25.985486690964827,0.008995185112868311,0.05198334816510605,1.0,True,
14,15,3.8816793634199405,23.592765664408958,0.7089927153981411,0.11692319124843933,6,10.742828632896593,0.09073157715426228,0.8466374858545868,0.5,False,forward_reverse_translation;forward_reverse_rotation
14,16,7.29101265002769,49.43618382074057,0.5923489278752436,0.12241125802320883,6,33.653658158107916,0.003104270324855819,0.13386021564092,1.0,True,
14,17,5.915950814087913,0.8461207481731609,0.6687795177728063,0.12452036369781098,6,8.953051466023156,0.011592867275090568,0.10132037554601482,1.0,True,
14,18,8.433581718971825,27.04528757318836,0.6196476790536196,0.12845389972151766,6,17.805501350543512,0.010071755472376367,0.13041952825792016,1.0,True,
14,19,9.056542482242314,79.47127430600666,0.5845660749506904,0.12584132662356765,6,30.335272352492893,0.016830803029543952,0.25747616717579747,1.0,True,
15,16,3.579287497246505,25.843418156331627,0.7032674772036475,0.1160221689285562,6,22.696890954829914,0.0009988867448377137,0.0903616813411077,1.0,True,
15,17,2.2400625117908257,24.438886412582114,0.7489009568140678,0.11923375870790395,6,6.944925131742929,0.02582458487951321,0.09503968088936432,1.0,True,
15,18,4.6956742726068,3.452521908779405,0.7165438713998661,0.1178392296206422,6,19.334165264362177,0.009354386824150452,0.1747840133793935,1.0,True,
15,19,5.176369821469625,55.87850864159772,0.6924358974358974,0.11506895717743917,6,20.417126084614868,0.012497900854286311,0.3255615001296683,1.0,True,
15,20,1.1877416357686716,128.54504202158432,0.6751867872591427,0.11901811643083532,6,27.381712286843683,2.670174543103617,1.9448445706434312,1.0,False,forward_reverse_translation;forward_reverse_rotation
16,17,2.956793995513645,50.28230456891372,0.6284461152882206,0.11136186275509914,6,25.18428705575399,0.015372505619716304,0.0751987172187524,1.0,True,
16,18,3.369822545690391,22.390896247552213,0.6921281286473868,0.10919235768364494,6,39.42922894015897,0.005355462743716019,0.036543030274398446,1.0,True,
16,19,2.313038703742191,30.035090485266096,0.8880188913745961,0.09683468613705355,6,11.709094307553842,0.006745004702224827,0.04851926363284082,1.0,True,
16,20,4.242518045976368,102.70162386525263,0.36698412698412697,0.13430062225326117,6,102.05441227788522,1.6194077849835204,3.5461435258834046,1.0,False,forward_reverse_translation;forward_reverse_rotation
16,21,9.189957911290794,153.31148960700713,0.23764328854924197,0.14863424676851908,6,75.99451631707726,0.01721606559516444,0.561012451825117,0.5,False,heldout_inlier_ratio;forward_reverse_rotation
17,18,2.517968959880004,27.89140832136152,0.7665916015366274,0.11462271017395576,6,9.759734204828526,0.003677259621955875,0.03455865038654393,1.0,True,
17,19,3.518310045065406,80.31739505417983,0.645738203957382,0.11467441399973677,6,29.935027493013713,0.002657916871055147,0.03996637099866608,1.0,True,
17,20,3.4199679241812992,152.98392843416642,0.2749902761571373,0.1378092692558536,6,36.98799912625142,0.9962499249666478,0.8583377913930432,0.5,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
17,21,8.339875108212771,156.4062058240796,0.4672368255565338,0.13751533768592306,6,32.19730139309397,3.7101311942981123,2.4525134629880094,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
17,22,10.944066586184825,150.8296621811644,0.4255952380952381,0.14178517078123323,6,38.71138397815585,1.8838258929727458,2.643696557684808,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
18,19,2.0416624616211574,52.42598673281832,0.6999343401181878,0.10876407223187459,6,36.91910298018587,0.0029722527816906435,0.028553700929610345,1.0,True,
18,20,5.836864764777489,125.0925201128049,0.5796614723267061,0.12778176089815013,6,32.46060078153021,0.08496447343578362,0.24110896992060832,0.5,False,forward_reverse_translation
18,21,10.84206419743439,175.70238585457497,0.23118979432439468,0.15492240575842026,6,95.57603256530417,0.3127879752995947,1.8359225701448998,1.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
18,22,13.461930516546937,178.72107050264088,0.20872354073123797,0.1539528469442395,6,158.96979855906298,5.509854679183967,5.400016950581102,1.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
18,23,10.787990019880349,164.1156966348418,0.3967277486910995,0.14073241205403234,6,67.81571155690442,0.037998014107336324,0.09667096210109852,1.0,True,
19,20,6.120105723142265,72.6665333799865,0.6260444787247719,0.1266849163783949,6,25.979821491778758,0.02278172414929769,0.1983479736758361,1.0,True,
19,21,11.201089261967727,123.27639912174082,0.22215292503430212,0.15090955425182226,6,96.8936157485445,2.289236102137041,0.9775110057344923,0.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
19,22,13.962230939038237,128.85294276465584,0.1883148831488315,0.1538013519108862,6,137.2212791172918,1.512900854675742,1.7779489198947585,1.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
19,23,10.877481155608614,143.45831663234054,0.37768025078369905,0.1432975121556297,6,38.93505210462377,2.6484748594978824,0.9762972275431752,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
19,24,10.384369483528566,177.87107120381796,0.43504761904761907,0.1392444656116938,6,41.9366060613255,0.04258350776081601,0.7879276702809289,1.0,False,forward_reverse_rotation
20,21,5.091648376409225,50.60986574175432,0.6607188376242672,0.12214671257866083,6,14.082798146569486,0.014717307768564562,0.07480162341419787,1.0,True,
20,22,7.842914217245693,56.186409384669375,0.576328684508104,0.13245799460807808,6,16.034011252152304,0.021651469263481868,0.4612054468064915,1.0,True,
20,23,4.953146569484805,70.79178325235414,0.6451819579702717,0.1210226317867539,6,12.51146899612653,0.018249896901540018,0.12115759970964086,1.0,True,
20,24,4.806112840058592,105.20453782384023,0.7383177570093458,0.11441422046802803,6,12.152737785424252,0.009011280440944078,0.06430292826078875,1.0,True,
20,25,7.7432318481763245,68.57831319871164,0.6182822702159718,0.12791257682460658,6,29.104161441720713,0.01470353026057929,0.3205553574328468,1.0,True,
21,22,2.836151129858731,5.576543642915048,0.7740636818348177,0.11564789267022943,6,12.209471497858695,0.006518431057241462,0.06991931948827856,1.0,True,
21,23,1.4635995041292513,20.181917510599828,0.862223327530465,0.10307112004551743,6,11.124777032517395,0.002678668765812511,0.01879593375546071,1.0,True,
21,24,2.7001854883863183,54.59467208208592,0.7795265676152102,0.11182158240581809,6,15.934013426145448,0.003491995501354943,0.037425651095358885,1.0,True,
21,25,3.6513937480713023,17.968447456957325,0.7340892465252378,0.12002239056891176,6,16.572162980052227,0.03773466739435234,0.28781074838303833,1.0,True,
21,26,4.368847767445087,60.912845043424156,0.7147358216190014,0.11997311573294335,6,17.3723156532691,0.01216183417643751,0.10312122071404906,1.0,True,
22,23,3.806551883906871,14.605373867684776,0.7408951563458002,0.1172672510975272,6,12.610213323576234,0.009206244303296198,0.0960198600148152,1.0,True,
22,24,4.999470711928798,49.01812843917085,0.6936064556176288,0.11914624513148228,6,13.20338761495324,0.006090737153725476,0.02755713841749006,1.0,True,
22,25,2.281002791409386,12.391903814042275,0.7356584485868911,0.11474070552638106,6,16.673633938013026,0.001961709159757097,0.011643770804742994,1.0,True,
22,26,1.990287035474152,55.33630140050909,0.7422594142259414,0.11765221626900067,6,18.880420396501982,0.007814937371704422,0.03934255356487579,1.0,True,
22,27,2.5532406896142295,80.6206767037528,0.7254925373134329,0.11870181965154772,6,21.309389349405333,0.018241823604788293,0.08279236858581901,1.0,True,
23,24,1.2663665558774873,34.41275457148608,0.7884810126582279,0.10662565692629541,6,10.611199681165658,0.0018823381482244372,0.020239211377623904,1.0,True,
23,25,5.050865141821003,2.213470053642503,0.6843137254901961,0.1217109193489842,6,15.15080906413716,0.01463985673592735,0.20460048921133533,1.0,True,
23,26,5.595299803053147,40.730927532824325,0.6772228989037758,0.12237954766026346,6,16.382334072569808,0.04518647006837217,0.20367965681897174,1.0,True,
23,27,6.240759831138249,66.01530283606803,0.6637469586374696,0.12340951265232125,6,20.64383986204704,0.010626193556947943,1.1817079481882706,1.0,False,forward_reverse_rotation
23,28,6.598772106458927,85.87759904182478,0.6720351390922401,0.12122778564083768,6,19.206570679040215,0.043540468662592216,0.2605147805386839,0.5,True,
24,25,6.316196707646632,36.626224625128586,0.6764267990074442,0.12367636388755723,6,21.50176872604184,0.03581640576644142,0.20465043373191275,1.0,True,
24,26,6.840027061556236,6.318172961338242,0.6524044389642417,0.12740222503880924,6,21.75476709306229,0.0620474433075452,0.12014838295938772,1.0,True,
24,27,7.477875812552711,31.60254826458195,0.6546798029556651,0.12425657449629156,6,26.295019203914542,0.05403266260961897,0.2126374478532295,1.0,True,
24,28,7.81303641449596,51.464844470338676,0.6614377470355731,0.12010155595807544,6,20.985000954928537,0.04949067679791638,0.25138526875322614,0.5,True,
24,29,7.4949096209288655,93.73390958258972,0.047106325706594884,0.16491171897379944,6,28.651559198797667,0.8785910837751835,4.6775880176920355,0.0,False,heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
25,26,1.433673687807028,42.944397586466835,0.9137395459976105,0.08148335762110498,6,16.56285514245561,0.003236736725553251,0.006670817258604747,1.0,True,
25,27,1.973107678535245,68.22877288971054,0.8596658711217183,0.10961458820309757,6,22.188153380460914,0.010624789877375612,0.04475651255675051,1.0,True,
25,28,2.578633669986193,88.09106909546726,0.8839157491622786,0.10490699814192775,6,16.172179483480598,0.0028341913749953818,0.01585308444597317,1.0,True,
25,29,1.4869736566208207,130.3601342077183,0.7857227558401518,0.10954358768244278,6,20.132087187715292,0.0032477187428175502,0.016542353808297643,1.0,True,
25,30,5.843711828111692,139.6773015481418,0.05061061531235322,0.15827452276344428,6,186.52424080569762,2.4401001990983646,2.630476332375907,0.0,False,heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
26,27,0.6711664435459649,25.284375303243706,0.9188612099644128,0.07520973241900301,6,20.799051531446725,0.00099208733077853,0.005485007138530622,1.0,True,
26,28,1.202247282991071,45.146671509000434,0.9182726623840114,0.07611768518866213,6,21.995419584098137,0.004984978187528897,0.011735070988964648,1.0,True,
26,29,0.8313560685788559,87.41573662125148,0.7856890251090416,0.1085139306178217,6,24.280602674778585,0.002586732426994246,0.12205883610742861,1.0,True,
26,30,5.554376083336843,177.37830086539105,0.7634835395750642,0.10495724850674515,6,34.80647378548566,0.008489213184549637,0.021733210949119494,1.0,True,
26,31,6.5424104132673175,156.01119868987084,0.7374054682955207,0.10737521663044328,6,38.310555401782864,0.007807741115783734,0.04372865220115068,0.5,True,
27,28,0.6147316450225401,19.862296205756735,0.9281131178707225,0.07220941715117642,6,20.092791877654474,0.002300778353404822,0.001589714984734129,1.0,True,
27,29,0.7540837235696874,62.13136131800778,0.7871188037207112,0.10966659884725233,6,24.009650087098127,0.005134403698946511,0.024348440992038003,1.0,True,
27,30,5.07252652550922,152.0939255621477,0.7922108208955224,0.10147035321534549,6,32.52026042532519,0.007488026390185518,0.03860522829819172,1.0,True,
27,31,6.285022243874859,178.7044260068885,0.7592097617664149,0.10539218018667616,6,48.70669958381935,0.0020937297862771895,0.027401753528281184,1.0,True,
27,32,5.778763736289045,138.47370648510574,0.7480278422273782,0.10408734715846861,6,42.21510199826032,0.004407463145301957,0.03490543724835993,0.5,True,
28,29,1.3242039147826599,42.26906511225104,0.787814381863266,0.10901449493832827,6,25.9136254196751,0.01570175790237293,0.035696604981368125,1.0,True,
28,30,5.091487440029177,132.23162935639098,0.7791159962581852,0.1043856075500982,6,36.50167792578953,0.011701496185342901,0.047047156803733815,1.0,True,
28,31,6.538757407908195,158.84212980112872,0.7449015266285981,0.10668967013687278,6,48.04636971250768,0.02442137437725171,0.6810283060803413,1.0,False,forward_reverse_rotation
28,32,5.963604730984572,158.33600269086236,0.7252210330386226,0.11213192172123396,6,59.78207706090093,0.01795092616246746,0.05811390678775167,0.0,False,multistart_instability
28,33,5.80807626014008,67.05379893079936,0.6692465836255895,0.10871806386783625,6,54.67376255043743,0.132593515882677,0.6593919290562373,0.5,False,forward_reverse_translation;forward_reverse_rotation
29,30,4.767831371266539,89.96256424413991,0.7320662880982732,0.10929719051930432,6,30.745355096911858,0.005463604154020641,0.01858348348785812,1.0,True,
29,31,5.715598450796842,116.57306468887764,0.7254562254562255,0.11323336570178014,6,49.09678922599784,0.005077799585122041,0.07265894091769737,1.0,True,
29,32,5.281749147864957,159.39493219688646,0.33356393404819557,0.1449978595558761,6,111.7227195582132,0.3588603464467423,0.15715476414253352,1.0,False,heldout_inlier_ratio;forward_reverse_translation
29,33,4.779166013738703,109.3228640430504,0.2850467289719626,0.13990657628540273,6,178.4302458902832,0.39012209563155037,0.27153598748575447,0.0,False,backend_not_converged;heldout_inlier_ratio;forward_reverse_translation;multistart_instability
30,31,2.604154101624297,26.610500444737717,0.8286237272623269,0.09646596945131831,6,41.03513305025278,0.018905314487389895,0.08632503464138006,1.0,True,
30,32,1.69105635082049,69.43236795274656,0.8065326633165829,0.09815063400019147,6,35.25635856167186,0.007341509941520377,0.023973741904830165,1.0,True,
30,33,3.2411277385703925,160.7145717128097,0.09253766757622493,0.15464713396746754,6,31.157195533215592,1.0417523955000538,7.5233256615684665,1.0,False,backend_not_converged;heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
31,32,0.9137201114724802,42.82186750800884,0.8146841206602162,0.09914628416022428,6,34.3317318149268,0.0693687705829607,0.45703051311645604,1.0,True,
31,33,1.4965271484681508,134.10407126807198,0.7551430598250177,0.1003246191461096,6,28.537349904719445,0.005559091155809675,0.033005318250111694,1.0,True,
32,33,1.9169426499676907,91.28220376006315,0.7585270860380031,0.0987597723287551,6,32.396623704761396,2.8694889670610495,5.798276157700327,0.0,False,forward_reverse_translation;forward_reverse_rotation;multistart_instability
1 i j rtk_translation_m rtk_rotation_deg heldout_inlier_ratio heldout_inlier_rmse_m hessian_rank hessian_condition reverse_translation_m reverse_rotation_deg multistart_success_rate accepted rejection_reasons
2 0 1 1.6721594124489447 24.171297449440814 0.8193962748876044 0.11049675306366954 6 14.013392649694936 0.026679665410762447 0.12399936190197167 1.0 True
3 0 2 2.0412175279332088 80.09074797031303 0.7525388867463684 0.11492533799491883 6 14.962108606929117 0.0018464756299783867 0.03093241101186373 1.0 True
4 0 3 6.30529961936688 79.9158388329924 0.628093901505486 0.12365050970311502 6 23.490589415548122 0.010524333328230958 0.23898233567764737 0.5 True
5 0 4 2.238422471863255 130.25781950514033 0.693351593625498 0.11485738628517483 6 16.305124521706706 0.003627491377787396 0.0495829610363469 1.0 True
6 0 5 7.5269579262553155 88.82606603101335 0.6015065913370998 0.12959859333012305 6 26.27293180169831 0.013710015050868782 0.26025123892797375 1.0 True
7 1 2 1.2843386040405174 55.9194505208722 0.7981310803891449 0.11167434332282765 6 13.484382276710306 0.0507423684848027 0.517099810570953 1.0 False forward_reverse_rotation
8 1 3 5.433888607887495 55.744541383551606 0.678820988438572 0.12109867185657658 6 27.478714016210855 0.008324315958294127 0.2568985217684905 1.0 True
9 1 4 1.5299424710613851 106.08652205569952 0.6772473651580905 0.1115749218038809 6 13.250326799303036 3.109603769112268 6.665689673243039 1.0 False forward_reverse_translation;forward_reverse_rotation
10 1 5 7.072560501394692 64.65476858157254 0.618779694923731 0.1278696355679149 6 21.376356908960656 0.012044684321696756 0.5185219918090542 1.0 False forward_reverse_rotation
11 1 6 8.049825623399226 8.10898363252498 0.02911760982402836 0.16814699881028172 6 51.15739724954147 2.3526085660101135 12.54864815902537 0.0 False backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
12 2 3 4.340252832276203 0.1749091373206093 0.7609663064208518 0.11583878636902087 6 13.299472485717942 0.007842434748069874 0.017111535671962216 1.0 True
13 2 4 0.2520257253555564 50.1670715348273 0.796748976299789 0.1148434235904791 6 12.041666425070192 0.011210942709506708 0.11462542679279858 1.0 True
14 2 5 5.8286926576588955 8.735318060700322 0.7112112112112112 0.12001318292058727 6 15.618628103418056 0.011299298862678088 0.02158353743310138 1.0 True
15 2 6 6.928094074716812 47.810466888347236 0.058659571772456606 0.1689308471089219 6 22.346184538451386 2.376212271528816 4.191297741726165 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
16 2 7 6.405228405426323 73.1220331951562 0.05777324320877439 0.16763553481687163 6 11.228331216247241 2.3764975144091216 1.7142235592052535 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
17 3 4 4.1070657333447205 50.34198067214791 0.6957378664695738 0.11619002437046365 6 17.76967737811838 3.222808271854619 15.590678196925943 1.0 False forward_reverse_translation;forward_reverse_rotation
18 3 5 2.2886212019715484 8.910227198020936 0.7989069680784996 0.10775905778143813 6 12.151555874812614 0.013532537604982006 0.06485728954506689 1.0 True
19 3 6 2.618668779147775 47.63555775102663 0.8435613682092555 0.11222309863990189 6 13.914901775514537 0.00951276519885504 0.09743636874086448 1.0 True
20 3 7 2.7963089245830335 72.9471240578356 0.8651898734177215 0.11184568072686091 6 12.975777248765162 0.010827690226297664 0.12520280777371842 1.0 True
21 3 8 2.6983089912812726 163.5692883655715 0.825590155700653 0.1078798726707026 6 12.959410142765158 0.005608807919239624 0.021536601402144962 1.0 True
22 4 5 5.5767898078953735 41.431753474126985 0.6652516676773802 0.12444359189600171 6 17.028714587649738 0.0031552835887398907 0.05824661275042354 1.0 True
23 4 6 6.68811709459501 97.97753842317455 0.03891480481217775 0.16130442983218543 6 53.12725754116488 5.386834276571879 3.248443974085464 1.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
24 4 7 6.153247042777442 123.28910472998353 0.01717321472695824 0.16597934102353687 6 197.50040966862915 1.8946664283973234 13.38395621317346 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
25 4 8 6.802787549686365 146.08873096228075 0.7129198332924737 0.11349036082807593 6 18.616925126379055 4.094017521116411 1.0439112418969816 1.0 False forward_reverse_translation;forward_reverse_rotation
26 4 9 3.018117089902297 158.11607430100386 0.3323991714390155 0.13218238008205907 6 82.91588951856485 0.17404817666776692 0.7491904391974432 1.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
27 5 6 2.0562758992403367 56.545784949047565 0.7604901596732269 0.11101745020162715 6 16.790595774247244 0.01028831511886355 0.031120237309440944 1.0 True
28 5 7 0.5812996709001959 81.85735125585653 0.8092687180764918 0.10777871969807898 6 15.203386410549202 0.010579733674272045 0.033334626234333836 1.0 True
29 5 8 2.6887856969568644 172.47951556359513 0.40909652700531457 0.13021927164196687 6 88.03642906190348 2.282274682790372 1.1841285568948536 1.0 False forward_reverse_translation;forward_reverse_rotation
30 5 9 7.501939677383991 160.45217222486954 0.3361179361179361 0.13367797847230906 6 83.15270070161475 5.255898884183195 7.177742907146106 0.5 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
31 5 10 7.507648391453363 143.54232919109307 0.5442391832766165 0.13173165083201846 6 26.337222871823244 4.175333583746659 14.054946454556381 1.0 False forward_reverse_translation;forward_reverse_rotation
32 6 7 1.9723544820714844 25.311566306808967 0.8539354187689203 0.10462253085152279 6 12.50291076661203 0.0028433142784691904 0.028424505435071433 1.0 True
33 6 8 0.7288530799256238 115.93373061454484 0.7757757757757757 0.11316400028465075 6 15.521747346102574 0.007224674181692249 0.10395594559619384 1.0 True
34 6 9 7.898758206937296 103.90638727582184 0.6009202835468226 0.1259448851291812 6 28.795189977892598 4.270523553799638 1.1342776748452013 0.5 False forward_reverse_translation;forward_reverse_rotation
35 6 10 8.382165572419371 159.91188585985944 0.048837495386886455 0.16976022756819517 6 33.03889916791793 8.842801960353667 9.008466157678757 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
36 6 11 11.12084001821623 138.54043874549885 0.026144624410151765 0.1725705028585339 6 71.57462790292871 2.4153309235424008 10.273490365819672 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
37 7 8 2.6747845281541447 90.62216430773587 0.8340050377833753 0.11132245152738934 6 16.17035077702852 0.004806949653405576 0.020340398656013788 1.0 True
38 7 9 8.06955581661561 78.59482096901287 0.6160971335586432 0.12637042722943573 6 24.178664977065605 4.367245914434154 12.796681469446304 1.0 False forward_reverse_translation;forward_reverse_rotation
39 7 10 8.088675434881791 134.6003195530505 0.04890429614956048 0.17569950505457485 6 17.12714594442953 9.4426170895353 17.952544168886714 0.0 False backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
40 7 11 10.488793105910844 113.2288724386899 0.03723199383746309 0.16334679446297104 6 88.61274561425562 7.5280305092863244 55.909263646297305 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
41 7 12 13.79542539595065 94.95104818613552 0.023342903507676944 0.1706723033660903 6 71.17833596146563 5.060797550499477 27.755426788516992 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
42 8 9 7.73033999229505 12.027343338722995 0.6407549981373402 0.12189583104066957 6 28.06713348388492 2.3076573667891433 3.487290408239611 1.0 False forward_reverse_translation;forward_reverse_rotation
43 8 10 8.378411682322204 43.97815524531458 0.6075420709986488 0.12933255488441212 6 20.429633989572718 4.865121579871581 2.927465091074779 1.0 False forward_reverse_translation;forward_reverse_rotation
44 8 11 11.214115106391473 22.60670813095403 0.03922067999490641 0.16246238376196148 6 54.700772476792416 2.594876837596059 7.9735312612345846 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
45 8 12 14.373410961212507 4.328883878399634 0.023529411764705882 0.1707990278738782 6 65.92814703722017 0.7170106443431138 1.1046762967347212 0.5 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
46 8 13 18.141079267476005 28.43334478424675 0.020491803278688523 0.1759614106055459 6 128.98300559552638 1.9608884223852157 1.569223825596656 0.0 False backend_not_converged;heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
47 9 10 1.9264207261313253 56.00549858403757 0.6576312576312576 0.1069384415347781 6 15.95767235456931 1.6379095873833018 4.808769928108965 1.0 False forward_reverse_translation;forward_reverse_rotation
48 9 11 4.747620352849464 34.63405146967702 0.5883320678309288 0.11815838246331258 6 21.526672921842795 2.059598786777497 11.82652375960602 1.0 False forward_reverse_translation;forward_reverse_rotation
49 9 12 7.208566899019793 16.356227217122623 0.5413589364844904 0.12469816852190199 6 38.03547459177405 1.1412129496099401 4.794166723137469 1.0 False forward_reverse_translation;forward_reverse_rotation
50 9 13 10.706998136562337 16.406001445523756 0.015145729922362225 0.16400783300033744 6 309.6688821904962 3.999785287368469 12.657822343539058 0.5 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation
51 9 14 7.911071381405571 14.085282580602351 0.5416463116756228 0.12859551377809345 6 26.721469573230685 0.011377143482079926 0.04588334980819398 1.0 True
52 10 11 3.0731501091601263 21.371447114360553 0.7131414267834794 0.12095106901516853 6 13.404202373771934 0.006897671932216771 0.18540056788520015 1.0 True
53 10 12 6.011368280631378 39.649271366914945 0.6117876278616659 0.1250022412120491 6 19.806559061723252 0.006509808900810373 0.0674238161099294 1.0 True
54 10 13 9.76425648692991 72.41150002956132 0.5244808055380743 0.1368926377190841 6 36.182352720806286 0.004651842966001154 0.17931102925270942 1.0 True
55 10 14 6.554187411792988 41.92021600343522 0.5996858385693572 0.12955691635611982 6 17.916357473627126 0.009593578345611885 0.0575378323241282 1.0 True
56 10 15 10.1706917218607 65.51298166784419 0.5048970366649924 0.13966622273060353 6 30.173625507677606 0.006074999657627903 0.048675594115713976 1.0 True
57 11 12 3.3258193587172027 18.277824252554396 0.6508076728924785 0.12017753597340275 6 15.211760062254436 0.016302173123952872 0.05141023051579196 1.0 True
58 11 13 7.195203402213438 51.04005291520078 0.5666710199817161 0.12966061077144855 6 26.1420797484443 0.010250004424021028 0.06307533415790957 1.0 True
59 11 14 3.634158560526847 20.548768889074672 0.64271407110666 0.12109534664165013 6 14.045896850674039 0.0076106764286992265 0.053501024445426364 1.0 True
60 11 15 7.446972831184529 44.14153455348363 0.570479416362689 0.12836105648415896 6 18.857231663145765 0.006898635427401595 0.028532809464236104 1.0 True
61 11 16 10.651790397923806 69.98495270981525 0.47336531178995206 0.13436934972977357 6 42.57049752296035 0.03569532774909474 0.4066994385898772 1.0 True
62 12 13 3.8697507070400543 32.762228662646386 0.7056733087955325 0.1168985924651875 6 14.509757354686162 0.0020816591736723326 0.08889045468170857 1.0 True
63 12 14 0.9871080784265185 2.2709446365202766 0.8745432399512789 0.09766070609034913 6 11.857755748379178 0.0024736851957500175 0.01806957610594206 1.0 True
64 12 15 4.171948395051693 25.86371030092923 0.7033426183844012 0.1210390118956012 6 11.859397045728281 0.02278023379659275 0.04927694758545221 1.0 True
65 12 16 7.331699780686257 51.70712845726085 0.5820235756385069 0.1245813395619955 6 32.351864267271324 0.009898398498331785 0.03808519306435069 1.0 True
66 12 17 6.344245780495681 1.4248238883471156 0.6542219994988725 0.12658997017247905 6 11.229930872030522 0.019742666743374927 0.07899239993213694 1.0 True
67 13 14 3.7992314627329202 30.491284026126113 0.6984766461034874 0.11406275275916469 6 13.924716870947337 0.012533601308866885 0.10861598809330086 1.0 True
68 13 15 0.9105848166450461 6.898518361717151 0.8794391298650243 0.0990320912382964 6 10.514819201987361 0.006638809913640662 0.04266354358349535 1.0 True
69 13 16 3.4957081323467 18.94489979461447 0.7273073505141552 0.10958532316684444 6 19.405056504086563 0.005441055528599314 0.12519365495729431 1.0 True
70 13 17 2.9366461487378266 31.337404774299262 0.7130265716137395 0.11779895987026272 6 12.816390530620648 0.013263327701592529 0.16221305155705523 1.0 True
71 13 18 5.248032013425982 3.445996452937746 0.7019876443728176 0.11940768524727288 6 25.985486690964827 0.008995185112868311 0.05198334816510605 1.0 True
72 14 15 3.8816793634199405 23.592765664408958 0.7089927153981411 0.11692319124843933 6 10.742828632896593 0.09073157715426228 0.8466374858545868 0.5 False forward_reverse_translation;forward_reverse_rotation
73 14 16 7.29101265002769 49.43618382074057 0.5923489278752436 0.12241125802320883 6 33.653658158107916 0.003104270324855819 0.13386021564092 1.0 True
74 14 17 5.915950814087913 0.8461207481731609 0.6687795177728063 0.12452036369781098 6 8.953051466023156 0.011592867275090568 0.10132037554601482 1.0 True
75 14 18 8.433581718971825 27.04528757318836 0.6196476790536196 0.12845389972151766 6 17.805501350543512 0.010071755472376367 0.13041952825792016 1.0 True
76 14 19 9.056542482242314 79.47127430600666 0.5845660749506904 0.12584132662356765 6 30.335272352492893 0.016830803029543952 0.25747616717579747 1.0 True
77 15 16 3.579287497246505 25.843418156331627 0.7032674772036475 0.1160221689285562 6 22.696890954829914 0.0009988867448377137 0.0903616813411077 1.0 True
78 15 17 2.2400625117908257 24.438886412582114 0.7489009568140678 0.11923375870790395 6 6.944925131742929 0.02582458487951321 0.09503968088936432 1.0 True
79 15 18 4.6956742726068 3.452521908779405 0.7165438713998661 0.1178392296206422 6 19.334165264362177 0.009354386824150452 0.1747840133793935 1.0 True
80 15 19 5.176369821469625 55.87850864159772 0.6924358974358974 0.11506895717743917 6 20.417126084614868 0.012497900854286311 0.3255615001296683 1.0 True
81 15 20 1.1877416357686716 128.54504202158432 0.6751867872591427 0.11901811643083532 6 27.381712286843683 2.670174543103617 1.9448445706434312 1.0 False forward_reverse_translation;forward_reverse_rotation
82 16 17 2.956793995513645 50.28230456891372 0.6284461152882206 0.11136186275509914 6 25.18428705575399 0.015372505619716304 0.0751987172187524 1.0 True
83 16 18 3.369822545690391 22.390896247552213 0.6921281286473868 0.10919235768364494 6 39.42922894015897 0.005355462743716019 0.036543030274398446 1.0 True
84 16 19 2.313038703742191 30.035090485266096 0.8880188913745961 0.09683468613705355 6 11.709094307553842 0.006745004702224827 0.04851926363284082 1.0 True
85 16 20 4.242518045976368 102.70162386525263 0.36698412698412697 0.13430062225326117 6 102.05441227788522 1.6194077849835204 3.5461435258834046 1.0 False forward_reverse_translation;forward_reverse_rotation
86 16 21 9.189957911290794 153.31148960700713 0.23764328854924197 0.14863424676851908 6 75.99451631707726 0.01721606559516444 0.561012451825117 0.5 False heldout_inlier_ratio;forward_reverse_rotation
87 17 18 2.517968959880004 27.89140832136152 0.7665916015366274 0.11462271017395576 6 9.759734204828526 0.003677259621955875 0.03455865038654393 1.0 True
88 17 19 3.518310045065406 80.31739505417983 0.645738203957382 0.11467441399973677 6 29.935027493013713 0.002657916871055147 0.03996637099866608 1.0 True
89 17 20 3.4199679241812992 152.98392843416642 0.2749902761571373 0.1378092692558536 6 36.98799912625142 0.9962499249666478 0.8583377913930432 0.5 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
90 17 21 8.339875108212771 156.4062058240796 0.4672368255565338 0.13751533768592306 6 32.19730139309397 3.7101311942981123 2.4525134629880094 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
91 17 22 10.944066586184825 150.8296621811644 0.4255952380952381 0.14178517078123323 6 38.71138397815585 1.8838258929727458 2.643696557684808 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
92 18 19 2.0416624616211574 52.42598673281832 0.6999343401181878 0.10876407223187459 6 36.91910298018587 0.0029722527816906435 0.028553700929610345 1.0 True
93 18 20 5.836864764777489 125.0925201128049 0.5796614723267061 0.12778176089815013 6 32.46060078153021 0.08496447343578362 0.24110896992060832 0.5 False forward_reverse_translation
94 18 21 10.84206419743439 175.70238585457497 0.23118979432439468 0.15492240575842026 6 95.57603256530417 0.3127879752995947 1.8359225701448998 1.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
95 18 22 13.461930516546937 178.72107050264088 0.20872354073123797 0.1539528469442395 6 158.96979855906298 5.509854679183967 5.400016950581102 1.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
96 18 23 10.787990019880349 164.1156966348418 0.3967277486910995 0.14073241205403234 6 67.81571155690442 0.037998014107336324 0.09667096210109852 1.0 True
97 19 20 6.120105723142265 72.6665333799865 0.6260444787247719 0.1266849163783949 6 25.979821491778758 0.02278172414929769 0.1983479736758361 1.0 True
98 19 21 11.201089261967727 123.27639912174082 0.22215292503430212 0.15090955425182226 6 96.8936157485445 2.289236102137041 0.9775110057344923 0.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
99 19 22 13.962230939038237 128.85294276465584 0.1883148831488315 0.1538013519108862 6 137.2212791172918 1.512900854675742 1.7779489198947585 1.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
100 19 23 10.877481155608614 143.45831663234054 0.37768025078369905 0.1432975121556297 6 38.93505210462377 2.6484748594978824 0.9762972275431752 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
101 19 24 10.384369483528566 177.87107120381796 0.43504761904761907 0.1392444656116938 6 41.9366060613255 0.04258350776081601 0.7879276702809289 1.0 False forward_reverse_rotation
102 20 21 5.091648376409225 50.60986574175432 0.6607188376242672 0.12214671257866083 6 14.082798146569486 0.014717307768564562 0.07480162341419787 1.0 True
103 20 22 7.842914217245693 56.186409384669375 0.576328684508104 0.13245799460807808 6 16.034011252152304 0.021651469263481868 0.4612054468064915 1.0 True
104 20 23 4.953146569484805 70.79178325235414 0.6451819579702717 0.1210226317867539 6 12.51146899612653 0.018249896901540018 0.12115759970964086 1.0 True
105 20 24 4.806112840058592 105.20453782384023 0.7383177570093458 0.11441422046802803 6 12.152737785424252 0.009011280440944078 0.06430292826078875 1.0 True
106 20 25 7.7432318481763245 68.57831319871164 0.6182822702159718 0.12791257682460658 6 29.104161441720713 0.01470353026057929 0.3205553574328468 1.0 True
107 21 22 2.836151129858731 5.576543642915048 0.7740636818348177 0.11564789267022943 6 12.209471497858695 0.006518431057241462 0.06991931948827856 1.0 True
108 21 23 1.4635995041292513 20.181917510599828 0.862223327530465 0.10307112004551743 6 11.124777032517395 0.002678668765812511 0.01879593375546071 1.0 True
109 21 24 2.7001854883863183 54.59467208208592 0.7795265676152102 0.11182158240581809 6 15.934013426145448 0.003491995501354943 0.037425651095358885 1.0 True
110 21 25 3.6513937480713023 17.968447456957325 0.7340892465252378 0.12002239056891176 6 16.572162980052227 0.03773466739435234 0.28781074838303833 1.0 True
111 21 26 4.368847767445087 60.912845043424156 0.7147358216190014 0.11997311573294335 6 17.3723156532691 0.01216183417643751 0.10312122071404906 1.0 True
112 22 23 3.806551883906871 14.605373867684776 0.7408951563458002 0.1172672510975272 6 12.610213323576234 0.009206244303296198 0.0960198600148152 1.0 True
113 22 24 4.999470711928798 49.01812843917085 0.6936064556176288 0.11914624513148228 6 13.20338761495324 0.006090737153725476 0.02755713841749006 1.0 True
114 22 25 2.281002791409386 12.391903814042275 0.7356584485868911 0.11474070552638106 6 16.673633938013026 0.001961709159757097 0.011643770804742994 1.0 True
115 22 26 1.990287035474152 55.33630140050909 0.7422594142259414 0.11765221626900067 6 18.880420396501982 0.007814937371704422 0.03934255356487579 1.0 True
116 22 27 2.5532406896142295 80.6206767037528 0.7254925373134329 0.11870181965154772 6 21.309389349405333 0.018241823604788293 0.08279236858581901 1.0 True
117 23 24 1.2663665558774873 34.41275457148608 0.7884810126582279 0.10662565692629541 6 10.611199681165658 0.0018823381482244372 0.020239211377623904 1.0 True
118 23 25 5.050865141821003 2.213470053642503 0.6843137254901961 0.1217109193489842 6 15.15080906413716 0.01463985673592735 0.20460048921133533 1.0 True
119 23 26 5.595299803053147 40.730927532824325 0.6772228989037758 0.12237954766026346 6 16.382334072569808 0.04518647006837217 0.20367965681897174 1.0 True
120 23 27 6.240759831138249 66.01530283606803 0.6637469586374696 0.12340951265232125 6 20.64383986204704 0.010626193556947943 1.1817079481882706 1.0 False forward_reverse_rotation
121 23 28 6.598772106458927 85.87759904182478 0.6720351390922401 0.12122778564083768 6 19.206570679040215 0.043540468662592216 0.2605147805386839 0.5 True
122 24 25 6.316196707646632 36.626224625128586 0.6764267990074442 0.12367636388755723 6 21.50176872604184 0.03581640576644142 0.20465043373191275 1.0 True
123 24 26 6.840027061556236 6.318172961338242 0.6524044389642417 0.12740222503880924 6 21.75476709306229 0.0620474433075452 0.12014838295938772 1.0 True
124 24 27 7.477875812552711 31.60254826458195 0.6546798029556651 0.12425657449629156 6 26.295019203914542 0.05403266260961897 0.2126374478532295 1.0 True
125 24 28 7.81303641449596 51.464844470338676 0.6614377470355731 0.12010155595807544 6 20.985000954928537 0.04949067679791638 0.25138526875322614 0.5 True
126 24 29 7.4949096209288655 93.73390958258972 0.047106325706594884 0.16491171897379944 6 28.651559198797667 0.8785910837751835 4.6775880176920355 0.0 False heldout_inlier_ratio;heldout_inlier_rmse;forward_reverse_translation;forward_reverse_rotation;multistart_instability
127 25 26 1.433673687807028 42.944397586466835 0.9137395459976105 0.08148335762110498 6 16.56285514245561 0.003236736725553251 0.006670817258604747 1.0 True
128 25 27 1.973107678535245 68.22877288971054 0.8596658711217183 0.10961458820309757 6 22.188153380460914 0.010624789877375612 0.04475651255675051 1.0 True
129 25 28 2.578633669986193 88.09106909546726 0.8839157491622786 0.10490699814192775 6 16.172179483480598 0.0028341913749953818 0.01585308444597317 1.0 True
130 25 29 1.4869736566208207 130.3601342077183 0.7857227558401518 0.10954358768244278 6 20.132087187715292 0.0032477187428175502 0.016542353808297643 1.0 True
131 25 30 5.843711828111692 139.6773015481418 0.05061061531235322 0.15827452276344428 6 186.52424080569762 2.4401001990983646 2.630476332375907 0.0 False heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation;multistart_instability
132 26 27 0.6711664435459649 25.284375303243706 0.9188612099644128 0.07520973241900301 6 20.799051531446725 0.00099208733077853 0.005485007138530622 1.0 True
133 26 28 1.202247282991071 45.146671509000434 0.9182726623840114 0.07611768518866213 6 21.995419584098137 0.004984978187528897 0.011735070988964648 1.0 True
134 26 29 0.8313560685788559 87.41573662125148 0.7856890251090416 0.1085139306178217 6 24.280602674778585 0.002586732426994246 0.12205883610742861 1.0 True
135 26 30 5.554376083336843 177.37830086539105 0.7634835395750642 0.10495724850674515 6 34.80647378548566 0.008489213184549637 0.021733210949119494 1.0 True
136 26 31 6.5424104132673175 156.01119868987084 0.7374054682955207 0.10737521663044328 6 38.310555401782864 0.007807741115783734 0.04372865220115068 0.5 True
137 27 28 0.6147316450225401 19.862296205756735 0.9281131178707225 0.07220941715117642 6 20.092791877654474 0.002300778353404822 0.001589714984734129 1.0 True
138 27 29 0.7540837235696874 62.13136131800778 0.7871188037207112 0.10966659884725233 6 24.009650087098127 0.005134403698946511 0.024348440992038003 1.0 True
139 27 30 5.07252652550922 152.0939255621477 0.7922108208955224 0.10147035321534549 6 32.52026042532519 0.007488026390185518 0.03860522829819172 1.0 True
140 27 31 6.285022243874859 178.7044260068885 0.7592097617664149 0.10539218018667616 6 48.70669958381935 0.0020937297862771895 0.027401753528281184 1.0 True
141 27 32 5.778763736289045 138.47370648510574 0.7480278422273782 0.10408734715846861 6 42.21510199826032 0.004407463145301957 0.03490543724835993 0.5 True
142 28 29 1.3242039147826599 42.26906511225104 0.787814381863266 0.10901449493832827 6 25.9136254196751 0.01570175790237293 0.035696604981368125 1.0 True
143 28 30 5.091487440029177 132.23162935639098 0.7791159962581852 0.1043856075500982 6 36.50167792578953 0.011701496185342901 0.047047156803733815 1.0 True
144 28 31 6.538757407908195 158.84212980112872 0.7449015266285981 0.10668967013687278 6 48.04636971250768 0.02442137437725171 0.6810283060803413 1.0 False forward_reverse_rotation
145 28 32 5.963604730984572 158.33600269086236 0.7252210330386226 0.11213192172123396 6 59.78207706090093 0.01795092616246746 0.05811390678775167 0.0 False multistart_instability
146 28 33 5.80807626014008 67.05379893079936 0.6692465836255895 0.10871806386783625 6 54.67376255043743 0.132593515882677 0.6593919290562373 0.5 False forward_reverse_translation;forward_reverse_rotation
147 29 30 4.767831371266539 89.96256424413991 0.7320662880982732 0.10929719051930432 6 30.745355096911858 0.005463604154020641 0.01858348348785812 1.0 True
148 29 31 5.715598450796842 116.57306468887764 0.7254562254562255 0.11323336570178014 6 49.09678922599784 0.005077799585122041 0.07265894091769737 1.0 True
149 29 32 5.281749147864957 159.39493219688646 0.33356393404819557 0.1449978595558761 6 111.7227195582132 0.3588603464467423 0.15715476414253352 1.0 False heldout_inlier_ratio;forward_reverse_translation
150 29 33 4.779166013738703 109.3228640430504 0.2850467289719626 0.13990657628540273 6 178.4302458902832 0.39012209563155037 0.27153598748575447 0.0 False backend_not_converged;heldout_inlier_ratio;forward_reverse_translation;multistart_instability
151 30 31 2.604154101624297 26.610500444737717 0.8286237272623269 0.09646596945131831 6 41.03513305025278 0.018905314487389895 0.08632503464138006 1.0 True
152 30 32 1.69105635082049 69.43236795274656 0.8065326633165829 0.09815063400019147 6 35.25635856167186 0.007341509941520377 0.023973741904830165 1.0 True
153 30 33 3.2411277385703925 160.7145717128097 0.09253766757622493 0.15464713396746754 6 31.157195533215592 1.0417523955000538 7.5233256615684665 1.0 False backend_not_converged;heldout_inlier_ratio;forward_reverse_translation;forward_reverse_rotation
154 31 32 0.9137201114724802 42.82186750800884 0.8146841206602162 0.09914628416022428 6 34.3317318149268 0.0693687705829607 0.45703051311645604 1.0 True
155 31 33 1.4965271484681508 134.10407126807198 0.7551430598250177 0.1003246191461096 6 28.537349904719445 0.005559091155809675 0.033005318250111694 1.0 True
156 32 33 1.9169426499676907 91.28220376006315 0.7585270860380031 0.0987597723287551 6 32.396623704761396 2.8694889670610495 5.798276157700327 0.0 False forward_reverse_translation;forward_reverse_rotation;multistart_instability
@@ -0,0 +1,416 @@
{
"schema_version": 1,
"success": true,
"convention": "T_RTK_lidar maps raw LiDAR points into the RTK navigation frame",
"equation": "A_RTK_ij X = X B_LiDAR_ij",
"frames": {
"RTK": {
"origin": "GGA positioning reference point; confirm ANT1/reference antenna in receiver configuration",
"x_axis": "horizontal projection of the rawHeading baseline direction reported by the receiver",
"y_axis": "left",
"z_axis": "up",
"yaw_enu_deg": "90 - rawHeadingDeg"
},
"LiDAR": "raw LiDAR sensor frame"
},
"backend": "small_gicp",
"measured_lidar_extrinsic_used_as_initial": false,
"body_heading_offset_used": false,
"body_antenna_lever_xy_used": false,
"translation_m": [
1.6362863962126635,
-0.2461267175734333,
0.0851285837804841
],
"rotation_rpy_deg_xyz": [
-0.7337275596198988,
1.3206501867905613,
-22.293138670917568
],
"quaternion_xyzw": [
-0.004053851502643108,
0.012544688899275593,
-0.19323028574532955,
0.9810648570503344
],
"matrix_4x4": [
[
0.9250093749023974,
0.37904117671318505,
0.026180960611867178,
1.6362863962126635
],
[
-0.3792445939369631,
0.9252912459175456,
0.0031061548487011197,
-0.2461267175734333
],
[
-0.023047653074967735,
-0.01280221013107426,
0.9996523941368298,
0.0851285837804841
],
[
0.0,
0.0,
0.0,
1.0
]
],
"quality": {
"stations": 34,
"pairs": 55,
"residuals": {
"pairs": 55,
"translation_m": {
"rms": 0.17789650760700804,
"median": 0.07286630775287825,
"p90": 0.3720138200025907,
"p95": 0.3991391086271845,
"max": 0.5307044931342686
},
"rotation_deg": {
"rms": 1.6408745761259504,
"median": 0.7722797079864339,
"p90": 2.06911907015371,
"p95": 4.199030922414834,
"max": 4.936245135060849
},
"per_pair": [
{
"pair_index": 0,
"translation_m": 0.0467393979562504,
"rotation_deg": 0.7657431692138433
},
{
"pair_index": 1,
"translation_m": 0.05689017243056396,
"rotation_deg": 0.808177090866613
},
{
"pair_index": 2,
"translation_m": 0.06397107958463491,
"rotation_deg": 0.3801068497009622
},
{
"pair_index": 3,
"translation_m": 0.1107952950892596,
"rotation_deg": 0.22993541870282783
},
{
"pair_index": 4,
"translation_m": 0.025928748934271984,
"rotation_deg": 0.6046131588115704
},
{
"pair_index": 5,
"translation_m": 0.07562027851221909,
"rotation_deg": 0.6309936647228139
},
{
"pair_index": 6,
"translation_m": 0.18916050340904586,
"rotation_deg": 0.26477854089271946
},
{
"pair_index": 7,
"translation_m": 0.043365771415170416,
"rotation_deg": 0.10315887875456749
},
{
"pair_index": 8,
"translation_m": 0.06454458096453627,
"rotation_deg": 0.8075022372885624
},
{
"pair_index": 9,
"translation_m": 0.05200877185535076,
"rotation_deg": 0.5226003188288877
},
{
"pair_index": 10,
"translation_m": 0.08066933230596428,
"rotation_deg": 0.6774188851574047
},
{
"pair_index": 11,
"translation_m": 0.02679009789021938,
"rotation_deg": 0.5921424571930466
},
{
"pair_index": 12,
"translation_m": 0.033564310488394096,
"rotation_deg": 1.0085274567011704
},
{
"pair_index": 13,
"translation_m": 0.11139219509163403,
"rotation_deg": 0.596821136461582
},
{
"pair_index": 14,
"translation_m": 0.06985048714143455,
"rotation_deg": 0.8031764622169965
},
{
"pair_index": 15,
"translation_m": 0.05926582195242711,
"rotation_deg": 2.1146215228832355
},
{
"pair_index": 16,
"translation_m": 0.053822686491612724,
"rotation_deg": 0.5853967911734774
},
{
"pair_index": 17,
"translation_m": 0.06095128937580921,
"rotation_deg": 0.44465865069314414
},
{
"pair_index": 18,
"translation_m": 0.1465573557185661,
"rotation_deg": 2.0008653910594214
},
{
"pair_index": 19,
"translation_m": 0.10402727477620027,
"rotation_deg": 0.1402969578428506
},
{
"pair_index": 20,
"translation_m": 0.06790583241641802,
"rotation_deg": 0.31916987926311186
},
{
"pair_index": 21,
"translation_m": 0.037917260718891004,
"rotation_deg": 0.6351484564817018
},
{
"pair_index": 22,
"translation_m": 0.03171219066433979,
"rotation_deg": 0.7611907317100989
},
{
"pair_index": 23,
"translation_m": 0.08117084986905364,
"rotation_deg": 1.5477927872017732
},
{
"pair_index": 24,
"translation_m": 0.07160138896826257,
"rotation_deg": 0.47596695875013556
},
{
"pair_index": 25,
"translation_m": 0.01792823137987938,
"rotation_deg": 0.3736004835939083
},
{
"pair_index": 26,
"translation_m": 0.07286630775287825,
"rotation_deg": 1.800095203725142
},
{
"pair_index": 27,
"translation_m": 0.18109166588563658,
"rotation_deg": 3.9699177938767174
},
{
"pair_index": 28,
"translation_m": 0.24825286214410192,
"rotation_deg": 4.619204763488191
},
{
"pair_index": 29,
"translation_m": 0.06588464658827484,
"rotation_deg": 0.8156362703186536
},
{
"pair_index": 30,
"translation_m": 0.07851282552634996,
"rotation_deg": 4.018956419097684
},
{
"pair_index": 31,
"translation_m": 0.16064262686816105,
"rotation_deg": 4.8460088207546645
},
{
"pair_index": 32,
"translation_m": 0.1352330525899733,
"rotation_deg": 4.936245135060849
},
{
"pair_index": 33,
"translation_m": 0.0649121457474399,
"rotation_deg": 1.99675397761927
},
{
"pair_index": 34,
"translation_m": 0.0460266060768811,
"rotation_deg": 1.2713317047431354
},
{
"pair_index": 35,
"translation_m": 0.11269690515103371,
"rotation_deg": 0.5444667992697017
},
{
"pair_index": 36,
"translation_m": 0.05570004024342942,
"rotation_deg": 0.6200055232178192
},
{
"pair_index": 37,
"translation_m": 0.006720345812259607,
"rotation_deg": 1.1919322914717754
},
{
"pair_index": 38,
"translation_m": 0.05571822171452726,
"rotation_deg": 0.8100733260702963
},
{
"pair_index": 39,
"translation_m": 0.03156268585174415,
"rotation_deg": 1.1994412303651207
},
{
"pair_index": 40,
"translation_m": 0.1396566575693052,
"rotation_deg": 1.219665033277036
},
{
"pair_index": 41,
"translation_m": 0.42067987248571403,
"rotation_deg": 1.1901837024442368
},
{
"pair_index": 42,
"translation_m": 0.5307044931342686,
"rotation_deg": 1.3118186035265211
},
{
"pair_index": 43,
"translation_m": 0.0278193588287066,
"rotation_deg": 0.5463366974423033
},
{
"pair_index": 44,
"translation_m": 0.08083543060888142,
"rotation_deg": 1.597876847929754
},
{
"pair_index": 45,
"translation_m": 0.3628770836511988,
"rotation_deg": 0.7452934588576191
},
{
"pair_index": 46,
"translation_m": 0.40429181316256,
"rotation_deg": 1.3531101551442906
},
{
"pair_index": 47,
"translation_m": 0.3857777439966788,
"rotation_deg": 0.596976162782461
},
{
"pair_index": 48,
"translation_m": 0.18867103421064077,
"rotation_deg": 0.5750764247339077
},
{
"pair_index": 49,
"translation_m": 0.3781049775701853,
"rotation_deg": 1.067294104551723
},
{
"pair_index": 50,
"translation_m": 0.33164608192922734,
"rotation_deg": 0.4502803262827393
},
{
"pair_index": 51,
"translation_m": 0.39693080668345215,
"rotation_deg": 0.7722797079864339
},
{
"pair_index": 52,
"translation_m": 0.05933929949217937,
"rotation_deg": 0.9106743636918464
},
{
"pair_index": 53,
"translation_m": 0.09592936224298491,
"rotation_deg": 0.8770147335571411
},
{
"pair_index": 54,
"translation_m": 0.10907903367697035,
"rotation_deg": 0.527094410495204
}
]
},
"weighted_jacobian_condition_number": 5.739204995062189,
"linearized_one_sigma": {
"translation_m": [
0.006273860396200483,
0.006271739194739563,
0.005766938056265489
],
"rotation_deg": [
0.06648607826531162,
0.07036185324485841,
0.1461990497815168
],
"warning": "conditional local estimate; bootstrap is the primary stability check"
},
"bootstrap": {
"runs": 100,
"order": [
"x_m",
"y_m",
"z_m",
"roll_deg",
"pitch_deg",
"yaw_deg"
],
"std": [
0.002418391924231912,
0.0018782458573924745,
0.0024162629680468473,
0.09973116386354614,
0.0717005619911583,
0.09954619224880032
],
"p025": [
1.6323740665381714,
-0.24963297512571514,
0.08157494287763181,
-0.9175278935375253,
1.1732724086153574,
-22.44062221365902
],
"p975": [
1.6411098578257144,
-0.24215579438024784,
0.09016027720271011,
-0.5264292351234366,
1.4226725074325792,
-22.072092788653528
]
}
},
"z_constraint": {
"observable_from_planar_AX_XB": false,
"method": "LiDAR ground planes plus externally supplied RTK reference-point height above ground",
"rtk_reference_height_above_ground_m": 0.8535,
"warning": "z is conditional on the supplied RTK antenna height; it is not independently identified by planar Ackermann motion"
},
"important_limit": "AX residual and bootstrap quantify internal consistency, not independent centimetre-grade absolute certification"
}
@@ -0,0 +1,48 @@
{
"final": {
"translation_m": [
1.6381793500373911,
-0.24084479868828831,
0.08448123595331278
],
"rotation_rpy_deg_xyz": [
-0.8171674587248069,
1.323288118779805,
-22.104163317857477
],
"pairs": 25,
"translation_rms_m": 0.10020667268070801,
"rotation_rms_deg": 1.2527941187072538,
"condition_number": 7.739413195936781
},
"backend_difference": {
"translation_m": 0.003889255293759414,
"rotation_deg": 0.1884307130161592,
"delta_matrix_4x4": [
[
0.9999968771376496,
0.0024968749848742764,
-0.00010644368722136346,
0.0008526003522697501
],
[
-0.0024970968314049877,
0.9999945974960792,
-0.0021376356258303525,
-0.003658904827736509
],
[
0.00010110570323801577,
0.0021378947504826257,
0.9999977095892133,
0.0010058801324768218
],
[
0.0,
0.0,
0.0,
1.0
]
]
}
}
@@ -0,0 +1,14 @@
# run目录
根README包含完整复现命令;这里仅列入口职责。
| 脚本 | 用途 |
|---|---|
| `run_full_pipeline.ps1` | 从逐站LiDAR dlog、RTK rscap、IMU rscap一直运行到最终`T_RTK_lidar` |
| `export_multisensor_stations.ps1` | 解析原始三传感器数据并按LiDAR帧生成combined NPZ |
| `prepare_multisensor_dataset.ps1` | 每站选一帧,生成yaw-only RTK参考轨迹和`frames_all` |
| `run_direct_rtk_lidar.ps1` | 从combined数据运行RTK直接标定和最终结果封装 |
| `run_single_dataset.ps1` | 执行地面、两个GICP后端、精筛、共识和AX=XB求解 |
| `view_result.ps1` | 打开3D运动对对比并打印数值增量 |
所有路径均为命令行参数;默认生成目录`work/``outputs/`不会提交Git。
@@ -0,0 +1,89 @@
param(
[Parameter(Mandatory = $true)][string]$DataRoot,
[Parameter(Mandatory = $true)][string]$OutputRoot,
[Parameter(Mandatory = $true)][string]$RtkCapture,
[Parameter(Mandatory = $true)][string]$ImuCapture,
[string]$LidarObject = "frontlidar",
[string]$Timezone = "+08:00",
[string[]]$StationNames = @(),
[int]$Stride = 1,
[double]$RtkMaxDtMs = 150.0,
[double]$ImuBeforeMs = 100.0,
[double]$ImuAfterMs = 100.0,
[switch]$SkipLidarExport,
[switch]$SkipSerialParsing
)
$ErrorActionPreference = "Stop"
$RepoRoot = Split-Path -Parent $PSScriptRoot
$Exporter = Join-Path $RepoRoot "tools\frontlidar_dlog_export.py"
$Builder = Join-Path $RepoRoot "tools\build_multisensor_npz.py"
$Parser = Join-Path $RepoRoot "tools\rscap_v2\parse_rtk_imu_v2.py"
$Auditor = Join-Path $RepoRoot "tools\rscap_v2\audit_capture_v2.py"
$ExportRoot = Join-Path $OutputRoot "export"
$ParsedRoot = Join-Path $OutputRoot "parsed"
$CombinedRoot = Join-Path $OutputRoot "combined"
function Run-Python {
param([string]$Stage, [string[]]$Arguments)
Write-Host "[$Stage]"
& python @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Stage failed with Python exit code $LASTEXITCODE"
}
}
foreach ($Path in @($DataRoot, $RtkCapture, $ImuCapture)) {
if (-not (Test-Path -LiteralPath $Path)) { throw "Input does not exist: $Path" }
}
if ($Stride -lt 1) { throw "Stride must be at least 1" }
if ($StationNames.Count -gt 0) {
$Stations = @($StationNames | ForEach-Object { Get-Item -LiteralPath (Join-Path $DataRoot $_) })
} else {
$Stations = @(Get-ChildItem -LiteralPath $DataRoot -Directory | Where-Object {
(Test-Path -LiteralPath (Join-Path $_.FullName "dobject")) -and
(Test-Path -LiteralPath (Join-Path $_.FullName "dobject_recording"))
} | Sort-Object Name)
}
if ($Stations.Count -eq 0) { throw "No station directory containing dobject and dobject_recording was found" }
New-Item -ItemType Directory -Force -Path $OutputRoot | Out-Null
if (-not $SkipSerialParsing) {
New-Item -ItemType Directory -Force -Path $ParsedRoot | Out-Null
Run-Python "capture audit" @($Auditor, $RtkCapture, $ImuCapture, "--out", (Join-Path $OutputRoot "capture_audit.json"))
Run-Python "RTK/IMU parse" @($Parser, "--rtk", $RtkCapture, "--imu", $ImuCapture, "--out", $ParsedRoot)
}
foreach ($Station in $Stations) {
$StationOut = Join-Path $ExportRoot $Station.Name
if (-not $SkipLidarExport) {
Run-Python "LiDAR station $($Station.Name)" @(
$Exporter, "--dlog", $Station.FullName, "--out", $StationOut,
"--object", $LidarObject, "--format", "npz", "--timezone", $Timezone,
"--stride", "$Stride", "--compress", "--skip-rtk", "--write-reports", "--resume"
)
}
if (-not (Test-Path -LiteralPath (Join-Path $StationOut "frames"))) {
throw "Exported frame directory is absent for station $($Station.Name): $StationOut"
}
}
$BuildArgs = @($Builder)
foreach ($Station in $Stations) {
$Frames = Join-Path (Join-Path $ExportRoot $Station.Name) "frames"
$BuildArgs += @("--lidar", "$($Station.Name)=$Frames")
}
$BuildArgs += @(
"--rtk", (Join-Path $ParsedRoot "rtk.jsonl"),
"--imu", (Join-Path $ParsedRoot "imu.jsonl"),
"--out", $CombinedRoot,
"--rtk-max-dt-ms", "$RtkMaxDtMs",
"--imu-before-ms", "$ImuBeforeMs",
"--imu-after-ms", "$ImuAfterMs",
"--overwrite"
)
Run-Python "LiDAR/RTK/IMU association" $BuildArgs
Write-Host "Completed stations: $($Stations.Count)"
Write-Host "Combined NPZ: $CombinedRoot"
@@ -0,0 +1,22 @@
param(
[Parameter(Mandatory = $true)][string]$CombinedRoot,
[Parameter(Mandatory = $true)][string]$Output,
[Parameter(Mandatory = $true)][double]$HeadingOffsetDeg,
[Parameter(Mandatory = $true)][double[]]$AntennaLever,
[string]$PoseName = "rtk_gga_raw_heading",
[int]$MinStations = 30,
[int]$ExpectedStations = 0,
[double]$HeadingStdLimitDeg = 0.5,
[switch]$Overwrite
)
$ErrorActionPreference = "Stop"
if ($AntennaLever.Count -ne 3) { throw "AntennaLever must contain X,Y,Z in body coordinates" }
$Repo = Split-Path -Parent $PSScriptRoot
$Args = @((Join-Path $Repo "tools\prepare_multisensor_station_dataset.py"), "--combined-root", $CombinedRoot,
"--output", $Output, "--pose-name", $PoseName, "--heading-offset-deg", "$HeadingOffsetDeg", "--antenna-lever") +
@($AntennaLever | ForEach-Object { "$_" }) + @("--min-stations", "$MinStations",
"--expected-stations", "$ExpectedStations", "--heading-std-limit-deg", "$HeadingStdLimitDeg")
if ($Overwrite) { $Args += "--overwrite" }
& python @Args
if ($LASTEXITCODE -ne 0) { throw "Multisensor dataset preparation failed" }
@@ -0,0 +1,36 @@
param(
[Parameter(Mandatory = $true)][string]$CombinedRoot,
[string]$OutputRoot = "",
[string]$WorkRoot = "",
[double]$RtkReferenceHeightAboveGroundM = 0.8535,
[int]$ExpectedStations = 34,
[int]$MinPairs = 20,
[int]$Bootstrap = 200
)
$ErrorActionPreference = "Stop"
$Repo = Split-Path -Parent $PSScriptRoot
if ([string]::IsNullOrWhiteSpace($OutputRoot)) { $OutputRoot = Join-Path $Repo "outputs\rtk_lidar_calibration" }
if ([string]::IsNullOrWhiteSpace($WorkRoot)) { $WorkRoot = Join-Path $Repo "work\prepared_rtk_direct" }
$Prepared = $WorkRoot
& (Join-Path $Repo "run\prepare_multisensor_dataset.ps1") `
-CombinedRoot $CombinedRoot -Output $Prepared -HeadingOffsetDeg 0 `
-AntennaLever @(0.0,0.0,0.0) -PoseName "rtk_gga_raw_heading" -MinStations 30 -ExpectedStations $ExpectedStations -Overwrite
if ($LASTEXITCODE -ne 0) { throw "RTK-direct dataset preparation failed" }
& (Join-Path $Repo "run\run_single_dataset.ps1") `
-Prepared $Prepared -OutputRoot $OutputRoot `
-ReferencePoseFile "reference_poses_rtk_gga_raw_heading.csv" `
-ReferenceHeight $RtkReferenceHeightAboveGroundM -MinPairs $MinPairs -Bootstrap $Bootstrap
if ($LASTEXITCODE -ne 0) { throw "RTK-direct calibration failed" }
$Finalize = @(
(Join-Path $Repo "code\finalize_direct_rtk_lidar.py"),
"--result-root", $OutputRoot,
"--reference-height", "$RtkReferenceHeightAboveGroundM"
)
& python @Finalize
if ($LASTEXITCODE -ne 0) { throw "Final result packaging failed" }
Write-Host "Final T_RTK_lidar: $(Join-Path $OutputRoot 'final_T_RTK_lidar.json')"
@@ -0,0 +1,32 @@
param(
[Parameter(Mandatory = $true)][string]$DataRoot,
[Parameter(Mandatory = $true)][string]$RtkCapture,
[Parameter(Mandatory = $true)][string]$ImuCapture,
[Parameter(Mandatory = $true)][string]$OutputRoot,
[string]$LidarObject = "frontlidar",
[string]$Timezone = "+08:00",
[double]$RtkReferenceHeightAboveGroundM = 0.8535,
[int]$ExpectedStations = 34,
[int]$MinPairs = 20,
[int]$Bootstrap = 200
)
$ErrorActionPreference = "Stop"
$ExportRoot = Join-Path $OutputRoot "exported"
$PreparedRoot = Join-Path $OutputRoot "prepared_rtk_direct"
$CalibrationRoot = Join-Path $OutputRoot "calibration"
& (Join-Path $PSScriptRoot "export_multisensor_stations.ps1") `
-DataRoot $DataRoot -RtkCapture $RtkCapture -ImuCapture $ImuCapture `
-OutputRoot $ExportRoot -LidarObject $LidarObject -Timezone $Timezone
if ($LASTEXITCODE -ne 0) { throw "Raw-data export failed" }
& (Join-Path $PSScriptRoot "run_direct_rtk_lidar.ps1") `
-CombinedRoot (Join-Path $ExportRoot "combined") `
-WorkRoot $PreparedRoot -OutputRoot $CalibrationRoot `
-RtkReferenceHeightAboveGroundM $RtkReferenceHeightAboveGroundM `
-ExpectedStations $ExpectedStations -MinPairs $MinPairs -Bootstrap $Bootstrap
if ($LASTEXITCODE -ne 0) { throw "RTK-LiDAR calibration failed" }
Write-Host "Final result: $(Join-Path $CalibrationRoot 'final_T_RTK_lidar.json')"
Write-Host "Prepared frames: $(Join-Path $PreparedRoot 'frames_all')"
@@ -0,0 +1,70 @@
param(
[Parameter(Mandatory = $true)][string]$Prepared,
[Parameter(Mandatory = $true)][string]$OutputRoot,
[string]$ReferencePoseFile = "reference_poses_rtk_gga_raw_heading.csv",
[double]$ReferenceHeight = 0.8535,
[int]$MinPairs = 20,
[int]$Bootstrap = 100
)
$ErrorActionPreference = "Stop"
$Repo = Split-Path -Parent $PSScriptRoot
$Code = Join-Path $Repo "code\rigorous_calibration.py"
$Refine = Join-Path $Repo "code\refine_pairs.py"
$Consensus = Join-Path $Repo "code\cross_backend_filter.py"
$Frames = Join-Path $Prepared "frames_all"
$ReferencePoses = Join-Path $Prepared $ReferencePoseFile
$Common = Join-Path $OutputRoot "common"
$Open = Join-Path $OutputRoot "open3d_gicp"
$Small = Join-Path $OutputRoot "small_gicp"
$ConsensusOut = Join-Path $OutputRoot "consensus"
function Run-Python {
param([string]$Stage, [string[]]$Arguments)
Write-Host "[$Stage]"
& python @Arguments
if ($LASTEXITCODE -ne 0) { throw "$Stage failed with Python exit code $LASTEXITCODE" }
}
foreach ($Path in @($Frames, $ReferencePoses)) {
if (-not (Test-Path -LiteralPath $Path)) { throw "Input does not exist: $Path" }
}
New-Item -ItemType Directory -Force -Path $Common,$Open,$Small,$ConsensusOut | Out-Null
$Ground = Join-Path $Common "ground_planes.csv"
Run-Python "ground planes" @($Code, "ground", "--frames", $Frames, "--output", $Ground)
foreach ($Backend in @("small_gicp", "open3d")) {
$Directory = if ($Backend -eq "small_gicp") { $Small } else { $Open }
$Raw = Join-Path $Directory "B_estimation.npz"
$QualityJson = Join-Path $Directory "B_quality.json"
$QualityCsv = Join-Path $Directory "B_quality.csv"
$PairArgs = @($Code, "pairs", "--backend", $Backend, "--frames", $Frames, "--reference-poses", $ReferencePoses,
"--output", $Raw, "--quality-json", $QualityJson, "--quality-csv", $QualityCsv,
"--min-pairs", "$MinPairs")
if ($Backend -eq "open3d") { $PairArgs += @("--max-gap", "3", "--multistart", "1", "--iterations", "40") }
Run-Python "$Backend pairs" $PairArgs
Run-Python "$Backend X-independent refinement" @(
$Refine, "--pairs", $Raw, "--quality-json", $QualityJson,
"--output", (Join-Path $Directory "B_refined.npz"), "--min-pairs", "$MinPairs"
)
Run-Python "$Backend calibration" @(
$Code, "calibrate", "--pairs", (Join-Path $Directory "B_refined.npz"),
"--ground-planes", $Ground, "--reference-height", "$ReferenceHeight",
"--bootstrap", "$Bootstrap", "--output", (Join-Path $Directory "extrinsic.json")
)
}
$ConsensusPairs = Join-Path $ConsensusOut "B_consensus.npz"
Run-Python "cross-backend consensus" @(
$Consensus, "--open3d-pairs", (Join-Path $Open "B_refined.npz"),
"--small-pairs", (Join-Path $Small "B_refined.npz"),
"--output", $ConsensusPairs, "--min-pairs", "$MinPairs"
)
Run-Python "consensus calibration" @(
$Code, "calibrate", "--pairs", $ConsensusPairs, "--ground-planes", $Ground,
"--reference-height", "$ReferenceHeight", "--bootstrap", "$Bootstrap",
"--output", (Join-Path $ConsensusOut "extrinsic.json")
)
Write-Host "Calibration results: $OutputRoot"
@@ -0,0 +1,19 @@
param(
[Parameter(Mandatory = $true)][string]$Frames,
[Parameter(Mandatory = $true)][string]$Pairs,
[Parameter(Mandatory = $true)][string]$Extrinsic,
[int]$PairIndex = 0,
[double]$LeftRollDeg = 0.0,
[double]$LeftPitchDeg = 0.0,
[double]$LeftYawDeg = 0.0
)
$ErrorActionPreference = "Stop"
$Repo = Split-Path -Parent $PSScriptRoot
foreach ($Path in @($Frames, $Pairs, $Extrinsic)) {
if (-not (Test-Path -LiteralPath $Path)) { throw "Input does not exist: $Path" }
}
& python (Join-Path $Repo "code\visualize_pair_3d.py") `
--frames $Frames --pairs $Pairs --extrinsic $Extrinsic --pair-index $PairIndex `
--left-rpy-deg $LeftRollDeg $LeftPitchDeg $LeftYawDeg
if ($LASTEXITCODE -ne 0) { throw "Visualization failed with Python exit code $LASTEXITCODE" }
@@ -0,0 +1,11 @@
# tools目录
| 文件 | 输入→输出 |
|---|---|
| `frontlidar_dlog_export.py` | LiDAR dlog → 逐帧原始点云NPZ;时间来自DObject post tick |
| `rscap_v2/parse_rtk_imu_v2.py` | RTK/IMU rscap → JSONL,保存校验状态、主机时间、GNSS/IMU设备字段和原始报文 |
| `rscap_v2/audit_capture_v2.py` | 检查rscap结构、时间范围和记录统计 |
| `build_multisensor_npz.py` | 按LiDAR帧最近邻关联GGA/heading,并附加IMU时间窗 → combined NPZ |
| `prepare_multisensor_station_dataset.py` | combined NPZ → 每站一帧`frames_all``reference_poses_*.csv` |
当前标定只使用LiDAR和RTK;IMU保持原始传感器坐标,不参与点云去畸变或外参求解。prepared阶段对站内有效RTK取平均、对heading取圆均值,并选择有效帧序列的中间LiDAR帧。
@@ -0,0 +1,312 @@
#!/usr/bin/env python3
"""Build one LiDAR-centric NPZ per frame with matched RTK and an IMU window.
Inputs are LiDAR frame NPZ files from frontlidar_dlog_export.py and parsed
RTK/IMU JSONL files from parse_rtk_imu_v2.py. Raw .rscap files remain the
traceability source; this script never modifies them.
"""
from __future__ import annotations
import argparse
import csv
import json
from pathlib import Path
from typing import Any
import numpy as np
GPS_EPOCH_UNIX_NS = 315964800 * 1_000_000_000
def parse_named_path(text: str) -> tuple[str, Path]:
if "=" not in text:
raise argparse.ArgumentTypeError("expected NAME=PATH")
name, raw_path = text.split("=", 1)
if not name.strip():
raise argparse.ArgumentTypeError("segment name is empty")
return name.strip(), Path(raw_path)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument(
"--lidar",
type=parse_named_path,
action="append",
required=True,
metavar="NAME=FRAMES_DIR",
help="Repeat for each LiDAR segment; directory contains exported *.npz frames.",
)
parser.add_argument("--rtk", type=Path, action="append", required=True, help="Parsed rtk.jsonl; repeat per session.")
parser.add_argument("--imu", type=Path, action="append", required=True, help="Parsed imu.jsonl; repeat per session.")
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--rtk-max-dt-ms", type=float, default=150.0)
parser.add_argument("--imu-before-ms", type=float, default=100.0)
parser.add_argument("--imu-after-ms", type=float, default=100.0)
parser.add_argument("--gps-utc-leap-seconds", type=int, default=18)
parser.add_argument("--overwrite", action="store_true")
return parser.parse_args()
def load_jsonl(paths: list[Path]) -> list[dict[str, Any]]:
rows: list[dict[str, Any]] = []
for source_index, path in enumerate(paths):
source_file = str(path.resolve())
with path.open("r", encoding="utf-8") as stream:
for line_number, line in enumerate(stream, start=1):
if not line.strip():
continue
row = json.loads(line)
row["_source_file"] = source_file
row["_source_index"] = source_index
row["_source_line"] = line_number
rows.append(row)
return rows
def utf8_array(value: Any) -> np.ndarray:
return np.frombuffer(str(value if value is not None else "").encode("utf-8"), dtype=np.uint8)
def scalar(array: np.ndarray) -> Any:
return array.reshape(-1)[0].item()
def nearest_index(times: np.ndarray, target: int) -> int:
if not len(times):
return -1
right = int(np.searchsorted(times, target, side="left"))
candidates = [index for index in (right - 1, right) if 0 <= index < len(times)]
return min(candidates, key=lambda index: abs(int(times[index]) - target))
def estimate_imu_times(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""Recover 100 Hz timing inside each serial chunk from device timestamps.
A capture chunk has one host receive timestamp but may contain several IMU
frames. The last frame is anchored to the chunk receive time and earlier
frames are moved backwards by their device timestamp difference.
"""
groups: dict[tuple[int, int], list[dict[str, Any]]] = {}
for row in rows:
if not row.get("crc_valid") or row.get("device_timestamp_ms") is None:
continue
key = (int(row["_source_index"]), int(row.get("source_chunk_sequence_last", -1)))
groups.setdefault(key, []).append(row)
result: list[dict[str, Any]] = []
for group in groups.values():
group.sort(key=lambda row: (int(row["device_timestamp_ms"]), int(row["_source_line"])))
last_device = int(group[-1]["device_timestamp_ms"])
host_ns = int(group[-1]["host_receive_utc_ns"])
for row in group:
delta_ms = (last_device - int(row["device_timestamp_ms"])) & 0xFFFFFFFF
if delta_ms > 60_000:
delta_ms = 0
copied = dict(row)
copied["estimated_time_ns"] = host_ns - delta_ms * 1_000_000
result.append(copied)
result.sort(key=lambda row: int(row["estimated_time_ns"]))
return result
def gnss_utc_ns(row: dict[str, Any], leap_seconds: int) -> int | None:
week, tow_ms = row.get("gnss_week"), row.get("gnss_tow_ms")
if week is None or tow_ms is None:
return None
seconds = int(week) * 604800 + float(tow_ms) / 1000.0 - leap_seconds
return GPS_EPOCH_UNIX_NS + int(round(seconds * 1_000_000_000))
def numeric_array(rows: list[dict[str, Any]], key: str, dtype: Any, default: Any) -> np.ndarray:
return np.asarray([row.get(key, default) if row.get(key) is not None else default for row in rows], dtype=dtype)
def raw_frame_matrix(rows: list[dict[str, Any]]) -> tuple[np.ndarray, np.ndarray]:
frames = [bytes.fromhex(str(row.get("raw_frame_hex", ""))) for row in rows]
lengths = np.asarray([len(frame) for frame in frames], dtype=np.int32)
width = max(lengths, default=0)
matrix = np.zeros((len(frames), width), dtype=np.uint8)
for index, frame in enumerate(frames):
matrix[index, : len(frame)] = np.frombuffer(frame, dtype=np.uint8)
return matrix, lengths
def add_rtk(values: dict[str, np.ndarray], prefix: str, row: dict[str, Any] | None, dt_ns: int | None) -> None:
values[f"{prefix}_valid"] = np.asarray([row is not None], dtype=np.uint8)
values[f"{prefix}_dt_ns"] = np.asarray([dt_ns or 0], dtype=np.int64)
values[f"{prefix}_host_receive_utc_ns"] = np.asarray([0], dtype=np.int64)
values[f"{prefix}_raw_utf8"] = utf8_array("")
values[f"{prefix}_source_file_utf8"] = utf8_array("")
values[f"{prefix}_source_raw_file_offset"] = np.asarray([-1], dtype=np.int64)
values[f"{prefix}_source_raw_byte_length"] = np.asarray([0], dtype=np.int32)
if row is None:
return
values[f"{prefix}_host_receive_utc_ns"] = np.asarray([row.get("host_receive_utc_ns", 0)], dtype=np.int64)
values[f"{prefix}_raw_utf8"] = utf8_array(row.get("raw_line", ""))
values[f"{prefix}_source_file_utf8"] = utf8_array(row.get("_source_file", ""))
values[f"{prefix}_source_raw_file_offset"] = np.asarray([row.get("source_raw_file_offset", -1)], dtype=np.int64)
values[f"{prefix}_source_raw_byte_length"] = np.asarray([row.get("source_raw_byte_length", 0)], dtype=np.int32)
def initialize_rtk_measurements(values: dict[str, np.ndarray]) -> None:
for key, dtype, default in (
("lat_deg", np.float64, np.nan), ("lon_deg", np.float64, np.nan),
("altitude_m", np.float64, np.nan), ("hdop", np.float64, np.nan),
("fix_quality", np.int32, -1), ("gga_satellites", np.int32, -1),
("differential_age_s", np.float64, np.nan),
("gnss_week", np.int32, -1), ("gnss_tow_ms", np.int64, -1),
("baseline_length_m", np.float64, np.nan), ("raw_heading_deg", np.float64, np.nan),
("pitch_deg", np.float64, np.nan), ("heading_stddev_deg", np.float64, np.nan),
("pitch_stddev_deg", np.float64, np.nan), ("heading_satellites", np.int32, -1),
("solution_satellites", np.int32, -1),
):
values[f"rtk_{key}"] = np.asarray([default], dtype=dtype)
values["rtk_fixed"] = np.asarray([0], dtype=np.uint8)
values["rtk_heading_solution_utf8"] = utf8_array("")
values["rtk_heading_gnss_utc_ns"] = np.asarray([0], dtype=np.int64)
values["rtk_heading_host_minus_gnss_ns"] = np.asarray([0], dtype=np.int64)
def main() -> int:
args = parse_args()
if args.out.exists() and any(args.out.iterdir()) and not args.overwrite:
raise FileExistsError(f"{args.out} is non-empty; pass --overwrite")
frames_out = args.out / "frames"
frames_out.mkdir(parents=True, exist_ok=True)
rtk_rows = load_jsonl(args.rtk)
gga = sorted(
[row for row in rtk_rows if row.get("type") == "GGA" and row.get("checksum_valid") and row.get("lat_deg") is not None],
key=lambda row: int(row["host_receive_utc_ns"]),
)
heading = sorted(
[row for row in rtk_rows if row.get("type") == "UNIHEADINGA" and row.get("checksum_valid") and row.get("heading_valid")],
key=lambda row: int(row["host_receive_utc_ns"]),
)
imu = estimate_imu_times(load_jsonl(args.imu))
gga_times = np.asarray([int(row["host_receive_utc_ns"]) for row in gga], dtype=np.int64)
heading_times = np.asarray([int(row["host_receive_utc_ns"]) for row in heading], dtype=np.int64)
imu_times = np.asarray([int(row["estimated_time_ns"]) for row in imu], dtype=np.int64)
manifest: list[dict[str, Any]] = []
global_index = 0
max_rtk_ns = int(args.rtk_max_dt_ms * 1_000_000)
before_ns = int(args.imu_before_ms * 1_000_000)
after_ns = int(args.imu_after_ms * 1_000_000)
for segment_name, frame_dir in args.lidar:
frame_paths = sorted(frame_dir.glob("*.npz"))
if not frame_paths:
raise FileNotFoundError(f"no NPZ frames under {frame_dir}")
for segment_index, source in enumerate(frame_paths):
with np.load(source, allow_pickle=False) as frame:
values = {key: np.asarray(frame[key]) for key in frame.files}
lidar_time_ns = int(scalar(values["unix_time_ns"]))
gga_index = nearest_index(gga_times, lidar_time_ns)
heading_index = nearest_index(heading_times, lidar_time_ns)
gga_row = gga[gga_index] if gga_index >= 0 else None
heading_row = heading[heading_index] if heading_index >= 0 else None
gga_dt = int(gga_times[gga_index]) - lidar_time_ns if gga_index >= 0 else None
heading_dt = int(heading_times[heading_index]) - lidar_time_ns if heading_index >= 0 else None
gga_ok = gga_row is not None and abs(gga_dt or 0) <= max_rtk_ns
heading_ok = heading_row is not None and abs(heading_dt or 0) <= max_rtk_ns
add_rtk(values, "rtk_gga", gga_row if gga_ok else None, gga_dt)
add_rtk(values, "rtk_heading", heading_row if heading_ok else None, heading_dt)
initialize_rtk_measurements(values)
if gga_ok and gga_row:
for key, dtype, default in (
("lat_deg", np.float64, np.nan), ("lon_deg", np.float64, np.nan),
("altitude_m", np.float64, np.nan), ("hdop", np.float64, np.nan),
("fix_quality", np.int32, -1), ("gga_satellites", np.int32, -1),
("differential_age_s", np.float64, np.nan),
):
values[f"rtk_{key}"] = np.asarray([gga_row.get(key, default)], dtype=dtype)
values["rtk_gga_satellites"] = np.asarray([gga_row.get("satellites", -1)], dtype=np.int32)
values["rtk_fixed"] = np.asarray([int(gga_row.get("fix_quality", -1)) in {4, 5}], dtype=np.uint8)
if heading_ok and heading_row:
for key, dtype, default in (
("gnss_week", np.int32, -1), ("gnss_tow_ms", np.int64, -1),
("baseline_length_m", np.float64, np.nan), ("raw_heading_deg", np.float64, np.nan),
("pitch_deg", np.float64, np.nan), ("heading_stddev_deg", np.float64, np.nan),
("pitch_stddev_deg", np.float64, np.nan),
("solution_satellites", np.int32, -1),
):
values[f"rtk_{key}"] = np.asarray([heading_row.get(key, default)], dtype=dtype)
values["rtk_heading_satellites"] = np.asarray([heading_row.get("satellites", -1)], dtype=np.int32)
values["rtk_heading_solution_utf8"] = utf8_array(heading_row.get("heading_solution", ""))
device_ns = gnss_utc_ns(heading_row, args.gps_utc_leap_seconds)
values["rtk_heading_gnss_utc_ns"] = np.asarray([device_ns or 0], dtype=np.int64)
values["rtk_heading_host_minus_gnss_ns"] = np.asarray(
[int(heading_row["host_receive_utc_ns"]) - device_ns if device_ns is not None else 0], dtype=np.int64
)
left = int(np.searchsorted(imu_times, lidar_time_ns - before_ns, side="left"))
right = int(np.searchsorted(imu_times, lidar_time_ns + after_ns, side="right"))
window = imu[left:right]
values["imu_window_count"] = np.asarray([len(window)], dtype=np.int32)
values["imu_valid"] = np.asarray([bool(window)], dtype=np.uint8)
values["imu_time_ns"] = numeric_array(window, "estimated_time_ns", np.int64, 0)
values["imu_host_receive_utc_ns"] = numeric_array(window, "host_receive_utc_ns", np.int64, 0)
for key in ("device_timestamp_ms", "pps_sync_stamp_ms", "tag"):
values[f"imu_{key}"] = numeric_array(window, key, np.int64, -1)
for key in (
"temperature_c", "air_pressure_pa", "accel_x_mps2", "accel_y_mps2", "accel_z_mps2",
"gyro_x_radps", "gyro_y_radps", "gyro_z_radps", "mag_x_ut", "mag_y_ut", "mag_z_ut",
"roll_deg", "pitch_deg", "yaw_deg", "quaternion_w", "quaternion_x", "quaternion_y", "quaternion_z",
):
values[f"imu_{key}"] = numeric_array(window, key, np.float64, np.nan)
values["imu_source_index"] = numeric_array(window, "_source_index", np.int32, -1)
values["imu_source_raw_file_offset"] = numeric_array(window, "source_raw_file_offset", np.int64, -1)
raw_matrix, raw_lengths = raw_frame_matrix(window)
values["imu_raw_frame_bytes"] = raw_matrix
values["imu_raw_frame_length"] = raw_lengths
values["imu_source_files_json_utf8"] = utf8_array(json.dumps([str(path.resolve()) for path in args.imu], ensure_ascii=False))
values["source_lidar_file_utf8"] = utf8_array(source.resolve())
values["segment_name_utf8"] = utf8_array(segment_name)
output = frames_out / f"{segment_name}_{segment_index:06d}.npz"
np.savez_compressed(output, **values)
manifest.append({
"global_index": global_index,
"segment": segment_name,
"segment_index": segment_index,
"output": str(output.relative_to(args.out)),
"source_lidar": str(source.resolve()),
"lidar_time_ns": lidar_time_ns,
"rtk_gga_dt_ns": gga_dt,
"rtk_heading_dt_ns": heading_dt,
"rtk_valid": gga_ok,
"heading_valid": heading_ok,
"rtk_fix_quality": gga_row.get("fix_quality") if gga_ok and gga_row else None,
"rtk_fixed": bool(gga_ok and gga_row and int(gga_row.get("fix_quality", -1)) in {4, 5}),
"imu_window_count": len(window),
})
global_index += 1
fields = sorted({key for row in manifest for key in row})
with (args.out / "manifest.csv").open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=fields)
writer.writeheader()
writer.writerows(manifest)
summary = {
"frames": len(manifest),
"segments": {name: sum(row["segment"] == name for row in manifest) for name, _ in args.lidar},
"rtk_valid": sum(bool(row["rtk_valid"]) for row in manifest),
"heading_valid": sum(bool(row["heading_valid"]) for row in manifest),
"rtk_fixed": sum(bool(row["rtk_fixed"]) for row in manifest),
"imu_window_nonempty": sum(int(row["imu_window_count"]) > 0 for row in manifest),
"rtk_max_dt_ms": args.rtk_max_dt_ms,
"imu_window_ms": [-args.imu_before_ms, args.imu_after_ms],
"time_basis": "LiDAR and serial host UTC; RTK GNSS time and IMU device time are retained for clock-model refinement",
"imu_orientation_warning": "IMU values are in the raw IMU sensor frame; no LiDAR/body extrinsic is applied",
}
(args.out / "dataset_summary.json").write_text(json.dumps(summary, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps(summary, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,170 @@
#!/usr/bin/env python3
"""Prepare one static LiDAR frame and one yaw-only RTK reference pose per NPZ segment."""
from __future__ import annotations
import argparse
import csv
import json
import math
import re
import shutil
from pathlib import Path
from typing import Any
import numpy as np
POSE_FIELDS = ["time", "x", "y", "z", "qx", "qy", "qz", "qw"]
def natural_key(value: str) -> list[Any]:
return [int(part) if part.isdigit() else part.lower() for part in re.split(r"(\d+)", value)]
def truth(value: Any) -> bool:
return str(value).strip().lower() in {"1", "true", "yes", "y"}
def circular_mean_deg(values: np.ndarray) -> float:
radians = np.deg2rad(values)
return float(np.rad2deg(math.atan2(np.mean(np.sin(radians)), np.mean(np.cos(radians)))) % 360.0)
def circular_std_deg(values: np.ndarray) -> float:
radians = np.deg2rad(values)
resultant = max(math.hypot(np.mean(np.cos(radians)), np.mean(np.sin(radians))), 1e-12)
return float(np.rad2deg(math.sqrt(-2.0 * math.log(resultant))))
def geodetic_to_ecef(lat_deg: float, lon_deg: float, height_m: float) -> np.ndarray:
a, e2 = 6378137.0, 6.69437999014e-3
lat, lon = math.radians(lat_deg), math.radians(lon_deg)
sin_lat, cos_lat, sin_lon, cos_lon = math.sin(lat), math.cos(lat), math.sin(lon), math.cos(lon)
n = a / math.sqrt(1.0 - e2 * sin_lat * sin_lat)
return np.array([(n + height_m) * cos_lat * cos_lon, (n + height_m) * cos_lat * sin_lon,
(n * (1.0 - e2) + height_m) * sin_lat], dtype=float)
def ecef_to_enu(ecef: np.ndarray, origin: np.ndarray, lat_deg: float, lon_deg: float) -> np.ndarray:
lat, lon = math.radians(lat_deg), math.radians(lon_deg)
slat, clat, slon, clon = math.sin(lat), math.cos(lat), math.sin(lon), math.cos(lon)
rotation = np.array([[-slon, clon, 0.0], [-slat * clon, -slat * slon, clat],
[clat * clon, clat * slon, slat]], dtype=float)
return rotation @ (ecef - origin)
def yaw_rotation(yaw: float) -> np.ndarray:
c, s = math.cos(yaw), math.sin(yaw)
return np.array([[c, -s, 0.0], [s, c, 0.0], [0.0, 0.0, 1.0]])
def scalar(data: np.lib.npyio.NpzFile, name: str) -> float:
return float(np.asarray(data[name]).reshape(-1)[0])
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--combined-root", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
parser.add_argument("--pose-name", default="rtk_gga_raw_heading")
parser.add_argument("--heading-offset-deg", type=float, required=True)
parser.add_argument("--antenna-lever", type=float, nargs=3, required=True, metavar=("X", "Y", "Z"))
parser.add_argument("--accepted-fixes", type=int, nargs="+", default=[4, 5])
parser.add_argument("--heading-std-limit-deg", type=float, default=0.5)
parser.add_argument("--min-stations", type=int, default=30)
parser.add_argument("--expected-stations", type=int, default=0)
parser.add_argument("--overwrite", action="store_true")
return parser.parse_args()
def main() -> int:
args = parse_args()
manifest_path = args.combined_root / "manifest.csv"
with manifest_path.open("r", encoding="utf-8-sig", newline="") as stream:
rows = list(csv.DictReader(stream))
required = {"segment", "output", "lidar_time_ns", "rtk_valid", "heading_valid", "rtk_fix_quality"}
if not rows or not required.issubset(rows[0]):
raise ValueError(f"{manifest_path} is empty or lacks {sorted(required)}")
groups: dict[str, list[dict[str, str]]] = {}
for row in rows:
groups.setdefault(row["segment"], []).append(row)
selected, summaries, rejected = [], [], []
accepted_fixes = set(args.accepted_fixes)
for segment in sorted(groups, key=natural_key):
group = sorted(groups[segment], key=lambda row: int(row["lidar_time_ns"]))
good = [row for row in group if truth(row["rtk_valid"]) and truth(row["heading_valid"])
and int(row["rtk_fix_quality"]) in accepted_fixes]
if not good:
rejected.append({"station": segment, "reason": "no associated fixed RTK position and valid heading"})
continue
samples = []
for row in good:
path = args.combined_root / Path(row["output"])
with np.load(path, allow_pickle=False) as data:
samples.append((scalar(data, "rtk_lat_deg"), scalar(data, "rtk_lon_deg"),
scalar(data, "rtk_altitude_m"), scalar(data, "rtk_raw_heading_deg"),
scalar(data, "rtk_pitch_deg"), scalar(data, "rtk_heading_stddev_deg")))
values = np.asarray(samples, dtype=float)
heading_std = circular_std_deg(values[:, 3])
if heading_std > args.heading_std_limit_deg:
rejected.append({"station": segment, "reason": f"heading std {heading_std:.4f} deg exceeds limit"})
continue
frame = good[len(good) // 2]
source = args.combined_root / Path(frame["output"])
selected.append({"station": segment, "source": source, "time": int(frame["lidar_time_ns"]) / 1e9,
"lat": float(np.mean(values[:, 0])), "lon": float(np.mean(values[:, 1])),
"alt": float(np.mean(values[:, 2])), "heading": circular_mean_deg(values[:, 3])})
summaries.append({"station": segment, "frames": len(group), "valid_fixed_frames": len(good),
"heading_mean_deg": circular_mean_deg(values[:, 3]),
"heading_circular_std_deg": heading_std, "rtk_pitch_mean_deg": float(np.mean(values[:, 4])),
"reported_heading_std_mean_deg": float(np.nanmean(values[:, 5])),
"altitude_std_m": float(np.std(values[:, 2])), "selected_source": str(source)})
if args.expected_stations and len(selected) != args.expected_stations:
raise RuntimeError(f"expected {args.expected_stations} usable stations, got {len(selected)}; rejected={rejected}")
if len(selected) < args.min_stations:
raise RuntimeError(f"need at least {args.min_stations} usable stations, got {len(selected)}; rejected={rejected}")
if args.output.exists() and any(args.output.iterdir()) and not args.overwrite:
raise FileExistsError(f"{args.output} is non-empty; pass --overwrite")
frames = args.output / "frames_all"
frames.mkdir(parents=True, exist_ok=True)
origin = selected[0]
origin_ecef = geodetic_to_ecef(origin["lat"], origin["lon"], origin["alt"])
lever = np.asarray(args.antenna_lever, dtype=float)
pose_rows = []
for index, item in enumerate(selected, 1):
destination = frames / f"station_{index:02d}.npz"
shutil.copy2(item["source"], destination)
antenna = ecef_to_enu(geodetic_to_ecef(item["lat"], item["lon"], item["alt"]), origin_ecef,
origin["lat"], origin["lon"])
corrected_heading = (item["heading"] + args.heading_offset_deg) % 360.0
yaw = math.radians(90.0 - corrected_heading)
reference_position = antenna - yaw_rotation(yaw) @ lever
pose_rows.append(dict(zip(POSE_FIELDS, [item["time"], *reference_position, 0.0, 0.0,
math.sin(yaw / 2.0), math.cos(yaw / 2.0)])))
summaries[index - 1].update({"sequence": index, "prepared_frame": destination.name,
"corrected_heading_deg": corrected_heading})
pose_path = args.output / f"reference_poses_{args.pose_name}.csv"
with pose_path.open("w", encoding="utf-8", newline="") as stream:
writer = csv.DictWriter(stream, fieldnames=POSE_FIELDS); writer.writeheader(); writer.writerows(pose_rows)
with (args.output / "station_summary.csv").open("w", encoding="utf-8", newline="") as stream:
fields = sorted({key for row in summaries for key in row})
writer = csv.DictWriter(stream, fieldnames=fields); writer.writeheader(); writer.writerows(summaries)
document = {"source_combined_root": str(args.combined_root.resolve()), "station_count": len(selected),
"rejected": rejected, "pose_csv": pose_path.name,
"selection_policy": "middle LiDAR frame among fixed-position and valid-heading associations",
"reference_pose_configuration": {"raw_heading_offset_deg": args.heading_offset_deg,
"antenna_lever_body_m": args.antenna_lever,
"orientation_model": "yaw-only, identical to the previous calibration workflow"},
"stations": [{"sequence": i + 1, "source_station": item["station"],
"source_frame": str(item["source"]), "prepared_frame": f"station_{i + 1:02d}.npz"}
for i, item in enumerate(selected)]}
(args.output / "manifest.json").write_text(json.dumps(document, ensure_ascii=False, indent=2), encoding="utf-8")
print(json.dumps({"prepared": str(args.output.resolve()), "stations": len(selected),
"rejected": rejected, "pose_csv": pose_path.name}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
@@ -0,0 +1,22 @@
from __future__ import annotations
import argparse
from pathlib import Path
from capture_format_v2 import file_summary, read_capture
from pipeline_common import write_json
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("captures", nargs="+", type=Path)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
summaries = [file_summary(read_capture(path)) for path in args.captures]
write_json(args.out, {"captures": summaries})
for summary in summaries:
print(summary)
if __name__ == "__main__":
main()
@@ -0,0 +1,254 @@
from __future__ import annotations
import binascii
import io
import struct
from dataclasses import dataclass, asdict
from pathlib import Path
from typing import BinaryIO, Iterator
FILE_MAGIC = "RAW_SERIAL_CAPTURE_FILE_V2"
RECORD_MAGIC = "RAW_SERIAL_RECORD_V2"
FOOTER_MAGIC = "RAW_SERIAL_CAPTURE_FOOTER_V2"
def read_7bit_int(stream: BinaryIO) -> int:
value = 0
shift = 0
while True:
raw = stream.read(1)
if not raw:
raise EOFError("truncated .NET string length")
value |= (raw[0] & 0x7F) << shift
if not raw[0] & 0x80:
return value
shift += 7
if shift > 35:
raise ValueError("invalid .NET string length")
def read_dotnet_string(stream: BinaryIO) -> str:
length = read_7bit_int(stream)
raw = stream.read(length)
if len(raw) != length:
raise EOFError("truncated .NET string")
return raw.decode("utf-8")
def read_i32(stream: BinaryIO) -> int:
raw = stream.read(4)
if len(raw) != 4:
raise EOFError("truncated int32")
return struct.unpack("<i", raw)[0]
def read_i64(stream: BinaryIO) -> int:
raw = stream.read(8)
if len(raw) != 8:
raise EOFError("truncated int64")
return struct.unpack("<q", raw)[0]
def read_u32(stream: BinaryIO) -> int:
raw = stream.read(4)
if len(raw) != 4:
raise EOFError("truncated uint32")
return struct.unpack("<I", raw)[0]
@dataclass(frozen=True)
class CaptureHeader:
sensor_kind: str
session_id: str
session_start_utc_ticks: int
session_start_monotonic_ticks: int
monotonic_frequency: int
port: str
baud: int
file_start_utc_ticks: int
@dataclass(frozen=True)
class RawChunk:
sequence: int
receive_utc_ticks: int
receive_monotonic_ticks: int
raw: bytes
record_file_offset: int
raw_file_offset: int
record_crc32: int
crc_valid: bool
@dataclass(frozen=True)
class CaptureFooter:
clean_close: bool
records: int
bytes: int
first_sequence: int
last_sequence: int
dropped_chunks: int
dropped_bytes: int
crc_valid: bool
@dataclass
class CaptureFile:
path: str
header: CaptureHeader
chunks: list[RawChunk]
footer: CaptureFooter | None
truncated_tail: bool = False
def read_header(stream: BinaryIO) -> CaptureHeader:
if read_dotnet_string(stream) != FILE_MAGIC:
raise ValueError("not a V2 raw capture file")
version = read_i32(stream)
if version != 2:
raise ValueError(f"unsupported capture version: {version}")
return CaptureHeader(
sensor_kind=read_dotnet_string(stream),
session_id=read_dotnet_string(stream),
session_start_utc_ticks=read_i64(stream),
session_start_monotonic_ticks=read_i64(stream),
monotonic_frequency=read_i64(stream),
port=read_dotnet_string(stream),
baud=read_i32(stream),
file_start_utc_ticks=read_i64(stream),
)
def parse_record_body(body: bytes, record_file_offset: int, record_crc: int) -> RawChunk:
stream = io.BytesIO(body)
if read_dotnet_string(stream) != RECORD_MAGIC:
raise ValueError("invalid record magic")
sequence = read_i64(stream)
receive_utc_ticks = read_i64(stream)
receive_monotonic_ticks = read_i64(stream)
raw_length = read_i32(stream)
if raw_length < 0 or raw_length > 64 * 1024 * 1024:
raise ValueError(f"invalid raw length: {raw_length}")
raw_offset = record_file_offset + 4 + stream.tell()
raw = stream.read(raw_length)
if len(raw) != raw_length:
raise EOFError("truncated raw bytes")
crc_valid = (binascii.crc32(body) & 0xFFFFFFFF) == record_crc
return RawChunk(
sequence=sequence,
receive_utc_ticks=receive_utc_ticks,
receive_monotonic_ticks=receive_monotonic_ticks,
raw=raw,
record_file_offset=record_file_offset,
raw_file_offset=raw_offset,
record_crc32=record_crc,
crc_valid=crc_valid,
)
def parse_footer(body: bytes, expected_crc: int) -> CaptureFooter:
stream = io.BytesIO(body)
if read_dotnet_string(stream) != FOOTER_MAGIC:
raise ValueError("invalid footer magic")
clean_close = stream.read(1) == b"\x01"
records = read_i64(stream)
raw_bytes = read_i64(stream)
first_sequence = read_i64(stream)
last_sequence = read_i64(stream)
dropped_chunks = read_i64(stream)
dropped_bytes = read_i64(stream)
return CaptureFooter(
clean_close=clean_close,
records=records,
bytes=raw_bytes,
first_sequence=first_sequence,
last_sequence=last_sequence,
dropped_chunks=dropped_chunks,
dropped_bytes=dropped_bytes,
crc_valid=(binascii.crc32(body) & 0xFFFFFFFF) == expected_crc,
)
def read_capture(path: Path) -> CaptureFile:
chunks: list[RawChunk] = []
footer = None
truncated = False
with path.open("rb") as stream:
header = read_header(stream)
while True:
record_offset = stream.tell()
length_raw = stream.read(4)
if not length_raw:
break
if len(length_raw) != 4:
truncated = True
break
length = struct.unpack("<i", length_raw)[0]
try:
if length == -1:
footer_length = read_i32(stream)
if footer_length < 0 or footer_length > 1024 * 1024:
raise ValueError("invalid footer length")
footer_body = stream.read(footer_length)
if len(footer_body) != footer_length:
raise EOFError("truncated footer")
footer = parse_footer(footer_body, read_u32(stream))
break
if length <= 0 or length > 64 * 1024 * 1024:
raise ValueError("invalid record length")
body = stream.read(length)
if len(body) != length:
raise EOFError("truncated record body")
record_crc = read_u32(stream)
chunks.append(parse_record_body(body, record_offset, record_crc))
except (EOFError, ValueError):
truncated = True
break
return CaptureFile(str(path), header, chunks, footer, truncated)
def sequence_gaps(chunks: list[RawChunk]) -> list[tuple[int, int, int]]:
result = []
for previous, current in zip(chunks, chunks[1:]):
if current.sequence > previous.sequence + 1:
result.append((previous.sequence, current.sequence, current.sequence - previous.sequence - 1))
return result
def file_summary(capture: CaptureFile) -> dict:
gaps = sequence_gaps(capture.chunks)
sequences = [chunk.sequence for chunk in capture.chunks]
return {
"path": capture.path,
"sensor": capture.header.sensor_kind,
"session_id": capture.header.session_id,
"port": capture.header.port,
"baud": capture.header.baud,
"chunks_read": len(capture.chunks),
"bytes_read": sum(len(chunk.raw) for chunk in capture.chunks),
"first_sequence": sequences[0] if sequences else None,
"last_sequence": sequences[-1] if sequences else None,
"missing_chunks": sum(gap[2] for gap in gaps),
"gap_count": len(gaps),
"bad_record_crc": sum(not chunk.crc_valid for chunk in capture.chunks),
"truncated_tail": capture.truncated_tail,
"footer": None if capture.footer is None else asdict(capture.footer),
"gaps": gaps[:100],
}
def iter_contiguous_segments(chunks: list[RawChunk]) -> Iterator[tuple[int, list[RawChunk]]]:
if not chunks:
return
segment_id = 0
current = [chunks[0]]
for previous, chunk in zip(chunks, chunks[1:]):
if chunk.sequence != previous.sequence + 1:
yield segment_id, current
segment_id += 1
current = [chunk]
else:
current.append(chunk)
yield segment_id, current
@@ -0,0 +1,35 @@
from __future__ import annotations
import argparse
from pathlib import Path
from capture_format_v2 import file_summary, read_capture
from pipeline_common_corrected import parse_imu_capture, parse_rtk_capture, write_json, write_jsonl
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--rtk", type=Path, required=True)
parser.add_argument("--imu", type=Path, required=True)
parser.add_argument("--out", type=Path, required=True)
args = parser.parse_args()
args.out.mkdir(parents=True, exist_ok=True)
rtk_capture = read_capture(args.rtk)
imu_capture = read_capture(args.imu)
rtk_rows = parse_rtk_capture(rtk_capture)
imu_rows = parse_imu_capture(imu_capture)
write_jsonl(args.out / "rtk.jsonl", rtk_rows)
write_jsonl(args.out / "imu.jsonl", imu_rows)
write_json(args.out / "parse_summary.json", {
"rtk_capture": file_summary(rtk_capture),
"imu_capture": file_summary(imu_capture),
"rtk_records": len(rtk_rows),
"rtk_checksum_valid": sum(bool(row.get("checksum_valid")) for row in rtk_rows),
"imu_frames": len(imu_rows),
"imu_crc_valid": sum(bool(row.get("crc_valid")) for row in imu_rows),
})
print(f"RTK records={len(rtk_rows)}, IMU frames={len(imu_rows)}")
if __name__ == "__main__":
main()
@@ -0,0 +1,300 @@
from __future__ import annotations
import binascii
import json
import math
import struct
from pathlib import Path
from typing import Iterable
from capture_format_v2 import CaptureFile, RawChunk, iter_contiguous_segments, read_capture
DOTNET_UNIX_EPOCH_TICKS = 621355968000000000
def ticks_to_unix_ns(ticks: int) -> int:
return (ticks - DOTNET_UNIX_EPOCH_TICKS) * 100
def safe_float(value: str, default=None):
try:
return float(value)
except (TypeError, ValueError):
return default
def safe_int(value: str, default=None):
try:
return int(value)
except (TypeError, ValueError):
return default
def nmea_checksum_valid(line: str) -> bool:
star = line.rfind("*")
if star < 0:
return False
try:
expected = int(line[star + 1:star + 3], 16)
except ValueError:
return False
value = 0
for char in line[1:star]:
value ^= ord(char)
return value == expected
def unicore_crc32(text: str) -> int:
crc = 0
for value in text.encode("ascii", "replace"):
crc ^= value
for _ in range(8):
crc = (crc >> 1) ^ (0xEDB88320 if crc & 1 else 0)
return crc & 0xFFFFFFFF
def unicore_checksum_valid(line: str) -> bool:
star = line.rfind("*")
if star < 0 or len(line) < star + 9:
return False
try:
expected = int(line[star + 1:star + 9], 16)
except ValueError:
return False
return unicore_crc32(line[1:star]) == expected
def parse_checksum(line: str) -> bool:
if line.startswith("$"):
return nmea_checksum_valid(line)
if line.startswith("#"):
return unicore_checksum_valid(line)
return False
def parse_nmea_latlon(value: str, hemisphere: str):
raw = safe_float(value)
if raw is None:
return None
degrees = math.floor(raw / 100.0)
result = degrees + (raw - degrees * 100.0) / 60.0
if hemisphere.upper() in ("S", "W"):
result = -result
return result
def parse_gga(line: str) -> dict:
fields = line[:line.rfind("*")].split(",")
if len(fields) < 10:
raise ValueError("GGA has too few fields")
return {
"type": "GGA",
"position_time_utc": fields[1],
"lat_deg": parse_nmea_latlon(fields[2], fields[3]),
"lon_deg": parse_nmea_latlon(fields[4], fields[5]),
"fix_quality": safe_int(fields[6], -1),
"satellites": safe_int(fields[7], -1),
"hdop": safe_float(fields[8]),
"altitude_m": safe_float(fields[9]),
"geoid_separation_m": safe_float(fields[11]) if len(fields) > 11 else None,
"differential_age_s": safe_float(fields[13]) if len(fields) > 13 else None,
"station_id": fields[14].strip('"') if len(fields) > 14 else "",
}
def parse_heading(line: str) -> dict:
before_crc = line[:line.rfind("*")]
header, payload = before_crc.split(";", 1)
header_fields = header.split(",")
fields = payload.split(",")
if len(fields) < 7:
raise ValueError("UNIHEADINGA has too few fields")
raw_heading = safe_float(fields[3])
return {
"type": "UNIHEADINGA",
"gnss_week": safe_int(header_fields[4]) if len(header_fields) > 4 else None,
"gnss_tow_ms": safe_int(header_fields[5]) if len(header_fields) > 5 else None,
"heading_status": fields[0],
"heading_solution": fields[1],
"baseline_length_m": safe_float(fields[2]),
"raw_heading_deg": raw_heading,
"pitch_deg": safe_float(fields[4]),
"heading_stddev_deg": safe_float(fields[6]),
"pitch_stddev_deg": safe_float(fields[7]) if len(fields) > 7 else None,
"station_id": fields[8].strip('"') if len(fields) > 8 else "",
"satellites": safe_int(fields[9], -1) if len(fields) > 9 else -1,
"solution_satellites": safe_int(fields[10], -1) if len(fields) > 10 else -1,
"observations": safe_int(fields[11], -1) if len(fields) > 11 else -1,
"multi_count": safe_int(fields[12], -1) if len(fields) > 12 else -1,
"heading_valid": fields[0] == "SOL_COMPUTED" and fields[1] in {"NARROW_INT", "NARROW_FLOAT"},
}
def chunk_source(chunks: list[RawChunk], offset: int, end: int) -> dict:
first = chunks[0]
last = chunks[-1]
cursor = 0
start_chunk = first
end_chunk = last
for chunk in chunks:
chunk_start = cursor
chunk_end = cursor + len(chunk.raw)
if chunk_start <= offset < chunk_end:
start_chunk = chunk
if chunk_start < end <= chunk_end:
end_chunk = chunk
break
cursor = chunk_end
return {
"source_segment_id": None,
"source_chunk_sequence_first": start_chunk.sequence,
"source_chunk_sequence_last": end_chunk.sequence,
"source_raw_file_offset": start_chunk.raw_file_offset + max(0, offset - sum(len(c.raw) for c in chunks if c.sequence < start_chunk.sequence)),
"source_raw_byte_length": max(0, end - offset),
}
def parse_rtk_capture(capture: CaptureFile) -> list[dict]:
rows = []
for segment_id, chunks in iter_contiguous_segments(capture.chunks):
stream = b"".join(chunk.raw for chunk in chunks)
cursor = 0
while cursor < len(stream):
newline = stream.find(b"\n", cursor)
if newline < 0:
break
end = newline + 1
raw_line = stream[cursor:end].rstrip(b"\r\n")
cursor = end
if not raw_line:
continue
line = raw_line.decode("ascii", "replace")
valid = parse_checksum(line)
row = {
"type": "UNKNOWN",
"raw_line": line,
"checksum_valid": valid,
"host_receive_utc_ns": ticks_to_unix_ns(chunks[-1].receive_utc_ticks),
"host_receive_monotonic_ticks": chunks[-1].receive_monotonic_ticks,
"source_segment_id": segment_id,
"source_byte_offset_in_segment": cursor - len(raw_line) - 1,
"source_byte_length": len(raw_line) + 1,
}
try:
if line.startswith("$GNGGA") or line.startswith("$GPGGA"):
row.update(parse_gga(line))
elif line.startswith("#UNIHEADINGA"):
row.update(parse_heading(line))
except ValueError as ex:
row["parse_error"] = str(ex)
rows.append(row)
return rows
def crc16_hi13(data: bytes) -> int:
crc = 0
for value in data:
crc ^= value << 8
for _ in range(8):
crc = ((crc << 1) ^ 0x1021) & 0xFFFF if crc & 0x8000 else (crc << 1) & 0xFFFF
return crc
def decode_hi91(frame: bytes) -> dict:
f32 = lambda i: struct.unpack_from("<f", frame, i)[0]
return {
"tag": 0x91,
"pps_sync_stamp_ms": int.from_bytes(frame[7:9], "little"),
"temperature_c": struct.unpack_from("<b", frame, 9)[0],
"air_pressure_pa": f32(10),
"device_timestamp_ms": int.from_bytes(frame[14:18], "little"),
"accel_x_mps2": f32(18) * 9.80665,
"accel_y_mps2": f32(22) * 9.80665,
"accel_z_mps2": f32(26) * 9.80665,
"gyro_x_radps": f32(30) * math.pi / 180.0,
"gyro_y_radps": f32(34) * math.pi / 180.0,
"gyro_z_radps": f32(38) * math.pi / 180.0,
"mag_x_ut": f32(42), "mag_y_ut": f32(46), "mag_z_ut": f32(50),
"roll_deg": f32(54), "pitch_deg": f32(58), "yaw_deg": f32(62),
"quaternion_w": f32(66), "quaternion_x": f32(70),
"quaternion_y": f32(74), "quaternion_z": f32(78),
}
def decode_hi92(frame: bytes) -> dict:
i16 = lambda i: struct.unpack_from("<h", frame, i)[0]
i32 = lambda i: struct.unpack_from("<i", frame, i)[0]
return {
"tag": 0x92,
"status": int.from_bytes(frame[7:9], "little"),
"temperature_c": struct.unpack_from("<b", frame, 9)[0],
"pps_sync_stamp_ms": int.from_bytes(frame[10:12], "little"),
"air_pressure_pa": i16(12) + 100000.0,
"heave_m": i16(14) * 0.001,
"gyro_x_radps": i16(16) * 0.001, "gyro_y_radps": i16(18) * 0.001, "gyro_z_radps": i16(20) * 0.001,
"accel_x_mps2": i16(22) * 0.0048828, "accel_y_mps2": i16(24) * 0.0048828, "accel_z_mps2": i16(26) * 0.0048828,
"mag_x_ut": i16(28) * 0.030517, "mag_y_ut": i16(30) * 0.030517, "mag_z_ut": i16(32) * 0.030517,
"roll_deg": i32(34) * 0.001, "pitch_deg": i32(38) * 0.001, "yaw_deg": i32(42) * 0.001,
"quaternion_w": i16(46) * 0.0001, "quaternion_x": i16(48) * 0.0001,
"quaternion_y": i16(50) * 0.0001, "quaternion_z": i16(52) * 0.0001,
}
def parse_imu_capture(capture: CaptureFile) -> list[dict]:
rows = []
for segment_id, chunks in iter_contiguous_segments(capture.chunks):
stream = b"".join(chunk.raw for chunk in chunks)
cursor = 0
while True:
start = stream.find(b"\x5a\xa5", cursor)
if start < 0 or start + 6 > len(stream):
break
payload_length = int.from_bytes(stream[start + 2:start + 4], "little")
frame_length = 6 + payload_length
if payload_length <= 0 or payload_length > 512:
cursor = start + 1
continue
if start + frame_length > len(stream):
break
frame = stream[start:start + frame_length]
expected = int.from_bytes(frame[4:6], "little")
actual = crc16_hi13(frame[:4] + frame[6:])
end = start + frame_length
source = chunk_source(chunks, start, end)
source["source_segment_id"] = segment_id
row = {
"type": "HI13",
"tag": frame[6],
"frame_length": frame_length,
"crc_valid": expected == actual,
"host_receive_utc_ns": ticks_to_unix_ns(chunks[-1].receive_utc_ticks),
"host_receive_monotonic_ticks": chunks[-1].receive_monotonic_ticks,
"source_segment_id": segment_id,
"source_byte_offset_in_segment": start,
"source_byte_length": frame_length,
"raw_frame_hex": frame.hex(),
}
if expected == actual:
try:
row.update(decode_hi91(frame) if frame[6] == 0x91 else decode_hi92(frame) if frame[6] == 0x92 else {})
except (IndexError, struct.error, ValueError) as ex:
row["parse_error"] = str(ex)
rows.append(row)
cursor = end
return rows
def write_jsonl(path: Path, rows: Iterable[dict]) -> None:
with path.open("w", encoding="utf-8", newline="\n") as stream:
for row in rows:
stream.write(json.dumps(row, ensure_ascii=False, separators=(",", ":")) + "\n")
def write_json(path: Path, value: dict) -> None:
path.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
def load_jsonl(path: Path) -> list[dict]:
with path.open("r", encoding="utf-8") as stream:
return [json.loads(line) for line in stream if line.strip()]
@@ -0,0 +1,106 @@
from __future__ import annotations
import bisect
from pipeline_common import *
from capture_format_v2 import CaptureFile, RawChunk, iter_contiguous_segments
_SPAN_CACHE: dict[int, tuple[list[RawChunk], list[int]]] = {}
def _chunk_starts(chunks: list[RawChunk]) -> list[int]:
key = id(chunks)
cached = _SPAN_CACHE.get(key)
if cached is not None and cached[0] is chunks:
return cached[1]
starts = []
cursor = 0
for chunk in chunks:
starts.append(cursor)
cursor += len(chunk.raw)
_SPAN_CACHE[key] = (chunks, starts)
return starts
def source_for_span(chunks: list[RawChunk], start: int, end: int, segment_id: int) -> dict:
starts = _chunk_starts(chunks)
start_index = max(0, min(len(chunks) - 1, bisect.bisect_right(starts, start) - 1))
end_index = max(start_index, min(len(chunks) - 1, bisect.bisect_left(starts, end) - 1))
start_chunk = chunks[start_index]
end_chunk = chunks[end_index]
return {
"source_segment_id": segment_id,
"source_chunk_sequence_first": start_chunk.sequence,
"source_chunk_sequence_last": end_chunk.sequence,
"source_raw_file_offset": start_chunk.raw_file_offset + (start - starts[start_index]),
"source_raw_byte_length": end - start,
"host_receive_utc_ns": ticks_to_unix_ns(end_chunk.receive_utc_ticks),
"host_receive_monotonic_ticks": end_chunk.receive_monotonic_ticks,
}
def parse_rtk_capture(capture: CaptureFile) -> list[dict]:
rows = []
for segment_id, chunks in iter_contiguous_segments(capture.chunks):
stream = b"".join(chunk.raw for chunk in chunks)
cursor = 0
while cursor < len(stream):
newline = stream.find(b"\n", cursor)
if newline < 0:
break
end = newline + 1
raw_line = stream[cursor:end].rstrip(b"\r\n")
start = cursor
cursor = end
if not raw_line:
continue
line = raw_line.decode("ascii", "replace")
row = {"type": "UNKNOWN", "raw_line": line, "checksum_valid": parse_checksum(line)}
row.update(source_for_span(chunks, start, end, segment_id))
try:
if line.startswith("$GNGGA") or line.startswith("$GPGGA"):
row.update(parse_gga(line))
elif line.startswith("#UNIHEADINGA"):
row.update(parse_heading(line))
except ValueError as ex:
row["parse_error"] = str(ex)
rows.append(row)
return rows
def parse_imu_capture(capture: CaptureFile) -> list[dict]:
rows = []
for segment_id, chunks in iter_contiguous_segments(capture.chunks):
stream = b"".join(chunk.raw for chunk in chunks)
cursor = 0
while True:
start = stream.find(b"\x5a\xa5", cursor)
if start < 0 or start + 6 > len(stream):
break
payload_length = int.from_bytes(stream[start + 2:start + 4], "little")
frame_length = 6 + payload_length
if payload_length <= 0 or payload_length > 512:
cursor = start + 1
continue
if start + frame_length > len(stream):
break
frame = stream[start:start + frame_length]
expected = int.from_bytes(frame[4:6], "little")
actual = crc16_hi13(frame[:4] + frame[6:])
end = start + frame_length
row = {
"type": "HI13",
"tag": frame[6],
"frame_length": frame_length,
"crc_valid": expected == actual,
"raw_frame_hex": frame.hex(),
}
row.update(source_for_span(chunks, start, end, segment_id))
if row["crc_valid"]:
try:
row.update(decode_hi91(frame) if frame[6] == 0x91 else decode_hi92(frame) if frame[6] == 0x92 else {})
except (IndexError, struct.error, ValueError) as ex:
row["parse_error"] = str(ex)
rows.append(row)
cursor = end
return rows