Skip to content

Commit

Permalink
Merge pull request #77 from GalBenZvi/bids
Browse files Browse the repository at this point in the history
WIP bids-compatible detectors
  • Loading branch information
ZviBaratz committed Nov 7, 2021
2 parents fc3e892 + a9c6a83 commit 759619e
Show file tree
Hide file tree
Showing 70 changed files with 2,255 additions and 421 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,7 @@ docs/_static/
docs/_templates/

# tox directory
.tox
.tox

# notebooks
*.ipynb
16 changes: 8 additions & 8 deletions docs/modules/dicom_parser.utils.siemens.csa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,26 @@ Module contents
Submodules
----------

dicom\_parser.utils.siemens.csa.data\_element module
dicom\_parser.utils.siemens.csa.ascii.element module
----------------------------------------------------

.. automodule:: dicom_parser.utils.siemens.csa.data_element
.. automodule:: dicom_parser.utils.siemens.csa.ascii.element
:members:
:undoc-members:
:show-inheritance:

dicom\_parser.utils.siemens.csa.header module
---------------------------------------------
dicom\_parser.utils.siemens.csa.ascii.header module
-----------------------------------------------------

.. automodule:: dicom_parser.utils.siemens.csa.header
.. automodule:: dicom_parser.utils.siemens.csa.ascii.header
:members:
:undoc-members:
:show-inheritance:

dicom\_parser.utils.siemens.csa.parser module
---------------------------------------------
dicom\_parser.utils.siemens.csa.ascii.parser module
-----------------------------------------------------

.. automodule:: dicom_parser.utils.siemens.csa.parser
.. automodule:: dicom_parser.utils.siemens.csa.ascii.parser
:members:
:undoc-members:
:show-inheritance:
2 changes: 1 addition & 1 deletion src/dicom_parser/data_elements/code_string.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,4 @@ def parse_value(self, value: str) -> str:
enum = self.TAG_TO_ENUM.get(self.tag)
if enum:
return self.parse_with_enum(value, enum)
return value
return value.strip()
2 changes: 1 addition & 1 deletion src/dicom_parser/data_elements/person_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ def parse_value(self, value: PydicomPersonName) -> dict:
component: getattr(value, component)
for component in self.COMPONENTS
}
return value
return value if value else {}
186 changes: 179 additions & 7 deletions src/dicom_parser/header.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,21 @@
import json
from pathlib import Path
from types import GeneratorType
from typing import Any, Iterable, List, Union
from typing import Any, Dict, Iterable, List, Tuple, Union

from pydicom.dataelem import DataElement as PydicomDataElement
from pydicom.dataset import FileDataset

from dicom_parser.data_element import DataElement
from dicom_parser.messages import INVALID_ELEMENT_IDENTIFIER
from dicom_parser.utils import read_file, requires_pandas
from dicom_parser.utils.bids.bids_detector import BidsDetector
from dicom_parser.utils.format_header_df import format_header_df
from dicom_parser.utils.plane import Plane
from dicom_parser.utils.private_tags import PRIVATE_TAGS
from dicom_parser.utils.sequence_detector.sequence_detector import \
SequenceDetector
from dicom_parser.utils.sequence_detector.sequence_detector import (
SequenceDetector,
)
from dicom_parser.utils.value_representation import ValueRepresentation
from dicom_parser.utils.vr_to_data_element import get_data_element_class

