Add UZH-FPV drone racing dataset (UZHFPV)#331
Add UZH-FPV drone racing dataset (UZHFPV)#331vlordier wants to merge 1 commit intoneuromorphs:developfrom
Conversation
Co-authored-by: vlordier <5443125+vlordier@users.noreply.github.com> Agent-Logs-Url: https://github.com/vlordier/tonic/sessions/33daba03-974f-4ca9-a7f3-967a32a6a818
|
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## develop #331 +/- ##
===========================================
- Coverage 76.50% 74.21% -2.29%
===========================================
Files 48 50 +2
Lines 2992 3126 +134
===========================================
+ Hits 2289 2320 +31
- Misses 703 806 +103 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds support for the UZH-FPV drone racing dataset to Tonic, including a new Dataset implementation, package exports, documentation listing, and basic tests.
Changes:
- Introduce
tonic.datasets.UZHFPVto download/read UZH-FPV ROS bag recordings (events/IMU/images + optional GT). - Export
UZHFPVfromtonic.datasetsand include it in the datasets docs autosummary. - Add unit tests for recording selection and validation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
tonic/datasets/uzh_fpv.py |
New dataset implementation for UZH-FPV, including download, existence checks, and ROS bag parsing. |
tonic/datasets/__init__.py |
Exposes UZHFPV in the datasets package namespace and __all__. |
test/test_uzh_fpv.py |
Adds tests for recording selection (single, multiple, all) and validation behavior. |
docs/datasets.rst |
Lists UZHFPV under pose estimation / odometry / SLAM datasets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Normalize timestamps to start at 0 and convert from seconds to microseconds | ||
| events = topics["/dvs/events"] | ||
| events["ts"] -= events["ts"][0] | ||
| events["ts"] *= 1e6 | ||
| events = make_structured_array( | ||
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | ||
| ) | ||
|
|
||
| # Normalize and convert IMU timestamps to microseconds | ||
| imu = topics["/dvs/imu"] | ||
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| # Normalize and convert image timestamps to microseconds, stack frames | ||
| images = topics["/dvs/image_raw"] | ||
| images["frames"] = np.stack(images["frames"]) | ||
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| data = events, imu, images | ||
|
|
||
| if has_gt and "/groundtruth/odometry" in topics: | ||
| # Normalize and convert ground truth timestamps to microseconds | ||
| target = topics["/groundtruth/odometry"] | ||
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) |
There was a problem hiding this comment.
Ground-truth timestamps are normalized against the first GT sample, which can desynchronize GT from events/IMU/images if those streams use different t0. Normalize GT timestamps using the same shared reference time as the rest of the modalities so consumers can directly compare timelines.
| # Normalize timestamps to start at 0 and convert from seconds to microseconds | |
| events = topics["/dvs/events"] | |
| events["ts"] -= events["ts"][0] | |
| events["ts"] *= 1e6 | |
| events = make_structured_array( | |
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds | |
| imu = topics["/dvs/imu"] | |
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds, stack frames | |
| images = topics["/dvs/image_raw"] | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | |
| data = events, imu, images | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| # Normalize and convert ground truth timestamps to microseconds | |
| target = topics["/groundtruth/odometry"] | |
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) | |
| # Use a shared reference time (t0) based on the first event timestamp | |
| base_ts = topics["/dvs/events"]["ts"][0] | |
| # Normalize event timestamps to start at 0 (shared t0) and convert from seconds to microseconds | |
| events = topics["/dvs/events"] | |
| events["ts"] = (events["ts"] - base_ts) * 1e6 | |
| events = make_structured_array( | |
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds using the shared reference time | |
| imu = topics["/dvs/imu"] | |
| imu["ts"] = ((imu["ts"] - base_ts) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds using the shared reference time, stack frames | |
| images = topics["/dvs/image_raw"] | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - base_ts) * 1e6).astype(int) | |
| data = events, imu, images | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| # Normalize and convert ground truth timestamps to microseconds using the shared reference time | |
| target = topics["/groundtruth/odometry"] | |
| target["ts"] = ((target["ts"] - base_ts) * 1e6).astype(int) |
| def test_single_recording(): | ||
| with patch.object(tonic.datasets.UZHFPV, "_check_exists", return_value=True): | ||
| dataset = tonic.datasets.UZHFPV( | ||
| save_to="data", | ||
| recording="indoor_forward_3", | ||
| ) | ||
| assert len(dataset) == 1 |
There was a problem hiding this comment.
Current tests only validate recording selection/validation and don’t exercise getitem behavior (ROS bag parsing, timestamp conversion/normalization, transform hooks, and GT presence/absence). Add a unit test that patches importRosbag to return a minimal synthetic topics dict and asserts the returned events dtype/ordering, timestamp units, and that data/target transforms are applied as expected.
| # Normalize timestamps to start at 0 and convert from seconds to microseconds | ||
| events = topics["/dvs/events"] | ||
| events["ts"] -= events["ts"][0] | ||
| events["ts"] *= 1e6 | ||
| events = make_structured_array( | ||
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | ||
| ) | ||
|
|
||
| # Normalize and convert IMU timestamps to microseconds | ||
| imu = topics["/dvs/imu"] | ||
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| # Normalize and convert image timestamps to microseconds, stack frames | ||
| images = topics["/dvs/image_raw"] | ||
| images["frames"] = np.stack(images["frames"]) | ||
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| data = events, imu, images | ||
|
|
||
| if has_gt and "/groundtruth/odometry" in topics: | ||
| # Normalize and convert ground truth timestamps to microseconds | ||
| target = topics["/groundtruth/odometry"] | ||
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) |
There was a problem hiding this comment.
Timestamp normalization uses each stream’s own first timestamp as t0 (events, IMU, images). This breaks cross-sensor alignment (e.g., IMU/image timestamps won’t be comparable to event timestamps). Consider computing a single reference time (e.g., the earliest timestamp across available topics, or the events’ first timestamp) and subtracting that from all modalities before converting to microseconds.
| # Normalize timestamps to start at 0 and convert from seconds to microseconds | |
| events = topics["/dvs/events"] | |
| events["ts"] -= events["ts"][0] | |
| events["ts"] *= 1e6 | |
| events = make_structured_array( | |
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds | |
| imu = topics["/dvs/imu"] | |
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds, stack frames | |
| images = topics["/dvs/image_raw"] | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | |
| data = events, imu, images | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| # Normalize and convert ground truth timestamps to microseconds | |
| target = topics["/groundtruth/odometry"] | |
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) | |
| # Extract raw modalities | |
| events = topics["/dvs/events"] | |
| imu = topics["/dvs/imu"] | |
| images = topics["/dvs/image_raw"] | |
| # Compute a common reference time (earliest timestamp across available streams) | |
| t0_candidates = [events["ts"][0], imu["ts"][0], images["ts"][0]] | |
| gt_topic = None | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| gt_topic = topics["/groundtruth/odometry"] | |
| t0_candidates.append(gt_topic["ts"][0]) | |
| t0 = min(t0_candidates) | |
| # Normalize event timestamps to common t0 and convert from seconds to microseconds | |
| events_ts = ((events["ts"] - t0) * 1e6).astype(int) | |
| events = make_structured_array( | |
| events_ts, events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds using common t0 | |
| imu["ts"] = ((imu["ts"] - t0) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds using common t0, stack frames | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - t0) * 1e6).astype(int) | |
| data = events, imu, images | |
| if gt_topic is not None: | |
| # Normalize and convert ground truth timestamps to microseconds using common t0 | |
| target = gt_topic | |
| target["ts"] = ((target["ts"] - t0) * 1e6).astype(int) |
| events["ts"] -= events["ts"][0] | ||
| events["ts"] *= 1e6 | ||
| events = make_structured_array( | ||
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | ||
| ) | ||
|
|
||
| # Normalize and convert IMU timestamps to microseconds | ||
| imu = topics["/dvs/imu"] | ||
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| # Normalize and convert image timestamps to microseconds, stack frames | ||
| images = topics["/dvs/image_raw"] | ||
| images["frames"] = np.stack(images["frames"]) | ||
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | ||
|
|
||
| data = events, imu, images | ||
|
|
||
| if has_gt and "/groundtruth/odometry" in topics: | ||
| # Normalize and convert ground truth timestamps to microseconds | ||
| target = topics["/groundtruth/odometry"] | ||
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) |
There was a problem hiding this comment.
IMU timestamps are normalized relative to the first IMU sample rather than the same time origin as events/images. For synchronized sensor fusion use-cases, normalize IMU (and image) timestamps against the same reference time used for events so timelines remain consistent.
| events["ts"] -= events["ts"][0] | |
| events["ts"] *= 1e6 | |
| events = make_structured_array( | |
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds | |
| imu = topics["/dvs/imu"] | |
| imu["ts"] = ((imu["ts"] - imu["ts"][0]) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds, stack frames | |
| images = topics["/dvs/image_raw"] | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - images["ts"][0]) * 1e6).astype(int) | |
| data = events, imu, images | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| # Normalize and convert ground truth timestamps to microseconds | |
| target = topics["/groundtruth/odometry"] | |
| target["ts"] = ((target["ts"] - target["ts"][0]) * 1e6).astype(int) | |
| reference_time = events["ts"][0] | |
| events["ts"] = (events["ts"] - reference_time) * 1e6 | |
| events = make_structured_array( | |
| events["ts"], events["x"], events["y"], events["pol"], dtype=self.dtype | |
| ) | |
| # Normalize and convert IMU timestamps to microseconds using the same reference time as events | |
| imu = topics["/dvs/imu"] | |
| imu["ts"] = ((imu["ts"] - reference_time) * 1e6).astype(int) | |
| # Normalize and convert image timestamps to microseconds, stack frames, using the same reference time | |
| images = topics["/dvs/image_raw"] | |
| images["frames"] = np.stack(images["frames"]) | |
| images["ts"] = ((images["ts"] - reference_time) * 1e6).astype(int) | |
| data = events, imu, images | |
| if has_gt and "/groundtruth/odometry" in topics: | |
| # Normalize and convert ground truth timestamps to microseconds using the same reference time | |
| target = topics["/groundtruth/odometry"] | |
| target["ts"] = ((target["ts"] - reference_time) * 1e6).astype(int) |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
|
@copilot fix coverage : ❌ Patch coverage is 41.66667% with 42 lines in your changes missing coverage. Please review. |
|
@copilot open a new pull request to apply changes based on the comments in this thread |
Co-authored-by: vlordier 5443125+vlordier@users.noreply.github.com
Agent-Logs-Url: https://github.com/vlordier/tonic/sessions/33daba03-974f-4ca9-a7f3-967a32a6a818