File size: 5,027 Bytes
3c4c67b
4eeba46
 
 
3c4c67b
 
 
 
 
 
 
 
 
 
 
aef1f5a
3c4c67b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
987c4be
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3c4c67b
aef1f5a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
"""Shared test fixtures."""

from __future__ import annotations

import tempfile
from pathlib import Path
from typing import TYPE_CHECKING

import nibabel as nib
import numpy as np
import pytest

from stroke_deepisles_demo.core.types import CaseFiles

if TYPE_CHECKING:
    from collections.abc import Generator


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
    """Create a temporary directory for test outputs."""
    with tempfile.TemporaryDirectory() as td:
        yield Path(td)


@pytest.fixture
def synthetic_nifti_3d(temp_dir: Path) -> Path:
    """Create a minimal synthetic 3D NIfTI file."""
    data = np.random.rand(10, 10, 10).astype(np.float32)
    img = nib.Nifti1Image(data, affine=np.eye(4))  # type: ignore
    path = temp_dir / "synthetic.nii.gz"
    nib.save(img, path)  # type: ignore
    return path


@pytest.fixture
def synthetic_case_files(temp_dir: Path) -> CaseFiles:
    """Create a complete set of synthetic case files."""
    # Create DWI
    dwi_data = np.random.rand(64, 64, 30).astype(np.float32)
    dwi_img = nib.Nifti1Image(dwi_data, affine=np.eye(4))  # type: ignore
    dwi_path = temp_dir / "dwi.nii.gz"
    nib.save(dwi_img, dwi_path)  # type: ignore

    # Create ADC
    adc_data = np.random.rand(64, 64, 30).astype(np.float32) * 2000
    adc_img = nib.Nifti1Image(adc_data, affine=np.eye(4))  # type: ignore
    adc_path = temp_dir / "adc.nii.gz"
    nib.save(adc_img, adc_path)  # type: ignore

    # Create mask
    mask_data = (np.random.rand(64, 64, 30) > 0.9).astype(np.uint8)
    mask_img = nib.Nifti1Image(mask_data, affine=np.eye(4))  # type: ignore
    mask_path = temp_dir / "mask.nii.gz"
    nib.save(mask_img, mask_path)  # type: ignore

    return CaseFiles(
        dwi=dwi_path,
        adc=adc_path,
        ground_truth=mask_path,
    )


@pytest.fixture
def synthetic_probability_mask(temp_dir: Path) -> Path:
    """
    Create a synthetic probability mask (float values 0.0-1.0).

    This simulates model output that may contain probability values
    rather than binary 0/1 masks. Used to test visualization handling
    of probability-valued segmentation masks.

    The mask has values ONLY at slice 5 to ensure get_slice_at_max_lesion selects it:
    - Outer region with low probability (0.3) - below 0.5 threshold
    - Inner region with high probability (0.8) - above 0.5 threshold

    See: docs/specs/23-slice-comparison-overlay-bug.md
    """
    mask_data = np.zeros((10, 10, 10), dtype=np.float32)

    # Only populate slice 5 to ensure it's selected as max lesion slice
    # Outer region: low confidence (below 0.5 threshold)
    mask_data[2:8, 2:8, 5] = 0.3
    # Inner region: high confidence (above 0.5 threshold) - this should be visible
    mask_data[3:7, 3:7, 5] = 0.8

    img = nib.Nifti1Image(mask_data, affine=np.eye(4))  # type: ignore
    path = temp_dir / "probability_mask.nii.gz"
    nib.save(img, path)  # type: ignore
    return path


@pytest.fixture
def synthetic_binary_mask(temp_dir: Path) -> Path:
    """Create a synthetic binary mask (0 or 1 values only)."""
    mask_data = np.zeros((10, 10, 10), dtype=np.uint8)
    mask_data[3:7, 3:7, 4:6] = 1  # Binary lesion region

    img = nib.Nifti1Image(mask_data, affine=np.eye(4))  # type: ignore
    path = temp_dir / "binary_mask.nii.gz"
    nib.save(img, path)  # type: ignore
    return path


@pytest.fixture
def synthetic_isles_dir(temp_dir: Path) -> Path:
    """
    Create synthetic ISLES24-like directory structure.

    Structure:
        temp_dir/
        β”œβ”€β”€ Images-DWI/
        β”‚   β”œβ”€β”€ sub-stroke0001_ses-02_dwi.nii.gz
        β”‚   └── sub-stroke0002_ses-02_dwi.nii.gz
        β”œβ”€β”€ Images-ADC/
        β”‚   β”œβ”€β”€ sub-stroke0001_ses-02_adc.nii.gz
        β”‚   └── sub-stroke0002_ses-02_adc.nii.gz
        └── Masks/
            β”œβ”€β”€ sub-stroke0001_ses-02_lesion-msk.nii.gz
            └── sub-stroke0002_ses-02_lesion-msk.nii.gz
    """
    dwi_dir = temp_dir / "Images-DWI"
    adc_dir = temp_dir / "Images-ADC"
    mask_dir = temp_dir / "Masks"

    dwi_dir.mkdir()
    adc_dir.mkdir()
    mask_dir.mkdir()

    for subject_num in [1, 2]:
        subject_id = f"sub-stroke{subject_num:04d}"

        # Create DWI
        dwi_data = np.random.rand(10, 10, 5).astype(np.float32)
        dwi_img = nib.Nifti1Image(dwi_data, affine=np.eye(4))  # type: ignore
        nib.save(dwi_img, dwi_dir / f"{subject_id}_ses-02_dwi.nii.gz")  # type: ignore

        # Create ADC
        adc_data = np.random.rand(10, 10, 5).astype(np.float32) * 2000
        adc_img = nib.Nifti1Image(adc_data, affine=np.eye(4))  # type: ignore
        nib.save(adc_img, adc_dir / f"{subject_id}_ses-02_adc.nii.gz")  # type: ignore

        # Create Mask
        mask_data = (np.random.rand(10, 10, 5) > 0.9).astype(np.uint8)
        mask_img = nib.Nifti1Image(mask_data, affine=np.eye(4))  # type: ignore
        nib.save(mask_img, mask_dir / f"{subject_id}_ses-02_lesion-msk.nii.gz")  # type: ignore

    return temp_dir