import os import shutil from pathlib import Path os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") import numpy as np import pandas as pd import pytest import torch from PIL import Image from rtnls_fundusprep.cli import _run_preprocessing from vascx_models.config import AppConfig from vascx_models.disc_circles import generate_disc_circles from vascx_models.inference import ( run_fovea_detection, run_quality_estimation, run_segmentation_disc, run_segmentation_vessels_and_av, ) from vascx_models.runtime import configure_runtime_environment from vascx_models.utils import batch_create_overlays from vascx_models.vessel_widths import ( measure_vessel_widths_between_disc_circle_pair, resolve_vessel_width_circle_pair, ) pytestmark = [pytest.mark.e2e, pytest.mark.slow] REPO_ROOT = Path(__file__).resolve().parents[1] SAMPLE_IMAGE = REPO_ROOT / "samples" / "fundus" / "original" / "DRIVE_22.png" EXPECTED_VESSEL_WIDTH_COLUMNS = [ "image_id", "inner_circle", "outer_circle", "inner_circle_radius_px", "outer_circle_radius_px", "connection_index", "sample_index", "x", "y", "width_px", "x_start", "y_start", "x_end", "y_end", "vessel_type", ] def _require_e2e_opt_in() -> None: if os.environ.get("VASCX_RUN_E2E") != "1": pytest.skip("Set VASCX_RUN_E2E=1 to run real-model end-to-end tests") def _device_or_skip(device_name: str) -> torch.device: if device_name == "cpu": return torch.device("cpu") if device_name == "cuda": if not torch.cuda.is_available(): pytest.skip("CUDA is not available in this environment") return torch.device("cuda:0") if device_name == "mps": if not torch.backends.mps.is_available(): pytest.skip("MPS is not available in this environment") return torch.device("mps") raise AssertionError(f"Unsupported device name: {device_name}") def _prepare_single_image_input(tmp_path: Path) -> tuple[str, Path, Path]: input_dir = tmp_path / "input" input_dir.mkdir() image_path = input_dir / SAMPLE_IMAGE.name shutil.copy2(SAMPLE_IMAGE, image_path) return SAMPLE_IMAGE.stem, image_path, input_dir def _assert_nonempty_mask(path: Path) -> None: assert path.exists() assert np.any(np.array(Image.open(path)) > 0) @pytest.mark.parametrize("device_name", ["cpu", "cuda", "mps"]) def test_single_image_pipeline_smoke(tmp_path: Path, device_name: str) -> None: _require_e2e_opt_in() configure_runtime_environment() device = _device_or_skip(device_name) app_config = AppConfig() image_id, image_path, input_dir = _prepare_single_image_input(tmp_path) output_dir = tmp_path / "output" output_dir.mkdir() preprocessed_rgb_dir = output_dir / "preprocessed_rgb" av_dir = output_dir / "artery_vein" vessels_dir = output_dir / "vessels" disc_dir = output_dir / "disc" disc_circles_dir = output_dir / "disc_circles" overlay_dir = output_dir / "overlays" preprocessed_rgb_dir.mkdir() av_dir.mkdir() vessels_dir.mkdir() disc_dir.mkdir() overlay_dir.mkdir() bounds_path = output_dir / "bounds.csv" quality_path = output_dir / "quality.csv" fovea_path = output_dir / "fovea.csv" disc_geometry_path = output_dir / "disc_geometry.csv" vessel_widths_path = output_dir / "vessel_widths.csv" _run_preprocessing( files=[image_path], ids=[image_id], rgb_path=preprocessed_rgb_dir, bounds_path=bounds_path, n_jobs=1, ) preprocessed_image_path = preprocessed_rgb_dir / f"{image_id}.png" df_quality = run_quality_estimation([preprocessed_image_path], ids=[image_id], device=device) df_quality.to_csv(quality_path) run_segmentation_vessels_and_av( rgb_paths=[preprocessed_image_path], ids=[image_id], av_path=av_dir, vessels_path=vessels_dir, artery_color=app_config.overlay.colors.artery, vein_color=app_config.overlay.colors.vein, vessel_color=app_config.overlay.colors.vessel, device=device, ) run_segmentation_disc( rgb_paths=[preprocessed_image_path], ids=[image_id], output_path=disc_dir, disc_color=app_config.overlay.colors.disc, device=device, ) df_disc_geometry = generate_disc_circles( disc_dir=disc_dir, circle_output_dir=disc_circles_dir, circles=app_config.overlay.circles, measurements_path=disc_geometry_path, ) inner_circle, outer_circle = resolve_vessel_width_circle_pair( app_config.overlay.circles, inner_circle_name=app_config.vessel_widths.inner_circle, outer_circle_name=app_config.vessel_widths.outer_circle, ) df_vessel_widths = measure_vessel_widths_between_disc_circle_pair( vessels_dir=vessels_dir, av_dir=av_dir, disc_geometry_path=disc_geometry_path, inner_circle=inner_circle, outer_circle=outer_circle, output_path=vessel_widths_path, samples_per_connection=app_config.vessel_widths.samples_per_connection, ) df_fovea = run_fovea_detection([preprocessed_image_path], ids=[image_id], device=device) df_fovea.to_csv(fovea_path) batch_create_overlays( rgb_dir=preprocessed_rgb_dir, output_dir=overlay_dir, av_dir=av_dir, disc_dir=disc_dir, vessels_dir=vessels_dir, circle_dirs={ circle.name: disc_circles_dir / circle.name for circle in app_config.overlay.circles }, vessel_width_data=df_vessel_widths, fovea_data={ index: (row["x_fovea"], row["y_fovea"]) for index, row in df_fovea.iterrows() }, overlay_config=app_config.overlay, ) assert df_quality.index.tolist() == [image_id] assert df_quality.columns.tolist() == ["q1", "q2", "q3"] assert np.isfinite(df_quality.to_numpy()).all() assert quality_path.exists() assert bounds_path.exists() assert preprocessed_image_path.exists() _assert_nonempty_mask(av_dir / f"{image_id}.png") _assert_nonempty_mask(vessels_dir / f"{image_id}.png") _assert_nonempty_mask(disc_dir / f"{image_id}.png") assert df_disc_geometry.index.tolist() == [image_id] assert float(df_disc_geometry.loc[image_id, "disc_radius_px"]) > 0.0 assert disc_geometry_path.exists() for circle in app_config.overlay.circles: _assert_nonempty_mask(disc_circles_dir / circle.name / f"{image_id}.png") assert vessel_widths_path.exists() df_vessel_widths_disk = pd.read_csv(vessel_widths_path) assert df_vessel_widths_disk.columns.tolist() == EXPECTED_VESSEL_WIDTH_COLUMNS assert df_vessel_widths.columns.tolist() == EXPECTED_VESSEL_WIDTH_COLUMNS if not df_vessel_widths.empty: assert df_vessel_widths["image_id"].eq(image_id).all() assert df_vessel_widths["vessel_type"].isin(["artery", "vein"]).all() assert (df_vessel_widths["width_px"] > 0).all() assert df_fovea.index.tolist() == [image_id] assert df_fovea.columns.tolist() == ["x_fovea", "y_fovea"] assert np.isfinite(df_fovea.to_numpy()).all() assert fovea_path.exists() overlay_path = overlay_dir / f"{image_id}.png" assert overlay_path.exists() assert Image.open(overlay_path).size == Image.open(preprocessed_image_path).size