Files
gps-denied-onboard/tests/e2e/test_euroc_vo_only.py
T
Yuzviak 5744ff65ac feat(02-03): apply module-level pytestmark to 37 test files
- Add pytestmark = [pytest.mark.<category>] to all 23 root test files and 14 e2e test files
- Marker distribution: 22 unit, 7 integration, 1 blackbox, 1 sitl, 5 e2e + 2 e2e integration
- Add import pytest to test_models.py, test_download.py, test_synthetic_adapter.py (were missing)
- Convert test_sitl_integration.py's bare pytestmark to list form preserving skipif guard
- Union of all 5 markers = 298/298 = 100% coverage; 216 tests pass with --strict-markers
2026-05-11 18:20:05 +03:00

138 lines
4.7 KiB
Python

"""VO-only diagnostic for EuRoC MH_01.
Bypasses the full pipeline (ESKF, satellite matching, recovery) and drives
ORBVisualOdometry directly on raw EuRoC frames. Reports the per-frame tracking
success rate so we can distinguish:
- ORB can track EuRoC frames → vo_success=0 in the full pipeline means the
ESKF/satellite layer is the problem, not VO itself.
- ORB cannot track EuRoC frames → VO backend is the root cause; need to swap
to a real SuperPoint+LightGlue model or cuVSLAM.
Skipped if EuRoC MH_01 is not installed.
"""
from __future__ import annotations
from pathlib import Path
import cv2
import numpy as np
import pytest
pytestmark = [pytest.mark.e2e]
from gps_denied.core.vo import ORBVisualOdometry
from gps_denied.schemas import CameraParameters
from gps_denied.testing.datasets.euroc import EuRoCAdapter
# Match CI-tier frame cap used in test_euroc.py
EUROC_MH01_MAX_FRAMES = 100
# EuRoC cam0 intrinsics from the dataset sensor.yaml (focal length ≈ 458.654 px)
# Expressed as physical params so CameraParameters can build K.
# sensor_width chosen so focal_length * (res_w / sensor_width) ≈ 458 px.
EUROC_CAM_PARAMS = CameraParameters(
focal_length=4.586, # mm (synthetic — tuned so f_px ≈ 458 px)
sensor_width=4.586, # mm → f_px = 4.586 * (752/4.586) = 752... no
sensor_height=4.586,
resolution_width=752,
resolution_height=480,
# principal_point omitted → uses image centre (376, 240)
)
# Actually compute correct params: f_px = focal_length * (res_w / sensor_w)
# We want f_px = 458.654. Set focal_length=458.654, sensor_width=752 so that
# f_px = 458.654 * (752/752) = 458.654.
EUROC_CAM_PARAMS = CameraParameters(
focal_length=458.654,
sensor_width=752.0,
sensor_height=480.0,
resolution_width=752,
resolution_height=480,
)
# Minimum acceptable VO tracking rate to call the integration healthy.
MIN_TRACKING_RATE = 0.70 # 70 % of consecutive frame pairs
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
async def test_euroc_mh01_orb_tracking_rate(euroc_mh01_root: Path):
"""ORB VO should track ≥70 % of consecutive EuRoC frame pairs.
If this xfails it means ORB itself cannot engage on EuRoC imagery — the
root cause of vo_success=0 in the full pipeline is the VO backend, not the
ESKF or satellite layer.
"""
adapter = EuRoCAdapter(euroc_mh01_root)
frames = list(adapter.iter_frames())[:EUROC_MH01_MAX_FRAMES]
vo = ORBVisualOdometry()
success_count = 0
total_pairs = 0
prev_image: np.ndarray | None = None
for frame in frames:
img = cv2.imread(frame.image_path, cv2.IMREAD_GRAYSCALE)
if img is None:
continue
if prev_image is not None:
pose = vo.compute_relative_pose(prev_image, img, EUROC_CAM_PARAMS)
total_pairs += 1
if pose is not None and pose.tracking_good:
success_count += 1
prev_image = img
tracking_rate = success_count / max(1, total_pairs)
# Always report numbers — visible in pytest -v output even on xfail.
summary = (
f"ORB tracking: {success_count}/{total_pairs} pairs "
f"({tracking_rate*100:.1f}%)"
)
if tracking_rate < MIN_TRACKING_RATE:
pytest.xfail(
f"{summary} — below {MIN_TRACKING_RATE*100:.0f}% threshold. "
"ORB struggles on EuRoC indoor imagery; upgrading to real "
"SuperPoint+LightGlue or cuVSLAM is required to make the full "
"pipeline viable on this dataset."
)
assert tracking_rate >= MIN_TRACKING_RATE, summary
@pytest.mark.e2e
@pytest.mark.needs_dataset
@pytest.mark.asyncio
async def test_euroc_mh01_orb_reports_nonzero_inliers(euroc_mh01_root: Path):
"""Sanity: ORB must find at least some inlier matches on at least one pair.
If *every* pair has zero inliers the images are likely being loaded
incorrectly (wrong path, wrong colour mode, etc.).
"""
adapter = EuRoCAdapter(euroc_mh01_root)
frames = list(adapter.iter_frames())[:20] # small — just a sanity check
vo = ORBVisualOdometry()
any_inliers = False
prev_image: np.ndarray | None = None
for frame in frames:
img = cv2.imread(frame.image_path, cv2.IMREAD_GRAYSCALE)
if img is None:
continue
if prev_image is not None:
pose = vo.compute_relative_pose(prev_image, img, EUROC_CAM_PARAMS)
if pose is not None and pose.inlier_count > 0:
any_inliers = True
break
prev_image = img
assert any_inliers, (
"ORB found zero inliers across the first 20 EuRoC frame pairs. "
"Check that image paths are correct and images load as valid grayscale arrays."
)