Skip to content

feat: Add CD/DVD DICOMDIR support for German MRI CDs #25

Description

@Liohtml

Problem

In Germany (and many other countries), MRI results are almost always delivered on CD/DVD. These discs follow the DICOM standard structure:

CD-ROM (D:\)
├── DICOMDIR              ← Standard index file (lists all studies/series/images)
├── DICOM/
│   ├── IM000001.dcm
│   ├── IM000002.dcm
│   └── ...
└── viewer.exe            ← Bundled Windows-only viewer (usually useless)

Currently, LocalProvider scans directories recursively for DICOM files, which works but:

  1. Misses the DICOMDIR - a structured index that gives instant access to the patient/study/series hierarchy
  2. Is slow on CDs - optical drives are slow; scanning hundreds of files takes minutes
  3. Doesn't detect CD drives - users have to manually find the mount path

Proposed Solution

Enhance LocalProvider (not a new provider) to auto-detect and parse DICOMDIR files.

Implementation Plan

1. Add DICOMDIR detection to LocalProvider._load_directory()

# src/medcheck/providers/local.py

def _load_directory(self, root: Path) -> list[DicomSeries]:
    # Check for DICOMDIR first (standard on CDs)
    dicomdir_path = root / "DICOMDIR"
    if dicomdir_path.exists():
        return self._load_from_dicomdir(dicomdir_path)
    
    # Fallback: recursive scan (existing behavior)
    return self._scan_directory(root)

2. Implement DICOMDIR parsing

def _load_from_dicomdir(self, dicomdir_path: Path) -> list[DicomSeries]:
    """Load DICOM data using DICOMDIR index (standard on CDs)."""
    from rich.console import Console
    console = Console()
    console.print("[blue]DICOMDIR found - using structured index[/blue]")
    
    ds = pydicom.dcmread(str(dicomdir_path))
    root = dicomdir_path.parent
    
    series_map: dict[str, list[Any]] = {}
    series_meta: dict[str, dict[str, Any]] = {}
    
    for patient_record in ds.patient_records:
        for study_record in patient_record.children:
            for series_record in study_record.children:
                series_desc = getattr(series_record, "SeriesDescription", 
                                      f"series_{getattr(series_record, 'SeriesNumber', 0)}")
                series_num = getattr(series_record, "SeriesNumber", 0)
                
                for image_record in series_record.children:
                    # DICOMDIR stores relative file paths
                    file_parts = image_record.ReferencedFileID
                    if isinstance(file_parts, str):
                        file_parts = [file_parts]
                    
                    dcm_path = root / Path(*file_parts)
                    if dcm_path.exists():
                        try:
                            dcm = pydicom.dcmread(str(dcm_path), force=True)
                            if hasattr(dcm, "PixelData"):
                                if series_desc not in series_map:
                                    series_map[series_desc] = []
                                    series_meta[series_desc] = {
                                        "series_number": series_num,
                                        "modality": getattr(series_record, "Modality", ""),
                                    }
                                series_map[series_desc].append(dcm)
                        except Exception:
                            continue
    
    result: list[DicomSeries] = []
    for desc, datasets in series_map.items():
        meta = series_meta.get(desc, {})
        result.append(DicomSeries(
            description=desc,
            series_number=meta.get("series_number", 0),
            modality=meta.get("modality", ""),
            slices=datasets,
            metadata=meta,
        ))
    
    console.print(f"[green]Loaded {len(result)} series from DICOMDIR[/green]")
    return sorted(result, key=lambda s: s.series_number)

3. Rename existing scan to _scan_directory()

def _scan_directory(self, root: Path) -> list[DicomSeries]:
    """Fallback: recursively scan directory for DICOM files."""
    # ... existing _load_directory code ...

4. Add tests

# tests/unit/test_providers/test_local_dicomdir.py

def test_load_from_dicomdir(tmp_path: Path):
    """Test that DICOMDIR is auto-detected and parsed."""
    # Create a minimal DICOMDIR structure
    dicom_dir = tmp_path / "DICOM"
    dicom_dir.mkdir()
    
    # Create test DICOM files
    _create_test_dicom(dicom_dir / "IM000001.dcm", "pd_sag", 1)
    _create_test_dicom(dicom_dir / "IM000002.dcm", "pd_sag", 1)
    
    # Create DICOMDIR (pydicom can generate these)
    # ... or use a fixture file
    
    provider = LocalProvider()
    result = provider.fetch(str(tmp_path), {})
    assert len(result) >= 1


def test_fallback_without_dicomdir(tmp_path: Path):
    """Test that directories without DICOMDIR still work (existing behavior)."""
    _create_test_dicom(tmp_path / "slice.dcm", "test", 1)
    provider = LocalProvider()
    result = provider.fetch(str(tmp_path), {})
    assert len(result) >= 1


def test_cd_drive_detection():
    """Test CD/DVD drive path detection (platform-specific)."""
    # Windows: D:\, E:\
    # Linux: /media/cdrom, /mnt/cdrom
    # This test verifies the detection logic, not actual hardware
    pass

Files to modify

File Change
src/medcheck/providers/local.py Add _load_from_dicomdir(), refactor _load_directory()
tests/unit/test_providers/test_local.py Add DICOMDIR tests
docs/quickstart.md Add "From CD" usage example
README.md Update data sources table

CLI usage after implementation

# Point directly at CD drive
medcheck analyze --source D:\

# Or mount point on Linux
medcheck analyze --source /media/cdrom

# Also works with copied CD contents
medcheck analyze --source ./copied-from-cd/

Nice-to-have (follow-up issues)

  • Auto-detect CD/DVD drives on the system
  • "Copy to local" option for faster re-analysis (--copy-local ./cache/)
  • Progress bar when reading from slow optical drives
  • Multi-study picker if CD contains multiple examinations
  • Web UI: "Read from CD" button

Context

This is a high-value feature for the German/European market where CD delivery is standard practice. Many patients receive CDs but have no way to analyze them beyond the bundled Windows viewer.

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions