|
|
"""Direct DeepISLES invocation without Docker. |
|
|
|
|
|
This module provides direct Python invocation of DeepISLES when running |
|
|
inside the DeepISLES Docker image (e.g., on HF Spaces). This avoids |
|
|
Docker-in-Docker which is not supported on HF Spaces. |
|
|
|
|
|
Usage: |
|
|
When running in HF Spaces, our container is based on isleschallenge/deepisles, |
|
|
which has all DeepISLES dependencies pre-installed. This module imports |
|
|
and calls DeepISLES directly. |
|
|
|
|
|
See: |
|
|
- https://github.com/ezequieldlrosa/DeepIsles |
|
|
- docs/specs/07-hf-spaces-deployment.md |
|
|
""" |
|
|
|
|
|
from __future__ import annotations |
|
|
|
|
|
import sys |
|
|
import time |
|
|
from dataclasses import dataclass |
|
|
from pathlib import Path |
|
|
|
|
|
from stroke_deepisles_demo.core.exceptions import DeepISLESError, MissingInputError |
|
|
from stroke_deepisles_demo.core.logging import get_logger |
|
|
from stroke_deepisles_demo.inference.deepisles import find_prediction_mask |
|
|
|
|
|
logger = get_logger(__name__) |
|
|
|
|
|
|
|
|
DEEPISLES_SEARCH_PATHS = [ |
|
|
"/app", |
|
|
"/DeepIsles", |
|
|
"/opt/deepisles", |
|
|
"/home/user/DeepIsles", |
|
|
] |
|
|
|
|
|
|
|
|
@dataclass(frozen=True) |
|
|
class DirectInvocationResult: |
|
|
"""Result of direct DeepISLES invocation.""" |
|
|
|
|
|
prediction_path: Path |
|
|
elapsed_seconds: float |
|
|
|
|
|
|
|
|
def _ensure_deepisles_importable() -> str: |
|
|
""" |
|
|
Ensure DeepISLES modules are importable by adding to sys.path. |
|
|
|
|
|
Returns: |
|
|
Path where DeepISLES was found |
|
|
|
|
|
Raises: |
|
|
DeepISLESError: If DeepISLES cannot be found |
|
|
""" |
|
|
for path in DEEPISLES_SEARCH_PATHS: |
|
|
if Path(path).exists(): |
|
|
if path not in sys.path: |
|
|
sys.path.insert(0, path) |
|
|
try: |
|
|
|
|
|
from src.isles22_ensemble import IslesEnsemble |
|
|
|
|
|
logger.debug("Found DeepISLES at %s", path) |
|
|
return path |
|
|
except ImportError: |
|
|
continue |
|
|
|
|
|
raise DeepISLESError( |
|
|
"DeepISLES modules not found. Direct invocation requires running " |
|
|
"inside the DeepISLES Docker image. Searched paths: " |
|
|
f"{DEEPISLES_SEARCH_PATHS}" |
|
|
) |
|
|
|
|
|
|
|
|
def validate_input_files( |
|
|
dwi_path: Path, |
|
|
adc_path: Path, |
|
|
flair_path: Path | None = None, |
|
|
) -> None: |
|
|
""" |
|
|
Validate that input files exist. |
|
|
|
|
|
Args: |
|
|
dwi_path: Path to DWI NIfTI file |
|
|
adc_path: Path to ADC NIfTI file |
|
|
flair_path: Optional path to FLAIR NIfTI file |
|
|
|
|
|
Raises: |
|
|
MissingInputError: If required files are missing |
|
|
""" |
|
|
if not dwi_path.exists(): |
|
|
raise MissingInputError(f"DWI file not found: {dwi_path}") |
|
|
if not adc_path.exists(): |
|
|
raise MissingInputError(f"ADC file not found: {adc_path}") |
|
|
if flair_path is not None and not flair_path.exists(): |
|
|
raise MissingInputError(f"FLAIR file not found: {flair_path}") |
|
|
|
|
|
|
|
|
def run_deepisles_direct( |
|
|
dwi_path: Path, |
|
|
adc_path: Path, |
|
|
output_dir: Path, |
|
|
*, |
|
|
flair_path: Path | None = None, |
|
|
fast: bool = True, |
|
|
skull_strip: bool = False, |
|
|
parallelize: bool = True, |
|
|
save_team_outputs: bool = False, |
|
|
results_mni: bool = False, |
|
|
) -> DirectInvocationResult: |
|
|
""" |
|
|
Run DeepISLES segmentation via direct Python invocation. |
|
|
|
|
|
This function calls the DeepISLES IslesEnsemble.predict_ensemble() method |
|
|
directly, bypassing Docker. It's used when running inside the DeepISLES |
|
|
container on HF Spaces. |
|
|
|
|
|
Args: |
|
|
dwi_path: Path to DWI NIfTI file (b=1000) |
|
|
adc_path: Path to ADC NIfTI file |
|
|
output_dir: Directory for output files |
|
|
flair_path: Optional path to FLAIR NIfTI file |
|
|
fast: If True, use SEALS model only (faster, no FLAIR needed) |
|
|
skull_strip: If True, perform skull stripping |
|
|
parallelize: If True, run models in parallel |
|
|
save_team_outputs: If True, save individual team outputs |
|
|
results_mni: If True, output results in MNI space |
|
|
|
|
|
Returns: |
|
|
DirectInvocationResult with path to prediction mask |
|
|
|
|
|
Raises: |
|
|
DeepISLESError: If invocation fails |
|
|
MissingInputError: If required input files are missing |
|
|
|
|
|
Example: |
|
|
>>> result = run_deepisles_direct( |
|
|
... dwi_path=Path("/data/dwi.nii.gz"), |
|
|
... adc_path=Path("/data/adc.nii.gz"), |
|
|
... output_dir=Path("/data/output"), |
|
|
... fast=True |
|
|
... ) |
|
|
>>> print(result.prediction_path) |
|
|
""" |
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
validate_input_files(dwi_path, adc_path, flair_path) |
|
|
|
|
|
|
|
|
deepisles_path = _ensure_deepisles_importable() |
|
|
|
|
|
|
|
|
try: |
|
|
from src.isles22_ensemble import IslesEnsemble |
|
|
except ImportError as e: |
|
|
raise DeepISLESError(f"Failed to import DeepISLES: {e}") from e |
|
|
|
|
|
|
|
|
output_dir.mkdir(parents=True, exist_ok=True) |
|
|
|
|
|
logger.info( |
|
|
"Running DeepISLES direct invocation: dwi=%s, adc=%s, flair=%s, fast=%s", |
|
|
dwi_path, |
|
|
adc_path, |
|
|
flair_path, |
|
|
fast, |
|
|
) |
|
|
|
|
|
try: |
|
|
|
|
|
stroke_segm = IslesEnsemble() |
|
|
|
|
|
|
|
|
stroke_segm.predict_ensemble( |
|
|
ensemble_path=deepisles_path, |
|
|
input_dwi_path=str(dwi_path), |
|
|
input_adc_path=str(adc_path), |
|
|
input_flair_path=str(flair_path) if flair_path else None, |
|
|
output_path=str(output_dir), |
|
|
skull_strip=skull_strip, |
|
|
fast=fast, |
|
|
save_team_outputs=save_team_outputs, |
|
|
results_mni=results_mni, |
|
|
parallelize=parallelize, |
|
|
) |
|
|
except Exception as e: |
|
|
logger.exception("DeepISLES inference failed") |
|
|
raise DeepISLESError(f"DeepISLES inference failed: {e}") from e |
|
|
|
|
|
|
|
|
prediction_path = find_prediction_mask(output_dir) |
|
|
|
|
|
elapsed = time.time() - start_time |
|
|
logger.info("DeepISLES direct invocation completed in %.2fs", elapsed) |
|
|
|
|
|
return DirectInvocationResult( |
|
|
prediction_path=prediction_path, |
|
|
elapsed_seconds=elapsed, |
|
|
) |
|
|
|