From 583060293763be071febf07285f0469ae1ad4885 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Mon, 8 Nov 2021 13:11:16 +0200 Subject: [PATCH 01/39] Improved sequence detection rules and WIP on BIDS paths. --- src/dicom_parser/header.py | 28 ++++++++++++++--- src/dicom_parser/utils/bids/header_queries.py | 18 ++++++++--- .../utils/bids/sequence_to_bids.py | 10 +++--- .../sequences/mr/dwi/diffusion.py | 5 +++ .../sequence_detector/sequences/mr/dwi/dwi.py | 6 ++-- .../sequences/mr/dwi/fieldmap.py | 25 --------------- .../sequences/mr/dwi/sbref.py | 31 +++++++++++++++++++ .../sequences/mr/func/bold.py | 6 ++++ .../sequences/mr/func/sbref.py | 15 +++++---- 9 files changed, 95 insertions(+), 49 deletions(-) delete mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index 30f8c2df..d6434c07 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -38,7 +38,7 @@ class Header: #: Header fields to pass to #: :class:`~dicom_parser.utils.sequence_detector.sequence_detector.SequenceDetector`. # noqa: E501 - sequence_identifiers = { + SEQUENCE_IDENTIFIERS = { "Magnetic Resonance": [ "ScanningSequence", "SequenceVariant", @@ -48,6 +48,8 @@ class Header: ] } + DICTIONARY_APPENDICES = {"Magnetic Resonance": ["infer_phase_encoding"]} + #: Column names to use when converting to dataframe. DATAFRAME_COLUMNS: Iterable[str] = ("Tag", "Keyword", "VR", "VM", "Value") @@ -196,8 +198,16 @@ def detect_sequence(self, verbose: bool = False) -> str: Imaging sequence name """ modality = self.get("Modality") - sequence_identifiers = self.sequence_identifiers.get(modality) - sequence_identifying_values = self.get(sequence_identifiers) + SEQUENCE_IDENTIFIERS = self.SEQUENCE_IDENTIFIERS.get(modality) + sequence_identifying_values = self.get(SEQUENCE_IDENTIFIERS) + for key, value in sequence_identifying_values.items(): + if value is None: + method = getattr(self, key, None) + if method is not None: + try: + sequence_identifying_values[key] = method() + except TypeError: + pass try: return self.sequence_detector.detect( modality, sequence_identifying_values, verbose=verbose @@ -529,10 +539,20 @@ def to_dict(self, parsed: bool = True) -> dict: dict Header information """ - return { + d = { data_element.keyword: self.get(data_element.tag, parsed=parsed) for data_element in self.data_elements } + modality = self.get("Modality") + appendices = self.DICTIONARY_APPENDICES.get(modality, []) + for appendix in appendices: + method = getattr(self, appendix, None) + if method is not None: + try: + d[appendix] = method() + except TypeError: + continue + return d @requires_pandas def to_dataframe( diff --git a/src/dicom_parser/utils/bids/header_queries.py b/src/dicom_parser/utils/bids/header_queries.py index 77c9d4ec..ea32e4f1 100644 --- a/src/dicom_parser/utils/bids/header_queries.py +++ b/src/dicom_parser/utils/bids/header_queries.py @@ -88,6 +88,9 @@ def find_task_name(header: dict) -> str: return task +PHASE_ENCODINGS = ("ap", "pa", "lr", "rl") + + def find_phase_encoding(header: dict) -> str: """ Finds correct value for the "dir" field of BIDS specification for EPI @@ -101,8 +104,15 @@ def find_phase_encoding(header: dict) -> str: Returns ------- str - Phase encoding direction (AP/PA) + Phase encoding direction """ - description = header.get("ProtocolName").lower() - pe = description.split("_")[-1] - return pe + try: + return header["infer_phase_encoding"] + except KeyError: + try: + description = header.get("ProtocolName").lower() + pe = description.split("_")[-1] + if pe in PHASE_ENCODINGS: + return pe + except (AttributeError, IndexError): + return diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 4511dd33..b1ef0c1b 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -21,11 +21,11 @@ "dir": find_phase_encoding, "suffix": "dwi", } -DWI_FIELDMAP = { - "data_type": "fmap", - "acq": "dwi", +DWI_SBREF = { + "data_type": "dwi", + "acq": "singleband", "dir": find_phase_encoding, - "suffix": "epi", + "suffix": "dwi", } FLAIR = {"data_type": "anat", "suffix": "FLAIR"} FUNCTIONAL_FIELDMAP = { @@ -59,7 +59,7 @@ "func_sbref": FUNCTIONAL_SBREF, "func_fieldmap": FUNCTIONAL_FIELDMAP, "dwi": DWI, - "dwi_fieldmap": DWI_FIELDMAP, + "dwi_sbref": DWI_SBREF, } #: Known BIDS field values by modality. SEQUENCE_TO_BIDS = {"Magnetic Resonance": MR_SEQUENCE_TO_BIDS} diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index 0441d51a..5009dc81 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -4,6 +4,11 @@ "value": "Echo Planar", "lookup": "exact", }, + { + "key": "SequenceVariant", + "value": ("Segmented k-Space", "Steady State"), + "lookup": "exact", + }, { "key": "ImageType", "value": [ diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py index f2dd4da6..b742c480 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py @@ -1,11 +1,11 @@ from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import ( DWI_RULES, ) -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.fieldmap import ( - DWI_FIELDMAP_RULES, +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.sbref import ( + DWI_SBREF_RULES, ) MR_DIFFUSION_SEQUENCES = { "dwi": DWI_RULES, - "dwi_fieldmap": DWI_FIELDMAP_RULES, + "dwi_sbref": DWI_SBREF_RULES, } diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py deleted file mode 100644 index cee203b7..00000000 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py +++ /dev/null @@ -1,25 +0,0 @@ -DWI_FIELDMAP_RULES = [ - { - "key": "ScanningSequence", - "value": "Echo Planar", - "lookup": "exact", - }, - { - "key": "ImageType", - "value": [ - ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), - ("ORIGINAL", "PRIMARY", "PHASE MAP", "ND"), - ("DERIVED", "PRIMARY", "DIFFUSION", "ADC", "ND", "NORM"), - ("DERIVED", "PRIMARY", "DIFFUSION", "FA", "ND", "NORM"), - ("DERIVED", "PRIMARY", "DIFFUSION", "TRACEW", "ND", "NORM"), - ], - "lookup": "exact", - "operator": "any", - }, - { - "key": "ScanOptions", - "value": ["PFP", ""], - "lookup": "in", - "operator": "any", - }, -] diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py new file mode 100644 index 00000000..dbf1fe62 --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py @@ -0,0 +1,31 @@ +DWI_SBREF_RULES = [ + { + "key": "ScanningSequence", + "value": "Echo Planar", + "lookup": "exact", + }, + { + "key": "SequenceVariant", + "value": ("Segmented k-Space", "Steady State"), + "lookup": "exact", + }, + { + "key": "ImageType", + "value": [ + ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), + # ("ORIGINAL", "PRIMARY", "PHASE MAP", "ND"), + # Siemens computed maps, should be handled separately. + # ("DERIVED", "PRIMARY", "DIFFUSION", "ADC", "ND", "NORM"), + # ("DERIVED", "PRIMARY", "DIFFUSION", "FA", "ND", "NORM"), + # ("DERIVED", "PRIMARY", "DIFFUSION", "TRACEW", "ND", "NORM"), + ], + "lookup": "exact", + "operator": "any", + }, + { + "key": "ScanOptions", + "value": ["PFP", ""], + "lookup": "exact", + "operator": "any", + }, +] diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py index d785ed2c..3c40f4a3 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py @@ -4,6 +4,12 @@ "value": "Echo Planar", "lookup": "exact", }, + { + "key": "SequenceVariant", + "value": ("Segmented k-Space", "Steady State", "Oversampling Phase"), + "lookup": "exact", + "operator": "any", + }, { "key": "ImageType", "value": [ diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py index aec5762b..cd80515a 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py @@ -6,17 +6,16 @@ }, { "key": "SequenceVariant", - "value": ["Steady State"], - "lookup": "in", - }, - { - "key": "ImageType", - "value": ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), + "value": ("Segmented k-Space", "Steady State", "Oversampling Phase"), "lookup": "exact", + "operator": "any", }, { - "key": "ScanOptions", - "value": ("PFP", "FS"), + "key": "ImageType", + "value": [ + ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), + ], "lookup": "exact", + "operator": "any", }, ] From 88672f1c04246b4b81548e1551e11402a526713f Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Mon, 8 Nov 2021 20:23:32 +0200 Subject: [PATCH 02/39] A couple of small fixes, as well as some WIP on phase encoding inference. --- src/dicom_parser/header.py | 26 ++++++++++++++----- src/dicom_parser/utils/bids/bids_detector.py | 2 +- .../utils/bids/sequence_to_bids.py | 2 +- .../utils/sequence_detector/messages.py | 2 +- 4 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index d6434c07..e96ca66f 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -60,10 +60,19 @@ class Header: #: 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": ""} + Plane.AXIAL: {"i": "PA", "i-": "AP", "j": "LR", "j-": "RL"}, + Plane.SAGITTAL: {"i": "SI", "i-": "IS", "j": "PA", "j-": "AP"}, + Plane.CORONAL: {"i": "SI", "i-": "IS", "j": "LR", "j-": "RL"}, } + """ + Experimental phase encoding direction label dictionary. + Due to the requirements of my personal dataset I am testing a dictionary + which assumes PIR encoding from the top left corner. There should be some + header information that can be used to infer the encoding scheme and make + this method more robust. + """ #: Infer image plane from the rounded ImageOrientationPatient value. #: Based on https://stackoverflow.com/a/56670334/4416932 @@ -198,19 +207,22 @@ def detect_sequence(self, verbose: bool = False) -> str: Imaging sequence name """ modality = self.get("Modality") - SEQUENCE_IDENTIFIERS = self.SEQUENCE_IDENTIFIERS.get(modality) - sequence_identifying_values = self.get(SEQUENCE_IDENTIFIERS) - for key, value in sequence_identifying_values.items(): + keys = self.SEQUENCE_IDENTIFIERS.get(modality) + if keys is None: + print(f"No sequence identifiers registered for {modality}!") + return + values = self.get(keys) + for key, value in values.items(): if value is None: method = getattr(self, key, None) if method is not None: try: - sequence_identifying_values[key] = method() + values[key] = method() except TypeError: pass try: return self.sequence_detector.detect( - modality, sequence_identifying_values, verbose=verbose + modality, values, verbose=verbose ) except NotImplementedError: pass diff --git a/src/dicom_parser/utils/bids/bids_detector.py b/src/dicom_parser/utils/bids/bids_detector.py index 6137362f..4dc2d296 100644 --- a/src/dicom_parser/utils/bids/bids_detector.py +++ b/src/dicom_parser/utils/bids/bids_detector.py @@ -92,7 +92,7 @@ def validate_fields(self, sequence: str, fields: dict) -> None: for required_key in self.REQUIRED_KEYS: if required_key not in fields: message = INVALID_SEQUENCE_KEYS.format( - required_key=required_key + required_key=required_key, fields=fields ) raise ValueError(message) return True diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index b1ef0c1b..838d0fab 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -40,7 +40,7 @@ "dir": find_phase_encoding, "suffix": "sbref", } -IREPI = ({"data_type": "anat", "inv": find_irepi_acq, "suffix": "IRT1"},) +IREPI = {"data_type": "anat", "inv": find_irepi_acq, "suffix": "IRT1"} MPRAGE = {"data_type": "anat", "ce": find_mprage_acq, "suffix": "T1w"} SPGR = {"data_type": "anat", "acq": "spgr", "suffix": "T1w"} FSPGR = {"data_type": "anat", "acq": "fspgr", "suffix": "T1w"} diff --git a/src/dicom_parser/utils/sequence_detector/messages.py b/src/dicom_parser/utils/sequence_detector/messages.py index b41cbf31..77e12ab9 100644 --- a/src/dicom_parser/utils/sequence_detector/messages.py +++ b/src/dicom_parser/utils/sequence_detector/messages.py @@ -13,7 +13,7 @@ INVALID_SEQUENCE: str = ( "There is no `{sequence}` BIDS naming definition avaliable yet!" ) -INVALID_SEQUENCE_KEYS: str = "All sequences' BIDS naming schemes must contain `{required_key}` definition." +INVALID_SEQUENCE_KEYS: str = "All sequences' BIDS naming schemes must contain `{required_key}` definition, please fix:\n{fields}" MISSING_RULE_KEY: str = "Missing key {key} in definition rule." # flake8: noqa: E501 From 69cc9ef43216b924f029e4e838d626e066dcb708 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Mon, 8 Nov 2021 20:43:13 +0200 Subject: [PATCH 03/39] Fixed DWI SBRef BIDS fields to enable easier IntendedFor deduction. --- src/dicom_parser/utils/bids/sequence_to_bids.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 838d0fab..a1698b1e 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -23,9 +23,9 @@ } DWI_SBREF = { "data_type": "dwi", - "acq": "singleband", + "acq": "dwi", "dir": find_phase_encoding, - "suffix": "dwi", + "suffix": "sbref", } FLAIR = {"data_type": "anat", "suffix": "FLAIR"} FUNCTIONAL_FIELDMAP = { From ea7fabfa967ed463078fa2a51f7260ab967f8dca Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Mon, 8 Nov 2021 21:05:04 +0200 Subject: [PATCH 04/39] Removed no sequence registered message for no modality because it created a mess of warnings for nested headers. --- src/dicom_parser/header.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index e96ca66f..9ace8ed3 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -207,6 +207,8 @@ def detect_sequence(self, verbose: bool = False) -> str: Imaging sequence name """ modality = self.get("Modality") + if modality is None: + return keys = self.SEQUENCE_IDENTIFIERS.get(modality) if keys is None: print(f"No sequence identifiers registered for {modality}!") From a47c1196b9bafcfc0bd7602e6ad02786db5aff77 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 9 Nov 2021 08:22:06 +0200 Subject: [PATCH 05/39] Added check for missing header values in sequence detection call. --- src/dicom_parser/header.py | 13 +++++++++++-- src/dicom_parser/messages.py | 1 + 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index 9ace8ed3..58080b54 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -10,7 +10,11 @@ from pydicom.dataset import FileDataset from dicom_parser.data_element import DataElement -from dicom_parser.messages import INVALID_ELEMENT_IDENTIFIER +from dicom_parser.messages import ( + INVALID_ELEMENT_IDENTIFIER, + MISSING_HEADER_INFO, + UNREGISTERED_MODALITY, +) 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 @@ -211,9 +215,14 @@ def detect_sequence(self, verbose: bool = False) -> str: return keys = self.SEQUENCE_IDENTIFIERS.get(modality) if keys is None: - print(f"No sequence identifiers registered for {modality}!") + message = UNREGISTERED_MODALITY.format(modality=modality) + print(message) return values = self.get(keys) + if values is None: + message = MISSING_HEADER_INFO.format(modality=modality, keys=keys) + print(message) + return for key, value in values.items(): if value is None: method = getattr(self, key, None) diff --git a/src/dicom_parser/messages.py b/src/dicom_parser/messages.py index fa86ded9..0259227c 100644 --- a/src/dicom_parser/messages.py +++ b/src/dicom_parser/messages.py @@ -12,5 +12,6 @@ INVALID_ELEMENT_IDENTIFIER: str = "Invalid data element identifier: {tag_or_keyword} of type {input_type}!\nData elements may only be queried using a string representing a keyword or a tuple of two strings representing a tag!" INVALID_INDEXING_OPERATOR: str = "Invalid indexing operator value ({key})! Must be of type str, tuple, int, or slice." INVALID_SERIES_DIRECTORY: str = "Series instances must be initialized with a valid directory path! Could not locate directory {path}." +UNREGISTERED_MODALITY: str = "No sequence identifiers registered for {modality}!" # flake8: noqa: E501 From ecb8937808a8103180a4b3339aa928772d875fcb Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 9 Nov 2021 08:23:28 +0200 Subject: [PATCH 06/39] Added missing message. --- src/dicom_parser/messages.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/messages.py b/src/dicom_parser/messages.py index 0259227c..48bd241a 100644 --- a/src/dicom_parser/messages.py +++ b/src/dicom_parser/messages.py @@ -12,6 +12,9 @@ INVALID_ELEMENT_IDENTIFIER: str = "Invalid data element identifier: {tag_or_keyword} of type {input_type}!\nData elements may only be queried using a string representing a keyword or a tuple of two strings representing a tag!" INVALID_INDEXING_OPERATOR: str = "Invalid indexing operator value ({key})! Must be of type str, tuple, int, or slice." INVALID_SERIES_DIRECTORY: str = "Series instances must be initialized with a valid directory path! Could not locate directory {path}." -UNREGISTERED_MODALITY: str = "No sequence identifiers registered for {modality}!" +UNREGISTERED_MODALITY: str = ( + "No sequence identifiers registered for {modality}!" +) +MISSING_HEADER_INFO: str = "No header information found for {modality} sequence detection using {keys}!" # flake8: noqa: E501 From 81bd708478bdd62f7162ac05d9de2c915a2a4a0d Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 9 Nov 2021 08:31:57 +0200 Subject: [PATCH 07/39] Made detected sequence a property so that it isn't computed for any initialized header. --- src/dicom_parser/header.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index 58080b54..7fe1a982 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -120,7 +120,6 @@ def __init__( self.bids_detector = bids_detector() self.raw = read_file(raw, read_data=False) self.manufacturer = self.get("Manufacturer") - self.detected_sequence = self.detect_sequence() self._as_dict = None def __getitem__(self, key: Union[str, tuple, list]) -> Any: @@ -821,3 +820,7 @@ def keys(self) -> List[str]: Header keywords """ return list(self.as_dict.keys()) + + @property + def detected_sequence(self) -> str: + return self.detect_sequence() From a573be040327d05803680a98a4f49597c73c21c6 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 9 Nov 2021 08:42:48 +0200 Subject: [PATCH 08/39] Added another rule for BOLD sequence detection and fixed tests. --- .../utils/sequence_detector/sequences/mr/func/bold.py | 5 ++++- tests/test_header.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py index 3c40f4a3..757919fa 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py @@ -6,7 +6,10 @@ }, { "key": "SequenceVariant", - "value": ("Segmented k-Space", "Steady State", "Oversampling Phase"), + "value": [ + ("Segmented k-Space", "Steady State"), + ("Segmented k-Space", "Steady State", "Oversampling Phase"), + ], "lookup": "exact", "operator": "any", }, diff --git a/tests/test_header.py b/tests/test_header.py index 44b9b3b8..0b3593a2 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -318,7 +318,7 @@ def test_to_dataframe_with_no_elements(self): def test_to_dict(self): value = self.header.to_dict() self.assertIsInstance(value, dict) - self.assertEqual(len(value), 119) + self.assertEqual(len(value), 120) def test_as_dict(self): value = self.header.as_dict @@ -334,7 +334,7 @@ def test_as_dict_is_cached(self): def test_keys(self): value = self.header.keys self.assertIsInstance(value, list) - self.assertEqual(len(value), 119) + self.assertEqual(len(value), 120) def test_unknown_value_representation(self): with self.assertRaises(ValueRepresentationError): From fe275305822646e9cee10f8313b120f9316320d2 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 9 Nov 2021 09:45:14 +0200 Subject: [PATCH 09/39] Reversed PE for axial scans with direction i. --- src/dicom_parser/header.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index 7fe1a982..a01d924f 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -66,7 +66,7 @@ class Header: PHASE_ENCODING_SIGN: Dict[int, str] = {0: "-", 1: ""} PHASE_ENCODING: Dict[Plane, Dict[str, str]] = { - Plane.AXIAL: {"i": "PA", "i-": "AP", "j": "LR", "j-": "RL"}, + Plane.AXIAL: {"i": "AP", "i-": "PA", "j": "LR", "j-": "RL"}, Plane.SAGITTAL: {"i": "SI", "i-": "IS", "j": "PA", "j-": "AP"}, Plane.CORONAL: {"i": "SI", "i-": "IS", "j": "LR", "j-": "RL"}, } From 1f8726feffe83e15503a7abdb7c0394b44d5bb74 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Sun, 14 Nov 2021 13:19:27 +0200 Subject: [PATCH 10/39] Added acq field to MPRAGE scans in order to prevent duplicate names. --- src/dicom_parser/utils/bids/header_queries.py | 26 ++++++++++++++++--- .../utils/bids/sequence_to_bids.py | 10 +++++-- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/src/dicom_parser/utils/bids/header_queries.py b/src/dicom_parser/utils/bids/header_queries.py index ea32e4f1..2756a893 100644 --- a/src/dicom_parser/utils/bids/header_queries.py +++ b/src/dicom_parser/utils/bids/header_queries.py @@ -6,25 +6,43 @@ INVALID_CHARACTERS: str = "!@#$%^&*()_-+=" -def find_mprage_acq(header: dict) -> str: +def find_mprage_ce(header: dict) -> str: """ - Finds correct value for the "acq" field of BIDS specification for MPRAGE + Finds correct value for the "ce" field of BIDS specification for MPRAGE sequences. Parameters ---------- header : dict - Dictionary containing DICOM's header. + Dictionary containing DICOM's header Returns ------- str - Either "corrected" or "uncorrected" in terms of bias field correction. + Either "corrected" or "uncorrected" in terms of bias field correction """ image_type = header.get("ImageType", "") return "corrected" if "NORM" in image_type else "uncorrected" +def find_mprage_acq(header: dict) -> str: + """ + Add the series number to the acq field to prevent duplication in case of + multiple scans in the same session. + + Parameters + ---------- + header : dict + Dictionary containing DICOM's header + + Returns + ------- + str + Series number + """ + return header.get("SeriesNumber") + + def find_irepi_acq(header: dict) -> str: """ Finds correct value for the "acq" field of BIDS specification for IR-EPI diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index a1698b1e..4ef05d9c 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -4,6 +4,7 @@ from dicom_parser.utils.bids.header_queries import ( find_irepi_acq, find_mprage_acq, + find_mprage_ce, find_phase_encoding, find_task_name, ) @@ -41,10 +42,15 @@ "suffix": "sbref", } IREPI = {"data_type": "anat", "inv": find_irepi_acq, "suffix": "IRT1"} -MPRAGE = {"data_type": "anat", "ce": find_mprage_acq, "suffix": "T1w"} +MPRAGE = { + "data_type": "anat", + "ce": find_mprage_ce, + "acq": find_mprage_acq, + "suffix": "T1w", +} SPGR = {"data_type": "anat", "acq": "spgr", "suffix": "T1w"} FSPGR = {"data_type": "anat", "acq": "fspgr", "suffix": "T1w"} -T2W = {"data_type": "anat", "ce": find_mprage_acq, "suffix": "T2w"} +T2W = {"data_type": "anat", "ce": find_mprage_ce, "suffix": "T2w"} #: BIDS fields used in Magnetic Resonance (MR) imaging and their associated #: definitions. From 31033d9ee461114c87120f04a7cd6730b3f823c9 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Sun, 14 Nov 2021 14:25:23 +0200 Subject: [PATCH 11/39] Updated tests. --- tests/utils/bids/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/bids/fixtures.py b/tests/utils/bids/fixtures.py index a688bc07..d538d89d 100644 --- a/tests/utils/bids/fixtures.py +++ b/tests/utils/bids/fixtures.py @@ -3,7 +3,7 @@ """ BIDS_PATH = { "mprage": { - "corrected": "sub-110/ses-201805011221/anat/sub-110_ses-201805011221_ce-corrected_T1w" + "corrected": "sub-110/ses-201805011221/anat/sub-110_ses-201805011221_acq-3_ce-corrected_T1w" } } From 64deda9482086cde415009fd18823742d519725b Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 13:51:04 +0200 Subject: [PATCH 12/39] Chaned phase encoding direction in BIDS path generation to use general FWD/REV labeling rather than AP/PA/RL/LR. --- src/dicom_parser/header.py | 50 +++++-------------- src/dicom_parser/utils/bids/header_queries.py | 7 +-- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index a01d924f..df339780 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -52,7 +52,9 @@ class Header: ] } - DICTIONARY_APPENDICES = {"Magnetic Resonance": ["infer_phase_encoding"]} + DICTIONARY_APPENDICES = { + "Magnetic Resonance": ["phase_encoding_direction"] + } #: Column names to use when converting to dataframe. DATAFRAME_COLUMNS: Iterable[str] = ("Tag", "Keyword", "VR", "VM", "Value") @@ -62,22 +64,9 @@ class Header: #: 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_DIRECTION: Dict[str, str] = {"ROW": "i", "COL": "j"} PHASE_ENCODING_SIGN: Dict[int, str] = {0: "-", 1: ""} - PHASE_ENCODING: Dict[Plane, Dict[str, str]] = { - Plane.AXIAL: {"i": "AP", "i-": "PA", "j": "LR", "j-": "RL"}, - Plane.SAGITTAL: {"i": "SI", "i-": "IS", "j": "PA", "j-": "AP"}, - Plane.CORONAL: {"i": "SI", "i-": "IS", "j": "LR", "j-": "RL"}, - } - """ - Experimental phase encoding direction label dictionary. - Due to the requirements of my personal dataset I am testing a dictionary - which assumes PIR encoding from the top left corner. There should be some - header information that can be used to infer the encoding scheme and make - this method more robust. - """ - #: Infer image plane from the rounded ImageOrientationPatient value. #: Based on https://stackoverflow.com/a/56670334/4416932 IOP_TO_PLANE: Dict[Tuple[int], Plane] = { @@ -568,12 +557,12 @@ def to_dict(self, parsed: bool = True) -> dict: modality = self.get("Modality") appendices = self.DICTIONARY_APPENDICES.get(modality, []) for appendix in appendices: - method = getattr(self, appendix, None) - if method is not None: + attribute = getattr(self, appendix, None) + if attribute is not None: try: - d[appendix] = method() + d[appendix] = attribute() except TypeError: - continue + d[appendix] = attribute return d @requires_pandas @@ -717,24 +706,7 @@ def get_phase_encoding_direction(self) -> str: 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: + def estimate_acquisition_plane(self) -> Plane: """ Returns the image plane (see :class:`dicom_parser.utils.plane.Plane`) based on the header's 'ImageOrientationPatient' (0x20, 0x37) tag. @@ -821,6 +793,10 @@ def keys(self) -> List[str]: """ return list(self.as_dict.keys()) + @property + def phase_encoding_direction(self) -> str: + return self.get_phase_encoding_direction() + @property def detected_sequence(self) -> str: return self.detect_sequence() diff --git a/src/dicom_parser/utils/bids/header_queries.py b/src/dicom_parser/utils/bids/header_queries.py index 2756a893..5fecaca7 100644 --- a/src/dicom_parser/utils/bids/header_queries.py +++ b/src/dicom_parser/utils/bids/header_queries.py @@ -106,7 +106,7 @@ def find_task_name(header: dict) -> str: return task -PHASE_ENCODINGS = ("ap", "pa", "lr", "rl") +PHASE_ENCODINGS = ("ap", "pa", "lr", "rl", "fwd", "rev") def find_phase_encoding(header: dict) -> str: @@ -125,8 +125,9 @@ def find_phase_encoding(header: dict) -> str: Phase encoding direction """ try: - return header["infer_phase_encoding"] - except KeyError: + phase_encoding = header["phase_encoding_direction"] + return "REV" if phase_encoding.endswith("-") else "FWD" + except (KeyError, AttributeError): try: description = header.get("ProtocolName").lower() pe = description.split("_")[-1] From f87ec127929f3d4f12de71f4b3c556470e5e38f8 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 14:18:18 +0200 Subject: [PATCH 13/39] Created computed DWI category for computed sequences and excluded from BIDS. --- src/dicom_parser/utils/bids/bids_detector.py | 7 ++++++- .../utils/bids/sequence_to_bids.py | 13 ++++++------- .../sequences/mr/dwi/computed.py | 19 +++++++++++++++++++ .../sequence_detector/sequences/mr/dwi/dwi.py | 4 ++++ 4 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py diff --git a/src/dicom_parser/utils/bids/bids_detector.py b/src/dicom_parser/utils/bids/bids_detector.py index 4dc2d296..22979de2 100644 --- a/src/dicom_parser/utils/bids/bids_detector.py +++ b/src/dicom_parser/utils/bids/bids_detector.py @@ -86,9 +86,14 @@ def validate_fields(self, sequence: str, fields: dict) -> None: fields : dict Dictionaty with sequence-specific BIDS fields and values """ - if not fields: + # Check for unregistered sequences. + if fields is None: warnings.warn(INVALID_SEQUENCE.format(sequence=sequence)) return False + # Check for sequences registered as not BIDS compatible, such as + # derived DWI data. + if fields is False: + return False for required_key in self.REQUIRED_KEYS: if required_key not in fields: message = INVALID_SEQUENCE_KEYS.format( diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 4ef05d9c..1fadc82f 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -1,13 +1,11 @@ """ Definition of the :attr:`SEQUENCE_TO_BIDS` dictionary. """ -from dicom_parser.utils.bids.header_queries import ( - find_irepi_acq, - find_mprage_acq, - find_mprage_ce, - find_phase_encoding, - find_task_name, -) +from dicom_parser.utils.bids.header_queries import (find_irepi_acq, + find_mprage_acq, + find_mprage_ce, + find_phase_encoding, + find_task_name) # Dictionaries (`Dict[str, Union[str, Callable]]``) associating MR sequences # with BIDS key/value pairs. @@ -66,6 +64,7 @@ "func_fieldmap": FUNCTIONAL_FIELDMAP, "dwi": DWI, "dwi_sbref": DWI_SBREF, + "dwi_computed": False, } #: Known BIDS field values by modality. SEQUENCE_TO_BIDS = {"Magnetic Resonance": MR_SEQUENCE_TO_BIDS} diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py new file mode 100644 index 00000000..b9676e12 --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py @@ -0,0 +1,19 @@ +DWI_COMPUTED_RULES = [ + { + "key": "ScanningSequence", + "value": "Echo Planar", + "lookup": "exact", + }, + { + "key": "ImageType", + "value": ["DERIVED", "PRIMARY", "DIFFUSION", "ND", "NORM"], + "lookup": "in", + "operator": "all", + }, + { + "key": "ImageType", + "value": ["FA", "ADC", "TRACEW"], + "lookup": "in", + "operator": "any", + }, +] diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py index b742c480..811b1433 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py @@ -1,3 +1,6 @@ +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.computed import ( + DWI_COMPUTED_RULES, +) from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import ( DWI_RULES, ) @@ -8,4 +11,5 @@ MR_DIFFUSION_SEQUENCES = { "dwi": DWI_RULES, "dwi_sbref": DWI_SBREF_RULES, + "dwi_computed": DWI_COMPUTED_RULES, } From 22f11788f4ffa0c991ae593cdf5bccc99c887d75 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 14:23:15 +0200 Subject: [PATCH 14/39] Fixed phase encoding test. --- tests/test_header.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_header.py b/tests/test_header.py index 0b3593a2..5cd07df7 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -342,7 +342,7 @@ def test_unknown_value_representation(self): def test_get_phase_encoding_direction(self): value = self.dwi_header.get_phase_encoding_direction() - expected = "j" + expected = "i" self.assertEqual(value, expected) def test_get_phase_encoding_direction_with_none(self): From f3bb60d2ff6f51157f01a673efdc079931a24755 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 14:51:25 +0200 Subject: [PATCH 15/39] Added more diffusion scans. --- .../utils/sequence_detector/sequences/mr/dwi/diffusion.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index 5009dc81..6575e899 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -6,8 +6,12 @@ }, { "key": "SequenceVariant", - "value": ("Segmented k-Space", "Steady State"), + "value": [ + ("Segmented k-Space", "Steady State"), + ("Segmented k-Space", "Spoiled"), + ], "lookup": "exact", + "operator": "any", }, { "key": "ImageType", From 1d412cc598e26919fd2ffe4f75725ec6483920e8 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 14:54:41 +0200 Subject: [PATCH 16/39] Removed ScanningSequence rule for DWI derived images. --- .../utils/sequence_detector/sequences/mr/dwi/computed.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py index b9676e12..913d7c80 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py @@ -1,9 +1,4 @@ DWI_COMPUTED_RULES = [ - { - "key": "ScanningSequence", - "value": "Echo Planar", - "lookup": "exact", - }, { "key": "ImageType", "value": ["DERIVED", "PRIMARY", "DIFFUSION", "ND", "NORM"], From 399479b18d2d57bbe3c03a01208c1c2f9dcf82c8 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:26:59 +0200 Subject: [PATCH 17/39] Improved multiple element header query. --- src/dicom_parser/header.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index df339780..62d8dbcb 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -528,7 +528,16 @@ def get( if isinstance(tag_or_keyword, (str, tuple)): value = get_method(tag_or_keyword) elif isinstance(tag_or_keyword, list): - value = {item: get_method(item) for item in tag_or_keyword} + value = { + item: self.get( + item, + default=default, + parsed=parsed, + missing_ok=missing_ok, + as_json=as_json, + ) + for item in tag_or_keyword + } except (KeyError, TypeError): if not missing_ok: raise From d44a20e2497587192d2d926b5afaebdadeee3fcf Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:32:34 +0200 Subject: [PATCH 18/39] Removed ScanOptions from diffusion sequence detection. --- .../utils/sequence_detector/sequences/mr/dwi/diffusion.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index 6575e899..c22bb3cd 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -41,11 +41,6 @@ "lookup": "exact", "operator": "any", }, - { - "key": "ScanOptions", - "value": ["PFP"], - "lookup": "in", - }, ] DWI_RULES_2 = [ { From b446b05cc5e16c7305dae4abb21022ac2d88cfee Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:37:30 +0200 Subject: [PATCH 19/39] More DWI sequence detection rules. --- .../utils/sequence_detector/sequences/mr/dwi/computed.py | 2 +- .../utils/sequence_detector/sequences/mr/dwi/diffusion.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py index 913d7c80..d7534ded 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py @@ -7,7 +7,7 @@ }, { "key": "ImageType", - "value": ["FA", "ADC", "TRACEW"], + "value": ["FA", "ADC", "TRACEW", "TENSOR"], "lookup": "in", "operator": "any", }, diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index c22bb3cd..a09c08d3 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -9,6 +9,7 @@ "value": [ ("Segmented k-Space", "Steady State"), ("Segmented k-Space", "Spoiled"), + ("Segmented k-Space", "Spoiled", "Oversampling Phase"), ], "lookup": "exact", "operator": "any", From 543b6170d72d3dd392055e31e8c7f7d2f264f0c6 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:41:53 +0200 Subject: [PATCH 20/39] Added localizer rule. --- .../sequence_detector/sequences/mr/anat/localizer.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py index 2dc3db8a..3965a115 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py @@ -13,8 +13,15 @@ }, { "key": "ImageType", - "value": ("ORIGINAL", "PRIMARY", "M", "NORM", "DIS2D"), - "lookup": "exact", + "value": ["ORIGINAL", "PRIMARY", "M", "NORM"], + "lookup": "in", + "operator": "all", + }, + { + "key": "ImageType", + "value": ["DIS2D", "ND"], + "lookup": "in", + "operator": "any", }, ] # GE From 79cdcd1aab6971858cf28b1e03ba1f6b68f9e2dc Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:46:50 +0200 Subject: [PATCH 21/39] Added functional SBRef rules. --- .../utils/sequence_detector/sequences/mr/func/sbref.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py index cd80515a..0923d141 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py @@ -6,7 +6,10 @@ }, { "key": "SequenceVariant", - "value": ("Segmented k-Space", "Steady State", "Oversampling Phase"), + "value": [ + ("Segmented k-Space", "Steady State"), + ("Segmented k-Space", "Steady State", "Oversampling Phase"), + ], "lookup": "exact", "operator": "any", }, @@ -14,6 +17,7 @@ "key": "ImageType", "value": [ ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), + ("ORIGINAL", "PRIMARY", "M", "ND", "NORM", "MOSAIC"), ], "lookup": "exact", "operator": "any", From 1ee328a20fc8df0d77353bfe66383151a87bac38 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 15:50:09 +0200 Subject: [PATCH 22/39] Another diffusion rule. --- .../utils/sequence_detector/sequences/mr/dwi/diffusion.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index a09c08d3..aed69bd7 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -10,6 +10,7 @@ ("Segmented k-Space", "Steady State"), ("Segmented k-Space", "Spoiled"), ("Segmented k-Space", "Spoiled", "Oversampling Phase"), + ("Segmented k-Space", "Steady State", "Oversampling Phase"), ], "lookup": "exact", "operator": "any", From 1396a8e92bd07ff296dea70d807cd8436aa97317 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 16:38:35 +0200 Subject: [PATCH 23/39] Expanded T2w rule. --- .../utils/sequence_detector/sequences/mr/anat/t2w.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/t2w.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/t2w.py index 10da1fdf..7c71e6a0 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/t2w.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/t2w.py @@ -6,8 +6,12 @@ }, { "key": "SequenceVariant", - "value": ("Segmented k-Space", "Spoiled"), + "value": [ + ("Segmented k-Space", "Spoiled"), + ("Segmented k-Space", "Spoiled", "Oversampling Phase"), + ], "lookup": "exact", + "operator": "any", }, ] T2W_RULES_2 = [ From ff23bbef5c7f708b5999dbdb475c6273464110dc Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 16:38:46 +0200 Subject: [PATCH 24/39] Added TIRM to anatomical sequences. --- .../utils/bids/sequence_to_bids.py | 14 +++++++----- .../sequences/mr/anat/anat.py | 4 ++++ .../sequences/mr/anat/tirm.py | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 1fadc82f..b4d55c68 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -1,11 +1,13 @@ """ Definition of the :attr:`SEQUENCE_TO_BIDS` dictionary. """ -from dicom_parser.utils.bids.header_queries import (find_irepi_acq, - find_mprage_acq, - find_mprage_ce, - find_phase_encoding, - find_task_name) +from dicom_parser.utils.bids.header_queries import ( + find_irepi_acq, + find_mprage_acq, + find_mprage_ce, + find_phase_encoding, + find_task_name, +) # Dictionaries (`Dict[str, Union[str, Callable]]``) associating MR sequences # with BIDS key/value pairs. @@ -48,6 +50,7 @@ } SPGR = {"data_type": "anat", "acq": "spgr", "suffix": "T1w"} FSPGR = {"data_type": "anat", "acq": "fspgr", "suffix": "T1w"} +TIRM = {"data_type": "anat", "acq": "tirm", "suffix": "T1w"} T2W = {"data_type": "anat", "ce": find_mprage_ce, "suffix": "T2w"} #: BIDS fields used in Magnetic Resonance (MR) imaging and their associated @@ -59,6 +62,7 @@ "ir_epi": IREPI, "t2w": T2W, "flair": FLAIR, + "tirm": TIRM, "bold": BOLD, "func_sbref": FUNCTIONAL_SBREF, "func_fieldmap": FUNCTIONAL_FIELDMAP, diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/anat.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/anat.py index d55392d4..800531ee 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/anat.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/anat.py @@ -19,6 +19,9 @@ from dicom_parser.utils.sequence_detector.sequences.mr.anat.t2w import ( T2W_RULES, ) +from dicom_parser.utils.sequence_detector.sequences.mr.anat.tirm import ( + TIRM_RULES, +) MR_ANATOMICAL_SEQUENCES = { "flair": FLAIR_RULES, @@ -28,4 +31,5 @@ "spgr": SPGR_RULES, "fspgr": FSPGR_RULES, "t2w": T2W_RULES, + "tirm": TIRM_RULES, } diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py new file mode 100644 index 00000000..4d6ff2df --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py @@ -0,0 +1,22 @@ +TIRM_RULES = [ + { + "key": "ScanningSequence", + "value": ("Spin Echo", "Inversion Recovery"), + "lookup": "exact", + }, + { + "key": "SequenceVariant", + "value": ( + "Segmented k-Space", + "Spoiled", + "MAG Prepared", + "Oversampling Phase", + ), + "lookup": "exact", + }, + { + "key": "ScanOptions", + "value": ["IR"], + "lookup": "in", + }, +] From 9e220fc6a91d6409179c989339f1706b18795aed Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 19:39:08 +0200 Subject: [PATCH 25/39] Generalized some localizer and dwi rules based on a sample dataset. --- .../sequence_detector/sequences/mr/anat/localizer.py | 2 +- .../utils/sequence_detector/sequences/mr/dwi/computed.py | 2 +- .../utils/sequence_detector/sequences/mr/dwi/diffusion.py | 8 ++++++++ 3 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py index 3965a115..b03f1302 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/localizer.py @@ -13,7 +13,7 @@ }, { "key": "ImageType", - "value": ["ORIGINAL", "PRIMARY", "M", "NORM"], + "value": ["ORIGINAL", "PRIMARY", "M"], "lookup": "in", "operator": "all", }, diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py index d7534ded..47fc428b 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py @@ -1,7 +1,7 @@ DWI_COMPUTED_RULES = [ { "key": "ImageType", - "value": ["DERIVED", "PRIMARY", "DIFFUSION", "ND", "NORM"], + "value": ["DERIVED", "PRIMARY", "DIFFUSION", "ND"], "lookup": "in", "operator": "all", }, diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index aed69bd7..71a4a5b8 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -18,6 +18,14 @@ { "key": "ImageType", "value": [ + ( + "ORIGINAL", + "PRIMARY", + "DIFFUSION", + "NONE", + "MB", + "ND", + ), ( "ORIGINAL", "PRIMARY", From 3bd62043d46afd8d23bed745e60d301e3446c592 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 19:54:48 +0200 Subject: [PATCH 26/39] Fixed bug causing image.data to raise an exception instead of simply returning None for images with no data element. --- src/dicom_parser/image.py | 3 ++- tests/test_image.py | 15 ++++++++------- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/dicom_parser/image.py b/src/dicom_parser/image.py index b62b3242..8ef3d7a8 100644 --- a/src/dicom_parser/image.py +++ b/src/dicom_parser/image.py @@ -574,7 +574,8 @@ def data(self) -> np.ndarray: np.ndarray Pixel data array """ - return self.fix_data() + if self._data is not None: + return self.fix_data() @property def default_relative_path(self) -> Path: diff --git a/tests/test_image.py b/tests/test_image.py index 2b4aa8db..41fd3fcc 100644 --- a/tests/test_image.py +++ b/tests/test_image.py @@ -8,13 +8,9 @@ from dicom_parser.utils.multi_frame.multi_frame import MultiFrame from dicom_parser.utils.siemens.mosaic import Mosaic -from tests.fixtures import ( - TEST_IMAGE_PATH, - TEST_IMAGE_RELATIVE_PATH, - TEST_MULTIFRAME, - TEST_RSFMRI_IMAGE_PATH, - TEST_SIEMENS_DWI_PATH, -) +from tests.fixtures import (TEST_IMAGE_PATH, TEST_IMAGE_RELATIVE_PATH, + TEST_MULTIFRAME, TEST_RSFMRI_IMAGE_PATH, + TEST_SIEMENS_DWI_PATH) class ImageTestCase(TestCase): @@ -257,3 +253,8 @@ def test_data_returned_with_missing_image_type(self): self.fail( f"Exception raised retreiving data for an image with a missing ImageType header field: {e}" # noqa: E501 ) + + def test_image_with_no_data_returns_none(self): + self.image._data = None + value = self.image.data + self.assertIsNone(value) From 2718d0c8fbecc87c6f8d1b95f29eb4dddf462bac Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 20:22:32 +0200 Subject: [PATCH 27/39] Added physiological logs rule. --- src/dicom_parser/utils/bids/sequence_to_bids.py | 13 ++++++------- .../sequence_detector/sequences/mr/mr_sequences.py | 4 ++++ .../sequences/mr/physio/__init__.py | 3 +++ .../sequence_detector/sequences/mr/physio/log.py | 8 ++++++++ .../sequence_detector/sequences/mr/physio/physio.py | 5 +++++ 5 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/physio/__init__.py create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/physio/log.py create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/physio/physio.py diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index b4d55c68..afd98a66 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -1,13 +1,11 @@ """ Definition of the :attr:`SEQUENCE_TO_BIDS` dictionary. """ -from dicom_parser.utils.bids.header_queries import ( - find_irepi_acq, - find_mprage_acq, - find_mprage_ce, - find_phase_encoding, - find_task_name, -) +from dicom_parser.utils.bids.header_queries import (find_irepi_acq, + find_mprage_acq, + find_mprage_ce, + find_phase_encoding, + find_task_name) # Dictionaries (`Dict[str, Union[str, Callable]]``) associating MR sequences # with BIDS key/value pairs. @@ -69,6 +67,7 @@ "dwi": DWI, "dwi_sbref": DWI_SBREF, "dwi_computed": False, + "physio_log": False, } #: Known BIDS field values by modality. SEQUENCE_TO_BIDS = {"Magnetic Resonance": MR_SEQUENCE_TO_BIDS} diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/mr_sequences.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/mr_sequences.py index 2a443156..e2679b19 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/mr_sequences.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/mr_sequences.py @@ -7,9 +7,13 @@ from dicom_parser.utils.sequence_detector.sequences.mr.func import ( MR_FUNCTIONAL_SEQUENCES, ) +from dicom_parser.utils.sequence_detector.sequences.mr.physio import ( + MR_PHYSIOLOGICAL_RULES, +) MR_SEQUENCE_RULES = { **MR_ANATOMICAL_SEQUENCES, **MR_DIFFUSION_SEQUENCES, **MR_FUNCTIONAL_SEQUENCES, + **MR_PHYSIOLOGICAL_RULES, } diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/__init__.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/__init__.py new file mode 100644 index 00000000..252f77ae --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/__init__.py @@ -0,0 +1,3 @@ +from dicom_parser.utils.sequence_detector.sequences.mr.physio.physio import ( + MR_PHYSIOLOGICAL_RULES, +) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/log.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/log.py new file mode 100644 index 00000000..2c2a852a --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/log.py @@ -0,0 +1,8 @@ +PHYSIO_LOG_RULES = [ + { + "key": "ImageType", + "value": [("ORIGINAL", "PRIMARY", "RAWDATA", "PHYSIO")], + "lookup": "exact", + "operator": "any", + }, +] diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/physio.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/physio.py new file mode 100644 index 00000000..ecb6791e --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/physio/physio.py @@ -0,0 +1,5 @@ +from dicom_parser.utils.sequence_detector.sequences.mr.physio.log import ( + PHYSIO_LOG_RULES, +) + +MR_PHYSIOLOGICAL_RULES = {"physio_log": PHYSIO_LOG_RULES} From c420a5ac5b9df7772fcdd8789f3894a1acc262fb Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Thu, 18 Nov 2021 20:32:41 +0200 Subject: [PATCH 28/39] More fixes according to existing database. --- .../sequence_detector/sequences/mr/dwi/diffusion.py | 10 ++++++++++ .../sequence_detector/sequences/mr/func/fieldmap.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index 71a4a5b8..c29b987b 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -26,6 +26,16 @@ "MB", "ND", ), + ( + "ORIGINAL", + "PRIMARY", + "DIFFUSION", + "NONE", + "MB", + "ND", + "NORM", + "MOSAIC", + ), ( "ORIGINAL", "PRIMARY", diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/fieldmap.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/fieldmap.py index 5ae0ce93..46403353 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/fieldmap.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/fieldmap.py @@ -20,7 +20,8 @@ }, { "key": "ScanOptions", - "value": ("PFP", "FS"), + "value": [("PFP", "FS"), "FS"], "lookup": "exact", + "operator": "any", }, ] From a2e02d7adb1ed395f7e416a44e4fe7a7276ddbff Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Fri, 19 Nov 2021 10:34:08 +0200 Subject: [PATCH 29/39] Simplified DWI ImageType rule. --- .../sequences/mr/dwi/diffusion.py | 46 ++----------------- .../sequences/mr/dwi/sbref.py | 4 -- 2 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index c29b987b..aa24929b 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -17,49 +17,9 @@ }, { "key": "ImageType", - "value": [ - ( - "ORIGINAL", - "PRIMARY", - "DIFFUSION", - "NONE", - "MB", - "ND", - ), - ( - "ORIGINAL", - "PRIMARY", - "DIFFUSION", - "NONE", - "MB", - "ND", - "NORM", - "MOSAIC", - ), - ( - "ORIGINAL", - "PRIMARY", - "DIFFUSION", - "NONE", - "MB", - "ND", - "MOSAIC", - ), - ( - "ORIGINAL", - "PRIMARY", - "DIFFUSION", - "NONE", - "ND", - "NORM", - "MOSAIC", - ), - ("ORIGINAL", "PRIMARY", "DIFFUSION", "NONE", "DIS2D", "MOSAIC"), - ("ORIGINAL", "PRIMARY", "DIFFUSION", "NONE", "DIS2D"), - ("ORIGINAL", "PRIMARY", "DIFFUSION", "NONE", "ND", "NORM"), - ], - "lookup": "exact", - "operator": "any", + "value": ["ORIGINAL", "PRIMARY", "DIFFUSION", "NONE"], + "lookup": "in", + "operator": "all", }, ] DWI_RULES_2 = [ diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py index dbf1fe62..9864a66a 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/sbref.py @@ -14,10 +14,6 @@ "value": [ ("ORIGINAL", "PRIMARY", "M", "ND", "MOSAIC"), # ("ORIGINAL", "PRIMARY", "PHASE MAP", "ND"), - # Siemens computed maps, should be handled separately. - # ("DERIVED", "PRIMARY", "DIFFUSION", "ADC", "ND", "NORM"), - # ("DERIVED", "PRIMARY", "DIFFUSION", "FA", "ND", "NORM"), - # ("DERIVED", "PRIMARY", "DIFFUSION", "TRACEW", "ND", "NORM"), ], "lookup": "exact", "operator": "any", From 749ce443631ab589f91b9232bfed19a25229ce38 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Fri, 19 Nov 2021 10:40:22 +0200 Subject: [PATCH 30/39] Generalized DWI computed maps rule. --- .../sequence_detector/sequences/mr/dwi/computed.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py index 47fc428b..e64e17b6 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py @@ -1,14 +1,8 @@ DWI_COMPUTED_RULES = [ { "key": "ImageType", - "value": ["DERIVED", "PRIMARY", "DIFFUSION", "ND"], + "value": ["DERIVED", "PRIMARY", "DIFFUSION"], "lookup": "in", "operator": "all", - }, - { - "key": "ImageType", - "value": ["FA", "ADC", "TRACEW", "TENSOR"], - "lookup": "in", - "operator": "any", - }, + } ] From 9f01c79578f41f3e7c995ace28a6c10a6f40f6fe Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Fri, 19 Nov 2021 10:45:12 +0200 Subject: [PATCH 31/39] Modified BOLD rules to catch SE-EPI fMRI scans from sample dataset. --- .../utils/sequence_detector/sequences/mr/func/bold.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py index 757919fa..c267e766 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py @@ -9,6 +9,7 @@ "value": [ ("Segmented k-Space", "Steady State"), ("Segmented k-Space", "Steady State", "Oversampling Phase"), + ("Segmented k-Space", "Spoiled"), ], "lookup": "exact", "operator": "any", @@ -16,6 +17,7 @@ { "key": "ImageType", "value": [ + ("ORIGINAL", "PRIMARY", "M", "ND", "NORM"), ("ORIGINAL", "PRIMARY", "M", "MB", "ND", "MOSAIC"), ("ORIGINAL", "PRIMARY", "M", "MB", "ND", "NORM", "MOSAIC"), ], From 77722482a83fc1d60db8e068db766651fa7b32a5 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Fri, 19 Nov 2021 10:54:08 +0200 Subject: [PATCH 32/39] Changed DWI computed to DWI derived. --- src/dicom_parser/utils/bids/sequence_to_bids.py | 14 ++++++++------ .../mr/dwi/{computed.py => derived.py} | 2 +- .../sequence_detector/sequences/mr/dwi/dwi.py | 17 +++++++---------- 3 files changed, 16 insertions(+), 17 deletions(-) rename src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/{computed.py => derived.py} (86%) diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index afd98a66..312f1b39 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -1,11 +1,13 @@ """ Definition of the :attr:`SEQUENCE_TO_BIDS` dictionary. """ -from dicom_parser.utils.bids.header_queries import (find_irepi_acq, - find_mprage_acq, - find_mprage_ce, - find_phase_encoding, - find_task_name) +from dicom_parser.utils.bids.header_queries import ( + find_irepi_acq, + find_mprage_acq, + find_mprage_ce, + find_phase_encoding, + find_task_name, +) # Dictionaries (`Dict[str, Union[str, Callable]]``) associating MR sequences # with BIDS key/value pairs. @@ -66,7 +68,7 @@ "func_fieldmap": FUNCTIONAL_FIELDMAP, "dwi": DWI, "dwi_sbref": DWI_SBREF, - "dwi_computed": False, + "dwi_derived": False, "physio_log": False, } #: Known BIDS field values by modality. diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/derived.py similarity index 86% rename from src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py rename to src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/derived.py index e64e17b6..d0ff5073 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/computed.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/derived.py @@ -1,4 +1,4 @@ -DWI_COMPUTED_RULES = [ +DWI_DERIVED_RULES = [ { "key": "ImageType", "value": ["DERIVED", "PRIMARY", "DIFFUSION"], diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py index 811b1433..58611651 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py @@ -1,15 +1,12 @@ -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.computed import ( - DWI_COMPUTED_RULES, -) -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import ( - DWI_RULES, -) -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.sbref import ( - DWI_SBREF_RULES, -) +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.derived import \ + DWI_DERIVED_RULES +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import \ + DWI_RULES +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.sbref import \ + DWI_SBREF_RULES MR_DIFFUSION_SEQUENCES = { "dwi": DWI_RULES, "dwi_sbref": DWI_SBREF_RULES, - "dwi_computed": DWI_COMPUTED_RULES, + "dwi_derived": DWI_DERIVED_RULES, } From d95a06e631e7fd1df58f6bd730b779c5d7a49663 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Sun, 21 Nov 2021 12:03:52 +0200 Subject: [PATCH 33/39] Removed acqisition label in MPARGE BIDS path genration. --- src/dicom_parser/utils/bids/header_queries.py | 18 ------------------ .../utils/bids/sequence_to_bids.py | 2 -- 2 files changed, 20 deletions(-) diff --git a/src/dicom_parser/utils/bids/header_queries.py b/src/dicom_parser/utils/bids/header_queries.py index 5fecaca7..3722f5bc 100644 --- a/src/dicom_parser/utils/bids/header_queries.py +++ b/src/dicom_parser/utils/bids/header_queries.py @@ -25,24 +25,6 @@ def find_mprage_ce(header: dict) -> str: return "corrected" if "NORM" in image_type else "uncorrected" -def find_mprage_acq(header: dict) -> str: - """ - Add the series number to the acq field to prevent duplication in case of - multiple scans in the same session. - - Parameters - ---------- - header : dict - Dictionary containing DICOM's header - - Returns - ------- - str - Series number - """ - return header.get("SeriesNumber") - - def find_irepi_acq(header: dict) -> str: """ Finds correct value for the "acq" field of BIDS specification for IR-EPI diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 312f1b39..89a07a68 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -3,7 +3,6 @@ """ from dicom_parser.utils.bids.header_queries import ( find_irepi_acq, - find_mprage_acq, find_mprage_ce, find_phase_encoding, find_task_name, @@ -45,7 +44,6 @@ MPRAGE = { "data_type": "anat", "ce": find_mprage_ce, - "acq": find_mprage_acq, "suffix": "T1w", } SPGR = {"data_type": "anat", "acq": "spgr", "suffix": "T1w"} From fad532d242b2186c4b5abd903e4bbf3f9b490c83 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Sun, 21 Nov 2021 12:41:11 +0200 Subject: [PATCH 34/39] Fixed expected MPRAGE BIDS destination. --- tests/utils/bids/fixtures.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils/bids/fixtures.py b/tests/utils/bids/fixtures.py index d538d89d..a688bc07 100644 --- a/tests/utils/bids/fixtures.py +++ b/tests/utils/bids/fixtures.py @@ -3,7 +3,7 @@ """ BIDS_PATH = { "mprage": { - "corrected": "sub-110/ses-201805011221/anat/sub-110_ses-201805011221_acq-3_ce-corrected_T1w" + "corrected": "sub-110/ses-201805011221/anat/sub-110_ses-201805011221_ce-corrected_T1w" } } From 306fd1bc1bf8d0419b8160957964efe081bcf5b7 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Mon, 22 Nov 2021 17:40:30 +0200 Subject: [PATCH 35/39] Changed DWI SBRef to assign fmap epi path. --- src/dicom_parser/utils/bids/sequence_to_bids.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 89a07a68..5f3ae0b0 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -22,10 +22,10 @@ "suffix": "dwi", } DWI_SBREF = { - "data_type": "dwi", + "data_type": "fmap", "acq": "dwi", "dir": find_phase_encoding, - "suffix": "sbref", + "suffix": "epi", } FLAIR = {"data_type": "anat", "suffix": "FLAIR"} FUNCTIONAL_FIELDMAP = { From b9cc8c21a1b90460fb1b10f1295a5f5ea1ff8862 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Tue, 23 Nov 2021 20:14:06 +0200 Subject: [PATCH 36/39] Added DWI fieldmap sequence detection. --- src/dicom_parser/header.py | 15 ++++++++-- .../sequences/mr/dwi/diffusion.py | 5 ++++ .../sequence_detector/sequences/mr/dwi/dwi.py | 21 +++++++++----- .../sequences/mr/dwi/fieldmap.py | 29 +++++++++++++++++++ 4 files changed, 60 insertions(+), 10 deletions(-) create mode 100644 src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index 62d8dbcb..bda1426e 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -49,6 +49,7 @@ class Header: "SeriesDescription", "ImageType", "ScanOptions", + "phase_encoding_direction", ] } @@ -65,7 +66,7 @@ class Header: #: Dictionary used to convert in-plane phase encoding direction to the #: NIfTI appropriate equivalent. PHASE_ENCODING_DIRECTION: Dict[str, str] = {"ROW": "i", "COL": "j"} - PHASE_ENCODING_SIGN: Dict[int, str] = {0: "-", 1: ""} + PHASE_ENCODING_SIGN: Dict[int, str] = {0: "", 1: "-"} #: Infer image plane from the rounded ImageOrientationPatient value. #: Based on https://stackoverflow.com/a/56670334/4416932 @@ -451,8 +452,16 @@ def get_parsed_value(self, tag_or_keyword) -> Any: Any Parsed data element value """ - data_element = self.get_data_element(tag_or_keyword) - return data_element.value + try: + data_element = self.get_data_element(tag_or_keyword) + except KeyError: + value = getattr(self, tag_or_keyword) + try: + return value() + except TypeError: + return value + else: + return data_element.value def get_private_tag(self, keyword: str) -> tuple: """ diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py index aa24929b..0b72676e 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/diffusion.py @@ -21,6 +21,11 @@ "lookup": "in", "operator": "all", }, + { + "key": "phase_encoding_direction", + "value": "-", + "lookup": "in", + }, ] DWI_RULES_2 = [ { diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py index 58611651..d0326c24 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/dwi.py @@ -1,12 +1,19 @@ -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.derived import \ - DWI_DERIVED_RULES -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import \ - DWI_RULES -from dicom_parser.utils.sequence_detector.sequences.mr.dwi.sbref import \ - DWI_SBREF_RULES +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.derived import ( + DWI_DERIVED_RULES, +) +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.diffusion import ( + DWI_RULES, +) +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.fieldmap import ( + DWI_FIELDMAP, +) +from dicom_parser.utils.sequence_detector.sequences.mr.dwi.sbref import ( + DWI_SBREF_RULES, +) MR_DIFFUSION_SEQUENCES = { "dwi": DWI_RULES, - "dwi_sbref": DWI_SBREF_RULES, "dwi_derived": DWI_DERIVED_RULES, + "dwi_fieldmap": DWI_FIELDMAP, + "dwi_sbref": DWI_SBREF_RULES, } diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py new file mode 100644 index 00000000..61839da4 --- /dev/null +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/dwi/fieldmap.py @@ -0,0 +1,29 @@ +DWI_FIELDMAP = [ + { + "key": "ScanningSequence", + "value": "Echo Planar", + "lookup": "exact", + }, + { + "key": "SequenceVariant", + "value": [ + ("Segmented k-Space", "Steady State"), + ("Segmented k-Space", "Spoiled"), + ("Segmented k-Space", "Spoiled", "Oversampling Phase"), + ("Segmented k-Space", "Steady State", "Oversampling Phase"), + ], + "lookup": "exact", + "operator": "any", + }, + { + "key": "ImageType", + "value": ["ORIGINAL", "PRIMARY", "DIFFUSION", "NONE"], + "lookup": "in", + "operator": "all", + }, + { + "key": "phase_encoding_direction", + "value": "-", + "lookup": "not in", + }, +] From 8ba9aa396f4a255d3dec4ca88c8734f4af39bceb Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Wed, 24 Nov 2021 15:02:44 +0200 Subject: [PATCH 37/39] Fixes to support DWI fieldmap generation. --- src/dicom_parser/header.py | 15 ++++++++++----- src/dicom_parser/utils/bids/header_queries.py | 2 +- src/dicom_parser/utils/bids/sequence_to_bids.py | 11 ++++++++--- .../sequence_detector/sequences/mr/func/bold.py | 5 +++++ .../sequence_detector/sequences/mr/func/sbref.py | 5 +++++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/dicom_parser/header.py b/src/dicom_parser/header.py index bda1426e..8f363771 100644 --- a/src/dicom_parser/header.py +++ b/src/dicom_parser/header.py @@ -454,12 +454,17 @@ def get_parsed_value(self, tag_or_keyword) -> Any: """ try: data_element = self.get_data_element(tag_or_keyword) - except KeyError: - value = getattr(self, tag_or_keyword) + except KeyError as e: + # Look for method or property. try: - return value() - except TypeError: - return value + value = getattr(self, tag_or_keyword) + except AttributeError: + raise KeyError(str(e)) + else: + try: + return value() + except TypeError: + return value else: return data_element.value diff --git a/src/dicom_parser/utils/bids/header_queries.py b/src/dicom_parser/utils/bids/header_queries.py index 3722f5bc..7e3865af 100644 --- a/src/dicom_parser/utils/bids/header_queries.py +++ b/src/dicom_parser/utils/bids/header_queries.py @@ -108,7 +108,7 @@ def find_phase_encoding(header: dict) -> str: """ try: phase_encoding = header["phase_encoding_direction"] - return "REV" if phase_encoding.endswith("-") else "FWD" + return "FWD" if phase_encoding.endswith("-") else "REV" except (KeyError, AttributeError): try: description = header.get("ProtocolName").lower() diff --git a/src/dicom_parser/utils/bids/sequence_to_bids.py b/src/dicom_parser/utils/bids/sequence_to_bids.py index 5f3ae0b0..5843d117 100644 --- a/src/dicom_parser/utils/bids/sequence_to_bids.py +++ b/src/dicom_parser/utils/bids/sequence_to_bids.py @@ -22,10 +22,14 @@ "suffix": "dwi", } DWI_SBREF = { - "data_type": "fmap", - "acq": "dwi", + "data_type": "dwi", "dir": find_phase_encoding, - "suffix": "epi", + "suffix": "sbref", +} +DWI_FIELDMAP = { + "data_type": "dwi", + "dir": find_phase_encoding, + "suffix": "dwi", } FLAIR = {"data_type": "anat", "suffix": "FLAIR"} FUNCTIONAL_FIELDMAP = { @@ -65,6 +69,7 @@ "func_sbref": FUNCTIONAL_SBREF, "func_fieldmap": FUNCTIONAL_FIELDMAP, "dwi": DWI, + "dwi_fieldmap": DWI_FIELDMAP, "dwi_sbref": DWI_SBREF, "dwi_derived": False, "physio_log": False, diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py index c267e766..9bcd0b7e 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py @@ -24,6 +24,11 @@ "lookup": "exact", "operator": "any", }, + { + "key": "ScanOptions", + "value": ("PFP", "FS"), + "lookup": "exact", + }, ] BOLD_RULES_2 = [ { diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py index 0923d141..c7a92b1c 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/sbref.py @@ -22,4 +22,9 @@ "lookup": "exact", "operator": "any", }, + { + "key": "ScanOptions", + "value": ("PFP", "FS"), + "lookup": "exact", + }, ] From 4dde7f3d6aa4185455e07859708f9c136aef2406 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Wed, 24 Nov 2021 15:30:19 +0200 Subject: [PATCH 38/39] Fixed tests. --- .../utils/sequence_detector/sequences/mr/func/bold.py | 3 ++- tests/test_header.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py index 9bcd0b7e..62fa93ff 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/func/bold.py @@ -26,8 +26,9 @@ }, { "key": "ScanOptions", - "value": ("PFP", "FS"), + "value": [("PFP", "FS"), "FS"], "lookup": "exact", + "operator": "any", }, ] BOLD_RULES_2 = [ diff --git a/tests/test_header.py b/tests/test_header.py index 5cd07df7..5d79511d 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -342,7 +342,7 @@ def test_unknown_value_representation(self): def test_get_phase_encoding_direction(self): value = self.dwi_header.get_phase_encoding_direction() - expected = "i" + expected = "i-" self.assertEqual(value, expected) def test_get_phase_encoding_direction_with_none(self): From 48cbaef3b22ce8952ddb69dddc277fd1095d64a4 Mon Sep 17 00:00:00 2001 From: Zvi Baratz Date: Fri, 14 Jan 2022 13:23:42 +0200 Subject: [PATCH 39/39] Tiny fix to TIRM sequence. --- .../utils/sequence_detector/sequences/mr/anat/tirm.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py index 4d6ff2df..bd0946d1 100644 --- a/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py +++ b/src/dicom_parser/utils/sequence_detector/sequences/mr/anat/tirm.py @@ -16,7 +16,7 @@ }, { "key": "ScanOptions", - "value": ["IR"], - "lookup": "in", + "value": ("IR", "SAT1"), + "lookup": "exact", }, ]