Skip to content

Commit

Permalink
Merge pull request #220 from DominikaZ/csa-header
Browse files Browse the repository at this point in the history
Add CSA header support using nibabel or dicom-parser
  • Loading branch information
marcelzwiers authored Jan 28, 2024
2 parents 269cff4 + 7214fa0 commit 14a6f22
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 10 deletions.
39 changes: 32 additions & 7 deletions bidscoin/bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from pathlib import Path
from typing import List, Set, Tuple, Union
from nibabel.parrec import parse_PAR_header
from nibabel.nicom import csareader
from pydicom import dcmread, fileset, datadict
from importlib.util import find_spec
if find_spec('bidscoin') is None:
Expand Down Expand Up @@ -581,7 +582,7 @@ def get_dicomfield(tagname: str, dicomfile: Path) -> Union[str, int]:
try: # Try Pydicom's hexadecimal tag number first
value = eval(f"dicomdata[{tagname}].value") # NB: This may generate e.g. UserWarning: Invalid value 'filepath' used with the 'in' operator: must be an element tag as a 2-tuple or int, or an element keyword
except (NameError, KeyError, SyntaxError):
value = dicomdata.get(tagname) if tagname in dicomdata else '' # Then try and see if it is an attribute name. NB: Do not use dicomdata.get(tagname, '') to avoid using its class attributes (e.g. 'filename')
value = dicomdata.get(tagname,'') if tagname in dicomdata else '' # Then try and see if it is an attribute name. NB: Do not use dicomdata.get(tagname, '') to avoid using its class attributes (e.g. 'filename')

# Try a recursive search
if not value and value != 0:
Expand All @@ -590,7 +591,35 @@ def get_dicomfield(tagname: str, dicomfile: Path) -> Union[str, int]:
value = elem.value
break

if not value and value!=0 and 'Modality' not in dicomdata:
# Try reading the Siemens CSA header. For V* versions the CSA header tag is (0029,1020), for XA versions (0021,1019). TODO: see if dicom_parser is supporting this
if not value and value != 0 and is_dicomfile_siemens(dicomfile):

if find_spec('dicom_parser'):
from dicom_parser import Image

for csa in ('CSASeriesHeaderInfo', 'CSAImageHeaderInfo'):
value = value if (value or value==0) else Image(dicomfile).header.get(csa)
for csatag in tagname.split('.'): # E.g. CSA tagname = 'SliceArray.Slice.instance_number.Position.Tra'
if isinstance(value, dict): # Final CSA header attributes in dictionary of dictionaries
value = value.get(csatag, '')
if 'value' in value: # Normal CSA (i.e. not MrPhoenixProtocol)
value = value['value']
if value != 0:
value = str(value or '')

else:

for type in ('Series', 'Image'):
value = value if (value or value==0) else csareader.get_csa_header(dicomdata, type)['tags']
for csatag in tagname.split('.'): # NB: Currently MrPhoenixProtocol is not supported
if isinstance(value, dict): # Final CSA header attributes in dictionary of dictionaries
value = value.get(csatag, {}).get('items', '')
if isinstance(value, list) and len(value) == 1:
value = value[0]
if value != 0:
value = str(value or '')

if not value and value != 0 and 'Modality' not in dicomdata:
raise ValueError(f"Missing mandatory DICOM 'Modality' field in: {dicomfile}")

# XA-30 enhanced DICOM hack: Catch missing EchoNumbers from ice-dims
Expand All @@ -605,11 +634,7 @@ def get_dicomfield(tagname: str, dicomfile: Path) -> Union[str, int]:

except Exception as dicomerror:
LOGGER.warning(f"Could not read {tagname} from {dicomfile}\n{dicomerror}")
try:
value = parse_x_protocol(tagname, dicomfile)
except Exception as dicomerror:
LOGGER.warning(f'Could not parse {tagname} from {dicomfile}\n{dicomerror}')
value = ''
value = ''

