diff --git a/src/gps_denied/testing/datasets/vpair.py b/src/gps_denied/testing/datasets/vpair.py new file mode 100644 index 0000000..f7e0884 --- /dev/null +++ b/src/gps_denied/testing/datasets/vpair.py @@ -0,0 +1,74 @@ +"""Adapter for VPAIR (Aerial Visual Place Recognition) dataset sample release.""" + +from __future__ import annotations + +import csv +from pathlib import Path +from typing import Iterator + +from gps_denied.testing.datasets.base import ( + DatasetAdapter, + DatasetCapabilities, + DatasetFrame, + DatasetIMU, + DatasetNotAvailableError, + DatasetPose, + PlatformClass, +) + + +class VPAIRAdapter(DatasetAdapter): + """Reads the VPAIR sample bundle (queries/ + poses.csv).""" + + def __init__(self, root: Path) -> None: + self._root = Path(root) + self._queries_dir = self._root / "queries" + self._poses_csv = self._root / "poses.csv" + if not (self._queries_dir.is_dir() and self._poses_csv.is_file()): + raise DatasetNotAvailableError( + f"VPAIR sample not found at {self._root} " + "(expected queries/ and poses.csv). " + "Download from https://github.com/AerVisLoc/vpair sample link on Zenodo." + ) + + @property + def name(self) -> str: + return f"vpair:{self._root.name}" + + @property + def capabilities(self) -> DatasetCapabilities: + return DatasetCapabilities( + has_raw_imu=False, # VPAIR ships poses, not raw IMU + has_rtk_gt=False, # GNSS/INS 1m accuracy, not RTK + has_loop_closures=False, + platform_class=PlatformClass.FIXED_WING, + ) + + def iter_frames(self) -> Iterator[DatasetFrame]: + with self._poses_csv.open() as fh: + reader = csv.DictReader(fh) + for idx, row in enumerate(reader): + yield DatasetFrame( + frame_idx=idx, + timestamp_ns=int(row["timestamp_ns"]), + image_path=str(self._queries_dir / row["filename"]), + ) + + def iter_imu(self) -> Iterator[DatasetIMU]: + return + yield # empty generator + + def iter_ground_truth(self) -> Iterator[DatasetPose]: + with self._poses_csv.open() as fh: + reader = csv.DictReader(fh) + for row in reader: + yield DatasetPose( + timestamp_ns=int(row["timestamp_ns"]), + lat=float(row["lat"]), + lon=float(row["lon"]), + alt=float(row["alt"]), + qx=float(row["qx"]), + qy=float(row["qy"]), + qz=float(row["qz"]), + qw=float(row["qw"]), + ) diff --git a/tests/e2e/test_vpair_adapter.py b/tests/e2e/test_vpair_adapter.py new file mode 100644 index 0000000..cdf6c6a --- /dev/null +++ b/tests/e2e/test_vpair_adapter.py @@ -0,0 +1,57 @@ +"""VPAIRAdapter unit tests with a fabricated vpair_sample/ layout.""" + +from pathlib import Path + +import pytest + +from gps_denied.testing.datasets.base import ( + DatasetNotAvailableError, + PlatformClass, +) +from gps_denied.testing.datasets.vpair import VPAIRAdapter + + +@pytest.fixture +def fake_vpair_root(tmp_path: Path) -> Path: + (tmp_path / "queries").mkdir() + for fn in ("q_00000.jpg", "q_00001.jpg"): + (tmp_path / "queries" / fn).write_bytes(b"\xff\xd8\xff\xd9") # minimal JPEG + (tmp_path / "poses.csv").write_text( + "filename,lat,lon,alt,qx,qy,qz,qw,timestamp_ns\n" + "q_00000.jpg,50.737,7.095,350.0,0,0,0,1,0\n" + "q_00001.jpg,50.7372,7.0952,350.0,0,0,0,1,1000000000\n" + ) + return tmp_path + + +def test_raises_when_missing(tmp_path: Path): + with pytest.raises(DatasetNotAvailableError): + VPAIRAdapter(tmp_path / "nope") + + +def test_capabilities_no_raw_imu(fake_vpair_root: Path): + adapter = VPAIRAdapter(fake_vpair_root) + cap = adapter.capabilities + assert cap.has_raw_imu is False + assert cap.platform_class == PlatformClass.FIXED_WING + + +def test_iter_frames(fake_vpair_root: Path): + adapter = VPAIRAdapter(fake_vpair_root) + frames = list(adapter.iter_frames()) + assert len(frames) == 2 + assert Path(frames[0].image_path).name == "q_00000.jpg" + assert frames[1].timestamp_ns == 1_000_000_000 + + +def test_iter_imu_empty(fake_vpair_root: Path): + adapter = VPAIRAdapter(fake_vpair_root) + assert list(adapter.iter_imu()) == [] + + +def test_iter_ground_truth(fake_vpair_root: Path): + adapter = VPAIRAdapter(fake_vpair_root) + gt = list(adapter.iter_ground_truth()) + assert len(gt) == 2 + assert gt[0].lat == pytest.approx(50.737) + assert gt[0].alt == pytest.approx(350.0)