Expand All @@ -34,9 +37,15 @@ class Header:
"""

#: Header fields to pass to
#: :class:`~dicom_parser.utils.sequence_detector.sequence_detector.SequenceDetector`.
#: :class:`~dicom_parser.utils.sequence_detector.sequence_detector.SequenceDetector`. # noqa: E501
sequence_identifiers = {
"Magnetic Resonance": ["ScanningSequence", "SequenceVariant"]
"Magnetic Resonance": [
"ScanningSequence",
"SequenceVariant",
"SeriesDescription",
"ImageType",
"ScanOptions",
]
}

#: Column names to use when converting to dataframe.
Expand All @@ -45,6 +54,23 @@ class Header:
#: Name of column to be used as an index when converting to dataframe.
DATAFRAME_INDEX: str = "Tag"

#: Dictionary used to convert in-plane phase encoding direction to the
#: NIfTI appropriate equivalent.
PHASE_ENCODING_DIRECTION: Dict[str, str] = {"COL": "i", "ROW": "j"}
PHASE_ENCODING_SIGN: Dict[int, str] = {0: "-", 1: ""}
PHASE_ENCODING: Dict[Plane, Dict[str, str]] = {
Plane.AXIAL: {"i": "LR", "i-": "RL", "j": "PA", "j-": "AP"},
# Plane.SAGITTAL: {"i": "PA", "i-": "AP", "j": ""}
}

#: Infer image plane from the rounded ImageOrientationPatient value.
#: Based on https://stackoverflow.com/a/56670334/4416932
IOP_TO_PLANE: Dict[Tuple[int], Plane] = {
(1, 0, 0, 0, 1, 0): Plane.AXIAL,
(1, 0, 0, 0, 0, -1): Plane.CORONAL,
(0, 1, 0, 0, 0, -1): Plane.SAGITTAL,
}

#: Will be prepended to the sequences section when printing the header.
_SEQUENCES_SECTION_TITLE: str = "\n\nSequences\n=========\n"

Expand All @@ -61,6 +87,7 @@ def __init__(
self,
raw: Union[FileDataset, str, Path],
sequence_detector=SequenceDetector,
bids_detector=BidsDetector,
):
"""
Header is meant to be initialized with a pydicom FileDataset
Expand All @@ -75,6 +102,7 @@ def __init__(
A utility class to automatically detect sequences
"""
self.sequence_detector = sequence_detector()
self.bids_detector = bids_detector()
self.raw = read_file(raw, read_data=False)
self.manufacturer = self.get("Manufacturer")
self.detected_sequence = self.detect_sequence()
Expand Down Expand Up @@ -152,11 +180,16 @@ def __repr__(self) -> str:
"""
return self.__str__()

def detect_sequence(self) -> str:
def detect_sequence(self, verbose: bool = False) -> str:
"""
Returns the detected imaging sequence using the modality's sequence
identifying header information.
Parameters
----------
verbose : bool
Whether to show evaluation logs
Returns
-------
str
Expand All @@ -167,7 +200,24 @@ def detect_sequence(self) -> str:
sequence_identifying_values = self.get(sequence_identifiers)
try:
return self.sequence_detector.detect(
modality, sequence_identifying_values
modality, sequence_identifying_values, verbose=verbose
)
except NotImplementedError:
pass

def build_bids_path(self) -> str:
"""
Returns the derived BIDS path for this series.
Returns
-------
str
Imaging sequence name
"""
modality = self.get("Modality")
try:
return self.bids_detector.build_path(
modality, self.detected_sequence, self.as_dict
)
except NotImplementedError:
pass
Expand Down Expand Up @@ -567,6 +617,128 @@ def keyword_contains(
matches.append(data_element)
return matches

def get_b_value(self) -> float:
"""
Returns the B value of Siemens DWI scans.
See Also
--------
:func:`b_value`
Returns
-------
float
B value
"""
csa = self.get(("0029", "1020"))
if csa is not None:
try:
return csa["Diffusion"]["BValue"]
except KeyError:
pass
return self.get("B_value")

def get_n_diffusion_directions(self) -> int:
"""
Returns the number of diffusion directions for DWI scans.
See Also
--------
:func:`n_diffusion_directions`
Returns
-------
int
Number of diffusion directions
"""
csa = self.get(("0029", "1020"), {})
try:
ascii_header = csa["MrPhoenixProtocol"]
return ascii_header["Diffusion"]["DiffDirections"]
except KeyError:
pass

def get_phase_encoding_direction(self) -> str:
"""
Returns NIfTI-style phase encoding direction information (i/j[-]).
Returns
-------
str
Phase encoding direction
"""
inplane_pe = self.get("InPlanePhaseEncodingDirection")
inplane_pe = self.PHASE_ENCODING_DIRECTION.get(inplane_pe)
image_csa = self.get(("0029", "1010"), {})
sign = image_csa.get("PhaseEncodingDirectionPositive", {})
sign = self.PHASE_ENCODING_SIGN.get(sign.get("value"))
if inplane_pe is not None and sign is not None:
return f"{inplane_pe}{sign}"

def infer_phase_encoding(self) -> str:
"""
Returns the applied phase encoding as defined in the AP/PA or LR/RL
format, based on the acquisition plane and phase encoding direction.
Returns
-------
str
Phase encoding as AP/PA/LR/RL
"""
plane = self.get_plane()
direction = self.get_phase_encoding_direction()
try:
return self.PHASE_ENCODING[plane][direction]
except KeyError:
pass

def get_plane(self) -> Plane:
"""
Returns the image plane (see :class:`dicom_parser.utils.plane.Plane`)
based on the header's 'ImageOrientationPatient' (0x20, 0x37) tag.
Returns
-------
Plane
Acquisition plane
"""
iop = self.get(("0020", "0037"))
if iop is not None:
iop = tuple(round(i) for i in iop)
return self.IOP_TO_PLANE.get(iop)

@property
def b_value(self) -> float:
"""
Returns the B value of Siemens scans.
See Also
--------
:func:`get_b_value`
Returns
-------
float
B value
"""
return self.get_b_value()

@property
def n_diffusion_directions(self) -> float:
"""
Returns the number of diffusion directions for DWI scans.
See Also
--------
:func:`get_n_diffusion_directions`
Returns
-------
int
Number of diffusion directions
"""
return self.get_n_diffusion_directions()

@property
def data_elements(self) -> GeneratorType:
"""
Expand Down
54 changes: 22 additions & 32 deletions src/dicom_parser/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -310,21 +310,6 @@ def get_b_matrix(self) -> np.ndarray:
"""
return self.header.get("B_matrix")

def get_b_value(self) -> float:
"""
Returns the B value of Siemens DWI scans.
See Also
--------
:func:`b_value`
Returns
-------
float
B value
"""
return self.header.get("B_value")

def get_q_vector(self) -> np.ndarray:
"""
Calculates Siemens DWI q-vector in voxel space.
Expand Down Expand Up @@ -418,6 +403,27 @@ def check_multi_frame(self) -> bool:
"""
return self.header.get("SOPClassUID") == "1.2.840.10008.5.1.4.1.1.4.1"

def get_bids_path(self) -> str:
"""
Build BIDS-appropriate path for the series.
Returns
-------
str
BIDS-appropriate path
"""
return self.header.build_bids_path()

@property
def bids_path(self) -> str:
"""
Builds BIDS-appropriate path according to DICOM's header
Returns
-------
str
BIDS-appropriate path
"""
return self.get_bids_path()

@property
def image_shape(self) -> Tuple[int, int]:
"""
Expand Down Expand Up @@ -555,7 +561,7 @@ def is_fmri(self) -> bool:
bool
Whether this image represents fMRI data
"""
return self.header.detected_sequence == "fMRI"
return self.header.detected_sequence == "bold"

@property
def data(self) -> np.ndarray:
Expand Down Expand Up @@ -604,22 +610,6 @@ def b_matrix(self) -> np.ndarray:
"""
return self.get_b_matrix()

@property
def b_value(self) -> float:
"""
Returns the B matrix of Siemens scans.
See Also
--------
:func:`get_b_value`
Returns
-------
float
B value
"""
return self.get_b_value()

@property
def q_vector(self) -> np.ndarray:
"""
Expand Down

0 comments on commit 759619e

Please sign in to comment.