mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-23 01:46:38 +00:00
test(e2e): add VPAIRAdapter (pose-only; fixed-wing capability)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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"]),
|
||||||
|
)
|
||||||
@@ -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)
|
||||||
Reference in New Issue
Block a user