"""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 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." )