mirror of
https://github.com/azaion/gps-denied-onboard.git
synced 2026-04-22 22:26:38 +00:00
c9bd45a098
Drives ORBVisualOdometry directly on raw EuRoC frames, bypassing ESKF and satellite layers. ORB achieves 100% tracking on 99 frame pairs, confirming that vo_success=0 in the full pipeline is caused by SequentialVisualOdometry's MockInferenceEngine (random keypoints → RANSAC failure), not by VO backend limitations on EuRoC indoor imagery. Two tests: tracking rate ≥70% (passes, currently 100%), and sanity check that at least one pair yields non-zero inliers (passes). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
136 lines
4.7 KiB
Python
136 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
|
|
|
|
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."
|
|
)
|