115 lines
4.2 KiB
Python
115 lines
4.2 KiB
Python
"""MRI sequence identification helpers."""
|
|
|
|
from typing import List
|
|
|
|
import pydicom
|
|
|
|
from dicom_mcp.constants import SequenceType
|
|
from dicom_mcp.helpers.tags import _safe_get_tag
|
|
|
|
|
|
def _identify_sequence_type(ds: pydicom.Dataset) -> SequenceType:
|
|
"""Identify the MRI sequence type from DICOM metadata.
|
|
|
|
Uses a two-tier approach:
|
|
1. Series description keyword matching (fast, covers named protocols)
|
|
2. DICOM acquisition tag inspection (structural fallback)
|
|
|
|
IMPORTANT: Keyword matching order matters. More specific terms (e.g., 't1_mapping',
|
|
'molli') must be checked before generic terms (e.g., 't1') to avoid false positives.
|
|
"""
|
|
series_desc = _safe_get_tag(ds, (0x0008, 0x103E), "").lower()
|
|
|
|
# --- Tier 1: keyword matches on series description ---
|
|
# NOTE: Order is intentional - specific patterns checked before generic ones
|
|
|
|
# Dixon variants (check before MOST/IDEAL since some descriptions contain both)
|
|
if any(kw in series_desc for kw in ["dixon", "ideal", "flex"]):
|
|
return SequenceType.DIXON
|
|
|
|
# T1 mapping protocols (MOLLI, NOLLI, ShMOLLI, SASHA, etc.)
|
|
# Must check before generic 't1' to avoid misclassification
|
|
if any(kw in series_desc for kw in ["molli", "nolli", "shmolli", "sasha"]):
|
|
return SequenceType.T1_MAPPING
|
|
|
|
# Multi-echo GRE (MOST protocol for T2*/PDFF)
|
|
if "most" in series_desc or "multi" in series_desc and "echo" in series_desc:
|
|
return SequenceType.MULTI_ECHO_GRE
|
|
|
|
# Localizer / survey / scout
|
|
if any(kw in series_desc for kw in ["localizer", "survey", "scout"]):
|
|
return SequenceType.LOCALIZER
|
|
|
|
# Standard named sequences (specific before generic)
|
|
if "flair" in series_desc:
|
|
return SequenceType.FLAIR
|
|
if "dwi" in series_desc or "diffusion" in series_desc:
|
|
return SequenceType.DWI
|
|
# Check 't2' before 't1' since 't2' is more specific
|
|
if "t2" in series_desc:
|
|
return SequenceType.T2
|
|
if "t1" in series_desc:
|
|
return SequenceType.T1
|
|
|
|
# --- Tier 2: inspect DICOM acquisition tags ---
|
|
|
|
image_type = _safe_get_tag(ds, (0x0008, 0x0008), "").lower()
|
|
if "dixon" in image_type or "ideal" in image_type:
|
|
return SequenceType.DIXON
|
|
|
|
scanning_seq = _safe_get_tag(ds, (0x0018, 0x0020), "")
|
|
seq_variant = _safe_get_tag(ds, (0x0018, 0x0021), "")
|
|
|
|
has_ir = "IR" in scanning_seq
|
|
has_se = "SE" in scanning_seq
|
|
has_gr = "GR" in scanning_seq
|
|
has_mp = "MP" in seq_variant # Magnetization Prepared
|
|
|
|
# GR + IR (+ MP) = gradient echo inversion recovery (MOLLI-like T1 mapping)
|
|
if has_gr and has_ir and has_mp:
|
|
return SequenceType.T1_MAPPING
|
|
|
|
# SE + IR = spin echo inversion recovery
|
|
if has_se and has_ir:
|
|
return SequenceType.SPIN_ECHO_IR
|
|
|
|
# Multi-echo detection via NumberOfEchoes tag
|
|
num_echoes = _safe_get_tag(ds, (0x0018, 0x0086), "")
|
|
try:
|
|
if int(num_echoes) > 1 and has_gr:
|
|
return SequenceType.MULTI_ECHO_GRE
|
|
except (ValueError, TypeError):
|
|
pass
|
|
|
|
return SequenceType.UNKNOWN
|
|
|
|
|
|
def _is_dixon_sequence(ds: pydicom.Dataset) -> bool:
|
|
"""Check if a DICOM file is from a Dixon sequence."""
|
|
return _identify_sequence_type(ds) == SequenceType.DIXON
|
|
|
|
|
|
def _get_dixon_image_types(ds: pydicom.Dataset) -> List[str]:
|
|
"""Extract Dixon image types (water, fat, in-phase, out-phase) from metadata."""
|
|
image_types = []
|
|
|
|
image_type_tag = ds.get((0x0008, 0x0008))
|
|
if image_type_tag:
|
|
image_type_str = str(image_type_tag.value).lower()
|
|
if "water" in image_type_str or "w" in image_type_str.split("\\"):
|
|
image_types.append("water")
|
|
if "fat" in image_type_str or "f" in image_type_str.split("\\"):
|
|
image_types.append("fat")
|
|
if "in" in image_type_str or "ip" in image_type_str:
|
|
image_types.append("in-phase")
|
|
if "out" in image_type_str or "op" in image_type_str:
|
|
image_types.append("out-phase")
|
|
|
|
series_desc = _safe_get_tag(ds, (0x0008, 0x103E), "").lower()
|
|
if "water" in series_desc and "water" not in image_types:
|
|
image_types.append("water")
|
|
if "fat" in series_desc and "fat" not in image_types:
|
|
image_types.append("fat")
|
|
|
|
return image_types if image_types else ["unknown"]
|