# Cast the dicom data type to int or str (i.e. to something that yaml.dump can handle)
if isinstance(value, int):
Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,11 @@ dependencies = ['pandas',
dcm2niix2bids = ['dcm2niix']
spec2nii2bids = ['spec2nii >= 0.6.1']
deface = ['pydeface']
siemens = ['dicom-parser']
# pet2bids = ['pypet2bids >= 1.0.12']
# phys2bidscoin = ['bioread >= 1.0.5', 'pymatreader >= 0.0.24', 'phys2bids >= 2.0.0, < 3.0.0']
all = ['bidscoin[dcm2niix2bids,spec2nii2bids,deface]'] # + pet2bids + phys2bidscoin
dev = ['bidscoin[spec2nii2bids,deface]', 'tox', 'pytest', 'jsonschema', 'sphinx-rtd-theme', 'myst-parser'] # + pet2bids + phys2bidscoin
all = ['bidscoin[dcm2niix2bids,spec2nii2bids,deface,siemens]'] # + pet2bids + phys2bidscoin
dev = ['bidscoin[spec2nii2bids,deface,siemens]', 'tox', 'pytest', 'jsonschema', 'sphinx-rtd-theme', 'myst-parser'] # + pet2bids + phys2bidscoin

[project.urls]
documentation = 'https://bidscoin.readthedocs.io'
Expand Down
35 changes: 34 additions & 1 deletion tests/test_bids.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ def dcm_file():
return Path(get_testdata_file('MR_small.dcm'))


@pytest.fixture(scope='module')
def dcm_file_csa():
return Path(data_path)/'1.dcm'


@pytest.fixture(scope='module')
def dicomdir():
return Path(get_testdata_file('DICOMDIR'))
Expand Down Expand Up @@ -56,7 +61,7 @@ def test_properties(self, datasource):
assert datasource.properties( 'filepath:.*/(.*?)_files/.*') == 'test' # path = [..]/pydicom/data/test_files/MR_small.dcm'
assert datasource.properties(r'filename:MR_(.*?)\.dcm') == 'small'
assert datasource.properties( 'filesize') == '9.60 kB'
assert datasource.properties( 'nrfiles') == 76
assert datasource.properties( 'nrfiles') in (75,76) # Depends on the pydicom version

def test_attributes(self, datasource, extdatasource):
assert datasource.attributes(r'PatientName:.*\^(.*?)1') == 'MR' # PatientName = 'CompressedSamples^MR1'
Expand Down Expand Up @@ -118,6 +123,34 @@ def test_get_datasource(dicomdir):
assert datasource.dataformat == 'DICOM'


def test_get_dicomfield(dcm_file_csa):

# -> Standard DICOM
value = bids.get_dicomfield('SeriesDescription', dcm_file_csa)
assert value == 'CBU_DTI_64D_1A'

# -> CSA Series header
value = bids.get_dicomfield('PhaseGradientAmplitude', dcm_file_csa)
assert value == '0.0'

# -> CSA Image header
value = bids.get_dicomfield('ImaCoilString', dcm_file_csa)
assert value == 'T:HEA;HEP'

value = bids.get_dicomfield('B_matrix', dcm_file_csa)
assert value == ''

# -> CSA MrPhoenixProtocol
value = bids.get_dicomfield('MrPhoenixProtocol.tProtocolName', dcm_file_csa)
assert value == 'CBU+AF8-DTI+AF8-64D+AF8-1A'

value = bids.get_dicomfield('MrPhoenixProtocol.sDiffusion', dcm_file_csa)
assert value == "{'lDiffWeightings': 2, 'alBValue': [None, 1000], 'lNoiseLevel': 40, 'lDiffDirections': 64, 'ulMode': 256}"

value = bids.get_dicomfield('MrPhoenixProtocol.sProtConsistencyInfo.tBaselineString', dcm_file_csa)
assert value == 'N4_VB17A_LATEST_20090307'


@pytest.mark.parametrize('template', bcoin.list_plugins()[1])
def test_load_check_template(template):

Expand Down

0 comments on commit 14a6f22

Please sign in to comment.