Skip to content

Commit

Permalink
Automatic conversion first pass. (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
wtclarke authored Feb 7, 2023
1 parent 79d7635 commit 1f13235
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 19 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
This document contains the Spec2nii release history in reverse chronological order.

0.6.4 (Tuesday 7th February 2023)
---------------------------------
- Added first pass at new `spec2nii auto` feature with automatic conversion for some formats.

0.6.3 (Monday 6th February 2023)
--------------------------------
- Added handling for GE presscsi and probe-s sequences.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ By default, spec2nii generates NIfTI files using the NIfTI-2 header format. Use

Manual overrides can be provided for incorrectly interpreted required header fields, namely SpectrometerFrequency, ResonantNucleus and dwell-time, by using the `--override_frequency`, `--override_nucleus`, and `--override_dwelltime` command line options.

### Automatic detection
`spec2nii auto FILE` will attempt an automatic conversion of the following formats: Twix, RDA, SPAR/SDAT, GE p-file, DICOM. Note that many features of the individual converters are not implemented in this automatic pathway. This feature should be regarded as somewhat experimental. For finer-grained control see the specific subcommands listed below.

### Twix
Call `spec2nii twix -v FILE` to view a list of contained MDH flags. -m can be used to specify which multi-raid file to convert if used on VE data.

Expand Down
117 changes: 98 additions & 19 deletions spec2nii/spec2nii.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,5 @@
""" spec2nii - tool for conversion of various MRS data formats to NIFTI format.
spec2nii converts the following formats to NIFTI files.
Supporting SVS:
Siemens "Twix" .dat format
Siemens DICOM
Philips SPAR/SDAT files
GE p-files
UIH DICOM
LCModel RAW
jMRUI text
Plain text
Supporting CSI/MRSI:
Siemens DICOM
UIH DICOM
This module contains the main class to be called as a script (through the main function).
Author: William Clarke <[email protected]>
Expand Down Expand Up @@ -65,6 +50,12 @@ def add_common_parameters(subparser):
subparser.add_argument('--verbose', action='store_true')
return subparser

# Auto subcommand - heuristic ID of file type
parser_auto = subparsers.add_parser('auto', help='Attempt automatic identification and conversion.')
parser_auto.add_argument('file', help='file to convert', type=Path)
parser_auto = add_common_parameters(parser_auto)
parser_auto.set_defaults(func=self.auto)

# Handle twix subcommand
parser_twix = subparsers.add_parser('twix', help='Convert from Siemens .dat twix format.')
parser_twix.add_argument('file', help='file to convert', type=Path)
Expand All @@ -91,10 +82,10 @@ def add_common_parameters(subparser):
parser_dicom.set_defaults(func=self.dicom)

# Handle rda subcommand
parser_dicom = subparsers.add_parser('rda', help='Convert from Siemens spectroscopy .rda format.')
parser_dicom.add_argument('file', help='file to convert', type=Path)
parser_dicom = add_common_parameters(parser_dicom)
parser_dicom.set_defaults(func=self.rda)
parser_rda = subparsers.add_parser('rda', help='Convert from Siemens spectroscopy .rda format.')
parser_rda.add_argument('file', help='file to convert', type=Path)
parser_rda = add_common_parameters(parser_rda)
parser_rda.set_defaults(func=self.rda)

# Handle UIH DICOM subcommand
parser_uih_dicom = subparsers.add_parser('uih', help='Convert from UIH DICOM format.')
Expand Down Expand Up @@ -376,6 +367,94 @@ def validate_write(self, verbose):
else:
raise self.NIfTIMRSWriteError(f'Output {out.name} in {out.parent} not found!')

# Attempt automatic file type identification
def auto(self, args):
"""Attempt automatic file type identification and conversion
Currently handles: Twix, RDA, SPAR/SDAT, GE pfile, DICOM
"""
from warnings import warn
warn('Automatic conversion is an experimental feature. Please verify the output carefully.')

# Twix format - ID by extension
if args.file.suffix.lower() == '.dat':
print("Attempting conversion as Siemens Twix (.dat), evalinfo = 'image'")
setattr(args, 'evalinfo', 'image')
setattr(args, 'quiet', False)
setattr(args, 'view', False)
for idx in range(5, 8):
setattr(args, f'dim{idx}', None)
setattr(args, f'tag{idx}', None)
setattr(args, 'remove_os', False)
self.twix(args)

# Siemens RDA format - ID by extension
elif args.file.suffix.lower() == '.rda':
print('Attempting conversion as Siemens RDA (.rda)')
self.rda(args)

# Philips SPAR/SDAT format - ID by extension(s)
elif args.file.suffix.lower() in ('.spar', '.sdat'):
print('Attempting conversion as Philips SPAR/SDAT pair')
if args.file.with_suffix('.SPAR').is_file()\
and args.file.with_suffix('.SDAT').is_file():
setattr(args, 'spar', args.file.with_suffix('.SPAR'))
setattr(args, 'sdat', args.file.with_suffix('.SDAT'))
else:
raise Spec2niiError(
'Unable to find both files in SPAR/SDAT pair.'
'Please use manual selection of subcomands. '
'e.g. spec2nii philips ...')
setattr(args, 'tags', ["DIM_DYN", None, None])
setattr(args, 'shape', None)
setattr(args, 'special', None)
self.philips(args)

# Philips DATA/LIST - Reject bcause of unknown aux file format.
elif args.file.suffix.lower() in ('.data', '.list'):
raise Spec2niiError(
'Automatic conversion not setup for data/list conversion. '
'Please use manual selection of subcomand. '
'e.g. spec2nii philips_dl ...')

# GE pfile (.7) format - ID by extension
elif args.file.suffix.lower() == '.7':
self.ge(args)

# If no matches assume DICOM - ID by loading file
else:
import pydicom as pdcm
try:
if args.file.is_dir():
files_in = \
sorted(args.file.rglob('*.IMA')) + \
sorted(args.file.rglob('*.ima')) + \
sorted(args.file.rglob('*.dcm'))
file = pdcm.read_file(files_in[0])
else:
file = pdcm.read_file(args.file)

manufacturer = file.Manufacturer
setattr(args, 'tag', None)
if manufacturer.lower() == 'siemens':
print('Attempting conversion as Siemens DICOM.')
setattr(args, 'voi', False)
self.dicom(args)
elif manufacturer.lower() == 'uih':
print('Attempting conversion as UIH DICOM.')
self.uih_dicom(args)
elif manufacturer.lower() == 'philips medical systems':
print('Attempting conversion as Philips DICOM.')
self.philips_dicom(args)
else:
raise Spec2niiError(f'Unknown DICOM manufacturer {manufacturer}.')
# No sucessful ID as DICOM - fail at automatic load.
except pdcm.errors.InvalidDicomError:
raise Spec2niiError(
'Unable to automatically identify file type. '
'Please use manual selection of subcomands. '
'e.g. spec2nii twix ..., spec2nii philips ...')

# Start of the specific format handling functions.
# Siemens twix (.dat) format
def twix(self, args):
Expand Down
113 changes: 113 additions & 0 deletions tests/test_auto.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'''Tests automatic conversion routines.
Copyright William Clarke, University of Oxford 2023
Subject to the BSD 3-Clause License.
'''

import subprocess
from pathlib import Path

test_base = Path(__file__).parent / 'spec2nii_test_data'
testdata = {
'twix': test_base / 'Siemens' / 'VEData' / 'Twix' / 'meas_MID00242_FID62747_svs_se_iso_tra_sat.dat',
'rda': test_base / 'Siemens' / 'XAData' / 'XA20' / 'rda' / 'spct_002.MR.MRI-LAB Test_Dir.5.1.114540.rda',
'spar': test_base / 'philips' / 'P1' / 'SV_PRESS_sh_6_2_raw_act.SPAR',
'data_list': test_base / 'philips' / 'hyper' / 'raw_226.list',
'ge_p': test_base / 'ge' / 'pFiles' / 'svMRS' / 'P03072.7',
'dicom_siemens': test_base / 'Siemens' / 'XAData' / 'XA20' / 'DICOM' / '26516628.dcm',
'dicom_uih': test_base / 'UIH' / 'mrs_data' / 'dicom' / 'svs_press_te144_SVS_801' / '00000001.dcm',
'dicom_philips': test_base / 'philips' / 'DICOM' / 'SV_phantom_center' / 'IM-0018-0002-0001.dcm'}


def test_auto_twix(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['twix'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()


def test_auto_rda(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['rda'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()


def test_auto_sdat_spar(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['spar'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()


def test_auto_ge(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['ge_p'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()


def test_auto_siemens_dicom(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['dicom_siemens'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()

subprocess.run([
'spec2nii', 'auto',
testdata['dicom_siemens'].parent,
'-o', tmp_path,
'-f', 'test_dir'])

assert (tmp_path / 'test_dir.nii.gz').is_file()


def test_auto_uih_dicom(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['dicom_uih'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()

subprocess.run([
'spec2nii', 'auto',
testdata['dicom_uih'].parent,
'-o', tmp_path,
'-f', 'test_dir'])

assert (tmp_path / 'test_dir.nii.gz').is_file()


def test_auto_philips_dicom(tmp_path):
subprocess.run([
'spec2nii', 'auto',
testdata['dicom_philips'],
'-o', tmp_path,
'-f', 'test_file'])

assert (tmp_path / 'test_file.nii.gz').is_file()

subprocess.run([
'spec2nii', 'auto',
testdata['dicom_philips'].parent,
'-o', tmp_path,
'-f', 'test_dir'])

assert (tmp_path / 'test_dir.nii.gz').is_file()

0 comments on commit 1f13235

Please sign in to comment.