Skip to content

Commit

Permalink
Classification of multi echo (#8)
Browse files Browse the repository at this point in the history
* ENH: classify multi-echo data captured with hypermepi sequence

* FIX: typo fix

* ENH: Ignore path when setting custom sequence

* MAINT: Tidy code.

* MAINT: Bump version to 3.3.0
  • Loading branch information
lmperry authored Oct 14, 2020
1 parent ee43177 commit ff02e54
Show file tree
Hide file tree
Showing 4 changed files with 47 additions and 43 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# scitran/dicom-mr-classifier
# stanfordcni/cni-dicom-mr-classifier
#
# Use pyDicom to classify raw DICOM data (zip) from Siemens, GE or Philips.
#
Expand Down
22 changes: 11 additions & 11 deletions classification_from_label.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
#!/usr/bin/env python
'''
"""
Infer acquisition classification by parsing the description label.
'''
"""

import re


def feature_check(label):
'''Check the label for a list of features.'''
"""Check the label for a list of features."""

feature_list = ['2D', 'AAscout', 'Spin-Echo', 'Gradient-Echo',
'EPI', 'WASSR', 'FAIR', 'FAIREST', 'PASL', 'EPISTAR',
Expand All @@ -29,7 +29,7 @@ def feature_check(label):


def measurement_check(label):
'''Check the label for a list of measurements.'''
"""Check the label for a list of measurements."""

measurement_list = ['MRA', 'CEST', 'T1rho', 'SVS', 'CSI', 'EPSI', 'BOLD',
'Phoenix','B0', 'B1', 'T1', 'T2', 'T2*', 'PD', 'MT',
Expand All @@ -39,20 +39,20 @@ def measurement_check(label):


def intent_check(label):
'''Check the label for a list of intents.'''
"""Check the label for a list of intents."""

intent_list = [ 'Localizer', 'Shim', 'Calibration', 'Fieldmap', 'Structural',
'Functional', 'Screenshot', 'Non-Image', 'Spectroscopy' ]

return _find_matches(label, intent_list)


def _find_matches(label, list):
def _find_matches(label, inlist):
"""For a given list find those entries that match a given label."""

matches = []

for l in list:
for l in inlist:
regex = _compile_regex(l)
if regex.findall(label):
matches.append(l)
Expand All @@ -79,8 +79,8 @@ def is_anatomy_t1(label):
regexes = [
re.compile('t1', re.IGNORECASE),
re.compile('t1w', re.IGNORECASE),
re.compile('(?=.*3d anat)(?![inplane])', re.IGNORECASE),
re.compile('(?=.*3d)(?=.*bravo)(?![inplane])', re.IGNORECASE),
re.compile('(?=.*3d anat)(?!inplane)', re.IGNORECASE),
re.compile('(?=.*3d)(?=.*bravo)(?!inplane)', re.IGNORECASE),
re.compile('spgr', re.IGNORECASE),
re.compile('tfl', re.IGNORECASE),
re.compile('mprage', re.IGNORECASE),
Expand Down Expand Up @@ -360,15 +360,15 @@ def infer_classification(label):
elif is_perfusion(label):
classification['Measurement'] = ['Perfusion']
elif is_susceptability(label):
classification['Measurement'] = ['Susceptability']
classification['Measurement'] = ['Susceptibility']
elif is_spectroscopy(label):
classification['Intent'] = ['Spectroscopy']
elif is_phase_map(label):
classification['Custom'] = ['Phase Map']
elif is_screenshot(label):
classification['Intent'] = ['Screenshot']
else:
print label.strip('\n') + ' --->>>> unknown'
print(label.strip('\n') + ' --->>>> unknown')


# Add features to classification
Expand Down
62 changes: 33 additions & 29 deletions dicom-mr-classifier.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ def get_session_label(dcm):
"""
Switch on manufacturer and either pull out the StudyID or the StudyInstanceUID
"""
session_label = ''
if ( dcm.get('Manufacturer') and (dcm.get('Manufacturer').find('GE') != -1 or dcm.get('Manufacturer').find('Philips') != -1 ) and dcm.get('StudyID')):
if dcm.get('Manufacturer') and (dcm.get('Manufacturer').find('GE') != -1 or dcm.get('Manufacturer').find('Philips') != -1) and dcm.get('StudyID'):
session_label = dcm.get('StudyID')
else:
session_label = dcm.get('StudyInstanceUID')
Expand Down Expand Up @@ -282,7 +281,7 @@ def get_csa_header(dcm):
value = raw_csa_header['tags'][tag]['items']
if len(value) == 1:
value = value[0]
if type(value) == str and ( len(value) > 0 and len(value) < 1024 ):
if type(value) == str and (0 < len(value) < 1024):
header[format_string(tag)] = format_string(value)
else:
header[format_string(tag)] = assign_type(value)
Expand All @@ -293,7 +292,7 @@ def get_csa_header(dcm):

def get_classification_from_string(value):
"""
Attempt to generate classificatoin from value string using custom context.
Attempt to generate classification from value string using custom context.
"""
result = {}

Expand Down Expand Up @@ -366,54 +365,58 @@ def get_custom_classification(label, config_file):

return None

def get_psd_classification(PSD, SERIES_DESCRIPTION):
def get_psd_classification(psd, series_description):
"""
Determine classification from the PSD
"""
classification = {}
# If this is from one of the muxarcepi sequences (CNI specific), then
# we use our knowledge of the sequence to classify the file.
if PSD.startswith('muxarcepi'):
if PSD.startswith('muxarcepi2'):
if psd.startswith('muxarcepi'):
if psd.startswith('muxarcepi2'):
classification['Measurement'] = ['Diffusion']
classification['Intent'] = ['Structural']
elif PSD.startswith('muxarcepi_IR'):
elif psd.startswith('muxarcepi_IR'):
classification['Measurement'] = ['T1']
classification['Intent'] = ['Structural']
classification['Features'] = ['Quantitative']
elif PSD == 'muxarcepi_me':
elif psd == 'muxarcepi_me':
classification['Measurement'] = ['T2*']
classification['Intent'] = ['Functional']
classification['Features'] = ['Multi-Echo']
elif PSD == 'muxarcepi' and SERIES_DESCRIPTION and SERIES_DESCRIPTION.find('fieldmap') == -1:
elif psd == 'muxarcepi' and series_description and series_description.find('fieldmap') == -1:
classification['Measurement'] = ['T2*']
classification['Intent'] = ['Functional']

# Use priors to determine classification for certain sequences
elif PSD == 'sprlio':
elif psd == 'sprlio':
classification['Measurement'] = ['T2*']
classification['Intent'] = ['Functional']
elif PSD == 'sprl_hos':
elif psd == 'sprl_hos':
classification['Intent'] = ['Shim']
elif PSD == 'spep_cni':
elif psd == 'spep_cni':
classification['Measurement'] = ['ASL']
classification['Intent'] = ['Functional']
elif PSD == 'sprt':
elif psd == 'sprt':
classification['Measurement'] = ['B0']
classification['Intent'] = ['Fieldmap']
elif PSD.startswith('nfl') or PSD.startswith('special') or PSD.startswith('probe-mega') or PSD.startswith('imspecial') or PSD.startswith('gaba'):
elif psd.startswith('nfl') or psd.startswith('special') or psd.startswith('probe-mega') or psd.startswith('imspecial') or psd.startswith('gaba'):
classification['Intent'] = ['Spectroscopy']
elif "hypermepi" in psd:
classification['Measurement'] = ['T2*']
classification['Intent'] = ['Functional']
classification['Features'] = ['Multi-Echo']

# Add the PSD to the custom classifications
custom = {'Custom': [PSD]}
custom = {'Custom': [psd]}
if isinstance(classification, dict):
classification.update(custom)
else:
classification = custom

# If there was no measuremet or Intent then get the class from the label.
if not classification.has_key('Measurement') or not classification.has_key('Intent'):
class2 = classification_from_label.infer_classification(SERIES_DESCRIPTION)
class2 = classification_from_label.infer_classification(series_description)
if class2:
classification.update(class2)

Expand All @@ -440,10 +443,10 @@ def dicom_classify(zip_file_path, outbase, timezone, config_file=None):
# Extract the last file in the zip to /tmp/ and read it
dcm = []
if zipfile.is_zipfile(zip_file_path):
zip = zipfile.ZipFile(zip_file_path)
num_files = len(zip.namelist())
_zip = zipfile.ZipFile(zip_file_path)
num_files = len(_zip.namelist())
for n in range((num_files -1), -1, -1):
dcm_path = zip.extract(zip.namelist()[n], '/tmp')
dcm_path = _zip.extract(_zip.namelist()[n], '/tmp')
if os.path.isfile(dcm_path):
try:
log.info('reading %s' % dcm_path)
Expand All @@ -470,11 +473,10 @@ def dicom_classify(zip_file_path, outbase, timezone, config_file=None):
os.sys.exit(1)

# Build metadata
metadata = {}
metadata = {'session': {}}

# Session metadata
metadata['session'] = {}
session_timestamp, acquisition_timestamp = get_timestamp(dcm, timezone);
session_timestamp, acquisition_timestamp = get_timestamp(dcm, timezone)
if session_timestamp:
metadata['session']['timestamp'] = session_timestamp
if hasattr(dcm, 'OperatorsName') and dcm.get('OperatorsName'):
Expand Down Expand Up @@ -515,10 +517,9 @@ def dicom_classify(zip_file_path, outbase, timezone, config_file=None):
pass

# File classification
dicom_file = {}
dicom_file['name'] = os.path.basename(zip_file_path)
dicom_file['modality'] = format_string(dcm.get('Modality', 'MR'))
dicom_file['classification'] = {}
dicom_file = {'name': os.path.basename(zip_file_path),
'modality': format_string(dcm.get('Modality', 'MR')),
'classification': {}}

# Acquisition metadata
metadata['acquisition'] = {}
Expand All @@ -530,7 +531,7 @@ def dicom_classify(zip_file_path, outbase, timezone, config_file=None):
dicom_file['info'] = get_dicom_header(dcm)

if dicom_file['info'].has_key('psd'):
PSD = dicom_file['info']['psd'].lower()
PSD = dicom_file['info']['psd'].lower().split('/')[-1]
else:
PSD = ''

Expand All @@ -543,23 +544,26 @@ def dicom_classify(zip_file_path, outbase, timezone, config_file=None):
metadata['acquisition']['label'] = series_desc

# Classification
classification = dict()
if series_desc:
classification = get_custom_classification(series_desc, config_file)
log.info('Custom classification from config: %s', classification)
# Add the PSD to the custom class
if classification and PSD:
#TODO: Check if custom already exists
custom = {'Custom': [PSD]}
if isinstance(classification, dict):
classification.update(custom)
else:
classification = custom

if not classification and PSD:
classification = get_psd_classification(PSD, series_desc)
log.info('Custom classification from PSD: %s', classification)

if not classification and series_desc:
classification = classification_from_label.infer_classification(series_desc)
log.info('Inferred classification from label: %s', classification)

if classification:
dicom_file['classification'] = classification

Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"source": "https://github.com/cni/cni-dicom-mr-classifier",
"license": "Apache-2.0",
"flywheel": "0",
"version": "3.2.1",
"version": "3.3.0",
"custom": {
"gear-builder": {
"category": "converter",
"image": "stanfordcni/cni-dicom-mr-classifier:3.2.1"
"image": "stanfordcni/cni-dicom-mr-classifier:3.3.0"
},
"flywheel": {
"suite": "Stanford CNI"
Expand Down

0 comments on commit ff02e54

Please sign in to comment.