支持 H32 DLogCapture(MSOP+DIFOP)站导出到 combined

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
lichun.qu
2026-08-05 10:17:23 +08:00
co-authored by Cursor
parent b2271d05ba
commit 69bb44bccd
15 changed files with 1168 additions and 105 deletions
+80 -29
View File
@@ -1,19 +1,22 @@
#!/usr/bin/env python3
"""One-shot export: raw H32/G90/N300 captures → RTKLiDAR ``combined/`` package.
Analogous to Lidar-IMU ``tools/export_rscap_to_v1.py``: raw ``.rscap`` in,
Analogous to Lidar-IMU ``tools/export_rscap_to_v1.py``: raw captures in,
calibration-ready intermediate out. Downstream prepare/solve consume ``combined/``
only (``manifest.csv`` + associated frame NPZs).
Expected raw layout:
Expected raw layout (new H32 DLogCapture):
stations/
001/h32.rscap
002/h32.rscap
001/ # dobject/ + dobject_recording/ (or 001/dlog/...)
002/
...
captures/ (paths passed explicitly)
rtk.rscap # G90: #PVTSLNA + #UNIHEADINGA
imu.rscap # N300 (associated only; not used in AX=XB)
captures/
rtk.rscap # G90: #PVTSLNA + #UNIHEADINGA
imu.rscap # N300 (associated only; not used in AX=XB)
Also accepts legacy per-station ``h32.rscap``, and older decoded-point-cloud dlog
stations (prefer ``--time-basis host`` for those).
Output under ``--out``:
@@ -22,9 +25,6 @@ Output under ``--out``:
combined/frames/*.npz + manifest.csv + dataset_summary.json
export_summary.json
Legacy dlog stations (``dobject`` + ``dobject_recording``) are still accepted;
use ``--time-basis host`` for those datasets.
Raw ``.rscap`` / dlog files are never modified.
"""
@@ -45,7 +45,14 @@ sys.path.insert(0, str(ROOT / "rscap_v2"))
from build_multisensor_npz import build_combined # noqa: E402
from capture_format_v2 import file_summary, read_capture # noqa: E402
from export_h32_rscap_station import export_station_h32, resolve_lidar_rscap # noqa: E402
from export_h32_rscap_station import ( # noqa: E402
export_station_h32,
export_station_h32_dlog,
is_h32_raw_dlog_station,
is_legacy_pointcloud_dlog_station,
resolve_lidar_rscap,
try_resolve_dlog_root,
)
from pipeline_common_corrected import ( # noqa: E402
parse_imu_capture,
parse_rtk_capture,
@@ -57,16 +64,21 @@ from pipeline_common_corrected import ( # noqa: E402
def is_h32_station(station: Path, capture_name: str) -> bool:
try:
resolve_lidar_rscap(station, capture_name)
return True
except FileNotFoundError:
return False
return True
def is_dlog_station(station: Path) -> bool:
return (station / "dobject").is_dir() and (station / "dobject_recording").is_dir()
return try_resolve_dlog_root(station) is not None
def discover_stations(stations_root: Path, names: list[str], capture_name: str) -> list[Path]:
def discover_stations(
stations_root: Path,
names: list[str],
capture_name: str,
msop_object: str,
) -> list[Path]:
if names:
stations = [stations_root / name for name in names]
missing = [str(path) for path in stations if not path.is_dir()]
@@ -77,13 +89,19 @@ def discover_stations(stations_root: Path, names: list[str], capture_name: str)
[
path
for path in stations_root.iterdir()
if path.is_dir() and (is_h32_station(path, capture_name) or is_dlog_station(path))
if path.is_dir()
and (
is_h32_station(path, capture_name)
or is_h32_raw_dlog_station(path, msop_object=msop_object)
or is_dlog_station(path)
)
],
key=lambda path: path.name,
)
if not stations:
raise FileNotFoundError(
f"no station with {capture_name}/lidar.rscap or dobject+dobject_recording under {stations_root}"
f"no station with H32 dlog/MSOP, {capture_name}/lidar.rscap, or "
f"dobject+dobject_recording under {stations_root}"
)
return stations
@@ -101,7 +119,7 @@ def export_legacy_dlog_station(
sys.executable,
str(exporter),
"--dlog",
str(station),
str(try_resolve_dlog_root(station) or station),
"--out",
str(out),
"--object",
@@ -154,6 +172,9 @@ def export_raw_to_combined(
out: Path,
station_names: list[str] | None = None,
lidar_capture_name: str = "h32.rscap",
msop_object: str = "frontlidar-msop-raw",
difop_object: str = "frontlidar-difop-raw",
require_difop: bool = True,
lidar_object: str = "frontlidar",
timezone: str = "+08:00",
stride: int = 1,
@@ -174,7 +195,6 @@ def export_raw_to_combined(
if out.exists() and any(out.iterdir()) and not overwrite:
raise FileExistsError(f"{out} is non-empty; pass --overwrite")
if overwrite and out.exists():
# Keep out root but clear known children so rebuild is deterministic.
for child in ("export", "parsed", "combined", "export_summary.json", "capture_audit.json"):
target = out / child
if target.is_dir():
@@ -187,15 +207,29 @@ def export_raw_to_combined(
parsed_root = out / "parsed"
combined_root = out / "combined"
stations = discover_stations(stations_root, station_names or [], lidar_capture_name)
stations = discover_stations(
stations_root, station_names or [], lidar_capture_name, msop_object
)
parse_summary = parse_serial(rtk_rscap, imu_rscap, parsed_root)
station_meta: list[dict[str, Any]] = []
lidar_segments: list[tuple[str, Path]] = []
saw_dlog = False
saw_legacy_dlog = False
for station in stations:
station_out = export_root / station.name
if is_h32_station(station, lidar_capture_name):
if is_h32_raw_dlog_station(station, msop_object=msop_object):
meta = export_station_h32_dlog(
station,
station_out,
msop_object=msop_object,
difop_object=difop_object,
require_difop=require_difop,
stride=stride,
write_reports=True,
resume=False,
)
kind = "h32_dlog_raw"
elif is_h32_station(station, lidar_capture_name):
meta = export_station_h32(
station,
station_out,
@@ -205,8 +239,10 @@ def export_raw_to_combined(
resume=False,
)
kind = "h32_rscap"
elif is_dlog_station(station):
saw_dlog = True
elif is_legacy_pointcloud_dlog_station(station, msop_object=msop_object) or is_dlog_station(
station
):
saw_legacy_dlog = True
export_legacy_dlog_station(
station,
station_out,
@@ -217,16 +253,18 @@ def export_raw_to_combined(
meta = {"source": str(station.resolve()), "kind": "legacy_dlog"}
kind = "legacy_dlog"
else:
raise RuntimeError(f"station {station.name} has neither H32 .rscap nor dlog layout")
raise RuntimeError(
f"station {station.name} has neither H32 raw dlog, .rscap, nor legacy dlog layout"
)
frames_dir = station_out / "frames"
if not frames_dir.is_dir() or not any(frames_dir.glob("*.npz")):
raise RuntimeError(f"no exported frames for station {station.name}: {frames_dir}")
lidar_segments.append((station.name, frames_dir))
station_meta.append({"station": station.name, "kind": kind, "frames_dir": str(frames_dir), **meta})
if saw_dlog and time_basis == "device_gnss":
if saw_legacy_dlog and time_basis == "device_gnss":
print(
"[warn] legacy dlog stations use host/DObject time; prefer --time-basis host",
"[warn] legacy point-cloud dlog stations use host/DObject time; prefer --time-basis host",
file=sys.stderr,
)
@@ -260,7 +298,9 @@ def export_raw_to_combined(
},
"timestamp_policy": {
"default_time_basis": time_basis,
"lidar_h32": "MSOP device timestamp → unix_time_ns",
"lidar_h32_dlog": "MSOP device timestamp → unix_time_ns; DIFOP channel angles for XYZ",
"lidar_h32_rscap": "MSOP device timestamp → unix_time_ns; default vertical angles",
"lidar_legacy_dlog": "DObject/host time; use time_basis=host",
"rtk": "GNSS week/TOW when time_basis=device_gnss; else host_receive_utc_ns",
"imu": "associated only; host-anchored device deltas in combined window",
"host_utc": "kept for audit; not the default calibration timeline for new captures",
@@ -281,8 +321,16 @@ def parse_args() -> argparse.Namespace:
parser.add_argument("--imu-rscap", type=Path, required=True, help="Continuous N300/IMU V2 .rscap")
parser.add_argument("--out", type=Path, required=True, help="Output package root (contains combined/)")
parser.add_argument("--station", action="append", default=[], help="Optional station name filter; repeatable")
parser.add_argument("--lidar-capture-name", default="h32.rscap")
parser.add_argument("--lidar-object", default="frontlidar", help="Legacy dlog DObject name")
parser.add_argument("--lidar-capture-name", default="h32.rscap", help="Legacy H32 .rscap filename")
parser.add_argument("--msop-object", default="frontlidar-msop-raw", help="Raw MSOP DObject name")
parser.add_argument("--difop-object", default="frontlidar-difop-raw", help="Raw DIFOP DObject name")
parser.add_argument(
"--require-difop",
action=argparse.BooleanOptionalAction,
default=True,
help="Require DIFOP channel angles for H32 raw dlog stations (default: true)",
)
parser.add_argument("--lidar-object", default="frontlidar", help="Legacy decoded point-cloud DObject name")
parser.add_argument("--timezone", default="+08:00", help="Legacy dlog tick timezone")
parser.add_argument("--stride", type=int, default=1)
parser.add_argument("--rtk-max-dt-ms", type=float, default=150.0)
@@ -304,6 +352,9 @@ def main() -> int:
out=args.out,
station_names=args.station,
lidar_capture_name=args.lidar_capture_name,
msop_object=args.msop_object,
difop_object=args.difop_object,
require_difop=args.require_difop,
lidar_object=args.lidar_object,
timezone=args.timezone,
stride=args.stride,