From 3c7060b3dcb5c85e031e449691fd8a5b379e067b Mon Sep 17 00:00:00 2001 From: Jacob Lambert Date: Thu, 22 Oct 2026 19:10:30 +0900 Subject: [PATCH] feat(schema): ego_pose.imu (raw IMU); acceleration is the gravity-free localization estimate --- docs/schema/v1/index.md | 2 +- docs/schema/v1/table.md | 5 ++++- t4_devkit/schema/v1/convert_v0.py | 5 ++++- t4_devkit/schema/v1/ego_pose.py | 9 ++++++++- t4_devkit/schema/v1/migrate_v0.py | 5 +++++ .../t4dataset_v1/annotation/ego_pose.json | 5 +++++ tests/sample/t4dataset_v1/make_sample_v1.py | 1 + tests/sample/t4dataset_v1/manifest.json | 2 +- tests/schema/v1/test_migrate_v0.py | 17 +++++++++++++++++ tests/schema/v1/test_tables.py | 1 + 10 files changed, 47 insertions(+), 5 deletions(-) diff --git a/docs/schema/v1/index.md b/docs/schema/v1/index.md index 84c03dc9..2107ba32 100644 --- a/docs/schema/v1/index.md +++ b/docs/schema/v1/index.md @@ -168,7 +168,7 @@ Each 0.x `calibrated_sensor` row becomes a `sensor` row, keeping its token. The Labels are `manual` unless 0.x marks them as autolabel (`provenance` = model names). `instance.nbr_annotations` and its first / last annotation are computed over its 3D boxes and 2D labels; 0.x tools count them differently (some write 0 for 2D labels). Categories without `index` get their position in `category.json`, as the 0.x devkit reads them (all set or none). `surface_ann` rows become `object_ann` rows without an instance, with `bbox` computed from the mask. 0.x visibility `unavailable` (or any unknown level) becomes `visibility_token: null`, and the aliases `v0-40`, `v40-60`, `v60-80`, `v80-100` map to `none`, `partial`, `most`, `full`. -`vehicle_state` rows get `source: vehicle_interface` (where the 0.x converter reads them) and shift `FORWARD`/`HIGH` becomes `DRIVE`; `additional_info.speed` is dropped and the velocities are `null`. The 0.x `traffic_light` table (`primitive_id`) and `traffic_light_instance_map` table (`traffic_light_linestring_id`, which wins if both list a lamp) link a lamp instance to its lanelet2 way (`type=traffic_light`) and have no states: the way becomes that instance's `map_element` (`type: way`), and the 1.0 `traffic_light` table stays empty. Source LiDARs of a concatenation have no frames of their own in 0.x: they take the concatenated channel's format and `fields`, and `frame_count` is the number of concatenated frames whose `.info` file lists points from them. +`vehicle_state` rows get `source: vehicle_interface` (where the 0.x converter reads them) and shift `FORWARD`/`HIGH` becomes `DRIVE`; `additional_info.speed` is dropped and the velocities are `null`. 0.x `ego_pose.acceleration` is dropped too: it held raw IMU readings whose frame and gravity sign vary by vehicle, and 1.0 `acceleration` is gravity-free; `acceleration` and `imu` come from the backfill. The 0.x `traffic_light` table (`primitive_id`) and `traffic_light_instance_map` table (`traffic_light_linestring_id`, which wins if both list a lamp) link a lamp instance to its lanelet2 way (`type=traffic_light`) and have no states: the way becomes that instance's `map_element` (`type: way`), and the 1.0 `traffic_light` table stays empty. Source LiDARs of a concatenation have no frames of their own in 0.x: they take the concatenated channel's format and `fields`, and `frame_count` is the number of concatenated frames whose `.info` file lists points from them. 1.0 requires some values that 0.x never stored. They are filled with these placeholders (`V0Converter.FALLBACKS`), which keep the dataset loadable but say nothing about it. In particular, `vehicle.model == "unknown"` means the vehicle dimensions are placeholders and must not be used, and `"unknown"` map IDs mean the map is not identified: diff --git a/docs/schema/v1/table.md b/docs/schema/v1/table.md index 524795b4..4cf28a06 100644 --- a/docs/schema/v1/table.md +++ b/docs/schema/v1/table.md @@ -93,7 +93,8 @@ ego_pose { "rotation": <[float;4]> -- Orientation as quaternion (w, x, y, z). "timestamp": -- Unix timestamp in microseconds. "twist": -- (vx, vy, vz, yaw_rate, pitch_rate, roll_rate) in the vehicle frame. - "acceleration": -- (ax, ay, az) in the vehicle frame, as an IMU measures it: gravity included (az ≈ +9.8 m/s² at rest). + "acceleration": -- (ax, ay, az) in the vehicle frame [m/s²], without gravity: the localization estimate, what Autoware feeds the planner. + "imu": -- (ax, ay, az, wx, wy, wz): the raw IMU in the vehicle frame, specific force with gravity (az ≈ +9.8 m/s² at rest) and angular velocity [rad/s]. "geocoordinate": -- (latitude, longitude, altitude). "source": -- Where the pose came from. "frame": -- Coordinate frame, e.g. "map". @@ -101,6 +102,8 @@ ego_pose { } ``` +An `ego_pose` sits at a sample time. `twist`, `acceleration`, `imu`, `geocoordinate` and `covariance` hold the latest message at or before `timestamp`, best effort; the pose itself is interpolated at `timestamp`. + ### Sensor - Filename: `sensor.json` diff --git a/t4_devkit/schema/v1/convert_v0.py b/t4_devkit/schema/v1/convert_v0.py index 6d44b238..bf906fe0 100644 --- a/t4_devkit/schema/v1/convert_v0.py +++ b/t4_devkit/schema/v1/convert_v0.py @@ -254,7 +254,10 @@ def convert_ego_pose(self, record: Record) -> Record: "rotation": record["rotation"], "timestamp": record["timestamp"], "twist": record.get("twist"), - "acceleration": record.get("acceleration"), + # 0.x acceleration is a raw IMU reading whose frame and gravity sign vary by + # vehicle; 1.0 acceleration is gravity-free. Both come from the backfill. + "acceleration": None, + "imu": None, "geocoordinate": record.get("geocoordinate"), "covariance": None, "source": fallbacks["source"], diff --git a/t4_devkit/schema/v1/ego_pose.py b/t4_devkit/schema/v1/ego_pose.py index 3b4e6da1..9250ddf4 100644 --- a/t4_devkit/schema/v1/ego_pose.py +++ b/t4_devkit/schema/v1/ego_pose.py @@ -34,11 +34,17 @@ class EgoPose(SchemaBase): rotation (Quaternion): Orientation in `frame`, given as quaternion [w, x, y, z]. timestamp (int): Unix time stamp [us]. twist (Vector6 | None): (vx, vy, vz, yaw_rate, pitch_rate, roll_rate) in the vehicle frame. - acceleration (Vector3 | None): (ax, ay, az) in the vehicle frame [m/s^2]. + acceleration (Vector3 | None): (ax, ay, az) in the vehicle frame [m/s^2], without gravity: + the localization estimate, what Autoware feeds the planner. + imu (Vector6 | None): (ax, ay, az, wx, wy, wz): the raw IMU in the vehicle frame, specific + force with gravity (az ≈ +9.8 m/s^2 at rest) and angular velocity [rad/s]. geocoordinate (Vector3 | None): (latitude, longitude, altitude). source (EgoPoseSource): Where the pose came from. frame (str): Coordinate frame of the pose, e.g. "map". covariance (list[float] | None): 6x6 pose covariance, row-major. + + `twist`, `acceleration`, `imu`, `geocoordinate` and `covariance` hold the latest message at + or before `timestamp`, best effort; the pose itself is interpolated at `timestamp`. """ translation: Vector3 = field(converter=Vector3) @@ -46,6 +52,7 @@ class EgoPose(SchemaBase): timestamp: int = field(validator=validators.instance_of(int)) twist: Vector6 | None = field(converter=converters.optional(Vector6)) acceleration: Vector3 | None = field(converter=converters.optional(Vector3)) + imu: Vector6 | None = field(converter=converters.optional(Vector6)) geocoordinate: Vector3 | None = field(converter=converters.optional(Vector3)) source: EgoPoseSource = field(converter=EgoPoseSource) frame: str = field(validator=(validators.instance_of(str), impossible_empty())) diff --git a/t4_devkit/schema/v1/migrate_v0.py b/t4_devkit/schema/v1/migrate_v0.py index 4ab8a075..560d1fef 100644 --- a/t4_devkit/schema/v1/migrate_v0.py +++ b/t4_devkit/schema/v1/migrate_v0.py @@ -209,6 +209,11 @@ def migrate( self.notes.append( "provenance.rosbag.record_start / record_end are the first / last sample times" ) + if any(record.get("acceleration") for record in self._load_v0("ego_pose")): + self.notes.append( + "ego_pose.acceleration dropped: 0.x stored raw IMU readings, whose frame and " + "gravity sign vary by vehicle; the backfill restores acceleration and imu" + ) manifest_info = self._manifest_info(dataset_id, version) manifest_info.update(fields) manifest_info["migration_notes"] = self.notes + list(fields.get("migration_notes", [])) diff --git a/tests/sample/t4dataset_v1/annotation/ego_pose.json b/tests/sample/t4dataset_v1/annotation/ego_pose.json index 0d846ce6..c44fb1c9 100644 --- a/tests/sample/t4dataset_v1/annotation/ego_pose.json +++ b/tests/sample/t4dataset_v1/annotation/ego_pose.json @@ -15,6 +15,7 @@ "timestamp": 1704067200000000, "twist": null, "acceleration": null, + "imu": null, "geocoordinate": null, "source": "localization", "frame": "map", @@ -36,6 +37,7 @@ "timestamp": 1704067200100000, "twist": null, "acceleration": null, + "imu": null, "geocoordinate": null, "source": "localization", "frame": "map", @@ -57,6 +59,7 @@ "timestamp": 1704067200000000, "twist": null, "acceleration": null, + "imu": null, "geocoordinate": null, "source": "localization", "frame": "map", @@ -78,6 +81,7 @@ "timestamp": 1704067200100000, "twist": null, "acceleration": null, + "imu": null, "geocoordinate": null, "source": "localization", "frame": "map", @@ -99,6 +103,7 @@ "timestamp": 1704067200200000, "twist": null, "acceleration": null, + "imu": null, "geocoordinate": null, "source": "localization", "frame": "map", diff --git a/tests/sample/t4dataset_v1/make_sample_v1.py b/tests/sample/t4dataset_v1/make_sample_v1.py index 9383d4bd..c5062359 100644 --- a/tests/sample/t4dataset_v1/make_sample_v1.py +++ b/tests/sample/t4dataset_v1/make_sample_v1.py @@ -154,6 +154,7 @@ def link(tokens: list[str], i: int) -> dict: "timestamp": timestamps[i], "twist": None, "acceleration": None, + "imu": None, "geocoordinate": None, "source": "localization", "frame": "map", diff --git a/tests/sample/t4dataset_v1/manifest.json b/tests/sample/t4dataset_v1/manifest.json index 95820f7d..1324412d 100644 --- a/tests/sample/t4dataset_v1/manifest.json +++ b/tests/sample/t4dataset_v1/manifest.json @@ -45,7 +45,7 @@ }, "ego_pose": { "path": "annotation/ego_pose.json", - "sha256": "41c24c1c135773c2037e63f8aafe9080e2345eedd8a6ec71872d5ef7cabf6ce2" + "sha256": "c797549026aa60e7e5aa5578511754d62de75c52fc9f079fd7c914796a1f6c79" }, "sensor": { "path": "annotation/sensor.json", diff --git a/tests/schema/v1/test_migrate_v0.py b/tests/schema/v1/test_migrate_v0.py index 27370fb3..13bf8839 100644 --- a/tests/schema/v1/test_migrate_v0.py +++ b/tests/schema/v1/test_migrate_v0.py @@ -306,3 +306,20 @@ def test_migrate_with_backfilled_metadata(tmp_path: Path) -> None: assert vehicles[0]["model"] == "j6_gen2" reports = sanity_check(output_root.parent.as_posix(), revision="1").reports assert not [report.id for report in reports if report.is_failed()] + + +def test_migrate_drops_v0_acceleration(tmp_path: Path) -> None: + """0.x `ego_pose.acceleration` (a raw IMU reading) is dropped, with a note.""" + sample = SampleCopy(tmp_path, SAMPLE_V0, name="v0") + sample.edit( + "ego_pose", lambda records: [dict(r, acceleration=[0.0, 0.0, -9.8]) for r in records] + ) + output_root = tmp_path / "t4dataset" / "1" + with pytest.warns(UserWarning): # the sample's fractional intensity stays float32 + manifest = V0Migrator(sample.root, output_root).migrate("t4dataset", 1, keep_images=True) + + poses = json.loads((output_root / "annotation" / "ego_pose.json").read_text()) + assert all(pose["acceleration"] is None and pose["imu"] is None for pose in poses) + assert any( + note.startswith("ego_pose.acceleration dropped") for note in manifest.migration_notes + ) diff --git a/tests/schema/v1/test_tables.py b/tests/schema/v1/test_tables.py index e0e59d52..8555ba0a 100644 --- a/tests/schema/v1/test_tables.py +++ b/tests/schema/v1/test_tables.py @@ -32,6 +32,7 @@ def _other_lidar_block(record: dict, records: list[dict]) -> None: "frame-without-file": ("sample_data", 0, _set(("filename",), None), TypeError), "frame-without-sample": ("sample_data", -1, _set(("sample_token",), None), TypeError), "covariance-length": ("ego_pose", 0, _set(("covariance",), [0.0] * 35), ValueError), + "imu-length": ("ego_pose", 0, _set(("imu",), [0.0] * 3), ValueError), "annotation-type": ("sample_annotation", 0, _set(("annotation_type",), "human"), ValueError), "confidence-range": ("sample_annotation", 0, _set(("confidence",), 1.5), ValueError), "instance-index-range": ("instance", 0, _set(("index",), 2**15), ValueError),