From 1270428880f452ef237e9d14a2c5b587c0e2042b Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sat, 9 Aug 2014 22:04:49 -0700 Subject: [PATCH 01/55] FIX: Support XYTZ order and multiple echos in PAR/REC files --- bin/parrec2nii | 61 +++++++++++++++++++++++++----------------- nibabel/arrayproxy.py | 2 +- nibabel/parrec.py | 56 ++++++++++++++++++++++---------------- nibabel/volumeutils.py | 47 ++++++++++++++++---------------- 4 files changed, 94 insertions(+), 72 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index d9e077b27a..2a255ae241 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -4,6 +4,7 @@ from __future__ import division, print_function, absolute_import from optparse import OptionParser, Option +import numpy as np import sys import os import gzip @@ -20,8 +21,8 @@ verbose_switch = False def get_opt_parser(): # use module docstring for help output p = OptionParser( - usage="%s [OPTIONS] \n\n" % sys.argv[0] + __doc__, - version="%prog " + nibabel.__version__) + usage="%s [OPTIONS] \n\n" % sys.argv[0] + __doc__, + version="%prog " + nibabel.__version__) p.add_option( Option("-v", "--verbose", action="store_true", @@ -31,8 +32,8 @@ def get_opt_parser(): Option("-o", "--output-dir", action="store", type="string", dest="outdir", default=None, - help=\ -"""Destination directory for NIfTI files. Default: current directory.""")) + help=""" +Destination directory for NIfTI files. Default: current directory.""")) p.add_option( Option("-c", "--compressed", action="store_true", dest="compressed", default=False, @@ -40,15 +41,15 @@ def get_opt_parser(): p.add_option( Option("--origin", action="store", dest="origin", default="scanner", - help=\ -"""Reference point of the q-form transformation of the NIfTI image. If 'scanner' + help=""" +Reference point of the q-form transformation of the NIfTI image. If 'scanner' the (0,0,0) coordinates will refer to the scanner's iso center. If 'fov', this coordinate will be the center of the recorded volume (field of view). Default: 'scanner'.""")) p.add_option( Option("--minmax", action="store", nargs=2, - dest="minmax", help=\ -"""Mininum and maximum settings to be stored in the NIfTI header. If any of + dest="minmax", help=""" +Mininum and maximum settings to be stored in the NIfTI header. If any of them is set to 'parse', the scaled data is scanned for the actual minimum and maximum. To bypass this potentially slow and memory intensive step (the data has to be scaled and fully loaded into memory), fixed values can be provided as @@ -58,13 +59,13 @@ as scan for the actual maximum (and vice versa). Default: 'parse parse'.""")) p.add_option( Option("--store-header", action="store_true", dest="store_header", default=False, - help=\ -"""If set, all information from the PAR header is stored in an extension of + help=""" +If set, all information from the PAR header is stored in an extension of the NIfTI file header. Default: off""")) p.add_option( Option("--scaling", action="store", dest="scaling", default='dv', - help=\ -"""Choose data scaling setting. The PAR header defines two different data + help=""" +Choose data scaling setting. The PAR header defines two different data scaling settings: 'dv' (values displayed on console) and 'fp' (floating point values). Either one can be chosen, or scaling can be disabled completely ('off'). Note that neither method will actually scale the data, but just store @@ -76,10 +77,12 @@ def verbose(msg, indent=0): if verbose_switch: print("%s%s" % (' ' * indent, msg)) + def error(msg, exit_code): sys.stderr.write(msg + '\n') sys.exit(exit_code) + def proc_file(infile, opts): # load the PAR header pr_img = pr.load(infile) @@ -87,6 +90,19 @@ def proc_file(infile, opts): # get the raw unscaled data form the REC file raw_data = pr_img.dataobj.get_unscaled() + # determine if our order is XYTZ instead of XYZT (Phillips scanners) + # If it's XYZT, then the slice number (Z) will iterate slowest (no diff) + order_rev = np.diff(pr_hdr.image_defs['slice number'][:2])[0] + assert order_rev in (0, 1) + order_rev = (order_rev == 0) + if raw_data.ndim > 3 and order_rev: + verbose('XYTZ order detected, reordering data') + assert raw_data.flags['C_CONTIGUOUS'] is True + reorder = [0, 1, 3, 2] + raw_data = np.reshape(raw_data, [raw_data.shape[n] for n in reorder], + order='F') + raw_data = np.transpose(raw_data, reorder) + # compute affine with desired origin affine = pr_hdr.get_affine(origin=opts.origin) @@ -96,7 +112,7 @@ def proc_file(infile, opts): if 'parse' in opts.minmax: # need to get the scaled data - verbose('Load (and scale) the data to determine value range') + verbose('Loading (and scaling) the data to determine value range') if opts.scaling == 'off': scaled_data = raw_data else: @@ -128,10 +144,10 @@ def proc_file(infile, opts): # image description descr = "%s;%s;%s;%s" % ( - pr_hdr.general_info['exam_name'], - pr_hdr.general_info['patient_name'], - pr_hdr.general_info['exam_date'].replace(' ',''), - pr_hdr.general_info['protocol_name']) + pr_hdr.general_info['exam_name'], + pr_hdr.general_info['patient_name'], + pr_hdr.general_info['exam_date'].replace(' ', ''), + pr_hdr.general_info['protocol_name']) nhdr.structarr['descrip'] = descr[:80] if pr_hdr.general_info['max_dynamics'] > 1: @@ -156,7 +172,7 @@ def proc_file(infile, opts): # figure out the output filename outfilename = splitext_addext(os.path.basename(infile))[0] - if not opts.outdir is None: + if opts.outdir is not None: # set output path outfilename = os.path.join(opts.outdir, outfilename) @@ -177,10 +193,7 @@ def proc_file(infile, opts): offset = nhdr.get_data_offset() seek_tell(outfile, offset, write0=True) # now the data itself, but prevent any casting or scaling - nibabel.volumeutils.array_to_file( - raw_data, - outfile, - offset=offset) + nibabel.volumeutils.array_to_file(raw_data, outfile, offset=offset) # done outfile.close() @@ -192,7 +205,7 @@ def main(): global verbose_switch verbose_switch = opts.verbose - if not opts.origin in ['scanner', 'fov']: + if opts.origin not in ['scanner', 'fov']: error("Unrecognized value for --origin: '%s'." % opts.origin, 1) # store any exceptions @@ -206,7 +219,7 @@ def main(): if len(errs): error('Caught %i exceptions. Dump follows:\n\n %s' - % (len(errs), '\n'.join(errs)), 1) + % (len(errs), '\n'.join(errs)), 1) else: verbose('Done') diff --git a/nibabel/arrayproxy.py b/nibabel/arrayproxy.py index ab0e99ee79..de8622d7f0 100644 --- a/nibabel/arrayproxy.py +++ b/nibabel/arrayproxy.py @@ -124,7 +124,7 @@ def __getitem__(self, slicer): self._shape, self._dtype, self._offset, - order = self.order) + order=self.order) # Upcast as necessary for big slopes, intercepts return apply_read_scaling(raw_data, self._slope, self._inter) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 0a3a73f4aa..6c1747fd0e 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -79,7 +79,7 @@ import warnings import numpy as np -import copy +from copy import deepcopy from .externals.six import binary_type from .py3k import asbytes @@ -336,13 +336,11 @@ def __init__(self, info, image_defs, default_scaling='dv'): # functionality # dtype dtype = np.typeDict[ - 'int' - + str(self._get_unique_image_prop('image pixel size')[0])] + 'int' + str(self._get_unique_image_prop('image pixel size')[0])] Header.__init__(self, data_dtype=dtype, shape=self.get_data_shape_in_file(), - zooms=self._get_zooms() - ) + zooms=self._get_zooms()) @classmethod def from_header(klass, header=None): @@ -359,9 +357,8 @@ def from_fileobj(klass, fileobj): return klass(info, image_defs) def copy(self): - return PARRECHeader( - copy.deepcopy(self.general_info), - self.image_defs.copy()) + return PARRECHeader(deepcopy(self.general_info), + self.image_defs.copy()) def _get_unique_image_prop(self, name): """Scan image definitions and return unique value of a property. @@ -423,7 +420,8 @@ def set_data_offset(self, offset): def get_ndim(self): """Return the number of dimensions of the image data.""" if self.general_info['max_dynamics'] > 1 \ - or self.general_info['max_gradient_orient'] > 1: + or self.general_info['max_gradient_orient'] > 1 \ + or self.general_info['max_echoes'] > 1: return 4 else: return 3 @@ -442,10 +440,11 @@ def _get_zooms(self): zooms[:3] = self.get_voxel_size() zooms[2] += slice_gap # time axis? - if len(zooms) > 3 and self.general_info['max_dynamics'] > 1: + if len(zooms) > 3 and self.general_info['max_dynamics'] > 1: # DTI also has 4D # Convert time from milliseconds to seconds zooms[3] = self.general_info['repetition_time'] / 1000. + # we leave it at the default (1) for 4D echo data return zooms def get_affine(self, origin='scanner'): @@ -521,30 +520,39 @@ def get_data_shape_in_file(self): n_slices : int number of slices n_vols : int - number of dynamic scans / number of directions in diffusion + number of dynamic scans, number of directions in diffusion, or + number of echos """ # e.g. number of volumes ndynamics = len(np.unique(self.image_defs['dynamic scan number'])) # DTI volumes (b-values-1 x directions) # there is some awkward exception to this rule for b-values > 2 # XXX need to get test image... - ndtivolumes = (self.general_info['max_diffusion_values'] - 1) \ - * self.general_info['max_gradient_orient'] + ndtivolumes = ((self.general_info['max_diffusion_values'] - 1) + * self.general_info['max_gradient_orient']) nslices = len(np.unique(self.image_defs['slice number'])) if not nslices == self.general_info['max_slices']: raise PARRECError("Header inconsistency: Found %i slices, " "but header claims to have %i." % (nslices, self.general_info['max_slices'])) + nechos = len(np.unique(self.image_defs['echo number'])) - inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) + # there should not be more than one: multiple dynamics, DTI, echos + lens = [ndynamics, ndtivolumes, nechos] + if sum(x > 1 for x in lens) > 1: + raise RuntimeError('Cannot have multiple dynamics, dtivolumes, ' + 'or echos in the same file, found %s of each, ' + 'respectively' % lens) - # there should not be both: multiple dynamics and DTI + inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) + shape = inplane_shape + (nslices,) if ndynamics > 1: - return inplane_shape + (nslices, ndynamics) + shape = shape + (ndynamics,) elif ndtivolumes > 1: - return inplane_shape + (nslices, ndtivolumes) - else: - return tuple(inplane_shape) + (nslices,) + shape = shape + (ndtivolumes,) + elif nechos > 1: + shape = shape + (nechos,) + return shape def get_data_scaling(self, method="dv"): """Returns scaling slope and intercept. @@ -574,11 +582,12 @@ def get_data_scaling(self, method="dv"): DV = PV * RS + RI FP = DV / (RS * SS) """ - # XXX: FP tends to become HUGE, DV seems to be more reasonable -> figure - # out which one means what + # XXX: FP tends to become HUGE, DV seems to be more reasonable -> + # figure out which one means what # although the is a per-image scaling in the header, it looks like # there is just one unique factor and intercept per whole image series + # XXX This is not always true, should throw exception if not scale_slope = self._get_unique_image_prop('scale slope') rescale_slope = self._get_unique_image_prop('rescale slope') rescale_intercept = self._get_unique_image_prop('rescale intercept') @@ -618,7 +627,8 @@ def get_slice_orientation(self): def raw_data_from_fileobj(self, fileobj): ''' Read unscaled data array from `fileobj` - Array axes correspond to x,y,z,t. + Array axes correspond to x,y,z,t. For other orderings, you + must reorder after the fact. Parameters ---------- @@ -673,7 +683,7 @@ class PARRECImage(SpatialImage): @classmethod def from_file_map(klass, file_map): with file_map['header'].get_prepare_fileobj('rt') as hdr_fobj: - hdr = PARRECHeader.from_fileobj(hdr_fobj) + hdr = klass.header_class.from_fileobj(hdr_fobj) rec_fobj = file_map['image'].get_prepare_fileobj() data = klass.ImageArrayProxy(rec_fobj, hdr) return klass(data, diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index 3b34ae245d..cdd86e6f6f 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -589,7 +589,7 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, # Shield special case div_none = divslope is None if not np.all( - np.isfinite((intercept, 1.0 if div_none else divslope))): + np.isfinite((intercept, 1.0 if div_none else divslope))): raise ValueError('divslope and intercept must be finite') if divslope == 0: raise ValueError('divslope cannot be zero') @@ -599,15 +599,14 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, out_dtype = in_dtype else: out_dtype = np.dtype(out_dtype) - if not offset is None: + if offset is not None: seek_tell(fileobj, offset) if (div_none or - (mn, mx) == (0, 0) or - (None not in (mn, mx) and mx < mn) - ): + (mn, mx) == (0, 0) or + (None not in (mn, mx) and mx < mn)): write_zeros(fileobj, data.size * out_dtype.itemsize) return - if not order in 'FC': + if order not in 'FC': raise ValueError('Order should be one of F or C') # Simple cases pre_clips = None if (mn, mx) == (None, None) else (mn, mx) @@ -615,10 +614,10 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, if in_dtype.type == np.void: if not null_scaling: raise ValueError('Cannot scale non-numeric types') - if not pre_clips is None: + if pre_clips is not None: raise ValueError('Cannot clip non-numeric types') return _write_data(data, fileobj, out_dtype, order) - if not pre_clips is None: + if pre_clips is not None: pre_clips = _dt_min_max(in_dtype, *pre_clips) if null_scaling and np.can_cast(in_dtype, out_dtype): return _write_data(data, fileobj, out_dtype, order, @@ -640,8 +639,8 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, assert out_kind in 'iu' if in_kind in 'iu': if null_scaling: - # Must be large int to small int conversion; add clipping to pre scale - # thresholds + # Must be large int to small int conversion; add clipping to + # pre scale thresholds mn, mx = _dt_min_max(in_dtype, mn, mx) mn_out, mx_out = _dt_min_max(out_dtype) pre_clips = max(mn, mn_out), min(mx, mx_out) @@ -693,7 +692,7 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, specials = specials / slope assert specials.dtype.type == w_type post_mn, post_mx, nan_fill = np.rint(specials) - if post_mn > post_mx: # slope could be negative + if post_mn > post_mx: # slope could be negative post_mn, post_mx = post_mx, post_mn # Make sure that the thresholds exclude any value that will get badly cast # to the integer type. This is not the same as using the maximumum of the @@ -710,7 +709,7 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, # rounding est_err = np.round(2 * np.finfo(w_type).eps * abs(inter / slope)) if ((nan_fill < both_mn and abs(nan_fill - both_mn) < est_err) or - (nan_fill > both_mx and abs(nan_fill - both_mx) < est_err)): + (nan_fill > both_mx and abs(nan_fill - both_mx) < est_err)): # nan_fill can be (just) outside clip range nan_fill = np.clip(nan_fill, both_mn, both_mx) else: @@ -723,24 +722,24 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, post_mx = np.min([post_mx, both_mx]) in_cast = None if cast_in_dtype == in_dtype else cast_in_dtype return _write_data(data, fileobj, out_dtype, order, - in_cast = in_cast, - pre_clips = pre_clips, - inter = inter, - slope = slope, - post_clips = (post_mn, post_mx), - nan_fill = nan_fill if nan2zero else None) + in_cast=in_cast, + pre_clips=pre_clips, + inter=inter, + slope=slope, + post_clips=(post_mn, post_mx), + nan_fill=nan_fill if nan2zero else None) def _write_data(data, fileobj, out_dtype, order, - in_cast = None, - pre_clips = None, - inter = 0., - slope = 1., - post_clips = None, - nan_fill = None): + in_cast=None, + pre_clips=None, + inter=0., + slope=1., + post_clips=None, + nan_fill=None): """ Write array `data` to `fileobj` as `out_dtype` type, layout `order` Does not modify `data` in-place. From 2b0d540699dc7ca06fe09a9b4df55f99f2385840 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Sun, 10 Aug 2014 17:29:58 -0700 Subject: [PATCH 02/55] ENH: Support for partial recordings, and overwrite checking --- bin/parrec2nii | 57 ++++++++++++-------- nibabel/parrec.py | 133 +++++++++++++++++++++++++++++----------------- 2 files changed, 117 insertions(+), 73 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 2a255ae241..3fe8c20a6c 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -25,30 +25,30 @@ def get_opt_parser(): version="%prog " + nibabel.__version__) p.add_option( - Option("-v", "--verbose", action="store_true", - dest="verbose", default=False, - help="Make some noise.")) + Option("-v", "--verbose", action="store_true", dest="verbose", + default=False, help="Make some noise.")) p.add_option( - Option("-o", "--output-dir", - action="store", type="string", dest="outdir", - default=None, - help=""" + Option("-o", "--output-dir", action="store", type="string", + dest="outdir", default=None, help=""" Destination directory for NIfTI files. Default: current directory.""")) p.add_option( Option("-c", "--compressed", action="store_true", dest="compressed", default=False, help="Whether to write compressed NIfTI files or not.")) p.add_option( - Option("--origin", action="store", - dest="origin", default="scanner", + Option("-p", "--permit-truncated", action="store_true", + dest="permit_truncated", default=False, help=""" +Permit conversion of truncated recordings. Support for this is experimental, +and results *must* be checked afterward for validity.""")) + p.add_option( + Option("--origin", action="store", dest="origin", default="scanner", help=""" Reference point of the q-form transformation of the NIfTI image. If 'scanner' the (0,0,0) coordinates will refer to the scanner's iso center. If 'fov', this coordinate will be the center of the recorded volume (field of view). Default: 'scanner'.""")) p.add_option( - Option("--minmax", action="store", nargs=2, - dest="minmax", help=""" + Option("--minmax", action="store", nargs=2, dest="minmax", help=""" Mininum and maximum settings to be stored in the NIfTI header. If any of them is set to 'parse', the scaled data is scanned for the actual minimum and maximum. To bypass this potentially slow and memory intensive step (the data @@ -57,9 +57,8 @@ space-separated pair, e.g. '5.4 120.4'. It is possible to set a fixed minimum as scan for the actual maximum (and vice versa). Default: 'parse parse'.""")) p.set_defaults(minmax=('parse', 'parse')) p.add_option( - Option("--store-header", action="store_true", - dest="store_header", default=False, - help=""" + Option("--store-header", action="store_true", dest="store_header", + default=False, help=""" If set, all information from the PAR header is stored in an extension of the NIfTI file header. Default: off""")) p.add_option( @@ -70,6 +69,10 @@ scaling settings: 'dv' (values displayed on console) and 'fp' (floating point values). Either one can be chosen, or scaling can be disabled completely ('off'). Note that neither method will actually scale the data, but just store the corresponding settings in the NIfTI header. Default: 'dv'""")) + p.add_option( + Option("--overwrite", action="store_true", dest="overwrite", + default=False, help=""" +Overwrite file if it exists. Default: False""")) return p @@ -84,8 +87,24 @@ def error(msg, exit_code): def proc_file(infile, opts): + # figure out the output filename, and see if it exists + outfilename = splitext_addext(os.path.basename(infile))[0] + if opts.outdir is not None: + # set output path + outfilename = os.path.join(opts.outdir, outfilename) + + # prep a file + if opts.compressed: + verbose('Using gzip compression') + outfilename += '.nii.gz' + else: + outfilename += '.nii' + if os.path.isfile(outfilename) and not opts.overwrite: + raise IOError('Output file "%s" exists, use --overwrite to ' + 'overwrite it' % outfilename) + # load the PAR header - pr_img = pr.load(infile) + pr_img = pr.load(infile, opts.permit_truncated) pr_hdr = pr_img.header # get the raw unscaled data form the REC file raw_data = pr_img.dataobj.get_unscaled() @@ -170,19 +189,11 @@ def proc_file(infile, opts): # finalize the header: set proper data offset, pixdims, ... nimg.update_header() - # figure out the output filename - outfilename = splitext_addext(os.path.basename(infile))[0] - if opts.outdir is not None: - # set output path - outfilename = os.path.join(opts.outdir, outfilename) - # prep a file if opts.compressed: verbose('Using gzip compression') - outfilename += '.nii.gz' outfile = gzip.open(outfilename, 'wb') else: - outfilename += '.nii' outfile = open(outfilename, 'wb') verbose('Writing %s' % outfilename) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 6c1747fd0e..0e21d49138 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -189,20 +189,20 @@ ('image_flip_angle', float), ('cardiac frequency', int,), ('minimum RR-interval', int,), - ('maximum RR-interval', int,), + ('maximum RR-interval', int,), ('TURBO factor', int,), ('Inversion delay', float), - ('diffusion b value number', int,), # (imagekey!) - ('gradient orientation number', int,), # (imagekey!) - ('contrast type', 'S30'), # XXX might be too short? - ('diffusion anisotropy type', 'S30'), # XXX might be too short? + ('diffusion b value number', int,), # (imagekey!) + ('gradient orientation number', int,), # (imagekey!) + ('contrast type', 'S30'), # XXX might be too short? + ('diffusion anisotropy type', 'S30'), # XXX might be too short? ('diffusion', float, (3,)), ('label type', int,), # (imagekey!) ] image_def_dtype = np.dtype(image_def_dtd) # slice orientation codes -slice_orientation_codes = Recoder((# code, label +slice_orientation_codes = Recoder(( # code, label (1, 'transverse'), (2, 'sagittal'), (3, 'coronal')), fields=('code', 'label')) @@ -217,13 +217,31 @@ class PARRECError(Exception): pass -def parse_PAR_header(fobj): +def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): + """Helper to alert user about truncated files and adjust computation""" + extra = (not must_exceed_one) or (n_expected > 1) + if extra and n_have != n_expected: + msg = ("Header inconsistency: Found <= %i %s, but expected %i." + % (n_have, name, n_expected)) + if not permit: + raise PARRECError(msg) + msg += " Assuming %i valid %s." % (n_have - 1, name) + warnings.warn(msg) + # we assume up to the penultimate data line is correct + n_have -= 1 + return n_have + + +def parse_PAR_header(fobj, permit_truncated=False): """Parse a PAR header and aggregate all information into useful containers. Parameters ---------- fobj : file-object The PAR header file object. + permit_truncated : bool + If True, a warning is emitted instead of an error when a truncated + recording is detected. Returns ------- @@ -246,14 +264,14 @@ def parse_PAR_header(fobj): # try to get the header version if line.count('image export tool'): version = line.split()[-1] - if not version in supported_versions: + if version not in supported_versions: warnings.warn( - "PAR/REC version '%s' is currently not " - "supported -- making an attempt to read " - "nevertheless. Please email the NiBabel " - "mailing list, if you are interested in " - "adding support for this version." - % version) + "PAR/REC version '%s' is currently not " + "supported -- making an attempt to read " + "nevertheless. Please email the NiBabel " + "mailing list, if you are interested in " + "adding support for this version." + % version) else: # just a comment continue @@ -309,6 +327,29 @@ def parse_PAR_header(fobj): image_defs[props[0]][i] = value item_counter += nelements + # DTI volumes (b-values-1 x directions) + # there is some awkward exception to this rule for b-values > 2 + # XXX need to get test image... + max_dti_volumes = ((general_info['max_diffusion_values'] - 1) + * general_info['max_gradient_orient']) + n_b = len(np.unique(image_defs['diffusion b value number'])) + n_grad = len(np.unique(image_defs['gradient orientation number'])) + n_dti_volumes = (n_b - 1) * n_grad + n_slices = len(np.unique(image_defs['slice number'])) + n_echoes = len(np.unique(image_defs['echo number'])) + n_dynamics = len(np.unique(image_defs['dynamic scan number'])) + pt = permit_truncated + n_slices = _check_truncation('slices', n_slices, + general_info['max_slices'], pt, False) + n_echoes = _check_truncation('echoes', n_echoes, + general_info['max_echoes'], pt, True) + n_dynamics = _check_truncation('dynamics', n_dynamics, + general_info['max_dynamics'], pt, True) + n_dti_volumes = _check_truncation('dti volumes', n_dti_volumes, + max_dti_volumes, pt, True) + general_info.update(n_dti_volumes=n_dti_volumes, n_echoes=n_echoes, + n_dynamics=n_dynamics, n_slices=n_slices, + max_dti_volumes=max_dti_volumes) return general_info, image_defs @@ -352,8 +393,8 @@ def from_header(klass, header=None): 'non-PARREC header.') @classmethod - def from_fileobj(klass, fileobj): - info, image_defs = parse_PAR_header(fileobj) + def from_fileobj(klass, fileobj, permit_truncated=False): + info, image_defs = parse_PAR_header(fileobj, permit_truncated) return klass(info, image_defs) def copy(self): @@ -419,9 +460,9 @@ def set_data_offset(self, offset): def get_ndim(self): """Return the number of dimensions of the image data.""" - if self.general_info['max_dynamics'] > 1 \ - or self.general_info['max_gradient_orient'] > 1 \ - or self.general_info['max_echoes'] > 1: + if self.general_info['n_dynamics'] > 1 \ + or self.general_info['n_dti_volumes'] > 1 \ + or self.general_info['n_echoes'] > 1: return 4 else: return 3 @@ -440,7 +481,7 @@ def _get_zooms(self): zooms[:3] = self.get_voxel_size() zooms[2] += slice_gap # time axis? - if len(zooms) > 3 and self.general_info['max_dynamics'] > 1: + if len(zooms) > 3 and self.general_info['n_dynamics'] > 1: # DTI also has 4D # Convert time from milliseconds to seconds zooms[3] = self.general_info['repetition_time'] / 1000. @@ -521,37 +562,24 @@ def get_data_shape_in_file(self): number of slices n_vols : int number of dynamic scans, number of directions in diffusion, or - number of echos + number of echoes """ - # e.g. number of volumes - ndynamics = len(np.unique(self.image_defs['dynamic scan number'])) - # DTI volumes (b-values-1 x directions) - # there is some awkward exception to this rule for b-values > 2 - # XXX need to get test image... - ndtivolumes = ((self.general_info['max_diffusion_values'] - 1) - * self.general_info['max_gradient_orient']) - nslices = len(np.unique(self.image_defs['slice number'])) - if not nslices == self.general_info['max_slices']: - raise PARRECError("Header inconsistency: Found %i slices, " - "but header claims to have %i." - % (nslices, self.general_info['max_slices'])) - nechos = len(np.unique(self.image_defs['echo number'])) - - # there should not be more than one: multiple dynamics, DTI, echos - lens = [ndynamics, ndtivolumes, nechos] + # there should not be more than one: multiple dynamics, DTI, echoes + lens = [self.general_info[x] for x in ['n_dynamics', 'n_dti_volumes', + 'n_echoes']] if sum(x > 1 for x in lens) > 1: - raise RuntimeError('Cannot have multiple dynamics, dtivolumes, ' - 'or echos in the same file, found %s of each, ' - 'respectively' % lens) + raise PARRECError('Cannot have multiple dynamics, dti volumes, ' + 'or echoes in the same file, found %s of each, ' + 'respectively' % lens) inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) - shape = inplane_shape + (nslices,) - if ndynamics > 1: - shape = shape + (ndynamics,) - elif ndtivolumes > 1: - shape = shape + (ndtivolumes,) - elif nechos > 1: - shape = shape + (nechos,) + shape = inplane_shape + (self.general_info['n_slices'],) + if self.general_info['n_dynamics'] > 1: + shape = shape + (self.general_info['n_dynamics'],) + elif self.general_info['n_dti_volumes'] > 1: + shape = shape + (self.general_info['n_dti_volumes'],) + elif self.general_info['n_echoes'] > 1: + shape = shape + (self.general_info['n_echoes'],) return shape def get_data_scaling(self, method="dv"): @@ -681,9 +709,11 @@ class PARRECImage(SpatialImage): ImageArrayProxy = ArrayProxy @classmethod - def from_file_map(klass, file_map): + def from_file_map(klass, file_map, permit_truncated=False): + pt = permit_truncated with file_map['header'].get_prepare_fileobj('rt') as hdr_fobj: - hdr = klass.header_class.from_fileobj(hdr_fobj) + hdr = klass.header_class.from_fileobj(hdr_fobj, + permit_truncated=pt) rec_fobj = file_map['image'].get_prepare_fileobj() data = klass.ImageArrayProxy(rec_fobj, hdr) return klass(data, @@ -693,4 +723,7 @@ def from_file_map(klass, file_map): file_map=file_map) -load = PARRECImage.load +def load(filename, permit_truncated=False): + file_map = PARRECImage.filespec_to_file_map(filename) + return PARRECImage.from_file_map(file_map, permit_truncated) +load.__doc__ = PARRECImage.load.__doc__ From 026b9eae409783dca38a7a76fcc89d441ffeedc9 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 18 Aug 2014 16:35:24 -0700 Subject: [PATCH 03/55] WIP: Add bvals/bvecs --- bin/parrec2nii | 85 +++++++++++++++++++++++++++++++++++++++-------- nibabel/parrec.py | 4 +-- 2 files changed, 73 insertions(+), 16 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 3fe8c20a6c..b7c8e16d92 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -40,6 +40,10 @@ Destination directory for NIfTI files. Default: current directory.""")) dest="permit_truncated", default=False, help=""" Permit conversion of truncated recordings. Support for this is experimental, and results *must* be checked afterward for validity.""")) + p.add_option( + Option("-b", "--bvs", action="store_true", dest="bvs", default=False, + help=""" +Output bvals/bvecs files in addition to NIFTI image.""")) p.add_option( Option("--origin", action="store", dest="origin", default="scanner", help=""" @@ -88,17 +92,17 @@ def error(msg, exit_code): def proc_file(infile, opts): # figure out the output filename, and see if it exists - outfilename = splitext_addext(os.path.basename(infile))[0] + basefilename = splitext_addext(os.path.basename(infile))[0] if opts.outdir is not None: # set output path - outfilename = os.path.join(opts.outdir, outfilename) + basefilename = os.path.join(opts.outdir, basefilename) # prep a file if opts.compressed: verbose('Using gzip compression') - outfilename += '.nii.gz' + outfilename = basefilename + '.nii.gz' else: - outfilename += '.nii' + outfilename = basefilename + '.nii' if os.path.isfile(outfilename) and not opts.overwrite: raise IOError('Output file "%s" exists, use --overwrite to ' 'overwrite it' % outfilename) @@ -106,6 +110,7 @@ def proc_file(infile, opts): # load the PAR header pr_img = pr.load(infile, opts.permit_truncated) pr_hdr = pr_img.header + # get the raw unscaled data form the REC file raw_data = pr_img.dataobj.get_unscaled() @@ -196,17 +201,69 @@ def proc_file(infile, opts): else: outfile = open(outfilename, 'wb') - verbose('Writing %s' % outfilename) - # first write the header - nhdr.set_slope_inter(slope, intercept) - nhdr.write_to(outfile) - # Seek to data offset position - offset = nhdr.get_data_offset() - seek_tell(outfile, offset, write0=True) - # now the data itself, but prevent any casting or scaling - nibabel.volumeutils.array_to_file(raw_data, outfile, offset=offset) + try: + verbose('Writing %s' % outfilename) + # first write the header + nhdr.set_slope_inter(slope, intercept) + nhdr.write_to(outfile) + # Seek to data offset position + offset = nhdr.get_data_offset() + seek_tell(outfile, offset, write0=True) + # now the data itself, but prevent any casting or scaling + nibabel.volumeutils.array_to_file(raw_data, outfile, offset=offset) + finally: + outfile.close() + + # write out bvals/bvecs if requested + if opts.bvs: + verbose('Writing .bvals and .bvecs files') + # bvals + gen_info = pr_hdr.general_info + shape = (gen_info['n_dti_volumes'], gen_info['n_slices']) + shape = (shape[1], shape[0]) if order_rev else shape + bvals = np.reshape(pr_hdr.image_defs['diffusion_b_factor'], shape) + bvals = bvals.T if order_rev else bvals + if not np.all(np.diff(bvals, axis=1) == 0): + raise RuntimeError('Could not output bvals: inconsistent values') + + # bvecs + shape = (3, pr_hdr.general_info['n_dti_volumes'], + pr_hdr.general_info['n_slices']) + shape = (shape[0], shape[2], shape[1]) if order_rev else shape + bvecs = np.reshape(pr_hdr.image_defs['diffusion'].T, shape) + bvecs = np.swapaxis(bvecs, 2, 1) if order_rev else bvecs + if not np.all(np.diff(bvecs, axis=2) == 0): + raise RuntimeError('Could not output bvecs: inconsistent values') + bvecs = bvecs[:, :, 0] + + # restore XYZ order to bvecs + assert np.all(bvecs[:, 0] == 0) # first col should be zeros + order = [] + mults = [] + for ii in range(3): + idx = np.where(bvecs[:, ii+1] != 0)[0] + assert len(idx) == 1 + order.append(idx[0]) + mults.append(bvecs[idx[0], ii+1]) + assert np.array_equal(np.unique(order), [0, 1, 2]) + assert np.array_equal(np.abs(mults), [1, 1, 1]) + bvecs = bvecs[order] * np.array(mults)[:, np.newaxis] + bvecs[bvecs == -0.0] = 0.0 + + # Actually write files + bval_fname = basefilename + '.bvals' + bvec_fname = basefilename + '.bvecs' + with open(bval_fname, 'w') as fid: + # np.savetxt could do this, but it's just a loop anyway + for val in bvals[:, 0]: + fid.write('%s ' % val) + fid.write('\n') + with open(bvec_fname, 'w') as fid: + for row in bvecs: + for val in row: + fid.write('%s ' % val) + fid.write('\n') # done - outfile.close() def main(): diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 0e21d49138..210face733 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -331,10 +331,10 @@ def parse_PAR_header(fobj, permit_truncated=False): # there is some awkward exception to this rule for b-values > 2 # XXX need to get test image... max_dti_volumes = ((general_info['max_diffusion_values'] - 1) - * general_info['max_gradient_orient']) + * general_info['max_gradient_orient']) + 1 n_b = len(np.unique(image_defs['diffusion b value number'])) n_grad = len(np.unique(image_defs['gradient orientation number'])) - n_dti_volumes = (n_b - 1) * n_grad + n_dti_volumes = (n_b - 1) * n_grad + 1 n_slices = len(np.unique(image_defs['slice number'])) n_echoes = len(np.unique(image_defs['echo number'])) n_dynamics = len(np.unique(image_defs['dynamic scan number'])) From 3fb1785f3f42f196f8b0b40b6682f8fb685575ef Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Mon, 18 Aug 2014 18:01:01 -0700 Subject: [PATCH 04/55] FIX: Working for variable scale factors --- bin/parrec2nii | 45 +++++++++++++++++--------- nibabel/parrec.py | 71 +++++++++++++++++++++++++++++------------- nibabel/volumeutils.py | 12 +++---- 3 files changed, 85 insertions(+), 43 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index b7c8e16d92..517b3f9713 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -72,7 +72,9 @@ Choose data scaling setting. The PAR header defines two different data scaling settings: 'dv' (values displayed on console) and 'fp' (floating point values). Either one can be chosen, or scaling can be disabled completely ('off'). Note that neither method will actually scale the data, but just store -the corresponding settings in the NIfTI header. Default: 'dv'""")) +the corresponding settings in the NIfTI header, unless non-uniform scaling +is used, in which case the data is stored in the file in scaled form. +Default: 'dv'""")) p.add_option( Option("--overwrite", action="store_true", dest="overwrite", default=False, help=""" @@ -116,9 +118,7 @@ def proc_file(infile, opts): # determine if our order is XYTZ instead of XYZT (Phillips scanners) # If it's XYZT, then the slice number (Z) will iterate slowest (no diff) - order_rev = np.diff(pr_hdr.image_defs['slice number'][:2])[0] - assert order_rev in (0, 1) - order_rev = (order_rev == 0) + order_rev = pr_hdr.order_xytz if raw_data.ndim > 3 and order_rev: verbose('XYTZ order detected, reordering data') assert raw_data.flags['C_CONTIGUOUS'] is True @@ -141,8 +141,7 @@ def proc_file(infile, opts): scaled_data = raw_data else: slope, intercept = pr_hdr.get_data_scaling(method=opts.scaling) - scaled_data = slope * raw_data - scaled_data += intercept + scaled_data = slope * raw_data + intercept if opts.minmax[0] == 'parse': nhdr.structarr['cal_min'] = scaled_data.min() else: @@ -183,13 +182,23 @@ def proc_file(infile, opts): # anatomical or DTI nhdr.set_xyzt_units('mm', 'unknown') - # get original scaling + # get original scaling, and decide if we scale in-place or not + scale_data = False if opts.scaling == 'off': - slope = 1.0 - intercept = 0.0 + slope = 1. + intercept = 0. else: + verbose('Using data scaling "%s"' % opts.scaling) slope, intercept = pr_hdr.get_data_scaling(method=opts.scaling) - nhdr.set_slope_inter(slope, intercept) + uni_s = np.unique(slope) + uni_i = np.unique(intercept) + if len(uni_s) == 1 and len(uni_i) == 1: + slope = uni_s[0] + intercept = uni_i[0] + else: + verbose("Multiple scale-factors detected, data will be saved " + "in scaled form") + scale_data = True # finalize the header: set proper data offset, pixdims, ... nimg.update_header() @@ -204,18 +213,22 @@ def proc_file(infile, opts): try: verbose('Writing %s' % outfilename) # first write the header - nhdr.set_slope_inter(slope, intercept) + if scale_data: + data = ((raw_data * slope) + intercept).astype(raw_data.dtype) + nhdr.set_slope_inter(1., 0.) + else: + nhdr.set_slope_inter(slope, intercept) + data = raw_data nhdr.write_to(outfile) - # Seek to data offset position + # Now the data offset = nhdr.get_data_offset() seek_tell(outfile, offset, write0=True) - # now the data itself, but prevent any casting or scaling - nibabel.volumeutils.array_to_file(raw_data, outfile, offset=offset) + nibabel.volumeutils.array_to_file(data, outfile, offset=offset) finally: outfile.close() # write out bvals/bvecs if requested - if opts.bvs: + if opts.bvs and pr_hdr.general_info['n_dti_volumes'] > 1: verbose('Writing .bvals and .bvecs files') # bvals gen_info = pr_hdr.general_info @@ -263,6 +276,8 @@ def proc_file(infile, opts): for val in row: fid.write('%s ' % val) fid.write('\n') + elif opts.bvs: + verbose('No DTI volumes detected, bvals and bvecs not written') # done diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 210face733..0273d90088 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -338,6 +338,7 @@ def parse_PAR_header(fobj, permit_truncated=False): n_slices = len(np.unique(image_defs['slice number'])) n_echoes = len(np.unique(image_defs['echo number'])) n_dynamics = len(np.unique(image_defs['dynamic scan number'])) + n_seq = len(np.unique(image_defs['scanning sequence'])) pt = permit_truncated n_slices = _check_truncation('slices', n_slices, general_info['max_slices'], pt, False) @@ -349,7 +350,8 @@ def parse_PAR_header(fobj, permit_truncated=False): max_dti_volumes, pt, True) general_info.update(n_dti_volumes=n_dti_volumes, n_echoes=n_echoes, n_dynamics=n_dynamics, n_slices=n_slices, - max_dti_volumes=max_dti_volumes) + max_dti_volumes=max_dti_volumes, n_seq=n_seq, + max_types=n_seq) return general_info, image_defs @@ -401,6 +403,13 @@ def copy(self): return PARRECHeader(deepcopy(self.general_info), self.image_defs.copy()) + @property + def order_xytz(self): + order_rev = np.diff(self.image_defs['slice number'][:2])[0] + assert order_rev in (0, 1) + order_rev = (order_rev == 0) + return order_rev + def _get_unique_image_prop(self, name): """Scan image definitions and return unique value of a property. @@ -432,6 +441,32 @@ def _get_unique_image_prop(self, name): else: return np.array([uprop[0] for uprop in uprops]) + def _get_broadcastable_prop(self, name): + """Scan image definitions and return broadcastable value of a property. + + If the requested property is an array this method gives + scale factors that can be combined and applied to the raw data. + + Parameters + ---------- + name : str + Name of the property + + Returns + ------- + value : array + """ + dims = self.get_data_shape_in_file()[2:] + prop = self.image_defs[name] + # this will break for truncated recs, but it's probably okay for now + assert np.prod(dims) == len(prop) + if self.order_xytz: + prop = prop.reshape(dims) + else: + prop = prop.reshape(dims[::-1]).T + prop = prop[np.newaxis, np.newaxis, ...] # array expansion + return prop + def get_voxel_size(self): """Returns the spatial extent of a voxel. @@ -462,7 +497,8 @@ def get_ndim(self): """Return the number of dimensions of the image data.""" if self.general_info['n_dynamics'] > 1 \ or self.general_info['n_dti_volumes'] > 1 \ - or self.general_info['n_echoes'] > 1: + or self.general_info['n_echoes'] > 1 \ + or self.general_info['n_seq'] > 1: return 4 else: return 3 @@ -566,7 +602,7 @@ def get_data_shape_in_file(self): """ # there should not be more than one: multiple dynamics, DTI, echoes lens = [self.general_info[x] for x in ['n_dynamics', 'n_dti_volumes', - 'n_echoes']] + 'n_echoes', 'n_seq']] if sum(x > 1 for x in lens) > 1: raise PARRECError('Cannot have multiple dynamics, dti volumes, ' 'or echoes in the same file, found %s of each, ' @@ -580,6 +616,8 @@ def get_data_shape_in_file(self): shape = shape + (self.general_info['n_dti_volumes'],) elif self.general_info['n_echoes'] > 1: shape = shape + (self.general_info['n_echoes'],) + elif self.general_info['n_seq'] > 1: + shape = shape + (self.general_info['n_seq'],) return shape def get_data_scaling(self, method="dv"): @@ -592,9 +630,9 @@ def get_data_scaling(self, method="dv"): Returns ------- - slope : float + slope : array scaling slope - intercept : float + intercept : array scaling intercept Notes @@ -610,34 +648,25 @@ def get_data_scaling(self, method="dv"): DV = PV * RS + RI FP = DV / (RS * SS) """ - # XXX: FP tends to become HUGE, DV seems to be more reasonable -> - # figure out which one means what - - # although the is a per-image scaling in the header, it looks like - # there is just one unique factor and intercept per whole image series - # XXX This is not always true, should throw exception if not - scale_slope = self._get_unique_image_prop('scale slope') - rescale_slope = self._get_unique_image_prop('rescale slope') - rescale_intercept = self._get_unique_image_prop('rescale intercept') + # These will be 3D or 4D + scale_slope = self._get_broadcastable_prop('scale slope') + rescale_slope = self._get_broadcastable_prop('rescale slope') + rescale_intercept = self._get_broadcastable_prop('rescale intercept') if method == 'dv': slope = rescale_slope intercept = rescale_intercept elif method == 'fp': - # actual slopes per definition above slope = 1.0 / scale_slope - # actual intercept per definition above intercept = rescale_intercept / (rescale_slope * scale_slope) else: raise ValueError("Unknown scling method '%s'." % method) return (slope, intercept) def get_slope_inter(self): - """ Utility method to get default slope, intercept scaling + """ Utility method to get default slope, intercept scaling arrays """ - return tuple( - np.asscalar(v) - for v in self.get_data_scaling(method=self.default_scaling)) + return self.get_data_scaling(method=self.default_scaling) def get_slice_orientation(self): """Returns the slice orientation label. @@ -695,8 +724,6 @@ def data_from_fileobj(self, fileobj): data = self.raw_data_from_fileobj(fileobj) # get scalings from header. Value of None means not present in header slope, inter = self.get_slope_inter() - slope = 1.0 if slope is None else slope - inter = 0.0 if inter is None else inter # Upcast as necessary for big slopes, intercepts return apply_read_scaling(data, slope, inter) diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index cdd86e6f6f..dac2c2a0e7 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -876,19 +876,19 @@ def seek_tell(fileobj, offset, write0=False): assert fileobj.tell() == offset -def apply_read_scaling(arr, slope = None, inter = None): +def apply_read_scaling(arr, slope=None, inter=None): """ Apply scaling in `slope` and `inter` to array `arr` - This is for loading the array from a file (as opposed to the reverse scaling - when saving an array to file) + This is for loading the array from a file (as opposed to the reverse + scaling when saving an array to file) Return data will be ``arr * slope + inter``. The trick is that we have to find a good precision to use for applying the scaling. The heuristic is that the data is always upcast to the higher of the types from `arr, `slope`, `inter` if `slope` and / or `inter` are not default values. If the - dtype of `arr` is an integer, then we assume the data more or less fills the - integer range, and upcast to a type such that the min, max of ``arr.dtype`` - * scale + inter, will be finite. + dtype of `arr` is an integer, then we assume the data more or less fills + the integer range, and upcast to a type such that the min, max of + ``arr.dtype`` * scale + inter, will be finite. Parameters ---------- From 8e66c6149e679c38e1eb2a1293482ccaa0db901f Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Aug 2014 00:49:16 -0700 Subject: [PATCH 05/55] ENH: Add dwell time --- bin/parrec2nii | 19 +++++++++++++++++++ nibabel/parrec.py | 27 +++++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/bin/parrec2nii b/bin/parrec2nii index 517b3f9713..d7749119bd 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -44,6 +44,19 @@ and results *must* be checked afterward for validity.""")) Option("-b", "--bvs", action="store_true", dest="bvs", default=False, help=""" Output bvals/bvecs files in addition to NIFTI image.""")) + p.add_option( + Option("-d", "--dwell-time", action="store_true", default=False, + dest="dwell_time", + help=""" +Calculate the scan dwell time. If supplied, the magnetic field strength +should also be supplied using --field-strength (default 3). The field strength +must be supplied because it is not encoded in the PAR/REC format.""")) + p.add_option( + Option("--field-strength", action="store", default=3., type="float", + dest="field_strength", help=""" +The magnetic field strength of the recording, only needed for --dwell-time. +The field strength must be supplied because it is not encoded in the PAR/REC +format.""")) p.add_option( Option("--origin", action="store", dest="origin", default="scanner", help=""" @@ -278,6 +291,12 @@ def proc_file(infile, opts): fid.write('\n') elif opts.bvs: verbose('No DTI volumes detected, bvals and bvecs not written') + if opts.dwell_time: + verbose('Writing .dwell_time file') + dwell_time = pr_hdr.get_dwell_time(opts.field_strength) + dwell_fname = basefilename + '.dwell_time' + with open(dwell_fname, 'w') as fid: + fid.write('%s\n' % dwell_time) # done diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 0273d90088..4de2cf6baa 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -410,6 +410,33 @@ def order_xytz(self): order_rev = (order_rev == 0) return order_rev + def get_dwell_time(self, field_strength=3.): + """Calculate the dwell time of the recording + + Parameters + ---------- + field_strength : float + Strength of the magnet in T, e.g. ``3.0`` for a 3T magnet + recording. Providing this value is necessary because the + field strength is not encoded in the PAR file. + + Returns + ------- + dwell_time : float + The dwell time in seconds. + """ + field_strength = float(field_strength) # Tesla + assert field_strength > 0. + water_fat_shift = self.general_info['water_fat_shift'] # pixels + echo_train_length = self.general_info['epi_factor'] # int + # constants + gyromagnetic_ratio = 42.57 # MHz/T + proton_water_fat_shift = 3.4 # ppm + dwell_time = ((echo_train_length - 1) * water_fat_shift / + (gyromagnetic_ratio * proton_water_fat_shift + * field_strength * (echo_train_length + 1))) + return dwell_time + def _get_unique_image_prop(self, name): """Scan image definitions and return unique value of a property. From 303542bee9bf883b9f0e9ba5a24069eb61b2a001 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Aug 2014 11:54:38 -0700 Subject: [PATCH 06/55] FIX: Verbose dwell output --- bin/parrec2nii | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index d7749119bd..ee0b943574 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -292,11 +292,12 @@ def proc_file(infile, opts): elif opts.bvs: verbose('No DTI volumes detected, bvals and bvecs not written') if opts.dwell_time: - verbose('Writing .dwell_time file') dwell_time = pr_hdr.get_dwell_time(opts.field_strength) + verbose('Writing dwell time (%0.3f sec) calculated assuming %sT magnet' + % (dwell_time, opts.field_strength)) dwell_fname = basefilename + '.dwell_time' with open(dwell_fname, 'w') as fid: - fid.write('%s\n' % dwell_time) + fid.write('%r\n' % dwell_time) # done From 3855e97a6431bc8e6d478d0873d41d6c60f9088e Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 19 Aug 2014 16:31:54 -0700 Subject: [PATCH 07/55] FIX: Fix dwell time --- bin/parrec2nii | 13 ++++++++----- nibabel/parrec.py | 5 ++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index ee0b943574..46a555f2a5 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -293,11 +293,14 @@ def proc_file(infile, opts): verbose('No DTI volumes detected, bvals and bvecs not written') if opts.dwell_time: dwell_time = pr_hdr.get_dwell_time(opts.field_strength) - verbose('Writing dwell time (%0.3f sec) calculated assuming %sT magnet' - % (dwell_time, opts.field_strength)) - dwell_fname = basefilename + '.dwell_time' - with open(dwell_fname, 'w') as fid: - fid.write('%r\n' % dwell_time) + if dwell_time is None: + verbose('No EPI factors, dwell time not written') + else: + verbose('Writing dwell time (%r sec) calculated assuming %sT ' + 'magnet' % (dwell_time, opts.field_strength)) + dwell_fname = basefilename + '.dwell_time' + with open(dwell_fname, 'w') as fid: + fid.write('%r\n' % dwell_time) # done diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 4de2cf6baa..88112619d9 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -423,12 +423,15 @@ def get_dwell_time(self, field_strength=3.): Returns ------- dwell_time : float - The dwell time in seconds. + The dwell time in seconds. Returns None if the dwell + time cannot be calculated (i.e., not using an EPI sequence). """ field_strength = float(field_strength) # Tesla assert field_strength > 0. water_fat_shift = self.general_info['water_fat_shift'] # pixels echo_train_length = self.general_info['epi_factor'] # int + if echo_train_length <= 0: + return None # constants gyromagnetic_ratio = 42.57 # MHz/T proton_water_fat_shift = 3.4 # ppm From 12ed9499a8a3e9eb89db0f4e689539760ff6cff5 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Thu, 21 Aug 2014 13:41:16 -0700 Subject: [PATCH 08/55] FIX: Fix numpy --- bin/parrec2nii | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 46a555f2a5..9b020277e0 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -257,7 +257,7 @@ def proc_file(infile, opts): pr_hdr.general_info['n_slices']) shape = (shape[0], shape[2], shape[1]) if order_rev else shape bvecs = np.reshape(pr_hdr.image_defs['diffusion'].T, shape) - bvecs = np.swapaxis(bvecs, 2, 1) if order_rev else bvecs + bvecs = np.swapaxes(bvecs, 2, 1) if order_rev else bvecs if not np.all(np.diff(bvecs, axis=2) == 0): raise RuntimeError('Could not output bvecs: inconsistent values') bvecs = bvecs[:, :, 0] From b4e5b6977c9b09c57e4575909d297a977fee4301 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Tue, 26 Aug 2014 16:56:04 -0700 Subject: [PATCH 09/55] FIX: Move most work to parrec --- bin/parrec2nii | 241 +++++++++++++--------------------- nibabel/__init__.py | 1 + nibabel/mriutils.py | 42 ++++++ nibabel/openers.py | 8 +- nibabel/parrec.py | 305 +++++++++++++++++++++++--------------------- 5 files changed, 300 insertions(+), 297 deletions(-) create mode 100644 nibabel/mriutils.py diff --git a/bin/parrec2nii b/bin/parrec2nii index 9b020277e0..043801681d 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -12,7 +12,7 @@ import nibabel import nibabel.parrec as pr import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext -from nibabel.volumeutils import seek_tell +from nibabel.volumeutils import seek_tell, array_to_file # global verbosity switch verbose_switch = False @@ -26,72 +26,82 @@ def get_opt_parser(): p.add_option( Option("-v", "--verbose", action="store_true", dest="verbose", - default=False, help="Make some noise.")) + default=False, + help="""Make some noise.""")) p.add_option( Option("-o", "--output-dir", action="store", type="string", - dest="outdir", default=None, help=""" -Destination directory for NIfTI files. Default: current directory.""")) + dest="outdir", default=None, + help="""Destination directory for NIfTI files. + Default: current directory.""")) p.add_option( Option("-c", "--compressed", action="store_true", dest="compressed", default=False, help="Whether to write compressed NIfTI files or not.")) p.add_option( Option("-p", "--permit-truncated", action="store_true", - dest="permit_truncated", default=False, help=""" -Permit conversion of truncated recordings. Support for this is experimental, -and results *must* be checked afterward for validity.""")) + dest="permit_truncated", default=False, + help="""Permit conversion of truncated recordings. Support for + this is experimental, and results *must* be checked + afterward for validity.""")) p.add_option( Option("-b", "--bvs", action="store_true", dest="bvs", default=False, - help=""" -Output bvals/bvecs files in addition to NIFTI image.""")) + help="""Output bvals/bvecs files in addition to NIFTI + image.""")) p.add_option( Option("-d", "--dwell-time", action="store_true", default=False, dest="dwell_time", - help=""" -Calculate the scan dwell time. If supplied, the magnetic field strength -should also be supplied using --field-strength (default 3). The field strength -must be supplied because it is not encoded in the PAR/REC format.""")) + help="""Calculate the scan dwell time. If supplied, the magnetic + field strength should also be supplied using + --field-strength (default 3). The field strength + must be supplied because it is not encoded in the + PAR/REC format.""")) p.add_option( Option("--field-strength", action="store", default=3., type="float", - dest="field_strength", help=""" -The magnetic field strength of the recording, only needed for --dwell-time. -The field strength must be supplied because it is not encoded in the PAR/REC -format.""")) + dest="field_strength", + help="""The magnetic field strength of the recording, only + needed for --dwell-time. The field strength must be + supplied because it is not encoded in the PAR/REC + format.""")) p.add_option( Option("--origin", action="store", dest="origin", default="scanner", - help=""" -Reference point of the q-form transformation of the NIfTI image. If 'scanner' -the (0,0,0) coordinates will refer to the scanner's iso center. If 'fov', this -coordinate will be the center of the recorded volume (field of view). Default: -'scanner'.""")) + help="""Reference point of the q-form transformation of the + NIfTI image. If 'scanner' the (0,0,0) coordinates will + refer to the scanner's iso center. If 'fov', this + coordinate will be the center of the recorded volume + (field of view). Default: 'scanner'.""")) p.add_option( - Option("--minmax", action="store", nargs=2, dest="minmax", help=""" -Mininum and maximum settings to be stored in the NIfTI header. If any of -them is set to 'parse', the scaled data is scanned for the actual minimum and -maximum. To bypass this potentially slow and memory intensive step (the data -has to be scaled and fully loaded into memory), fixed values can be provided as -space-separated pair, e.g. '5.4 120.4'. It is possible to set a fixed minimum -as scan for the actual maximum (and vice versa). Default: 'parse parse'.""")) + Option("--minmax", action="store", nargs=2, dest="minmax", + help="""Mininum and maximum settings to be stored in the NIfTI + header. If any of them is set to 'parse', the scaled + data is scanned for the actual minimum and maximum. + To bypass this potentially slow and memory intensive + step (the data has to be scaled and fully loaded into + memory), fixed values can be provided as space-separated + pair, e.g. '5.4 120.4'. It is possible to set a fixed + minimum as scan for the actual maximum (and vice versa). + Default: 'parse parse'.""")) p.set_defaults(minmax=('parse', 'parse')) p.add_option( Option("--store-header", action="store_true", dest="store_header", - default=False, help=""" -If set, all information from the PAR header is stored in an extension of -the NIfTI file header. Default: off""")) + default=False, + help="""If set, all information from the PAR header is stored + in an extension ofthe NIfTI file header. + Default: off""")) p.add_option( Option("--scaling", action="store", dest="scaling", default='dv', - help=""" -Choose data scaling setting. The PAR header defines two different data -scaling settings: 'dv' (values displayed on console) and 'fp' (floating point -values). Either one can be chosen, or scaling can be disabled completely -('off'). Note that neither method will actually scale the data, but just store -the corresponding settings in the NIfTI header, unless non-uniform scaling -is used, in which case the data is stored in the file in scaled form. -Default: 'dv'""")) + help="""Choose data scaling setting. The PAR header defines two + different data scaling settings: 'dv' (values displayed + on console) and 'fp' (floating point values). Either + one can be chosen, or scaling can be disabled completely + ('off'). Note that neither method will actually scale + the data, but just store the corresponding settings in + the NIfTI header, unless non-uniform scaling is used, + in which case the data is stored in the file in scaled + form. Default: 'dv'""")) p.add_option( Option("--overwrite", action="store_true", dest="overwrite", - default=False, help=""" -Overwrite file if it exists. Default: False""")) + default=False, + help="""Overwrite file if it exists. Default: False""")) return p @@ -122,29 +132,13 @@ def proc_file(infile, opts): raise IOError('Output file "%s" exists, use --overwrite to ' 'overwrite it' % outfilename) - # load the PAR header - pr_img = pr.load(infile, opts.permit_truncated) + # load the PAR header and data + scaling = None if opts.scaling == 'off' else opts.scaling + pr_img = pr.load(infile, opts.permit_truncated, scaling) pr_hdr = pr_img.header - - # get the raw unscaled data form the REC file raw_data = pr_img.dataobj.get_unscaled() - - # determine if our order is XYTZ instead of XYZT (Phillips scanners) - # If it's XYZT, then the slice number (Z) will iterate slowest (no diff) - order_rev = pr_hdr.order_xytz - if raw_data.ndim > 3 and order_rev: - verbose('XYTZ order detected, reordering data') - assert raw_data.flags['C_CONTIGUOUS'] is True - reorder = [0, 1, 3, 2] - raw_data = np.reshape(raw_data, [raw_data.shape[n] for n in reorder], - order='F') - raw_data = np.transpose(raw_data, reorder) - - # compute affine with desired origin affine = pr_hdr.get_affine(origin=opts.origin) - - # create an nifti image instance -- to get a matching header - nimg = nifti1.Nifti1Image(raw_data, affine) + nimg = nifti1.Nifti1Image(raw_data, affine, pr_hdr) nhdr = nimg.header if 'parse' in opts.minmax: @@ -165,26 +159,14 @@ def proc_file(infile, opts): nhdr.structarr['cal_max'] = float(opts.minmax[1]) # container for potential NIfTI1 header extensions - exts = nifti1.Nifti1Extensions() - if opts.store_header: + exts = nifti1.Nifti1Extensions() # dump the full PAR header content into an extension - fobj = open(infile, 'r') - hdr_dump = fobj.read() - dump_ext = nifti1.Nifti1Extension('comment', hdr_dump) - fobj.close() + with open(infile, 'r') as fobj: + hdr_dump = fobj.read() + dump_ext = nifti1.Nifti1Extension('comment', hdr_dump) exts.append(dump_ext) - - # put any extensions into the image - nimg.extra['extensions'] = exts - - # image description - descr = "%s;%s;%s;%s" % ( - pr_hdr.general_info['exam_name'], - pr_hdr.general_info['patient_name'], - pr_hdr.general_info['exam_date'].replace(' ', ''), - pr_hdr.general_info['protocol_name']) - nhdr.structarr['descrip'] = descr[:80] + nimg.extra['extensions'] = exts if pr_hdr.general_info['max_dynamics'] > 1: # fMRI @@ -194,25 +176,6 @@ def proc_file(infile, opts): else: # anatomical or DTI nhdr.set_xyzt_units('mm', 'unknown') - - # get original scaling, and decide if we scale in-place or not - scale_data = False - if opts.scaling == 'off': - slope = 1. - intercept = 0. - else: - verbose('Using data scaling "%s"' % opts.scaling) - slope, intercept = pr_hdr.get_data_scaling(method=opts.scaling) - uni_s = np.unique(slope) - uni_i = np.unique(intercept) - if len(uni_s) == 1 and len(uni_i) == 1: - slope = uni_s[0] - intercept = uni_i[0] - else: - verbose("Multiple scale-factors detected, data will be saved " - "in scaled form") - scale_data = True - # finalize the header: set proper data offset, pixdims, ... nimg.update_header() @@ -223,76 +186,50 @@ def proc_file(infile, opts): else: outfile = open(outfilename, 'wb') - try: - verbose('Writing %s' % outfilename) - # first write the header - if scale_data: - data = ((raw_data * slope) + intercept).astype(raw_data.dtype) - nhdr.set_slope_inter(1., 0.) - else: - nhdr.set_slope_inter(slope, intercept) - data = raw_data - nhdr.write_to(outfile) - # Now the data - offset = nhdr.get_data_offset() - seek_tell(outfile, offset, write0=True) - nibabel.volumeutils.array_to_file(data, outfile, offset=offset) - finally: - outfile.close() + # get original scaling, and decide if we scale in-place or not + if opts.scaling == 'off': + slope = np.array([1.]) + intercept = np.array([0.]) + else: + verbose('Using data scaling "%s"' % opts.scaling) + slope, intercept = pr_hdr.get_data_scaling(method=opts.scaling) + verbose('Writing %s' % outfilename) + if not np.any(np.diff(slope)) and not np.any(np.diff(intercept)): + # Single scalefactor case + nhdr.set_slope_inter(slope.ravel()[0], intercept.ravel()[0]) + data = raw_data + else: + # Multi scalefactor case + nhdr.set_slope_inter(1, 0) + nhdr.set_data_dtype(np.float64) + data = pr_img.dataobj.__array__() + nhdr.write_to(outfile) + offset = nhdr.get_data_offset() + seek_tell(outfile, offset, write0=True) + array_to_file(data, outfile, offset=offset) # write out bvals/bvecs if requested if opts.bvs and pr_hdr.general_info['n_dti_volumes'] > 1: verbose('Writing .bvals and .bvecs files') - # bvals - gen_info = pr_hdr.general_info - shape = (gen_info['n_dti_volumes'], gen_info['n_slices']) - shape = (shape[1], shape[0]) if order_rev else shape - bvals = np.reshape(pr_hdr.image_defs['diffusion_b_factor'], shape) - bvals = bvals.T if order_rev else bvals - if not np.all(np.diff(bvals, axis=1) == 0): - raise RuntimeError('Could not output bvals: inconsistent values') - - # bvecs - shape = (3, pr_hdr.general_info['n_dti_volumes'], - pr_hdr.general_info['n_slices']) - shape = (shape[0], shape[2], shape[1]) if order_rev else shape - bvecs = np.reshape(pr_hdr.image_defs['diffusion'].T, shape) - bvecs = np.swapaxes(bvecs, 2, 1) if order_rev else bvecs - if not np.all(np.diff(bvecs, axis=2) == 0): - raise RuntimeError('Could not output bvecs: inconsistent values') - bvecs = bvecs[:, :, 0] - - # restore XYZ order to bvecs - assert np.all(bvecs[:, 0] == 0) # first col should be zeros - order = [] - mults = [] - for ii in range(3): - idx = np.where(bvecs[:, ii+1] != 0)[0] - assert len(idx) == 1 - order.append(idx[0]) - mults.append(bvecs[idx[0], ii+1]) - assert np.array_equal(np.unique(order), [0, 1, 2]) - assert np.array_equal(np.abs(mults), [1, 1, 1]) - bvecs = bvecs[order] * np.array(mults)[:, np.newaxis] - bvecs[bvecs == -0.0] = 0.0 - - # Actually write files + bvals, bvecs = pr_hdr.get_bvals_bvecs() bval_fname = basefilename + '.bvals' bvec_fname = basefilename + '.bvecs' with open(bval_fname, 'w') as fid: # np.savetxt could do this, but it's just a loop anyway - for val in bvals[:, 0]: + for val in bvals: fid.write('%s ' % val) fid.write('\n') with open(bvec_fname, 'w') as fid: - for row in bvecs: + for row in bvecs.T: for val in row: fid.write('%s ' % val) fid.write('\n') elif opts.bvs: verbose('No DTI volumes detected, bvals and bvecs not written') if opts.dwell_time: - dwell_time = pr_hdr.get_dwell_time(opts.field_strength) + dwell_time = nibabel.calculate_dwell_time( + pr_hdr.get_water_fat_shift(), pr_hdr.get_echo_train_length(), + opts.field_strength) if dwell_time is None: verbose('No EPI factors, dwell time not written') else: @@ -322,6 +259,8 @@ def main(): proc_file(infile, opts) except Exception as e: errs.append('%s: %s' % (infile, e)) + raise # XXX REMOVE + print('') if len(errs): error('Caught %i exceptions. Dump follows:\n\n %s' diff --git a/nibabel/__init__.py b/nibabel/__init__.py index 8178052de1..16cbce79f8 100644 --- a/nibabel/__init__.py +++ b/nibabel/__init__.py @@ -63,6 +63,7 @@ apply_orientation, aff2axcodes) from .imageclasses import class_map, ext_map from . import trackvis +from .mriutils import calculate_dwell_time # be friendly on systems with ancient numpy -- no tests, but at least # importable diff --git a/nibabel/mriutils.py b/nibabel/mriutils.py new file mode 100644 index 0000000000..1a230571c0 --- /dev/null +++ b/nibabel/mriutils.py @@ -0,0 +1,42 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +""" +Utilities for calculations related to MRI +""" + +__all__ = ['calculate_dwell_time'] + +GYROMAGNETIC_RATIO = 42.576 # MHz/T for nucleus +PROTON_WATER_FAT_SHIFT = 3.4 # ppm + + +def calculate_dwell_time(water_fat_shift, echo_train_length, + field_strength=3.0): + """Calculate the dwell time + + Parameters + ---------- + water_fat_shift : float + The water fat shift of the recording, in pixels. + echo_train_length : int + The echo train length of the imaging sequence. + field_strength : float + Strength of the magnet in T, e.g. ``3.0`` for a 3T magnet + recording. Providing this value is necessary because the + field strength is not encoded in the PAR file. + + Returns + ------- + dwell_time : float + The dwell time in seconds. Returns None if the dwell + time cannot be calculated (i.e., not using an EPI sequence). + """ + field_strength = float(field_strength) # Tesla + assert field_strength > 0. + if echo_train_length <= 0: + return None + # constants + dwell_time = ((echo_train_length - 1) * water_fat_shift / + (GYROMAGNETIC_RATIO * PROTON_WATER_FAT_SHIFT + * field_strength * (echo_train_length + 1))) + return dwell_time diff --git a/nibabel/openers.py b/nibabel/openers.py index 74857496c9..64198cff33 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -9,7 +9,7 @@ """ Context manager openers for various fileobject types """ -from os.path import splitext +from os.path import splitext, isfile import gzip import bz2 @@ -49,6 +49,12 @@ def __init__(self, fileish, *args, **kwargs): self._name = None return _, ext = splitext(fileish) + # allow for ext to be lower or upper case + if not isfile(fileish): + if isfile(_ + ext.upper()): + fileish = _ + ext.upper() + else: + raise IOError('File not found: %s' % fileish) if ext in self.compress_ext_map: is_compressor = True opener, arg_names = self.compress_ext_map[ext] diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 88112619d9..4e5f497730 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -86,29 +86,28 @@ from .spatialimages import SpatialImage, Header from .eulerangles import euler2mat -from .volumeutils import Recoder, array_from_file, apply_read_scaling -from .arrayproxy import ArrayProxy -from .affines import from_matvec, dot_reduce +from .volumeutils import Recoder, array_from_file, BinOpener +from .affines import from_matvec, dot_reduce, apply_affine # PSL to RAS affine -PSL_TO_RAS = np.array([[0, 0, -1, 0], # L -> R - [-1, 0, 0, 0], # P -> A - [0, 1, 0, 0], # S -> S +PSL_TO_RAS = np.array([[0, 0, -1, 0], # L -> R + [-1, 0, 0, 0], # P -> A + [0, 1, 0, 0], # S -> S [0, 0, 0, 1]]) # Acquisition (tra/sag/cor) to PSL axes # These come from looking at transverse, sagittal, coronal datasets where we # can see the LR, PA, SI orientation of the slice axes from the scanned object ACQ_TO_PSL = dict( - transverse = np.array([[ 0, 1, 0, 0], # P - [ 0, 0, 1, 0], # S - [ 1, 0, 0, 0], # L - [ 0, 0, 0, 1]]), - sagittal = np.diag([1, -1, -1, 1]), - coronal = np.array([[ 0, 0, 1, 0], # P - [ 0, -1, 0, 0], # S - [ 1, 0, 0, 0], # L - [ 0, 0, 0, 1]]) + transverse=np.array([[0, 1, 0, 0], # P + [0, 0, 1, 0], # S + [1, 0, 0, 0], # L + [0, 0, 0, 1]]), + sagittal=np.diag([1, -1, -1, 1]), + coronal=np.array([[0, 0, 1, 0], # P + [0, -1, 0, 0], # S + [1, 0, 0, 0], # L + [0, 0, 0, 1]]) ) # PAR header versions we claim to understand supported_versions = ['V4.2'] @@ -331,10 +330,13 @@ def parse_PAR_header(fobj, permit_truncated=False): # there is some awkward exception to this rule for b-values > 2 # XXX need to get test image... max_dti_volumes = ((general_info['max_diffusion_values'] - 1) - * general_info['max_gradient_orient']) + 1 + * general_info['max_gradient_orient']) n_b = len(np.unique(image_defs['diffusion b value number'])) n_grad = len(np.unique(image_defs['gradient orientation number'])) - n_dti_volumes = (n_b - 1) * n_grad + 1 + n_dti_volumes = (n_b - 1) * n_grad + # XXX TODO This needs to be a conditional! + max_dti_volumes += 1 + n_dti_volumes += 1 n_slices = len(np.unique(image_defs['slice number'])) n_echoes = len(np.unique(image_defs['echo number'])) n_dynamics = len(np.unique(image_defs['dynamic scan number'])) @@ -355,9 +357,76 @@ def parse_PAR_header(fobj, permit_truncated=False): return general_info, image_defs +def _data_from_rec(rec_fileobj, in_shape, dtype, slice_indices, out_shape, + scaling=None): + """Get data from REC file + + Parameters + ---------- + rec_fileobj : file-like + The file to process. + in_shape : tuple + The input shape inferred from the PAR file. + dtype : dtype + The datatype. + slice_indices : array of int + The indices used to re-index the resulting array properly. + out_shape : tuple + The output shape. + scaling : array | None + Scaling to use. + + Returns + ------- + data : array + The scaled and sorted array. + """ + rec_data = array_from_file(in_shape, dtype, rec_fileobj) + rec_data = rec_data[..., slice_indices] + rec_data = rec_data.reshape(out_shape, order='F') + if not scaling is None: + # Don't do in-place b/c this goes int16 -> float64 + rec_data = rec_data * scaling[0] + scaling[1] + return rec_data + + +class PARRECArrayProxy(object): + def __init__(self, file_like, header, scaling): + self.file_like = file_like + # Copies of values needed to read array + self._shape = header.get_data_shape() + self._dtype = header.get_data_dtype() + self._slice_indices = header.sorted_slice_indices + self._slice_scaling = header.get_data_scaling(scaling) + self._rec_shape = header.get_rec_shape() + + @property + def shape(self): + return self._shape + + @property + def dtype(self): + return self._dtype + + @property + def is_proxy(self): + return True + + def get_unscaled(self): + with BinOpener(self.file_like) as fileobj: + return _data_from_rec(fileobj, self._rec_shape, self._dtype, + self._slice_indices, self._shape) + + def __array__(self): + with BinOpener(self.file_like) as fileobj: + return _data_from_rec(fileobj, self._rec_shape, self._dtype, + self._slice_indices, self._shape, + scaling=self._slice_scaling) + + class PARRECHeader(Header): """PAR/REC header""" - def __init__(self, info, image_defs, default_scaling='dv'): + def __init__(self, info, image_defs): """ Parameters ---------- @@ -367,14 +436,10 @@ def __init__(self, info, image_defs, default_scaling='dv'): image_defs : array Structured array with image definitions from the PAR file (as returned by `parse_PAR_header()`). - default_scaling : {'dv', 'fp'} - Default scaling method to use for :meth:`get_slope_inter`` - see - :meth:`get_data_scaling` for detail """ self.general_info = info self.image_defs = image_defs self._slice_orientation = None - self.default_scaling = default_scaling # charge with basic properties to be able to use base class # functionality # dtype @@ -403,42 +468,52 @@ def copy(self): return PARRECHeader(deepcopy(self.general_info), self.image_defs.copy()) - @property - def order_xytz(self): - order_rev = np.diff(self.image_defs['slice number'][:2])[0] - assert order_rev in (0, 1) - order_rev = (order_rev == 0) - return order_rev + def as_analyze_map(self): + return dict(descr="%s;%s;%s;%s" + % (self.general_info['exam_name'], + self.general_info['patient_name'], + self.general_info['exam_date'].replace(' ', ''), + self.general_info['protocol_name'])) - def get_dwell_time(self, field_strength=3.): - """Calculate the dwell time of the recording + def get_water_fat_shift(self): + """Water fat shift, in pixels""" + return self.general_info['water_fat_shift'] - Parameters - ---------- - field_strength : float - Strength of the magnet in T, e.g. ``3.0`` for a 3T magnet - recording. Providing this value is necessary because the - field strength is not encoded in the PAR file. + def get_echo_train_length(self): + """Echo train length of the recording""" + return self.general_info['epi_factor'] + + def get_q_vectors(self): + """Get Q vectors from the data Returns ------- - dwell_time : float - The dwell time in seconds. Returns None if the dwell - time cannot be calculated (i.e., not using an EPI sequence). + q_vectors : array + Array of q vectors (bvals * bvecs). """ - field_strength = float(field_strength) # Tesla - assert field_strength > 0. - water_fat_shift = self.general_info['water_fat_shift'] # pixels - echo_train_length = self.general_info['epi_factor'] # int - if echo_train_length <= 0: - return None - # constants - gyromagnetic_ratio = 42.57 # MHz/T - proton_water_fat_shift = 3.4 # ppm - dwell_time = ((echo_train_length - 1) * water_fat_shift / - (gyromagnetic_ratio * proton_water_fat_shift - * field_strength * (echo_train_length + 1))) - return dwell_time + bvals, bvecs = self.get_bvals_bvecs() + return bvecs * bvals[:, np.newaxis] + + def get_bvals_bvecs(self): + """Get bvals and bvecs from data + + Returns + ------- + b_vals : array + Array of b values, shape (n_directions,). + b_vectors : array + Array of b vectors, shape (n_directions, 3). + """ + reorder = self.sorted_slice_indices + bvals = self.image_defs['diffusion_b_factor'][reorder] + bvecs = self.image_defs['diffusion'][reorder] + shape = self.get_data_shape_in_file() + bvals = bvals[::shape[-1]] + bvecs = bvecs[::shape[-1]] + # rotate bvecs to match stored image orientation + permute_to_psl = ACQ_TO_PSL[self.get_slice_orientation()] + bvecs = apply_affine(np.linalg.inv(permute_to_psl), bvecs) + return bvals, bvecs def _get_unique_image_prop(self, name): """Scan image definitions and return unique value of a property. @@ -471,32 +546,6 @@ def _get_unique_image_prop(self, name): else: return np.array([uprop[0] for uprop in uprops]) - def _get_broadcastable_prop(self, name): - """Scan image definitions and return broadcastable value of a property. - - If the requested property is an array this method gives - scale factors that can be combined and applied to the raw data. - - Parameters - ---------- - name : str - Name of the property - - Returns - ------- - value : array - """ - dims = self.get_data_shape_in_file()[2:] - prop = self.image_defs[name] - # this will break for truncated recs, but it's probably okay for now - assert np.prod(dims) == len(prop) - if self.order_xytz: - prop = prop.reshape(dims) - else: - prop = prop.reshape(dims[::-1]).T - prop = prop[np.newaxis, np.newaxis, ...] # array expansion - return prop - def get_voxel_size(self): """Returns the spatial extent of a voxel. @@ -679,24 +728,23 @@ def get_data_scaling(self, method="dv"): FP = DV / (RS * SS) """ # These will be 3D or 4D - scale_slope = self._get_broadcastable_prop('scale slope') - rescale_slope = self._get_broadcastable_prop('rescale slope') - rescale_intercept = self._get_broadcastable_prop('rescale intercept') - + scale_slope = self.image_defs['scale slope'] + rescale_slope = self.image_defs['rescale slope'] + rescale_intercept = self.image_defs['rescale intercept'] if method == 'dv': - slope = rescale_slope - intercept = rescale_intercept + slope, intercept = rescale_slope, rescale_intercept elif method == 'fp': slope = 1.0 / scale_slope intercept = rescale_intercept / (rescale_slope * scale_slope) else: raise ValueError("Unknown scling method '%s'." % method) - return (slope, intercept) - - def get_slope_inter(self): - """ Utility method to get default slope, intercept scaling arrays - """ - return self.get_data_scaling(method=self.default_scaling) + reorder = self.sorted_slice_indices + slope = slope[reorder] + intercept = intercept[reorder] + shape = (1, 1) + self.get_data_shape()[2:] + slope = slope.reshape(shape, order='F') + intercept = intercept.reshape(shape, order='F') + return slope, intercept def get_slice_orientation(self): """Returns the slice orientation label. @@ -706,56 +754,26 @@ def get_slice_orientation(self): orientation : {'transverse', 'sagittal', 'coronal'} """ if self._slice_orientation is None: - self._slice_orientation = \ - slice_orientation_codes.label[ - self._get_unique_image_prop('slice orientation')[0]] + lab = self._get_unique_image_prop('slice orientation')[0] + self._slice_orientation = slice_orientation_codes.label[lab] return self._slice_orientation - def raw_data_from_fileobj(self, fileobj): - ''' Read unscaled data array from `fileobj` - - Array axes correspond to x,y,z,t. For other orderings, you - must reorder after the fact. - - Parameters - ---------- - fileobj : file-like - Must be open, and implement ``read`` and ``seek`` methods - - Returns - ------- - arr : ndarray - unscaled data array - ''' - dtype = self.get_data_dtype() - shape = self.get_data_shape() - offset = self.get_data_offset() - return array_from_file(shape, dtype, fileobj, offset) - - def data_from_fileobj(self, fileobj): - ''' Read scaled data array from `fileobj` - - Use this routine to get the scaled image data from an image file - `fileobj`, given a header `self`. "Scaled" means, with any header - scaling factors applied to the raw data in the file. Use - `raw_data_from_fileobj` to get the raw data. + def get_rec_shape(self): + inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) + return inplane_shape + (len(self.image_defs),) - Parameters - ---------- - fileobj : file-like - Must be open, and implement ``read`` and ``seek`` methods + @property + def sorted_slice_indices(self): + """Indices to sort (and maybe discard) slices in REC file - Returns - ------- - arr : ndarray - scaled data array - ''' - # read unscaled data - data = self.raw_data_from_fileobj(fileobj) - # get scalings from header. Value of None means not present in header - slope, inter = self.get_slope_inter() - # Upcast as necessary for big slopes, intercepts - return apply_read_scaling(data, slope, inter) + Returns list for indexing into a single dimension of an array. + """ + # No attempt to detect missing combinations or early stop + keys = ['slice number', 'scanning sequence', 'image_type_mr', + 'gradient orientation number', 'dynamic scan number', + 'echo number'] + keys = [self.image_defs[k] for k in keys] + return np.lexsort(keys) class PARRECImage(SpatialImage): @@ -763,24 +781,21 @@ class PARRECImage(SpatialImage): header_class = PARRECHeader files_types = (('image', '.rec'), ('header', '.par')) - ImageArrayProxy = ArrayProxy + ImageArrayProxy = PARRECArrayProxy @classmethod - def from_file_map(klass, file_map, permit_truncated=False): + def from_file_map(klass, file_map, permit_truncated, scaling): pt = permit_truncated with file_map['header'].get_prepare_fileobj('rt') as hdr_fobj: hdr = klass.header_class.from_fileobj(hdr_fobj, permit_truncated=pt) rec_fobj = file_map['image'].get_prepare_fileobj() - data = klass.ImageArrayProxy(rec_fobj, hdr) - return klass(data, - hdr.get_affine(), - header=hdr, - extra=None, + data = klass.ImageArrayProxy(rec_fobj, hdr, scaling) + return klass(data, hdr.get_affine(), header=hdr, extra=None, file_map=file_map) -def load(filename, permit_truncated=False): +def load(filename, permit_truncated=False, scaling='dv'): file_map = PARRECImage.filespec_to_file_map(filename) - return PARRECImage.from_file_map(file_map, permit_truncated) + return PARRECImage.from_file_map(file_map, permit_truncated, scaling) load.__doc__ = PARRECImage.load.__doc__ From 702ab1505e0c149fca442656375fd4352751e650 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 27 Aug 2014 13:53:33 -0700 Subject: [PATCH 10/55] FIX: Fix for truncated data --- bin/parrec2nii | 20 +++++--------------- nibabel/parrec.py | 33 +++++++++++++++++++++++---------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 043801681d..e655e25c50 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -167,15 +167,6 @@ def proc_file(infile, opts): dump_ext = nifti1.Nifti1Extension('comment', hdr_dump) exts.append(dump_ext) nimg.extra['extensions'] = exts - - if pr_hdr.general_info['max_dynamics'] > 1: - # fMRI - nhdr.structarr['pixdim'][4] = pr_hdr.general_info['repetition_time'] - # store units -- always mm and msec - nhdr.set_xyzt_units('mm', 'msec') - else: - # anatomical or DTI - nhdr.set_xyzt_units('mm', 'unknown') # finalize the header: set proper data offset, pixdims, ... nimg.update_header() @@ -212,20 +203,20 @@ def proc_file(infile, opts): if opts.bvs and pr_hdr.general_info['n_dti_volumes'] > 1: verbose('Writing .bvals and .bvecs files') bvals, bvecs = pr_hdr.get_bvals_bvecs() - bval_fname = basefilename + '.bvals' - bvec_fname = basefilename + '.bvecs' - with open(bval_fname, 'w') as fid: + with open(basefilename + '.bvals', 'w') as fid: # np.savetxt could do this, but it's just a loop anyway for val in bvals: fid.write('%s ' % val) fid.write('\n') - with open(bvec_fname, 'w') as fid: + with open(basefilename + '.bvecs', 'w') as fid: for row in bvecs.T: for val in row: fid.write('%s ' % val) fid.write('\n') elif opts.bvs: verbose('No DTI volumes detected, bvals and bvecs not written') + + # write out dwell time if requested if opts.dwell_time: dwell_time = nibabel.calculate_dwell_time( pr_hdr.get_water_fat_shift(), pr_hdr.get_echo_train_length(), @@ -235,8 +226,7 @@ def proc_file(infile, opts): else: verbose('Writing dwell time (%r sec) calculated assuming %sT ' 'magnet' % (dwell_time, opts.field_strength)) - dwell_fname = basefilename + '.dwell_time' - with open(dwell_fname, 'w') as fid: + with open(basefilename + '.dwell_time', 'w') as fid: fid.write('%r\n' % dwell_time) # done diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 4e5f497730..2ec9c011ea 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -88,6 +88,7 @@ from .eulerangles import euler2mat from .volumeutils import Recoder, array_from_file, BinOpener from .affines import from_matvec, dot_reduce, apply_affine +from .nifti1 import unit_codes # PSL to RAS affine PSL_TO_RAS = np.array([[0, 0, -1, 0], # L -> R @@ -469,11 +470,20 @@ def copy(self): self.image_defs.copy()) def as_analyze_map(self): - return dict(descr="%s;%s;%s;%s" - % (self.general_info['exam_name'], - self.general_info['patient_name'], - self.general_info['exam_date'].replace(' ', ''), - self.general_info['protocol_name'])) + """Convert PAR parameters to NIFTI1 format""" + # Entries in the dict correspond to the parameters found in + # the NIfTI1 header, specifically in nifti1.py `header_dtd` defs. + # Here we set the parameters we can to simplify PAR/REC + # to NIfTI conversion. + descr = ("%s;%s;%s;%s" + % (self.general_info['exam_name'], + self.general_info['patient_name'], + self.general_info['exam_date'].replace(' ', ''), + self.general_info['protocol_name']))[:80] # max len + is_fmri = (self.general_info['max_dynamics'] > 1) + t = 'msec' if is_fmri else 'unknown' + xyzt_units = unit_codes['mm'] + unit_codes[t] + return dict(descr=descr, xyzt_units=xyzt_units) # , pixdim=pixdim) def get_water_fat_shift(self): """Water fat shift, in pixels""" @@ -507,7 +517,7 @@ def get_bvals_bvecs(self): reorder = self.sorted_slice_indices bvals = self.image_defs['diffusion_b_factor'][reorder] bvecs = self.image_defs['diffusion'][reorder] - shape = self.get_data_shape_in_file() + shape = self.get_data_shape() bvals = bvals[::shape[-1]] bvecs = bvecs[::shape[-1]] # rotate bvecs to match stored image orientation @@ -595,12 +605,9 @@ def _get_zooms(self): # voxel size (inplaneX, inplaneY, slices) zooms[:3] = self.get_voxel_size() zooms[2] += slice_gap - # time axis? if len(zooms) > 3 and self.general_info['n_dynamics'] > 1: - # DTI also has 4D # Convert time from milliseconds to seconds zooms[3] = self.general_info['repetition_time'] / 1000. - # we leave it at the default (1) for 4D echo data return zooms def get_affine(self, origin='scanner'): @@ -767,13 +774,19 @@ def sorted_slice_indices(self): """Indices to sort (and maybe discard) slices in REC file Returns list for indexing into a single dimension of an array. + + If the recording is truncated, this will take care of discarding + any indices that are not meant to be used. """ # No attempt to detect missing combinations or early stop keys = ['slice number', 'scanning sequence', 'image_type_mr', 'gradient orientation number', 'dynamic scan number', 'echo number'] keys = [self.image_defs[k] for k in keys] - return np.lexsort(keys) + # Figure out how many we need to remove from the end, and trim them + # Based on our sorting, they should always be last + n_used = np.prod(self.get_data_shape()[2:]) + return np.lexsort(keys)[:n_used] class PARRECImage(SpatialImage): From d27761c1a742740ca59fd9b471a6b4e74fdfed63 Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 27 Aug 2014 15:53:55 -0700 Subject: [PATCH 11/55] FIX: Fix tests --- nibabel/tests/test_parrec.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 2e56d1b986..3dc012a78a 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -96,25 +96,25 @@ def test_header(): assert_equal(hdr.get_data_dtype(), np.dtype(np.int16)) assert_equal(hdr.get_zooms(), (3.75, 3.75, 8.0, 2.0)) assert_equal(hdr.get_data_offset(), 0) - assert_almost_equal(hdr.get_slope_inter(), - (1.2903541326522827, 0.0), 5) + si = np.array([np.unique(x) for x in hdr.get_data_scaling()]).ravel() + assert_almost_equal(si, (1.2903541326522827, 0.0), 5) def test_header_scaling(): hdr = PARRECHeader(HDR_INFO, HDR_DEFS) - fp_scaling = np.squeeze(hdr.get_data_scaling('fp')) - dv_scaling = np.squeeze(hdr.get_data_scaling('dv')) + def_scaling = [np.unique(x) for x in hdr.get_data_scaling()] + fp_scaling = [np.unique(x) for x in hdr.get_data_scaling('fp')] + dv_scaling = [np.unique(x) for x in hdr.get_data_scaling('dv')] # Check default is dv scaling - assert_array_equal(np.squeeze(hdr.get_data_scaling()), dv_scaling) + assert_array_equal(def_scaling, dv_scaling) # And that it's almost the same as that from the converted nifti - assert_almost_equal(dv_scaling, (1.2903541326522827, 0.0), 5) + assert_almost_equal(dv_scaling, [[1.2903541326522827], [0.0]], 5) # Check that default for get_slope_inter is dv scaling - for hdr in (hdr, PARRECHeader(HDR_INFO, HDR_DEFS, default_scaling='dv')): - assert_array_equal(hdr.get_slope_inter(), dv_scaling) + for hdr in (hdr, PARRECHeader(HDR_INFO, HDR_DEFS)): + scaling = [np.unique(x) for x in hdr.get_data_scaling()] + assert_array_equal(scaling, dv_scaling) # Check we can change the default assert_false(np.all(fp_scaling == dv_scaling)) - fp_hdr = PARRECHeader(HDR_INFO, HDR_DEFS, default_scaling='fp') - assert_array_equal(fp_hdr.get_slope_inter(), fp_scaling) def test_orientation(): @@ -146,7 +146,7 @@ def test_affine(): fov = hdr.get_affine(origin='fov') assert_array_equal(default, scanner) # rotation part is same - assert_array_equal(scanner[:3, :3], fov[:3,:3]) + assert_array_equal(scanner[:3, :3], fov[:3, :3]) # offset not assert_false(np.all(scanner[:3, 3] == fov[:3, 3])) # Regression test against what we were getting before From 4580262c9ca7800861f5e8e7c7ff8181eec0885c Mon Sep 17 00:00:00 2001 From: Eric Larson Date: Wed, 27 Aug 2014 15:59:34 -0700 Subject: [PATCH 12/55] FIX: Fix openers --- nibabel/openers.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/nibabel/openers.py b/nibabel/openers.py index 64198cff33..ab71161394 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -53,8 +53,6 @@ def __init__(self, fileish, *args, **kwargs): if not isfile(fileish): if isfile(_ + ext.upper()): fileish = _ + ext.upper() - else: - raise IOError('File not found: %s' % fileish) if ext in self.compress_ext_map: is_compressor = True opener, arg_names = self.compress_ext_map[ext] From 30cab8232cf251fcadd928f5764110e1eacb6685 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 25 Sep 2014 21:01:00 -0400 Subject: [PATCH 13/55] BF: fix the help string formatting Help strings got a bit wonky with the PEP8 changes. --- bin/parrec2nii | 96 +++++++++++++++++++++++++++----------------------- 1 file changed, 52 insertions(+), 44 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index e655e25c50..39e18fd3ef 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -18,12 +18,15 @@ from nibabel.volumeutils import seek_tell, array_to_file verbose_switch = False +def _oneline(big_str): + return ' '.join(L.strip() for L in big_str.splitlines()) + + def get_opt_parser(): # use module docstring for help output p = OptionParser( usage="%s [OPTIONS] \n\n" % sys.argv[0] + __doc__, version="%prog " + nibabel.__version__) - p.add_option( Option("-v", "--verbose", action="store_true", dest="verbose", default=False, @@ -31,8 +34,8 @@ def get_opt_parser(): p.add_option( Option("-o", "--output-dir", action="store", type="string", dest="outdir", default=None, - help="""Destination directory for NIfTI files. - Default: current directory.""")) + help=_oneline("""Destination directory for NIfTI files. + Default: current directory."""))) p.add_option( Option("-c", "--compressed", action="store_true", dest="compressed", default=False, @@ -40,68 +43,73 @@ def get_opt_parser(): p.add_option( Option("-p", "--permit-truncated", action="store_true", dest="permit_truncated", default=False, - help="""Permit conversion of truncated recordings. Support for - this is experimental, and results *must* be checked - afterward for validity.""")) + help=_oneline( + """Permit conversion of truncated recordings. Support for + this is experimental, and results *must* be checked + afterward for validity."""))) p.add_option( Option("-b", "--bvs", action="store_true", dest="bvs", default=False, - help="""Output bvals/bvecs files in addition to NIFTI - image.""")) + help=_oneline( + """Output bvals/bvecs files in addition to NIFTI + image."""))) p.add_option( Option("-d", "--dwell-time", action="store_true", default=False, dest="dwell_time", - help="""Calculate the scan dwell time. If supplied, the magnetic - field strength should also be supplied using - --field-strength (default 3). The field strength - must be supplied because it is not encoded in the - PAR/REC format.""")) + help=_oneline( + """Calculate the scan dwell time. If supplied, the magnetic + field strength should also be supplied using + --field-strength (default 3). The field strength must be + supplied because it is not encoded in the PAR/REC + format."""))) p.add_option( Option("--field-strength", action="store", default=3., type="float", dest="field_strength", - help="""The magnetic field strength of the recording, only - needed for --dwell-time. The field strength must be - supplied because it is not encoded in the PAR/REC - format.""")) + help=_oneline( + """The magnetic field strength of the recording, only needed + for --dwell-time. The field strength must be supplied + because it is not encoded in the PAR/REC format."""))) p.add_option( Option("--origin", action="store", dest="origin", default="scanner", - help="""Reference point of the q-form transformation of the - NIfTI image. If 'scanner' the (0,0,0) coordinates will - refer to the scanner's iso center. If 'fov', this - coordinate will be the center of the recorded volume - (field of view). Default: 'scanner'.""")) + help=_oneline( + """Reference point of the q-form transformation of the NIfTI + image. If 'scanner' the (0,0,0) coordinates will refer to + the scanner's iso center. If 'fov', this coordinate will be + the center of the recorded volume (field of view). Default: + 'scanner'."""))) p.add_option( Option("--minmax", action="store", nargs=2, dest="minmax", - help="""Mininum and maximum settings to be stored in the NIfTI - header. If any of them is set to 'parse', the scaled - data is scanned for the actual minimum and maximum. - To bypass this potentially slow and memory intensive - step (the data has to be scaled and fully loaded into - memory), fixed values can be provided as space-separated - pair, e.g. '5.4 120.4'. It is possible to set a fixed - minimum as scan for the actual maximum (and vice versa). - Default: 'parse parse'.""")) + help=_oneline( + """Mininum and maximum settings to be stored in the NIfTI + header. If any of them is set to 'parse', the scaled data is + scanned for the actual minimum and maximum. To bypass this + potentially slow and memory intensive step (the data has to + be scaled and fully loaded into memory), fixed values can be + provided as space-separated pair, e.g. '5.4 120.4'. It is + possible to set a fixed minimum as scan for the actual + maximum (and vice versa). Default: 'parse parse'."""))) p.set_defaults(minmax=('parse', 'parse')) p.add_option( Option("--store-header", action="store_true", dest="store_header", default=False, - help="""If set, all information from the PAR header is stored - in an extension ofthe NIfTI file header. - Default: off""")) + help=_oneline( + """If set, all information from the PAR header is stored in + an extension ofthe NIfTI file header. Default: off"""))) p.add_option( Option("--scaling", action="store", dest="scaling", default='dv', - help="""Choose data scaling setting. The PAR header defines two - different data scaling settings: 'dv' (values displayed - on console) and 'fp' (floating point values). Either - one can be chosen, or scaling can be disabled completely - ('off'). Note that neither method will actually scale - the data, but just store the corresponding settings in - the NIfTI header, unless non-uniform scaling is used, - in which case the data is stored in the file in scaled - form. Default: 'dv'""")) + help=_oneline( + """Choose data scaling setting. The PAR header defines two + different data scaling settings: 'dv' (values displayed on + console) and 'fp' (floating point values). Either one can be + chosen, or scaling can be disabled completely ('off'). Note + that neither method will actually scale the data, but just + store the corresponding settings in the NIfTI header, unless + non-uniform scaling is used, in which case the data is + stored in the file in scaled form. Default: 'dv'"""))) p.add_option( Option("--overwrite", action="store_true", dest="overwrite", default=False, - help="""Overwrite file if it exists. Default: False""")) + help=_oneline("""Overwrite file if it exists. Default: + False"""))) return p From 18a597642219af1c0b3eef7b0661373fb6c3b821 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Wed, 1 Oct 2014 16:42:08 -0400 Subject: [PATCH 14/55] DOC+TST: test array_to_file can use array-like ``array_to_file`` in fact accepts array-like. --- nibabel/tests/test_utils.py | 5 +++++ nibabel/volumeutils.py | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/nibabel/tests/test_utils.py b/nibabel/tests/test_utils.py index 47a3c77e23..d6787357ec 100644 --- a/nibabel/tests/test_utils.py +++ b/nibabel/tests/test_utils.py @@ -131,6 +131,11 @@ def test_array_to_file(): data_back = write_return(arr, str_io, ndt, 0, intercept, scale) assert_array_almost_equal(arr, data_back) + # Test array-like + str_io = BytesIO() + array_to_file(arr.tolist(), str_io, float) + data_back = array_from_file(arr.shape, float, str_io) + assert_array_almost_equal(arr, data_back) def test_a2f_intercept_scale(): diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index dac2c2a0e7..dec319b404 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -524,8 +524,8 @@ def array_to_file(data, fileobj, out_dtype=None, offset=0, Parameters ---------- - data : array - array to write + data : array-like + array or array-like to write. fileobj : file-like file-like object implementing ``write`` method. out_dtype : None or dtype, optional From 3052f661f680349a82c9bbb6d95c996aeb6b7ee2 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Wed, 1 Oct 2014 16:48:30 -0400 Subject: [PATCH 15/55] RF: more idiomatic writing of array data in parrec Let ``array_to_file`` cast data to array if necessary. Don't need to use ``write0`` flag to seek_tell because output file type can only be uncompressed or .gz; let ``array_to_file`` do seek. --- bin/parrec2nii | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 39e18fd3ef..73104d1886 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -12,7 +12,7 @@ import nibabel import nibabel.parrec as pr import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext -from nibabel.volumeutils import seek_tell, array_to_file +from nibabel.volumeutils import array_to_file # global verbosity switch verbose_switch = False @@ -196,16 +196,15 @@ def proc_file(infile, opts): if not np.any(np.diff(slope)) and not np.any(np.diff(intercept)): # Single scalefactor case nhdr.set_slope_inter(slope.ravel()[0], intercept.ravel()[0]) - data = raw_data + data_obj = raw_data else: # Multi scalefactor case nhdr.set_slope_inter(1, 0) nhdr.set_data_dtype(np.float64) - data = pr_img.dataobj.__array__() + data_obj = pr_img.dataobj nhdr.write_to(outfile) offset = nhdr.get_data_offset() - seek_tell(outfile, offset, write0=True) - array_to_file(data, outfile, offset=offset) + array_to_file(data_obj, outfile, offset=offset) # write out bvals/bvecs if requested if opts.bvs and pr_hdr.general_info['n_dti_volumes'] > 1: From 0961dede942a6bb048abfc7a073fde97eba46b3c Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 15:23:36 -0400 Subject: [PATCH 16/55] NF: add nibabel-data and routines to use it Add nibabel-data directory and routines to find it / use it. --- .gitmodules | 3 ++ .travis.yml | 2 ++ nibabel-data/README.rst | 17 +++++++++++ nibabel-data/nitest-balls1 | 1 + nibabel/tests/nibabel_data.py | 47 ++++++++++++++++++++++++++++++ nibabel/tests/test_nibabel_data.py | 32 ++++++++++++++++++++ 6 files changed, 102 insertions(+) create mode 100644 .gitmodules create mode 100644 nibabel-data/README.rst create mode 160000 nibabel-data/nitest-balls1 create mode 100644 nibabel/tests/nibabel_data.py create mode 100644 nibabel/tests/test_nibabel_data.py diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..1567824d93 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "nibabel-data/nitest-balls1"] + path = nibabel-data/nitest-balls1 + url = https://github.com/yarikoptic/nitest-balls1 diff --git a/.travis.yml b/.travis.yml index 70d286383b..0596882de0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,6 +27,8 @@ before_install: # e.g. pip install -r requirements.txt # --use-mirrors install: - python setup.py install + # Point to nibabel data directory + - export NIBABEL_DATA_DIR="$PWD/nibabel-data" # command to run tests, e.g. python setup.py test script: # Change into an innocuous directory and find tests from installation diff --git a/nibabel-data/README.rst b/nibabel-data/README.rst new file mode 100644 index 0000000000..c8fa9f3a92 --- /dev/null +++ b/nibabel-data/README.rst @@ -0,0 +1,17 @@ +############ +Nibabel data +############ + +This subdirectory contains data repositories for testing. + +The data repositories should not be included in source or binary +distributions. + +A some point we might remove this directory from the source distribution and +make the data packages available with a more formal data package format. + +For the moment the tests can find this data path by: + +* Using the contents of the ``NIBABEL_DATA_DIR`` environment variable; +* Looking for this ``nibabel-data`` directory in the directory above (closer + to the root directory) the directory containing the ``nibabel`` package. diff --git a/nibabel-data/nitest-balls1 b/nibabel-data/nitest-balls1 new file mode 160000 index 0000000000..2cd07d86e2 --- /dev/null +++ b/nibabel-data/nitest-balls1 @@ -0,0 +1 @@ +Subproject commit 2cd07d86e2cc2d3c612d5d4d659daccd7a58f126 diff --git a/nibabel/tests/nibabel_data.py b/nibabel/tests/nibabel_data.py new file mode 100644 index 0000000000..2c08b75199 --- /dev/null +++ b/nibabel/tests/nibabel_data.py @@ -0,0 +1,47 @@ +""" Functions / decorators for finding / requiring nibabel-data directory +""" + +from os import environ +from os.path import dirname, realpath, join as pjoin, isdir, exists + +from numpy.testing.decorators import skipif + + +def get_nibabel_data(): + """ Return path to nibabel-data or None if missing + + First use ``NIBABEL_DATA_DIR`` environment variable. + + If this variable is missing then look for data in directory below package + directory. + """ + nibabel_data = environ.get('NIBABEL_DATA_DIR') + if nibabel_data is None: + mod = __import__('nibabel') + containing_path = dirname(dirname(realpath(mod.__file__))) + nibabel_data = pjoin(containing_path, 'nibabel-data') + return nibabel_data if isdir(nibabel_data) else None + + +def needs_nibabel_data(subdir = None): + """ Decorator for tests needing nibabel-data + + Parameters + ---------- + subdir : None or str + Subdirectory we need in nibabel-data directory. If None, only require + nibabel-data directory itself. + + Returns + ------- + skip_dec : decorator + Decorator skipping tests if required directory not present + """ + nibabel_data = get_nibabel_data() + if nibabel_data is None: + return skipif(True, "Need nibabel-data directory for this test") + if subdir is None: + return skipif(False) + required_path = pjoin(nibabel_data, subdir) + return skipif(not exists(required_path), + "Need {0} for these tests".format(required_path)) diff --git a/nibabel/tests/test_nibabel_data.py b/nibabel/tests/test_nibabel_data.py new file mode 100644 index 0000000000..885f2439f5 --- /dev/null +++ b/nibabel/tests/test_nibabel_data.py @@ -0,0 +1,32 @@ +""" Tests for ``get_nibabel_data`` +""" + +import os +from os.path import dirname, realpath, join as pjoin, isdir + +from . import nibabel_data as nibd + +from nose.tools import assert_equal + +MY_DIR = dirname(__file__) + + +def setup_module(): + nibd.environ = {} + + +def teardown_module(): + nibd.environ = os.environ + + +def test_get_nibabel_data(): + # Test getting directory + local_data = realpath(pjoin(MY_DIR, '..', '..', 'nibabel-data')) + if isdir(local_data): + assert_equal(nibd.get_nibabel_data(), local_data) + else: + assert_equal(nibd.get_nibabel_data(), None) + nibd.environ['NIBABEL_DATA_DIR'] = 'not_a_path' + assert_equal(nibd.get_nibabel_data(), None) + nibd.environ['NIBABEL_DATA_DIR'] = MY_DIR + assert_equal(nibd.get_nibabel_data(), MY_DIR) From a53a54a3a16ff5a81b87b17f3ff11c04ae649ff7 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 17:55:59 -0400 Subject: [PATCH 17/55] TST: add test of loading PAR images Load PAR images and compare to NIfTI image when we have them. Raise SkipTest for fieldmap image, which doesn't match the NIfTI. --- nibabel/tests/test_parrec_data.py | 68 +++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 nibabel/tests/test_parrec_data.py diff --git a/nibabel/tests/test_parrec_data.py b/nibabel/tests/test_parrec_data.py new file mode 100644 index 0000000000..1ac05cbc30 --- /dev/null +++ b/nibabel/tests/test_parrec_data.py @@ -0,0 +1,68 @@ +""" Test we can correctly import example PARREC files +""" +from __future__ import print_function, absolute_import + +from glob import glob +from os.path import join as pjoin, basename, splitext, exists + +import numpy as np + +import nibabel as nib +from ..parrec import load + +from .nibabel_data import get_nibabel_data, needs_nibabel_data + +from nose import SkipTest +from nose.tools import assert_true, assert_false, assert_equal + +from numpy.testing import assert_almost_equal + +NIBABEL_DATA = get_nibabel_data() + +if not NIBABEL_DATA is None: + BALLS = pjoin(NIBABEL_DATA, 'nitest-balls1') + + +# Amount by which affine translation differs from NIFTI conversion +AFF_OFF = [-0.93644031, -0.95572686, 0.03288748] + + +@needs_nibabel_data('nitest-balls1') +def test_loading(): + # Test loading of parrec files + for par in glob(pjoin(BALLS, 'PARREC', '*.PAR')): + par_root, ext = splitext(basename(par)) + # NA.PAR appears to be a localizer, with three slices in each of the + # three orientations: sagittal; coronal, transverse + if par_root == 'NA': + continue + # Check we can load the image + pimg = load(par) + assert_equal(pimg.shape[:3], (80, 80, 10)) + # Compare against NIFTI if present + nifti_fname = pjoin(BALLS, 'NIFTI', par_root + '.nii.gz') + if exists(nifti_fname): + nimg = nib.load(nifti_fname) + assert_almost_equal(nimg.affine[:3, :3], pimg.affine[:3, :3], 3) + # The translation part is always off by the same ammout + aff_off = pimg.affine[:3, 3] - nimg.affine[:3, 3] + assert_almost_equal(aff_off, AFF_OFF, 4) + # The difference is max in the order of 0.5 voxel + vox_sizes = np.sqrt((nimg.affine[:3, :3] ** 2).sum(axis=0)) + assert_true(np.all(np.abs(aff_off / vox_sizes) <= 0.5)) + # The data is very close, unless it's the fieldmap + if par_root != 'fieldmap': + assert_true(np.allclose(pimg.dataobj, nimg.dataobj)) + # Not sure what's going on with the fieldmap image - TBA + + +@needs_nibabel_data('nitest-balls1') +def test_fieldmap(): + # Test fieldmap image + # Exploring the DICOM suggests that the first volume is magnitude and the + # second is phase. The NIfTI has very odd scaling, being all negative. + fieldmap_par = pjoin(BALLS, 'PARREC', 'fieldmap.PAR') + fieldmap_nii = pjoin(BALLS, 'NIFTI', 'fieldmap.nii.gz') + pimg = load(fieldmap_par) + nimg = nib.load(fieldmap_nii) + raise SkipTest('Fieldmap remains puzzling') From d652525c0c4869d784e3c73039234b21dc61b1cf Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 20:50:59 -0400 Subject: [PATCH 18/55] TST: add tests for parrec2nii on balls data Convert all the balls images we can and test against previous nifti conversions. --- nibabel/tests/test_scripts.py | 39 ++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index 39f91ac3a2..cf520c6f19 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -7,8 +7,10 @@ from __future__ import division, print_function, absolute_import import os -from os.path import dirname, join as pjoin, abspath +from os.path import (dirname, join as pjoin, abspath, splitext, basename, + exists) import re +from glob import glob import numpy as np @@ -20,6 +22,8 @@ from numpy.testing import assert_almost_equal from .scriptrunner import ScriptRunner +from .nibabel_data import needs_nibabel_data +from .test_parrec_data import BALLS, AFF_OFF def _proc_stdout(stdout): @@ -96,3 +100,36 @@ def test_parrec2nii(): (data.min(), data.max(), data.mean()), (0.0, 2299.4110643863678, 194.95876256117265))) assert_almost_equal(vox_size(img.get_affine()), (3.75, 3.75, 8)) + + +@script_test +@needs_nibabel_data('nitest-balls1') +def test_parrec2nii_with_data(): + # Use nibabel-data to test conversion + with InTemporaryDirectory(): + for par in glob(pjoin(BALLS, 'PARREC', '*.PAR')): + par_root, ext = splitext(basename(par)) + # NA.PAR appears to be a localizer, with three slices in each of + # the three orientations: sagittal; coronal, transverse + if par_root == 'NA': + continue + # Do conversion + run_command(['parrec2nii', par]) + conved_img = load(par_root + '.nii') + assert_equal(conved_img.shape[:3], (80, 80, 10)) + # Test against converted NIfTI + nifti_fname = pjoin(BALLS, 'NIFTI', par_root + '.nii.gz') + if exists(nifti_fname): + nimg = load(nifti_fname) + assert_almost_equal(nimg.affine[:3, :3], + conved_img.affine[:3, :3], 3) + # The translation part is always off by the same ammout + aff_off = conved_img.affine[:3, 3] - nimg.affine[:3, 3] + assert_almost_equal(aff_off, AFF_OFF, 4) + # The difference is max in the order of 0.5 voxel + vox_sizes = np.sqrt((nimg.affine[:3, :3] ** 2).sum(axis=0)) + assert_true(np.all(np.abs(aff_off / vox_sizes) <= 0.5)) + # The data is very close, unless it's the fieldmap + if par_root != 'fieldmap': + assert_true(np.allclose(conved_img.dataobj, + nimg.dataobj)) From 68ffc14fd2eb712dbaf51b3c6f7f748a39f8bcde Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 20:54:00 -0400 Subject: [PATCH 19/55] RF: remove debug raise statement --- bin/parrec2nii | 2 -- 1 file changed, 2 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 73104d1886..506863847f 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -256,8 +256,6 @@ def main(): proc_file(infile, opts) except Exception as e: errs.append('%s: %s' % (infile, e)) - raise # XXX REMOVE - print('') if len(errs): error('Caught %i exceptions. Dump follows:\n\n %s' From b0ee791b64cdb7af7388be982bc04e506bdf9ea3 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 22:20:05 -0400 Subject: [PATCH 20/55] RF: sort of deglobal the verbose flag Stick the verbose flag on the function itself for neatness. --- bin/parrec2nii | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 506863847f..6d5b64b9ad 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -14,9 +14,6 @@ import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext from nibabel.volumeutils import array_to_file -# global verbosity switch -verbose_switch = False - def _oneline(big_str): return ' '.join(L.strip() for L in big_str.splitlines()) @@ -114,7 +111,7 @@ def get_opt_parser(): def verbose(msg, indent=0): - if verbose_switch: + if verbose.switch: print("%s%s" % (' ' * indent, msg)) @@ -242,8 +239,7 @@ def main(): parser = get_opt_parser() (opts, infiles) = parser.parse_args() - global verbose_switch - verbose_switch = opts.verbose + verbose.switch = opts.verbose if opts.origin not in ['scanner', 'fov']: error("Unrecognized value for --origin: '%s'." % opts.origin, 1) From 5058f14513ecd4c9dbf16bba079ef40fa6fdae23 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 2 Oct 2014 22:32:51 -0400 Subject: [PATCH 21/55] RF: remove redundant set of extensions The code was setting the header extensions into the image, but this doesn't have any effect on writing the header, and we aren't using the image for anything. --- bin/parrec2nii | 1 - 1 file changed, 1 deletion(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 6d5b64b9ad..fe15eca54b 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -171,7 +171,6 @@ def proc_file(infile, opts): hdr_dump = fobj.read() dump_ext = nifti1.Nifti1Extension('comment', hdr_dump) exts.append(dump_ext) - nimg.extra['extensions'] = exts # finalize the header: set proper data offset, pixdims, ... nimg.update_header() From 7709ddd6b3320b9e8cf841d41809553d4e26caed Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 11:33:36 -0400 Subject: [PATCH 22/55] RF: refactor mriutils; remove from top level Refactor mriutils dwell time calculation to raise errors instead of returning None. Do simple tests. Remove calculate_dwell_time from top level (but import mriutils module). --- bin/parrec2nii | 11 +++++---- nibabel/__init__.py | 2 +- nibabel/mriutils.py | 43 ++++++++++++++++++++-------------- nibabel/tests/test_mriutils.py | 34 +++++++++++++++++++++++++++ 4 files changed, 68 insertions(+), 22 deletions(-) create mode 100644 nibabel/tests/test_mriutils.py diff --git a/bin/parrec2nii b/bin/parrec2nii index fe15eca54b..feef48ff71 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -10,6 +10,7 @@ import os import gzip import nibabel import nibabel.parrec as pr +from nibabel.mriutils import calculate_dwell_time, MRIError import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext from nibabel.volumeutils import array_to_file @@ -221,10 +222,12 @@ def proc_file(infile, opts): # write out dwell time if requested if opts.dwell_time: - dwell_time = nibabel.calculate_dwell_time( - pr_hdr.get_water_fat_shift(), pr_hdr.get_echo_train_length(), - opts.field_strength) - if dwell_time is None: + try: + dwell_time = calculate_dwell_time( + pr_hdr.get_water_fat_shift(), + pr_hdr.get_echo_train_length(), + opts.field_strength) + except MRIError: verbose('No EPI factors, dwell time not written') else: verbose('Writing dwell time (%r sec) calculated assuming %sT ' diff --git a/nibabel/__init__.py b/nibabel/__init__.py index 16cbce79f8..a87469c56b 100644 --- a/nibabel/__init__.py +++ b/nibabel/__init__.py @@ -63,7 +63,7 @@ apply_orientation, aff2axcodes) from .imageclasses import class_map, ext_map from . import trackvis -from .mriutils import calculate_dwell_time +from . import mriutils # be friendly on systems with ancient numpy -- no tests, but at least # importable diff --git a/nibabel/mriutils.py b/nibabel/mriutils.py index 1a230571c0..a56df30a0b 100644 --- a/nibabel/mriutils.py +++ b/nibabel/mriutils.py @@ -1,17 +1,27 @@ # emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## """ Utilities for calculations related to MRI """ +from __future__ import division -__all__ = ['calculate_dwell_time'] +__all__ = ['dwell_time'] -GYROMAGNETIC_RATIO = 42.576 # MHz/T for nucleus +GYROMAGNETIC_RATIO = 42.576 # MHz/T for hydrogen nucleus PROTON_WATER_FAT_SHIFT = 3.4 # ppm -def calculate_dwell_time(water_fat_shift, echo_train_length, - field_strength=3.0): +class MRIError(ValueError): + pass + + +def calculate_dwell_time(water_fat_shift, echo_train_length, field_strength): """Calculate the dwell time Parameters @@ -21,22 +31,21 @@ def calculate_dwell_time(water_fat_shift, echo_train_length, echo_train_length : int The echo train length of the imaging sequence. field_strength : float - Strength of the magnet in T, e.g. ``3.0`` for a 3T magnet - recording. Providing this value is necessary because the - field strength is not encoded in the PAR file. + Strength of the magnet in Tesla, e.g. 3.0 for a 3T magnet recording. Returns ------- dwell_time : float - The dwell time in seconds. Returns None if the dwell - time cannot be calculated (i.e., not using an EPI sequence). + The dwell time in seconds. + + Raises + ------ + MRIError if values are out of range """ - field_strength = float(field_strength) # Tesla - assert field_strength > 0. + if field_strength < 0: + raise MRIError("Field strength should be positive") if echo_train_length <= 0: - return None - # constants - dwell_time = ((echo_train_length - 1) * water_fat_shift / - (GYROMAGNETIC_RATIO * PROTON_WATER_FAT_SHIFT - * field_strength * (echo_train_length + 1))) - return dwell_time + raise MRIError("Echo train length should be >= 1") + return ((echo_train_length - 1) * water_fat_shift / + (GYROMAGNETIC_RATIO * PROTON_WATER_FAT_SHIFT + * field_strength * (echo_train_length + 1))) diff --git a/nibabel/tests/test_mriutils.py b/nibabel/tests/test_mriutils.py new file mode 100644 index 0000000000..f65f6b2d26 --- /dev/null +++ b/nibabel/tests/test_mriutils.py @@ -0,0 +1,34 @@ +# emacs: -*- mode: python-mode; py-indent-offset: 4; indent-tabs-mode: nil -*- +# vi: set ft=python sts=4 ts=4 sw=4 et: +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +# +# See COPYING file distributed along with the NiBabel package for the +# copyright and license terms. +# +### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## +""" Testing mriutils module +""" +from __future__ import division + +import numpy as np + +from numpy.testing import (assert_almost_equal, + assert_array_equal) + +from nose.tools import (assert_true, assert_false, assert_raises, + assert_equal, assert_not_equal) + + +from ..mriutils import calculate_dwell_time, MRIError + + +def test_calculate_dwell_time(): + # Test dwell time calculation + # This tests only that the calculation does what it appears to; needs some + # external check + assert_almost_equal(calculate_dwell_time(3.3, 2, 3), + 3.3 / (42.576 * 3.4 * 3 * 3)) + # Echo train length of 1 is valid, but returns 0 dwell time + assert_almost_equal(calculate_dwell_time(3.3, 1, 3), 0) + assert_raises(MRIError, calculate_dwell_time, 3.3, 0, 3.0) + assert_raises(MRIError, calculate_dwell_time, 3.3, 2, -0.1) From 25b9e03eebcec7cd61ee60d1121dcbe605677cb8 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 11:39:15 -0400 Subject: [PATCH 23/55] RF: require explicit field strength for dwell time Don't default to 3T - it would be very easy for someone using the script not to notice that default and then get incorrect values. --- bin/parrec2nii | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index feef48ff71..8244508971 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -60,7 +60,7 @@ def get_opt_parser(): supplied because it is not encoded in the PAR/REC format."""))) p.add_option( - Option("--field-strength", action="store", default=3., type="float", + Option("--field-strength", action="store", type="float", dest="field_strength", help=_oneline( """The magnetic field strength of the recording, only needed @@ -245,6 +245,8 @@ def main(): if opts.origin not in ['scanner', 'fov']: error("Unrecognized value for --origin: '%s'." % opts.origin, 1) + if opts.dwell_time and opts.field_strength is None: + error('Need --field-strength for dwell time calculation', 1) # store any exceptions errs = [] From f4c2272438cdff2087cc6bdeb26adce1a6a0dc80 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 11:40:33 -0400 Subject: [PATCH 24/55] TST: test some options to parrec2nii Test overwrite, bvs, dwell-time options to parrec2nii script. --- nibabel/tests/test_scripts.py | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index cf520c6f19..1282bb33e3 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -17,7 +17,8 @@ from ..tmpdirs import InTemporaryDirectory from ..loadsave import load -from nose.tools import assert_true, assert_not_equal, assert_equal +from nose.tools import (assert_true, assert_false, assert_not_equal, + assert_equal) from numpy.testing import assert_almost_equal @@ -133,3 +134,31 @@ def test_parrec2nii_with_data(): if par_root != 'fieldmap': assert_true(np.allclose(conved_img.dataobj, nimg.dataobj)) + with InTemporaryDirectory(): + # Test some options + dti_par = pjoin(BALLS, 'PARREC', 'DTI.PAR') + run_command(['parrec2nii', dti_par]) + assert_true(exists('DTI.nii')) + assert_false(exists('DTI.bvals')) + assert_false(exists('DTI.bvecs')) + # Does not overwrite unless option given + code, stdout, stderr = run_command(['parrec2nii', dti_par], + check_code=False) + assert_equal(code, 1) + # Writes bvals, bvecs files if asked + run_command(['parrec2nii', '--overwrite', '--bvs', dti_par]) + assert_true(exists('DTI.bvals')) + assert_true(exists('DTI.bvecs')) + assert_false(exists('DTI.dwell_time')) + # Need field strength if requesting dwell time + code, _, _, = run_command( + ['parrec2nii', '--overwrite', '--dwell-time', dti_par], + check_code=False) + assert_equal(code, 1) + run_command( + ['parrec2nii', '--overwrite', '--dwell-time', + '--field-strength', '3', dti_par]) + exp_dwell = (26 * 9.087) / (42.576 * 3.4 * 3 * 28) + with open('DTI.dwell_time', 'rt') as fobj: + contents = fobj.read().strip() + assert_almost_equal(float(contents), exp_dwell) From ca9430d613b1846c34c614896e763bcc57f365df Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 12:06:07 -0400 Subject: [PATCH 25/55] RF: revert fix allowing upper case file extensions I thought about this for a while, but I don't like it, because it changes the behavior of nibabel globally. Previously if you asked to open ``my_image.NII`` it would barf, now it might find an image when it previously it did not. This is probably what the user intended, but it's still a guess. It's not hard to specify the upper case file extension on systems that need it. The next commit adds an explicit function to ignore extension case for parrec2nii. --- nibabel/openers.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/nibabel/openers.py b/nibabel/openers.py index ab71161394..74857496c9 100644 --- a/nibabel/openers.py +++ b/nibabel/openers.py @@ -9,7 +9,7 @@ """ Context manager openers for various fileobject types """ -from os.path import splitext, isfile +from os.path import splitext import gzip import bz2 @@ -49,10 +49,6 @@ def __init__(self, fileish, *args, **kwargs): self._name = None return _, ext = splitext(fileish) - # allow for ext to be lower or upper case - if not isfile(fileish): - if isfile(_ + ext.upper()): - fileish = _ + ext.upper() if ext in self.compress_ext_map: is_compressor = True opener, arg_names = self.compress_ext_map[ext] From fe092d56100929eed67b1639256922dc04515ffb Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 13:23:56 -0400 Subject: [PATCH 26/55] NF: function for upper/lower case file extensions Restore ignoring case in file extensions by using an explicit function. --- bin/parrec2nii | 3 ++- nibabel/tests/test_utils.py | 28 ++++++++++++++++++++++++++++ nibabel/volumeutils.py | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 8244508971..cc5fc81274 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -13,7 +13,7 @@ import nibabel.parrec as pr from nibabel.mriutils import calculate_dwell_time, MRIError import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext -from nibabel.volumeutils import array_to_file +from nibabel.volumeutils import array_to_file, fname_ext_ul_case def _oneline(big_str): @@ -140,6 +140,7 @@ def proc_file(infile, opts): # load the PAR header and data scaling = None if opts.scaling == 'off' else opts.scaling + infile = fname_ext_ul_case(infile) pr_img = pr.load(infile, opts.permit_truncated, scaling) pr_hdr = pr_img.header raw_data = pr_img.dataobj.get_unscaled() diff --git a/nibabel/tests/test_utils.py b/nibabel/tests/test_utils.py index d6787357ec..c387c1cd77 100644 --- a/nibabel/tests/test_utils.py +++ b/nibabel/tests/test_utils.py @@ -9,6 +9,8 @@ ''' Test for volumeutils module ''' from __future__ import division +from os.path import exists + from ..externals.six import BytesIO import tempfile import warnings @@ -23,6 +25,7 @@ array_to_file, allopen, # for backwards compatibility BinOpener, + fname_ext_ul_case, calculate_scale, can_cast, write_zeros, @@ -825,6 +828,31 @@ def test_BinOpener(): assert_true(hasattr(fobj.fobj, 'compress')) +def test_fname_ext_ul_case(): + # Get filename ignoring the case of the filename extension + with InTemporaryDirectory(): + with open('afile.TXT', 'wt') as fobj: + fobj.write('Interesting information') + # OSX usually has case-insensitive file systems; Windows also + os_cares_case = not exists('afile.txt') + with open('bfile.txt', 'wt') as fobj: + fobj.write('More interesting information') + # If there is no file, the case doesn't change + assert_equal(fname_ext_ul_case('nofile.txt'), 'nofile.txt') + assert_equal(fname_ext_ul_case('nofile.TXT'), 'nofile.TXT') + # If there is a file, accept upper or lower case for ext + if os_cares_case: + assert_equal(fname_ext_ul_case('afile.txt'), 'afile.TXT') + assert_equal(fname_ext_ul_case('bfile.TXT'), 'bfile.txt') + else: + assert_equal(fname_ext_ul_case('afile.txt'), 'afile.txt') + assert_equal(fname_ext_ul_case('bfile.TXT'), 'bfile.TXT') + assert_equal(fname_ext_ul_case('afile.TXT'), 'afile.TXT') + assert_equal(fname_ext_ul_case('bfile.txt'), 'bfile.txt') + # Not mixed case though + assert_equal(fname_ext_ul_case('afile.TxT'), 'afile.TxT') + + def test_allopen(): # This import into volumeutils is for compatibility. The code is the # ``openers`` module. diff --git a/nibabel/volumeutils.py b/nibabel/volumeutils.py index dec319b404..1cabc62597 100644 --- a/nibabel/volumeutils.py +++ b/nibabel/volumeutils.py @@ -15,6 +15,7 @@ import numpy as np +from os.path import exists, splitext from .casting import (shared_range, type_info, OK_FLOATS) from .openers import Opener @@ -1514,6 +1515,38 @@ class BinOpener(Opener): compress_ext_map['.mgz'] = Opener.gz_def +def fname_ext_ul_case(fname): + """ `fname` with ext changed to upper / lower case if file exists + + Check for existence of `fname`. If it does exist, return unmodified. If + it doesn't, check for existence of `fname` with case changed from lower to + upper, or upper to lower. Return this modified `fname` if it exists. + Otherwise return `fname` unmodified + + Parameters + ---------- + fname : str + filename. + + Returns + ------- + mod_fname : str + filename, maybe with extension of opposite case + """ + if exists(fname): + return fname + froot, ext = splitext(fname) + if ext == ext.lower(): + mod_fname = froot + ext.upper() + if exists(mod_fname): + return mod_fname + elif ext == ext.upper(): + mod_fname = froot + ext.lower() + if exists(mod_fname): + return mod_fname + return fname + + def allopen(fileish, *args, **kwargs): """ Compatibility wrapper for old ``allopen`` function From 9209f9840f227c072b45d19d70f307eec23f264c Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 20:19:01 -0400 Subject: [PATCH 27/55] RF: refactor parse_PARREC into smaller functions Makes testing easier. I think the code is easier to read. --- nibabel/parrec.py | 203 +++++++++++++++++++++++++++------------------- 1 file changed, 121 insertions(+), 82 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 2ec9c011ea..eda468977e 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -80,6 +80,7 @@ import warnings import numpy as np from copy import deepcopy +import re from .externals.six import binary_type from .py3k import asbytes @@ -217,91 +218,68 @@ class PARRECError(Exception): pass -def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): - """Helper to alert user about truncated files and adjust computation""" - extra = (not must_exceed_one) or (n_expected > 1) - if extra and n_have != n_expected: - msg = ("Header inconsistency: Found <= %i %s, but expected %i." - % (n_have, name, n_expected)) - if not permit: - raise PARRECError(msg) - msg += " Assuming %i valid %s." % (n_have - 1, name) - warnings.warn(msg) - # we assume up to the penultimate data line is correct - n_have -= 1 - return n_have - - -def parse_PAR_header(fobj, permit_truncated=False): - """Parse a PAR header and aggregate all information into useful containers. - - Parameters - ---------- - fobj : file-object - The PAR header file object. - permit_truncated : bool - If True, a warning is emitted instead of an error when a truncated - recording is detected. - - Returns - ------- - general_info : dict - Contains all "General Information" from the header file - image_info : ndarray - Structured array with fields giving all "Image information" in the - header - """ - # containers for relevant header lines - general_info = {} - image_info = [] +def _split_header(fobj): + """ Split header into `version`, `gen_dict`, `image_lines` """ version = None - - # single pass through the header + gen_dict = {} + image_lines = [] + # Small state-machine + state = 'top-header' for line in fobj: - # no junk line = line.strip() - if line.startswith('#'): - # try to get the header version - if line.count('image export tool'): + if line == '': + continue + if state == 'top-header': + if not line.startswith('#'): + state = 'general-info' + elif 'image export tool' in line: version = line.split()[-1] - if version not in supported_versions: - warnings.warn( - "PAR/REC version '%s' is currently not " - "supported -- making an attempt to read " - "nevertheless. Please email the NiBabel " - "mailing list, if you are interested in " - "adding support for this version." - % version) - else: - # just a comment - continue - elif line.startswith('.'): - # read 'general information' and store in a dict - first_colon = line[1:].find(':') + 1 - key = line[1:first_colon].strip() - value = line[first_colon + 1:].strip() - # get props for this hdr field - props = _hdr_key_dict[key] - # turn values into meaningful dtype - if len(props) == 2: - # only dtype spec and no shape - value = props[1](value) - elif len(props) == 3: - # array with dtype and shape - value = np.fromstring(value, props[1], sep=' ') - value.shape = props[2] - general_info[props[0]] = value - elif line: - # anything else is an image definition: store for later - # processing - image_info.append(line) - + if state == 'general-info': + if not line.startswith('.'): + state = 'comment-block' + else: # Let match raise error for unexpected field format + key, value = GEN_RE.match(line).groups() + gen_dict[key] = value + if state == 'comment-block': + if not line.startswith('#'): + state = 'image-info' + if state == 'image-info': + if line.startswith('#'): + break + image_lines.append(line) + return version, gen_dict, image_lines + + +GEN_RE = re.compile(r".\s+(.*?)\s*:\s+(.*)") + + +def _process_gen_dict(gen_dict): + """ Process `gen_dict` key, values into `general_info` + """ + general_info = {} + for key, value in gen_dict.items(): + # get props for this hdr field + props = _hdr_key_dict[key] + # turn values into meaningful dtype + if len(props) == 2: + # only dtype spec and no shape + value = props[1](value) + elif len(props) == 3: + # array with dtype and shape + value = np.fromstring(value, props[1], sep=' ') + value.shape = props[2] + general_info[props[0]] = value + return general_info + + +def _process_image_lines(image_lines): + """ Process image information definition lines + """ # postproc image def props # create an array for all image defs - image_defs = np.zeros(len(image_info), dtype=image_def_dtype) - + image_defs = np.zeros(len(image_lines), dtype=image_def_dtype) # for every image definition - for i, line in enumerate(image_info): + for i, line in enumerate(image_lines): items = line.split() item_counter = 0 # for all image properties we know about @@ -326,7 +304,27 @@ def parse_PAR_header(fobj, permit_truncated=False): # store image_defs[props[0]][i] = value item_counter += nelements + return image_defs + +def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): + """Helper to alert user about truncated files and adjust computation""" + extra = (not must_exceed_one) or (n_expected > 1) + if extra and n_have != n_expected: + msg = ("Header inconsistency: Found <= %i %s, but expected %i." + % (n_have, name, n_expected)) + if not permit: + raise PARRECError(msg) + msg += " Assuming %i valid %s." % (n_have - 1, name) + warnings.warn(msg) + # we assume up to the penultimate data line is correct + n_have -= 1 + return n_have + + +def _calc_extras(general_info, image_defs, permit_truncated): + """ Calculate, return values from `general info`, `image_defs` + """ # DTI volumes (b-values-1 x directions) # there is some awkward exception to this rule for b-values > 2 # XXX need to get test image... @@ -351,10 +349,51 @@ def parse_PAR_header(fobj, permit_truncated=False): general_info['max_dynamics'], pt, True) n_dti_volumes = _check_truncation('dti volumes', n_dti_volumes, max_dti_volumes, pt, True) - general_info.update(n_dti_volumes=n_dti_volumes, n_echoes=n_echoes, - n_dynamics=n_dynamics, n_slices=n_slices, - max_dti_volumes=max_dti_volumes, n_seq=n_seq, - max_types=n_seq) + return dict(n_dti_volumes=n_dti_volumes, + n_echoes=n_echoes, + n_dynamics=n_dynamics, + n_slices=n_slices, + max_dti_volumes=max_dti_volumes, + n_seq=n_seq, + max_types=n_seq) + + +def one_line(long_str): + """ Make maybe mutli-line `long_str` into one long line """ + return ' '.join(line.strip() for line in long_str.splitlines()) + + +def parse_PAR_header(fobj, permit_truncated=False): + """Parse a PAR header and aggregate all information into useful containers. + + Parameters + ---------- + fobj : file-object + The PAR header file object. + permit_truncated : bool, optional + If True, a warning is emitted instead of an error when a truncated + recording is detected. + + Returns + ------- + general_info : dict + Contains all "General Information" from the header file + image_info : ndarray + Structured array with fields giving all "Image information" in the + header + """ + # single pass through the header + version, gen_dict, image_lines = _split_header(fobj) + if version not in supported_versions: + warnings.warn(one_line( + """ PAR/REC version '{0}' is currently not supported -- making an + attempt to read nevertheless. Please email the NiBabel mailing + list, if you are interested in adding support for this version. + """.format(version))) + general_info = _process_gen_dict(gen_dict) + image_defs = _process_image_lines(image_lines) + extra_info = _calc_extras(general_info, image_defs, permit_truncated) + general_info.update(extra_info) return general_info, image_defs From 6b2382445209de79983d85cbb456cf5983df2430 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 20:19:53 -0400 Subject: [PATCH 28/55] RF: parrec2nii uses one_line function from parrec Now parrec has one_line function, use that instead of version defined in parrec2nii. --- bin/parrec2nii | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index cc5fc81274..869cd1d846 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -10,16 +10,13 @@ import os import gzip import nibabel import nibabel.parrec as pr +from nibabel.parrec import one_line from nibabel.mriutils import calculate_dwell_time, MRIError import nibabel.nifti1 as nifti1 from nibabel.filename_parser import splitext_addext from nibabel.volumeutils import array_to_file, fname_ext_ul_case -def _oneline(big_str): - return ' '.join(L.strip() for L in big_str.splitlines()) - - def get_opt_parser(): # use module docstring for help output p = OptionParser( @@ -32,7 +29,7 @@ def get_opt_parser(): p.add_option( Option("-o", "--output-dir", action="store", type="string", dest="outdir", default=None, - help=_oneline("""Destination directory for NIfTI files. + help=one_line("""Destination directory for NIfTI files. Default: current directory."""))) p.add_option( Option("-c", "--compressed", action="store_true", @@ -41,19 +38,19 @@ def get_opt_parser(): p.add_option( Option("-p", "--permit-truncated", action="store_true", dest="permit_truncated", default=False, - help=_oneline( + help=one_line( """Permit conversion of truncated recordings. Support for this is experimental, and results *must* be checked afterward for validity."""))) p.add_option( Option("-b", "--bvs", action="store_true", dest="bvs", default=False, - help=_oneline( + help=one_line( """Output bvals/bvecs files in addition to NIFTI image."""))) p.add_option( Option("-d", "--dwell-time", action="store_true", default=False, dest="dwell_time", - help=_oneline( + help=one_line( """Calculate the scan dwell time. If supplied, the magnetic field strength should also be supplied using --field-strength (default 3). The field strength must be @@ -62,13 +59,13 @@ def get_opt_parser(): p.add_option( Option("--field-strength", action="store", type="float", dest="field_strength", - help=_oneline( + help=one_line( """The magnetic field strength of the recording, only needed for --dwell-time. The field strength must be supplied because it is not encoded in the PAR/REC format."""))) p.add_option( Option("--origin", action="store", dest="origin", default="scanner", - help=_oneline( + help=one_line( """Reference point of the q-form transformation of the NIfTI image. If 'scanner' the (0,0,0) coordinates will refer to the scanner's iso center. If 'fov', this coordinate will be @@ -76,7 +73,7 @@ def get_opt_parser(): 'scanner'."""))) p.add_option( Option("--minmax", action="store", nargs=2, dest="minmax", - help=_oneline( + help=one_line( """Mininum and maximum settings to be stored in the NIfTI header. If any of them is set to 'parse', the scaled data is scanned for the actual minimum and maximum. To bypass this @@ -89,12 +86,12 @@ def get_opt_parser(): p.add_option( Option("--store-header", action="store_true", dest="store_header", default=False, - help=_oneline( + help=one_line( """If set, all information from the PAR header is stored in an extension ofthe NIfTI file header. Default: off"""))) p.add_option( Option("--scaling", action="store", dest="scaling", default='dv', - help=_oneline( + help=one_line( """Choose data scaling setting. The PAR header defines two different data scaling settings: 'dv' (values displayed on console) and 'fp' (floating point values). Either one can be @@ -106,7 +103,7 @@ def get_opt_parser(): p.add_option( Option("--overwrite", action="store_true", dest="overwrite", default=False, - help=_oneline("""Overwrite file if it exists. Default: + help=one_line("""Overwrite file if it exists. Default: False"""))) return p From 5e223ab22f69d0310f70d8d8f3608eb1ef36f10b Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 20:55:10 -0400 Subject: [PATCH 29/55] RF: refactor process of image lines Simplify logic of processing lines -- I didn't understand it so I found myself rewriting. --- nibabel/parrec.py | 29 +++++++++++------------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index eda468977e..b3fdb35e80 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -82,7 +82,7 @@ from copy import deepcopy import re -from .externals.six import binary_type +from .externals.six import string_types from .py3k import asbytes from .spatialimages import SpatialImage, Header @@ -284,26 +284,19 @@ def _process_image_lines(image_lines): item_counter = 0 # for all image properties we know about for props in image_def_dtd: - if np.issubdtype(image_defs[props[0]].dtype, binary_type): - # simple string - image_defs[props[0]][i] = asbytes(items[item_counter]) - item_counter += 1 - elif len(props) == 2: - # prop with numerical dtype - if props[1] == 'S30': - 1/0 - image_defs[props[0]][i] = props[1](items[item_counter]) + if len(props) == 2: + name, np_type = props + value = items[item_counter] + if not np.dtype(np_type).kind == 'S': + value = np_type(value) item_counter += 1 elif len(props) == 3: - # array prop with dtype - nelements = np.prod(props[2]) - # get as many elements as necessary - itms = items[item_counter:item_counter + nelements] - # convert to array with dtype - value = np.fromstring(" ".join(itms), props[1], sep=' ') - # store - image_defs[props[0]][i] = value + name, np_type, shape = props + nelements = np.prod(shape) + value = items[item_counter:item_counter + nelements] + value = [np_type(v) for v in value] item_counter += nelements + image_defs[name][i] = value return image_defs From 709cfcd16cb69d9682c5ac7137018ae1c0fe9382 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 3 Oct 2014 22:16:50 -0400 Subject: [PATCH 30/55] RF: remove unused calculated values Remove values returned from _calc_extra that are not used elsewhere in the code. --- nibabel/parrec.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index b3fdb35e80..2b46fa1064 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -346,9 +346,7 @@ def _calc_extras(general_info, image_defs, permit_truncated): n_echoes=n_echoes, n_dynamics=n_dynamics, n_slices=n_slices, - max_dti_volumes=max_dti_volumes, - n_seq=n_seq, - max_types=n_seq) + n_seq=n_seq) def one_line(long_str): From 14a17280c8fa5dec26c6c34e656778ce25033209 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Sat, 4 Oct 2014 21:08:58 -0400 Subject: [PATCH 31/55] NF: add nitest-balls1 PAR files for testing These are also in the ``nibabel-data`` submodule, but the PAR files are small enough that we can put them in the main repo, so we can test without needing the data repo. --- COPYING | 14 ++ nibabel/tests/data/DTI.PAR | 182 +++++++++++++++++++++++++ nibabel/tests/data/NA.PAR | 111 +++++++++++++++ nibabel/tests/data/T1.PAR | 112 +++++++++++++++ nibabel/tests/data/T2-interleaved.PAR | 112 +++++++++++++++ nibabel/tests/data/T2.PAR | 112 +++++++++++++++ nibabel/tests/data/T2_-interleaved.PAR | 122 +++++++++++++++++ nibabel/tests/data/T2_.PAR | 122 +++++++++++++++++ nibabel/tests/data/fieldmap.PAR | 122 +++++++++++++++++ 9 files changed, 1009 insertions(+) create mode 100644 nibabel/tests/data/DTI.PAR create mode 100644 nibabel/tests/data/NA.PAR create mode 100644 nibabel/tests/data/T1.PAR create mode 100644 nibabel/tests/data/T2-interleaved.PAR create mode 100644 nibabel/tests/data/T2.PAR create mode 100644 nibabel/tests/data/T2_-interleaved.PAR create mode 100644 nibabel/tests/data/T2_.PAR create mode 100644 nibabel/tests/data/fieldmap.PAR diff --git a/COPYING b/COPYING index 835f251c25..87abe50f92 100644 --- a/COPYING +++ b/COPYING @@ -191,3 +191,17 @@ The files:: are from http://psydata.ovgu.de/philips_achieva_testfiles, and released under the PDDL version 1.0 available at http://opendatacommons.org/licenses/pddl/1.0/ + +The files: + + nibabel/nibabel/tests/data/DTI.PAR + nibabel/nibabel/tests/data/NA.PAR + nibabel/nibabel/tests/data/T1.PAR + nibabel/nibabel/tests/data/T2-interleaved.PAR + nibabel/nibabel/tests/data/T2.PAR + nibabel/nibabel/tests/data/T2_-interleaved.PAR + nibabel/nibabel/tests/data/T2_.PAR + nibabel/nibabel/tests/data/fieldmap.PAR + +are from https://github.com/yarikoptic/nitest-balls1, also released under the +the PDDL version 1.0 available at http://opendatacommons.org/licenses/pddl/1.0/ diff --git a/nibabel/tests/data/DTI.PAR b/nibabel/tests/data/DTI.PAR new file mode 100644 index 0000000000..73e78a5072 --- /dev/null +++ b/nibabel/tests/data/DTI.PAR @@ -0,0 +1,182 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_12_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : WIP DTI SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 12 +. Reconstruction nr : 1 +. Scan Duration [sec] : 10.5 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : DwiSE +. Scan resolution (x, y) : 76 62 +. Scan mode : MS +. Repetition time [ms] : 1166.614 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 9.087 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 1 +. EPI factor <0,1=no EPI> : 27 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 1 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 2 +. Max. number of gradient orients : 7 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 1 0 16 81 80 80 0.00000 22.15092 1.35565e-003 69 120 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 2 1 1 1 0 1 1 16 81 80 80 0.00000 22.15092 1.35565e-003 322 560 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 3 1 1 1 0 1 2 16 81 80 80 0.00000 22.15092 1.35565e-003 688 1195 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 4 1 1 1 0 1 3 16 81 80 80 0.00000 22.15092 1.35565e-003 1407 2447 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 5 1 1 1 0 1 4 16 81 80 80 0.00000 22.15092 1.35565e-003 653 1135 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 6 1 1 1 0 1 5 16 81 80 80 0.00000 22.15092 1.35565e-003 502 873 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 7 1 1 1 0 1 6 16 81 80 80 0.00000 22.15092 1.35565e-003 365 635 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 8 1 1 1 0 1 7 16 81 80 80 0.00000 22.15092 1.35565e-003 301 524 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 9 1 1 1 0 1 8 16 81 80 80 0.00000 22.15092 1.35565e-003 450 783 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 10 1 1 1 0 1 9 16 81 80 80 0.00000 22.15092 1.35565e-003 38 66 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 1 0 0 -0.667 -0.667 -0.333 1 + 1 1 1 1 0 1 10 16 81 80 80 0.00000 22.15092 1.35565e-003 65 112 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 2 1 1 1 0 1 11 16 81 80 80 0.00000 22.15092 1.35565e-003 339 589 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 3 1 1 1 0 1 12 16 81 80 80 0.00000 22.15092 1.35565e-003 721 1253 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 4 1 1 1 0 1 13 16 81 80 80 0.00000 22.15092 1.35565e-003 1581 2748 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 5 1 1 1 0 1 14 16 81 80 80 0.00000 22.15092 1.35565e-003 640 1112 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 6 1 1 1 0 1 15 16 81 80 80 0.00000 22.15092 1.35565e-003 471 819 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 7 1 1 1 0 1 16 16 81 80 80 0.00000 22.15092 1.35565e-003 367 638 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 8 1 1 1 0 1 17 16 81 80 80 0.00000 22.15092 1.35565e-003 214 373 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 9 1 1 1 0 1 18 16 81 80 80 0.00000 22.15092 1.35565e-003 436 758 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 10 1 1 1 0 1 19 16 81 80 80 0.00000 22.15092 1.35565e-003 47 82 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 2 0 0 -0.333 0.667 -0.667 1 + 1 1 1 1 0 1 20 16 81 80 80 0.00000 22.15092 1.35565e-003 66 115 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 2 1 1 1 0 1 21 16 81 80 80 0.00000 22.15092 1.35565e-003 334 581 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 3 1 1 1 0 1 22 16 81 80 80 0.00000 22.15092 1.35565e-003 746 1297 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 4 1 1 1 0 1 23 16 81 80 80 0.00000 22.15092 1.35565e-003 1400 2433 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 5 1 1 1 0 1 24 16 81 80 80 0.00000 22.15092 1.35565e-003 631 1096 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 6 1 1 1 0 1 25 16 81 80 80 0.00000 22.15092 1.35565e-003 438 761 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 7 1 1 1 0 1 26 16 81 80 80 0.00000 22.15092 1.35565e-003 374 650 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 8 1 1 1 0 1 27 16 81 80 80 0.00000 22.15092 1.35565e-003 259 450 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 9 1 1 1 0 1 28 16 81 80 80 0.00000 22.15092 1.35565e-003 436 757 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 10 1 1 1 0 1 29 16 81 80 80 0.00000 22.15092 1.35565e-003 50 86 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 3 0 0 -0.667 0.333 0.667 1 + 1 1 1 1 0 1 30 16 81 80 80 0.00000 22.15092 1.35565e-003 67 116 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 2 1 1 1 0 1 31 16 81 80 80 0.00000 22.15092 1.35565e-003 312 542 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 3 1 1 1 0 1 32 16 81 80 80 0.00000 22.15092 1.35565e-003 694 1206 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 4 1 1 1 0 1 33 16 81 80 80 0.00000 22.15092 1.35565e-003 1422 2471 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 5 1 1 1 0 1 34 16 81 80 80 0.00000 22.15092 1.35565e-003 626 1088 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 6 1 1 1 0 1 35 16 81 80 80 0.00000 22.15092 1.35565e-003 472 820 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 7 1 1 1 0 1 36 16 81 80 80 0.00000 22.15092 1.35565e-003 345 600 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 8 1 1 1 0 1 37 16 81 80 80 0.00000 22.15092 1.35565e-003 312 542 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 9 1 1 1 0 1 38 16 81 80 80 0.00000 22.15092 1.35565e-003 457 794 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 10 1 1 1 0 1 39 16 81 80 80 0.00000 22.15092 1.35565e-003 48 83 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 4 0 0 -0.707 -0.000 -0.707 1 + 1 1 1 1 0 1 40 16 81 80 80 0.00000 22.15092 1.35565e-003 55 95 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 2 1 1 1 0 1 41 16 81 80 80 0.00000 22.15092 1.35565e-003 355 618 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 3 1 1 1 0 1 42 16 81 80 80 0.00000 22.15092 1.35565e-003 738 1284 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 4 1 1 1 0 1 43 16 81 80 80 0.00000 22.15092 1.35565e-003 1440 2504 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 5 1 1 1 0 1 44 16 81 80 80 0.00000 22.15092 1.35565e-003 676 1174 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 6 1 1 1 0 1 45 16 81 80 80 0.00000 22.15092 1.35565e-003 502 872 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 7 1 1 1 0 1 46 16 81 80 80 0.00000 22.15092 1.35565e-003 368 639 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 8 1 1 1 0 1 47 16 81 80 80 0.00000 22.15092 1.35565e-003 330 573 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 9 1 1 1 0 1 48 16 81 80 80 0.00000 22.15092 1.35565e-003 483 839 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 10 1 1 1 0 1 49 16 81 80 80 0.00000 22.15092 1.35565e-003 55 95 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 5 0 0 -0.707 0.707 0.000 1 + 1 1 1 1 0 1 50 16 81 80 80 0.00000 22.15092 1.35565e-003 56 97 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 2 1 1 1 0 1 51 16 81 80 80 0.00000 22.15092 1.35565e-003 359 624 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 3 1 1 1 0 1 52 16 81 80 80 0.00000 22.15092 1.35565e-003 818 1422 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 4 1 1 1 0 1 53 16 81 80 80 0.00000 22.15092 1.35565e-003 1526 2652 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 5 1 1 1 0 1 54 16 81 80 80 0.00000 22.15092 1.35565e-003 645 1121 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 6 1 1 1 0 1 55 16 81 80 80 0.00000 22.15092 1.35565e-003 474 824 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 7 1 1 1 0 1 56 16 81 80 80 0.00000 22.15092 1.35565e-003 386 671 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 8 1 1 1 0 1 57 16 81 80 80 0.00000 22.15092 1.35565e-003 235 409 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 9 1 1 1 0 1 58 16 81 80 80 0.00000 22.15092 1.35565e-003 406 705 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 10 1 1 1 0 1 59 16 81 80 80 0.00000 22.15092 1.35565e-003 50 87 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 6 0 0 -0.000 0.707 0.707 1 + 1 1 1 1 0 1 60 16 81 80 80 0.00000 22.15092 1.35565e-003 52 90 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 2 1 1 1 0 1 61 16 81 80 80 0.00000 22.15092 1.35565e-003 691 1201 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 3 1 1 1 0 1 62 16 81 80 80 0.00000 22.15092 1.35565e-003 1598 2777 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 4 1 1 1 0 1 63 16 81 80 80 0.00000 22.15092 1.35565e-003 4569 7943 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 5 1 1 1 0 1 64 16 81 80 80 0.00000 22.15092 1.35565e-003 1526 2653 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 6 1 1 1 0 1 65 16 81 80 80 0.00000 22.15092 1.35565e-003 1070 1860 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 7 1 1 1 0 1 66 16 81 80 80 0.00000 22.15092 1.35565e-003 836 1453 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 8 1 1 1 0 1 67 16 81 80 80 0.00000 22.15092 1.35565e-003 562 978 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 9 1 1 1 0 1 68 16 81 80 80 0.00000 22.15092 1.35565e-003 1073 1865 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 10 1 1 1 0 1 69 16 81 80 80 0.00000 22.15092 1.35565e-003 42 72 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 7 0 0 0.000 0.000 0.000 1 + 1 1 1 1 0 1 70 16 81 80 80 0.00000 22.15092 1.35565e-003 53 92 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 2 1 1 1 0 1 71 16 81 80 80 0.00000 22.15092 1.35565e-003 322 561 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 3 1 1 1 0 1 72 16 81 80 80 0.00000 22.15092 1.35565e-003 718 1248 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 4 1 1 1 0 1 73 16 81 80 80 0.00000 22.15092 1.35565e-003 1440 2503 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 5 1 1 1 0 1 74 16 81 80 80 0.00000 22.15092 1.35565e-003 636 1105 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 6 1 1 1 0 1 75 16 81 80 80 0.00000 22.15092 1.35565e-003 467 811 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 7 1 1 1 0 1 76 16 81 80 80 0.00000 22.15092 1.35565e-003 355 616 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 8 1 1 1 0 1 77 16 81 80 80 0.00000 22.15092 1.35565e-003 254 441 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 9 1 1 1 0 1 78 16 81 80 80 0.00000 22.15092 1.35565e-003 442 768 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + 10 1 1 1 0 1 79 16 81 80 80 0.00000 22.15092 1.35565e-003 33 57 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 91.00 0.00 0.00 1000.00 1 90.00 0 0 0 27 0.0 2 7 0 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/NA.PAR b/nibabel/tests/data/NA.PAR new file mode 100644 index 0000000000..77e3b9c218 --- /dev/null +++ b/nibabel/tests/data/NA.PAR @@ -0,0 +1,111 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_4_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : Survey_32ch_HeadCoil +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 4 +. Reconstruction nr : 1 +. Scan Duration [sec] : 30.3 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 9 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Anterior-Posterior +. Technique : T1TFE +. Scan resolution (x, y) : 256 128 +. Scan mode : MS +. Repetition time [ms] : 9.816 +. FOV (ap,fh,rl) [mm] : 250.000 250.000 50.000 +. Water Fat shift [pixels] : 3.497 +. Angulation midslice(ap,fh,rl)[degr]: 0.000 0.000 0.000 +. Off Centre midslice(ap,fh,rl) [mm] : -20.000 20.000 0.000 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 0 +. EPI factor <0,1=no EPI> : 1 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 50 256 256 0.00000 4.06325 1.28441e-002 1070 1860 0.00 0.00 0.00 -20.00 20.00 20.00 10.000 10.000 0 2 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 50 256 256 0.00000 4.06325 1.28441e-002 777 1351 0.00 0.00 0.00 -20.00 20.00 0.00 10.000 10.000 0 2 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 50 256 256 0.00000 4.06325 1.28441e-002 480 835 0.00 0.00 0.00 -20.00 20.00 -20.00 10.000 10.000 0 2 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 50 256 256 0.00000 4.06325 1.28441e-002 1645 2859 0.00 0.00 0.00 -20.00 20.00 0.00 10.000 10.000 0 3 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 50 256 256 0.00000 4.06325 1.28441e-002 902 1567 0.00 0.00 0.00 0.00 20.00 0.00 10.000 10.000 0 3 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 50 256 256 0.00000 4.06325 1.28441e-002 109 190 0.00 0.00 0.00 20.00 20.00 0.00 10.000 10.000 0 3 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 50 256 256 0.00000 4.06325 1.28441e-002 1241 2156 0.00 0.00 0.00 0.00 20.00 0.00 10.000 10.000 0 1 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 50 256 256 0.00000 4.06325 1.28441e-002 941 1636 0.00 0.00 0.00 0.00 40.00 0.00 10.000 10.000 0 1 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 50 256 256 0.00000 4.06325 1.28441e-002 150 260 0.00 0.00 0.00 0.00 60.00 0.00 10.000 10.000 0 1 0 2 0.977 0.977 4.60 0.00 0.00 0.00 1 15.00 0 0 0 64 0.0 1 1 7 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/T1.PAR b/nibabel/tests/data/T1.PAR new file mode 100644 index 0000000000..4abc1987e5 --- /dev/null +++ b/nibabel/tests/data/T1.PAR @@ -0,0 +1,112 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_6_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : T1 SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 6 +. Reconstruction nr : 1 +. Scan Duration [sec] : 65 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : T1TFE +. Scan resolution (x, y) : 76 62 +. Scan mode : 3D +. Repetition time [ms] : 4.364 +. FOV (ap,fh,rl) [mm] : 130.000 100.000 154.375 +. Water Fat shift [pixels] : 1.117 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 0 +. EPI factor <0,1=no EPI> : 1 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 81 80 80 0.00000 1.26032 2.84925e-005 133 231 -1.98 0.55 0.02 -18.79 -22.82 -16.42 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 81 80 80 0.00000 1.26032 2.84925e-005 294 512 -1.98 0.55 0.02 -18.79 -12.82 -16.77 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 81 80 80 0.00000 1.26032 2.84925e-005 427 742 -1.98 0.55 0.02 -18.80 -2.83 -17.11 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 81 80 80 0.00000 1.26032 2.84925e-005 565 982 -1.98 0.55 0.02 -18.80 7.17 -17.46 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 81 80 80 0.00000 1.26032 2.84925e-005 474 825 -1.98 0.55 0.02 -18.80 17.16 -17.80 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 81 80 80 0.00000 1.26032 2.84925e-005 1070 1860 -1.98 0.55 0.02 -18.81 27.15 -18.15 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 81 80 80 0.00000 1.26032 2.84925e-005 1179 2049 -1.98 0.55 0.02 -18.81 37.15 -18.49 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 81 80 80 0.00000 1.26032 2.84925e-005 427 742 -1.98 0.55 0.02 -18.81 47.14 -18.84 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 81 80 80 0.00000 1.26032 2.84925e-005 175 304 -1.98 0.55 0.02 -18.82 57.14 -19.19 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + 10 1 1 1 0 2 9 16 81 80 80 0.00000 1.26032 2.84925e-005 114 199 -1.98 0.55 0.02 -18.82 67.13 -19.53 10.000 0.000 0 1 0 2 1.912 1.912 2.08 0.00 0.00 0.00 1 8.00 0 0 0 7 0.0 1 1 7 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/T2-interleaved.PAR b/nibabel/tests/data/T2-interleaved.PAR new file mode 100644 index 0000000000..da7d3c0032 --- /dev/null +++ b/nibabel/tests/data/T2-interleaved.PAR @@ -0,0 +1,112 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_8_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : T2-interleaved SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 8 +. Reconstruction nr : 1 +. Scan Duration [sec] : 8 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : TSE +. Scan resolution (x, y) : 76 56 +. Scan mode : MS +. Repetition time [ms] : 1000.000 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 2.479 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 1 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 0 +. EPI factor <0,1=no EPI> : 1 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 1 0 16 81 80 80 0.00000 8.38730 7.95870e-003 1070 1860 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 1 1 0 1 1 16 81 80 80 0.00000 8.38730 7.95870e-003 11500 19991 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 1 1 0 1 2 16 81 80 80 0.00000 8.38730 7.95870e-003 16200 28161 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 1 1 0 1 3 16 81 80 80 0.00000 8.38730 7.95870e-003 67043 116541 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 1 1 0 1 4 16 81 80 80 0.00000 8.38730 7.95870e-003 80065 139178 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 1 1 0 1 5 16 81 80 80 0.00000 8.38730 7.95870e-003 30352 52762 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 1 1 0 1 6 16 81 80 80 0.00000 8.38730 7.95870e-003 11471 19940 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 1 1 0 1 7 16 81 80 80 0.00000 8.38730 7.95870e-003 8085 14055 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 1 1 0 1 8 16 81 80 80 0.00000 8.38730 7.95870e-003 4902 8521 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 1 1 0 1 9 16 81 80 80 0.00000 8.38730 7.95870e-003 4201 7302 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/T2.PAR b/nibabel/tests/data/T2.PAR new file mode 100644 index 0000000000..819b45a185 --- /dev/null +++ b/nibabel/tests/data/T2.PAR @@ -0,0 +1,112 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_7_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : T2 SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 7 +. Reconstruction nr : 1 +. Scan Duration [sec] : 8 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : TSE +. Scan resolution (x, y) : 76 56 +. Scan mode : MS +. Repetition time [ms] : 1000.000 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 2.479 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 1 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 0 +. EPI factor <0,1=no EPI> : 1 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 1 0 16 81 80 80 0.00000 11.66129 5.47580e-003 1070 1860 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 1 1 0 1 1 16 81 80 80 0.00000 11.66129 5.47580e-003 11765 20450 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 1 1 0 1 2 16 81 80 80 0.00000 11.66129 5.47580e-003 16140 28057 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 1 1 0 1 3 16 81 80 80 0.00000 11.66129 5.47580e-003 70823 123112 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 1 1 0 1 4 16 81 80 80 0.00000 11.66129 5.47580e-003 75089 130529 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 1 1 0 1 5 16 81 80 80 0.00000 11.66129 5.47580e-003 29296 50926 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 1 1 0 1 6 16 81 80 80 0.00000 11.66129 5.47580e-003 12039 20927 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 1 1 0 1 7 16 81 80 80 0.00000 11.66129 5.47580e-003 7482 13006 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 1 1 0 1 8 16 81 80 80 0.00000 11.66129 5.47580e-003 4821 8380 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 1 1 0 1 9 16 81 80 80 0.00000 11.66129 5.47580e-003 4027 7000 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 80.00 0.00 0.00 0.00 1 90.00 0 0 0 15 0.0 1 1 8 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/T2_-interleaved.PAR b/nibabel/tests/data/T2_-interleaved.PAR new file mode 100644 index 0000000000..d73ab881d5 --- /dev/null +++ b/nibabel/tests/data/T2_-interleaved.PAR @@ -0,0 +1,122 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_10_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : T2*-interleaved SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 10 +. Reconstruction nr : 1 +. Scan Duration [sec] : 12 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 2 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : FEEPI +. Scan resolution (x, y) : 76 62 +. Scan mode : MS +. Repetition time [ms] : 2000.000 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 8.014 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 1 +. EPI factor <0,1=no EPI> : 27 +. Dynamic scan <0=no 1=yes> ? : 1 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 81 80 80 0.00000 239.84469 1.19452e-003 1070 1860 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 81 80 80 0.00000 239.84469 1.19452e-003 1873 3256 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 81 80 80 0.00000 239.84469 1.19452e-003 3531 6138 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 81 80 80 0.00000 239.84469 1.19452e-003 8062 14015 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 81 80 80 0.00000 239.84469 1.19452e-003 4139 7194 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 81 80 80 0.00000 239.84469 1.19452e-003 6787 11798 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 81 80 80 0.00000 239.84469 1.19452e-003 1906 3314 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 81 80 80 0.00000 239.84469 1.19452e-003 1147 1993 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 81 80 80 0.00000 239.84469 1.19452e-003 1116 1940 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 1 1 0 2 9 16 81 80 80 0.00000 239.84469 1.19452e-003 933 1622 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 1 1 2 1 0 2 10 16 81 80 80 0.00000 239.84469 1.19452e-003 1089 1892 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 2 1 0 2 11 16 81 80 80 0.00000 239.84469 1.19452e-003 1826 3175 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 2 1 0 2 12 16 81 80 80 0.00000 239.84469 1.19452e-003 3655 6353 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 2 1 0 2 13 16 81 80 80 0.00000 239.84469 1.19452e-003 7595 13203 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 2 1 0 2 14 16 81 80 80 0.00000 239.84469 1.19452e-003 3657 6357 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 2 1 0 2 15 16 81 80 80 0.00000 239.84469 1.19452e-003 7312 12710 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 2 1 0 2 16 16 81 80 80 0.00000 239.84469 1.19452e-003 1663 2891 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 2 1 0 2 17 16 81 80 80 0.00000 239.84469 1.19452e-003 1278 2221 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 2 1 0 2 18 16 81 80 80 0.00000 239.84469 1.19452e-003 1041 1809 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 2 1 0 2 19 16 81 80 80 0.00000 239.84469 1.19452e-003 891 1549 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/T2_.PAR b/nibabel/tests/data/T2_.PAR new file mode 100644 index 0000000000..d37ef17f53 --- /dev/null +++ b/nibabel/tests/data/T2_.PAR @@ -0,0 +1,122 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_9_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : T2* SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 9 +. Reconstruction nr : 1 +. Scan Duration [sec] : 12 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 2 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : FEEPI +. Scan resolution (x, y) : 76 62 +. Scan mode : MS +. Repetition time [ms] : 2000.000 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 8.014 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 1 +. EPI factor <0,1=no EPI> : 27 +. Dynamic scan <0=no 1=yes> ? : 1 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 81 80 80 0.00000 251.05495 1.18150e-003 1070 1860 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 81 80 80 0.00000 251.05495 1.18150e-003 1697 2951 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 81 80 80 0.00000 251.05495 1.18150e-003 5912 10277 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 81 80 80 0.00000 251.05495 1.18150e-003 11675 20295 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 81 80 80 0.00000 251.05495 1.18150e-003 3596 6251 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 81 80 80 0.00000 251.05495 1.18150e-003 7385 12838 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 81 80 80 0.00000 251.05495 1.18150e-003 1846 3209 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 81 80 80 0.00000 251.05495 1.18150e-003 1121 1948 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 81 80 80 0.00000 251.05495 1.18150e-003 1001 1741 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 1 1 0 2 9 16 81 80 80 0.00000 251.05495 1.18150e-003 912 1586 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 1 2 1.912 1.912 35.00 0.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 1 1 2 1 0 2 10 16 81 80 80 0.00000 251.05495 1.18150e-003 1129 1963 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 2 1 2 1 0 2 11 16 81 80 80 0.00000 251.05495 1.18150e-003 1682 2924 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 3 1 2 1 0 2 12 16 81 80 80 0.00000 251.05495 1.18150e-003 6115 10629 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 4 1 2 1 0 2 13 16 81 80 80 0.00000 251.05495 1.18150e-003 10693 18587 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 5 1 2 1 0 2 14 16 81 80 80 0.00000 251.05495 1.18150e-003 3571 6208 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 6 1 2 1 0 2 15 16 81 80 80 0.00000 251.05495 1.18150e-003 6788 11799 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 7 1 2 1 0 2 16 16 81 80 80 0.00000 251.05495 1.18150e-003 1840 3198 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 8 1 2 1 0 2 17 16 81 80 80 0.00000 251.05495 1.18150e-003 1095 1903 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 9 1 2 1 0 2 18 16 81 80 80 0.00000 251.05495 1.18150e-003 1092 1898 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + 10 1 2 1 0 2 19 16 81 80 80 0.00000 251.05495 1.18150e-003 957 1664 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 2 2 1.912 1.912 35.00 2.00 0.00 0.00 1 90.00 0 0 0 27 0.0 1 1 8 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== diff --git a/nibabel/tests/data/fieldmap.PAR b/nibabel/tests/data/fieldmap.PAR new file mode 100644 index 0000000000..099871a718 --- /dev/null +++ b/nibabel/tests/data/fieldmap.PAR @@ -0,0 +1,122 @@ +# === DATA DESCRIPTION FILE ====================================================== +# +# CAUTION - Investigational device. +# Limited by Federal Law to investigational use. +# +# Dataset name: H:\Export\05aug14_test_samples_11_1 +# +# CLINICAL TRYOUT Research image export tool V4.2 +# +# === GENERAL INFORMATION ======================================================== +# +. Patient name : 05aug14test +. Examination name : test +. Protocol name : WIP fieldmap SENSE +. Examination date/time : 2014.08.05 / 11:27:34 +. Series Type : Image MRSERIES +. Acquisition nr : 11 +. Reconstruction nr : 1 +. Scan Duration [sec] : 11.3 +. Max. number of cardiac phases : 1 +. Max. number of echoes : 1 +. Max. number of slices/locations : 10 +. Max. number of dynamics : 1 +. Max. number of mixes : 1 +. Patient position : Head First Supine +. Preparation direction : Right-Left +. Technique : FFE +. Scan resolution (x, y) : 76 62 +. Scan mode : MS +. Repetition time [ms] : 188.384 +. FOV (ap,fh,rl) [mm] : 130.000 120.970 154.375 +. Water Fat shift [pixels] : 0.347 +. Angulation midslice(ap,fh,rl)[degr]: -1.979 0.546 0.019 +. Off Centre midslice(ap,fh,rl) [mm] : -18.805 22.157 -17.977 +. Flow compensation <0=no 1=yes> ? : 0 +. Presaturation <0=no 1=yes> ? : 0 +. Phase encoding velocity [cm/sec] : 0.000000 0.000000 0.000000 +. MTC <0=no 1=yes> ? : 0 +. SPIR <0=no 1=yes> ? : 1 +. EPI factor <0,1=no EPI> : 1 +. Dynamic scan <0=no 1=yes> ? : 0 +. Diffusion <0=no 1=yes> ? : 0 +. Diffusion echo time [ms] : 0.0000 +. Max. number of diffusion values : 1 +. Max. number of gradient orients : 1 +. Number of label types <0=no ASL> : 0 +# +# === PIXEL VALUES ============================================================= +# PV = pixel value in REC file, FP = floating point value, DV = displayed value on console +# RS = rescale slope, RI = rescale intercept, SS = scale slope +# DV = PV * RS + RI FP = DV / (RS * SS) +# +# === IMAGE INFORMATION DEFINITION ============================================= +# The rest of this file contains ONE line per image, this line contains the following information: +# +# slice number (integer) +# echo number (integer) +# dynamic scan number (integer) +# cardiac phase number (integer) +# image_type_mr (integer) +# scanning sequence (integer) +# index in REC file (in images) (integer) +# image pixel size (in bits) (integer) +# scan percentage (integer) +# recon resolution (x y) (2*integer) +# rescale intercept (float) +# rescale slope (float) +# scale slope (float) +# window center (integer) +# window width (integer) +# image angulation (ap,fh,rl in degrees ) (3*float) +# image offcentre (ap,fh,rl in mm ) (3*float) +# slice thickness (in mm ) (float) +# slice gap (in mm ) (float) +# image_display_orientation (integer) +# slice orientation ( TRA/SAG/COR ) (integer) +# fmri_status_indication (integer) +# image_type_ed_es (end diast/end syst) (integer) +# pixel spacing (x,y) (in mm) (2*float) +# echo_time (float) +# dyn_scan_begin_time (float) +# trigger_time (float) +# diffusion_b_factor (float) +# number of averages (integer) +# image_flip_angle (in degrees) (float) +# cardiac frequency (bpm) (integer) +# minimum RR-interval (in ms) (integer) +# maximum RR-interval (in ms) (integer) +# TURBO factor <0=no turbo> (integer) +# Inversion delay (in ms) (float) +# diffusion b value number (imagekey!) (integer) +# gradient orientation number (imagekey!) (integer) +# contrast type (string) +# diffusion anisotropy type (string) +# diffusion (ap, fh, rl) (3*float) +# label type (ASL) (imagekey!) (integer) +# +# === IMAGE INFORMATION ========================================================== +# sl ec dyn ph ty idx pix scan% rec size (re)scale window angulation offcentre thick gap info spacing echo dtime ttime diff avg flip freq RR-int turbo delay b grad cont anis diffusion L.ty + + 1 1 1 1 0 2 0 16 81 80 80 0.00000 10.40049 1.18623e-003 1070 1860 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 2 1 1 1 0 2 1 16 81 80 80 0.00000 10.40049 1.18623e-003 3042 5288 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 3 1 1 1 0 2 2 16 81 80 80 0.00000 10.40049 1.18623e-003 2802 4871 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 4 1 1 1 0 2 3 16 81 80 80 0.00000 10.40049 1.18623e-003 3542 6157 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 5 1 1 1 0 2 4 16 81 80 80 0.00000 10.40049 1.18623e-003 3267 5679 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 6 1 1 1 0 2 5 16 81 80 80 0.00000 10.40049 1.18623e-003 1350 2346 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 7 1 1 1 0 2 6 16 81 80 80 0.00000 10.40049 1.18623e-003 536 931 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 8 1 1 1 0 2 7 16 81 80 80 0.00000 10.40049 1.18623e-003 516 897 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 9 1 1 1 0 2 8 16 81 80 80 0.00000 10.40049 1.18623e-003 464 807 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 10 1 1 1 0 2 9 16 81 80 80 0.00000 10.40049 1.18623e-003 265 461 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 1 1 1 1 3 4 10 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.79 -33.29 -16.06 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 2 1 1 1 3 4 11 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.79 -20.97 -16.49 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 3 1 1 1 3 4 12 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.80 -8.65 -16.91 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 4 1 1 1 3 4 13 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.80 3.67 -17.34 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 5 1 1 1 3 4 14 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.80 16.00 -17.76 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 6 1 1 1 3 4 15 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.81 28.32 -18.19 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 7 1 1 1 3 4 16 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.81 40.64 -18.62 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 8 1 1 1 3 4 17 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.82 52.96 -19.04 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 9 1 1 1 3 4 18 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.82 65.29 -19.47 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + 10 1 1 1 3 4 19 16 81 80 80 -500.00000 0.24420 4.09500e+000 0 1000 -1.98 0.55 0.02 -18.82 77.61 -19.89 10.000 2.330 0 1 0 2 1.912 1.912 7.00 0.00 0.00 0.00 1 55.00 0 0 0 1 0.0 1 1 7 0 0.000 0.000 0.000 1 + +# === END OF DATA DESCRIPTION FILE =============================================== From 45e91eb2eafaa4b2dc621fcee1cba449475bd95c Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 16:12:35 -0400 Subject: [PATCH 32/55] RF: move permit_truncated to header init Move permit_truncated flag to header initialization, and fuse calculated values in header, rather than in parsing function. It seemed clearer to me to put the parsing in a parsing function and the post-processing in the header creation. --- nibabel/parrec.py | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 2b46fa1064..5ce2e8efe4 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -354,16 +354,13 @@ def one_line(long_str): return ' '.join(line.strip() for line in long_str.splitlines()) -def parse_PAR_header(fobj, permit_truncated=False): +def parse_PAR_header(fobj): """Parse a PAR header and aggregate all information into useful containers. Parameters ---------- fobj : file-object The PAR header file object. - permit_truncated : bool, optional - If True, a warning is emitted instead of an error when a truncated - recording is detected. Returns ------- @@ -383,8 +380,6 @@ def parse_PAR_header(fobj, permit_truncated=False): """.format(version))) general_info = _process_gen_dict(gen_dict) image_defs = _process_image_lines(image_lines) - extra_info = _calc_extras(general_info, image_defs, permit_truncated) - general_info.update(extra_info) return general_info, image_defs @@ -457,19 +452,23 @@ def __array__(self): class PARRECHeader(Header): """PAR/REC header""" - def __init__(self, info, image_defs): + def __init__(self, info, image_defs, permit_truncated=False): """ Parameters ---------- info : dict - "General information" from the PAR file (as returned by - `parse_PAR_header()`). + "General information" from the PAR file (as returned by + `parse_PAR_header()`). image_defs : array - Structured array with image definitions from the PAR file (as - returned by `parse_PAR_header()`). + Structured array with image definitions from the PAR file (as + returned by `parse_PAR_header()`). + permit_truncated : bool, optional + If True, a warning is emitted instead of an error when a truncated + recording is detected. """ self.general_info = info self.image_defs = image_defs + self.general_info.update(_calc_extras(info, image_defs, permit_truncated)) self._slice_orientation = None # charge with basic properties to be able to use base class # functionality @@ -492,8 +491,8 @@ def from_header(klass, header=None): @classmethod def from_fileobj(klass, fileobj, permit_truncated=False): - info, image_defs = parse_PAR_header(fileobj, permit_truncated) - return klass(info, image_defs) + info, image_defs = parse_PAR_header(fileobj) + return klass(info, image_defs, permit_truncated) def copy(self): return PARRECHeader(deepcopy(self.general_info), From fe8c8bc53c81cd5119b4b30b287451ef15123e54 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 15:58:46 -0400 Subject: [PATCH 33/55] NF: add helpers to find missing slices Routines to find volumes from a list of slices, and find missing volumes from slices lists. --- nibabel/parrec.py | 65 ++++++++++++++++++++++++++++++++++++ nibabel/tests/test_parrec.py | 51 +++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 5ce2e8efe4..78385459fc 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -300,6 +300,71 @@ def _process_image_lines(image_lines): return image_defs +def vol_numbers(slice_nos): + """ Calculate volume numbers inferred from slice numbers `slice_nos` + + The volume number for each slice is the number of times this slice has + occurred previously in the `slice_nos` sequence + + Parameters + ---------- + slice_nos : sequence + Sequence of slice numbers, e.g. ``[1, 2, 3, 4, 1, 2, 3, 4]``. + + Returns + ------- + vol_nos : list + A list, the same length of `slice_nos` giving the volume number for + each corresponding slice number. + """ + counter = {} + vol_nos = [] + for s_no in slice_nos: + count = counter.setdefault(s_no, 0) + vol_nos.append(count) + counter[s_no] += 1 + return vol_nos + + +def vol_is_full(slice_nos, slice_max, slice_min=1): + """ Vector with True for slices in complete volume, False otherwise + + Parameters + ---------- + slice_nos : sequence + Sequence of slice numbers, e.g. ``[1, 2, 3, 4, 1, 2, 3, 4]``. + slice_max : int + Highest slice number for a full slice set. Slice set will be + ``range(slice_min, slice_max+1)``. + slice_min : int + Lowest slice number for full slice set. + + Returns + ------- + is_full : array + Bool vector with True for slices in full volumes, False for slices in + partial volumes. A full volume is a volume with all slices in the + ``slice set`` as defined above. + + Raises + ------ + ValueError if any `slice_nos` value is outside slice set. + """ + slice_set = set(range(slice_min, slice_max + 1)) + if not slice_set.issuperset(slice_nos): + raise ValueError( + 'Slice numbers outside inclusive range {0} to {1}'.format( + slice_min, slice_max)) + vol_nos = np.array(vol_numbers(slice_nos)) + slice_nos = np.asarray(slice_nos) + is_full = np.ones(slice_nos.shape, dtype=bool) + for vol_no in set(vol_nos): + ours = vol_nos == vol_no + if not set(slice_nos[ours]) == slice_set: + is_full[ours] = False + return is_full + + def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): """Helper to alert user about truncated files and adjust computation""" extra = (not must_exceed_one) or (n_expected > 1) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 3dc012a78a..9d68f6a080 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -2,11 +2,13 @@ """ from os.path import join as pjoin, dirname +from glob import glob import numpy as np from numpy import array as npa -from ..parrec import parse_PAR_header, PARRECHeader, PARRECError +from ..parrec import (parse_PAR_header, PARRECHeader, PARRECError, vol_numbers, + vol_is_full) from ..openers import Opener from numpy.testing import (assert_almost_equal, @@ -164,3 +166,50 @@ def test_affine_regression(): with open(fname, 'rt') as fobj: hdr = PARRECHeader.from_fileobj(fobj) assert_almost_equal(hdr.get_affine(), exp_affine) + + +def test_vol_number(): + # Test algorithm for calculating volume number + assert_array_equal(vol_numbers([1, 3, 0]), [0, 0, 0]) + assert_array_equal(vol_numbers([1, 3, 0, 0]), [ 0, 0, 0, 1]) + assert_array_equal(vol_numbers([1, 3, 0, 0, 0]), [0, 0, 0, 1, 2]) + assert_array_equal(vol_numbers([1, 3, 0, 0, 4]), [0, 0, 0, 1, 0]) + assert_array_equal(vol_numbers([1, 3, 0, 3, 1, 0]), + [0, 0, 0, 1, 1, 1]) + assert_array_equal(vol_numbers([1, 3, 0, 3, 1, 0, 4]), + [0, 0, 0, 1, 1, 1, 0]) + assert_array_equal(vol_numbers([1, 3, 0, 3, 1, 0, 3, 1, 0]), + [0, 0, 0, 1, 1, 1, 2, 2, 2]) + + +def test_vol_is_full(): + assert_array_equal(vol_is_full([3, 2, 1], 3), True) + assert_array_equal(vol_is_full([3, 2, 1], 4), False) + assert_array_equal(vol_is_full([4, 2, 1], 4), False) + assert_array_equal(vol_is_full([3, 2, 4, 1], 4), True) + assert_array_equal(vol_is_full([3, 2, 1], 3, 0), False) + assert_array_equal(vol_is_full([3, 2, 0, 1], 3, 0), True) + assert_raises(ValueError, vol_is_full, [2, 1, 0], 2) + assert_raises(ValueError, vol_is_full, [3, 2, 1], 3, 2) + assert_array_equal(vol_is_full([3, 2, 1, 2, 3, 1], 3), + [True] * 6) + assert_array_equal(vol_is_full([3, 2, 1, 2, 3], 3), + [True, True, True, False, False]) + + +def test_vol_calculations(): + # Test vol_is_full on sample data + for par in glob(pjoin(DATA_PATH, '*.PAR')): + with open(par, 'rt') as fobj: + gen_info, slice_info = parse_PAR_header(fobj) + slice_nos = slice_info['slice number'] + max_slice = gen_info['max_slices'] + assert_equal(set(slice_nos), set(range(1, max_slice + 1))) + assert_array_equal(vol_is_full(slice_nos, max_slice), True) + if par.endswith('NA.PAR'): + continue # Cannot parse this one + # Fourth dimension shows same number of volumes as vol_numbers + hdr = PARRECHeader(gen_info, slice_info) + shape = hdr.get_data_shape() + d4 = 1 if len(shape) == 3 else shape[3] + assert_equal(max(vol_numbers(slice_nos)), d4 - 1) From 5d36e580fe7255310d509080e5cfc70db4ac42f5 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 16:23:33 -0400 Subject: [PATCH 34/55] TST: add tests for diffusion parameters --- nibabel/tests/test_parrec.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 9d68f6a080..6e468def12 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -213,3 +213,14 @@ def test_vol_calculations(): shape = hdr.get_data_shape() d4 = 1 if len(shape) == 3 else shape[3] assert_equal(max(vol_numbers(slice_nos)), d4 - 1) + + +def test_diffusion_parameters(): + # Check getting diffusion parameters from diffusion example + dti_par = pjoin(DATA_PATH, 'DTI.PAR') + with open(dti_par, 'rt') as fobj: + dti_hdr = PARRECHeader.from_fileobj(fobj) + assert_equal(dti_hdr.get_data_shape(), (80, 80, 10, 8)) + assert_equal(dti_hdr.general_info['diffusion'], 1) + bvals, bvecs = dti_hdr.get_bvals_bvecs() + # assert_almost_equal(bvals, [1000] * 6 + [0, 0]) From cb9f4fe35b7c54820c0274c1d272fa1b3cf3e52f Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 17:24:04 -0400 Subject: [PATCH 35/55] RF: refactor slice sorting using vol calculations Use ``vol_numbers`` and ``vol_is_full`` functions to sort REC slices into slices, volumes, putting partial volumes at end. This fixes a bug in the case when sorting with slice number and one other varying key, where the second key has the same value for more than one volume. In this case the two slice 1s get sorted together, followed by the 2 slice 2s, etc. This was happening using the diffusion gradient id as a key, because the b0 and the ADC volume have the same diffusion gradient id == 7 in DTI.PAR. Test in the next commit, when testing bvals, bvecs. --- nibabel/parrec.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 78385459fc..65bec011ee 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -867,16 +867,16 @@ def get_rec_shape(self): def sorted_slice_indices(self): """Indices to sort (and maybe discard) slices in REC file - Returns list for indexing into a single dimension of an array. + Returns list for indexing into the last (third) dimension of the REC + data array, and (equivalently) the only dimension of + ``self.image_defs``. - If the recording is truncated, this will take care of discarding - any indices that are not meant to be used. + If the recording is truncated, the returned indices take care of + discarding any indices that are not meant to be used. """ - # No attempt to detect missing combinations or early stop - keys = ['slice number', 'scanning sequence', 'image_type_mr', - 'gradient orientation number', 'dynamic scan number', - 'echo number'] - keys = [self.image_defs[k] for k in keys] + slice_nos = self.image_defs['slice number'] + is_full = vol_is_full(slice_nos, self.general_info['max_slices']) + keys = (slice_nos, vol_numbers(slice_nos), np.logical_not(is_full)) # Figure out how many we need to remove from the end, and trim them # Based on our sorting, they should always be last n_used = np.prod(self.get_data_shape()[2:]) From 19624d184a1f02556c18be85d26b0a9aff3e92b5 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 17:31:20 -0400 Subject: [PATCH 36/55] BF+TST : fix and test selection of bvals, bvecs The change in sorting fixed a problem with sorting slices, allowing me to test bvals, bvecs. Check that bvals, bvecs do not vary over volume. Get bvals, bvecs for first slice in each volume. Test these are as expected from DTI.PAR values. --- nibabel/parrec.py | 16 +++++++++++----- nibabel/tests/test_parrec.py | 20 +++++++++++++++++++- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 65bec011ee..d8390c32ab 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -609,11 +609,17 @@ def get_bvals_bvecs(self): Array of b vectors, shape (n_directions, 3). """ reorder = self.sorted_slice_indices - bvals = self.image_defs['diffusion_b_factor'][reorder] - bvecs = self.image_defs['diffusion'][reorder] - shape = self.get_data_shape() - bvals = bvals[::shape[-1]] - bvecs = bvecs[::shape[-1]] + n_slices, n_vols = self.get_data_shape()[-2:] + bvals = self.image_defs['diffusion_b_factor'][reorder].reshape( + (n_slices, n_vols), order='F') + # All bvals within volume should be the same + assert not np.any(np.diff(bvals, axis=0)) + bvals = bvals[0] + bvecs = self.image_defs['diffusion'][reorder].reshape( + (n_slices, n_vols, 3), order='F') + # All 3 values of bvecs should be same within volume + assert not np.any(np.diff(bvecs, axis=0)) + bvecs = bvecs[0] # rotate bvecs to match stored image orientation permute_to_psl = ACQ_TO_PSL[self.get_slice_orientation()] bvecs = apply_affine(np.linalg.inv(permute_to_psl), bvecs) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 6e468def12..a71f0e30b1 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -91,6 +91,19 @@ [ 0. , 0. , 0. , 1. ]]), } +# Original values for b values in DTI.PAR, still in PSL orientation +DTI_PAR_BVECS = np.array([[-0.667, -0.667, -0.333], + [-0.333, 0.667, -0.667], + [-0.667, 0.333, 0.667], + [-0.707, -0.000, -0.707], + [-0.707, 0.707, 0.000], + [-0.000, 0.707, 0.707], + [ 0.000, 0.000, 0.000], + [ 0.000, 0.000, 0.000]]) + +# DTI.PAR values for bvecs +DTI_PAR_BVALS = [1000] * 6 + [0, 1000] + def test_header(): hdr = PARRECHeader(HDR_INFO, HDR_DEFS) @@ -223,4 +236,9 @@ def test_diffusion_parameters(): assert_equal(dti_hdr.get_data_shape(), (80, 80, 10, 8)) assert_equal(dti_hdr.general_info['diffusion'], 1) bvals, bvecs = dti_hdr.get_bvals_bvecs() - # assert_almost_equal(bvals, [1000] * 6 + [0, 0]) + assert_almost_equal(bvals, DTI_PAR_BVALS) + # DTI_PAR_BVECS gives bvecs copied from first slice each vol in DTI.PAR + # Permute to match bvec directions to acquisition directions + assert_almost_equal(bvecs, DTI_PAR_BVECS[:, [2, 0, 1]]) + # Check q vectors + assert_almost_equal(dti_hdr.get_q_vectors(), bvals[:, None] * bvecs) From 0bb5b4e04fa2aaca44567a25ae233b2cebd26fc3 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 17:54:51 -0400 Subject: [PATCH 37/55] RF: None from diffusion params if not diffusion Return None from diffusion parameters getters if the acquisition is not diffusion. --- nibabel/parrec.py | 19 +++++++++++++------ nibabel/tests/test_parrec.py | 14 +++++++++++++- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index d8390c32ab..167ba39b97 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -592,10 +592,13 @@ def get_q_vectors(self): Returns ------- - q_vectors : array - Array of q vectors (bvals * bvecs). + q_vectors : None or array + Array of q vectors (bvals * bvecs), or None if not a diffusion + acquisition. """ bvals, bvecs = self.get_bvals_bvecs() + if (bvals, bvecs) == (None, None): + return None return bvecs * bvals[:, np.newaxis] def get_bvals_bvecs(self): @@ -603,11 +606,15 @@ def get_bvals_bvecs(self): Returns ------- - b_vals : array - Array of b values, shape (n_directions,). - b_vectors : array - Array of b vectors, shape (n_directions, 3). + b_vals : None or array + Array of b values, shape (n_directions,), or None if not a + diffusion acquisition. + b_vectors : None or array + Array of b vectors, shape (n_directions, 3), or None if not a + diffusion acquisition. """ + if self.general_info['diffusion'] == 0: + return None, None reorder = self.sorted_slice_indices n_slices, n_vols = self.get_data_shape()[-2:] bvals = self.image_defs['diffusion_b_factor'][reorder].reshape( diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index a71f0e30b1..c5de88cf6a 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -1,7 +1,7 @@ """ Testing parrec module """ -from os.path import join as pjoin, dirname +from os.path import join as pjoin, dirname, basename from glob import glob import numpy as np @@ -242,3 +242,15 @@ def test_diffusion_parameters(): assert_almost_equal(bvecs, DTI_PAR_BVECS[:, [2, 0, 1]]) # Check q vectors assert_almost_equal(dti_hdr.get_q_vectors(), bvals[:, None] * bvecs) + + +def test_null_diffusion_params(): + # Test non-diffusion PARs return None for diffusion params + for par in glob(pjoin(DATA_PATH, '*.PAR')): + if basename(par) in ('DTI.PAR', 'NA.PAR'): + continue + with open(par, 'rt') as fobj: + gen_info, slice_info = parse_PAR_header(fobj) + hdr = PARRECHeader(gen_info, slice_info) + assert_equal(hdr.get_bvals_bvecs(), (None, None)) + assert_equal(hdr.get_q_vectors(), None) From e5feaae0a725e82aa81b86a6eb6f5ea87231ee96 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 18:10:10 -0400 Subject: [PATCH 38/55] RF+TST: script uses new API to detect no diffusion Use API to detect non-diffusion scans by looking for None, None return value from ``hdr.get_bvals_bvecs()``. Test written values in bvals, bvecs files. --- bin/parrec2nii | 25 +++++++++++++------------ nibabel/tests/test_scripts.py | 6 ++++-- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/bin/parrec2nii b/bin/parrec2nii index 869cd1d846..166bcfdcb2 100755 --- a/bin/parrec2nii +++ b/bin/parrec2nii @@ -202,21 +202,22 @@ def proc_file(infile, opts): array_to_file(data_obj, outfile, offset=offset) # write out bvals/bvecs if requested - if opts.bvs and pr_hdr.general_info['n_dti_volumes'] > 1: - verbose('Writing .bvals and .bvecs files') + if opts.bvs: bvals, bvecs = pr_hdr.get_bvals_bvecs() - with open(basefilename + '.bvals', 'w') as fid: - # np.savetxt could do this, but it's just a loop anyway - for val in bvals: - fid.write('%s ' % val) - fid.write('\n') - with open(basefilename + '.bvecs', 'w') as fid: - for row in bvecs.T: - for val in row: + if (bvals, bvecs) == (None, None): + verbose('No DTI volumes detected, bvals and bvecs not written') + else: + verbose('Writing .bvals and .bvecs files') + with open(basefilename + '.bvals', 'w') as fid: + # np.savetxt could do this, but it's just a loop anyway + for val in bvals: fid.write('%s ' % val) fid.write('\n') - elif opts.bvs: - verbose('No DTI volumes detected, bvals and bvecs not written') + with open(basefilename + '.bvecs', 'w') as fid: + for row in bvecs.T: + for val in row: + fid.write('%s ' % val) + fid.write('\n') # write out dwell time if requested if opts.dwell_time: diff --git a/nibabel/tests/test_scripts.py b/nibabel/tests/test_scripts.py index 1282bb33e3..7719fdf723 100644 --- a/nibabel/tests/test_scripts.py +++ b/nibabel/tests/test_scripts.py @@ -24,6 +24,7 @@ from .scriptrunner import ScriptRunner from .nibabel_data import needs_nibabel_data +from .test_parrec import DTI_PAR_BVECS, DTI_PAR_BVALS from .test_parrec_data import BALLS, AFF_OFF @@ -147,8 +148,9 @@ def test_parrec2nii_with_data(): assert_equal(code, 1) # Writes bvals, bvecs files if asked run_command(['parrec2nii', '--overwrite', '--bvs', dti_par]) - assert_true(exists('DTI.bvals')) - assert_true(exists('DTI.bvecs')) + assert_almost_equal(np.loadtxt('DTI.bvals'), DTI_PAR_BVALS) + assert_almost_equal(np.loadtxt('DTI.bvecs'), + DTI_PAR_BVECS[:, [2, 0, 1]].T) assert_false(exists('DTI.dwell_time')) # Need field strength if requesting dwell time code, _, _, = run_command( From 95c549e743cdb1d5a06c8b18b46a56a8bc433d47 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 19:23:21 -0400 Subject: [PATCH 39/55] RF: refactor getting number of slices and volumes Use new vol_numbers, vol_is_full functions to calculate the number of volumes. --- nibabel/parrec.py | 45 ++++++++++++++++----------------------------- 1 file changed, 16 insertions(+), 29 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 167ba39b97..a49f218e14 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -689,16 +689,6 @@ def set_data_offset(self, offset): if offset != 0: raise PARRECError("PAR header assumes offset 0") - def get_ndim(self): - """Return the number of dimensions of the image data.""" - if self.general_info['n_dynamics'] > 1 \ - or self.general_info['n_dti_volumes'] > 1 \ - or self.general_info['n_echoes'] > 1 \ - or self.general_info['n_seq'] > 1: - return 4 - else: - return 3 - def _get_zooms(self): """Compute image zooms from header data. @@ -707,7 +697,8 @@ def _get_zooms(self): # slice orientation for the whole image series slice_gap = self._get_unique_image_prop('slice gap')[0] # scaling per image axis - zooms = np.ones(self.get_ndim()) + n_dim = 4 if self._get_n_vols() > 1 else 3 + zooms = np.ones(n_dim) # spatial axes correspond to voxelsize + inter slice gap # voxel size (inplaneX, inplaneY, slices) zooms[:3] = self.get_voxel_size() @@ -778,6 +769,17 @@ def get_affine(self, origin='scanner'): # Currently in PSL; apply PSL -> RAS return np.dot(PSL_TO_RAS, psl_aff) + def _get_n_slices(self): + """ Get number of slices for output data """ + return len(set(self.image_defs['slice number'])) + + def _get_n_vols(self): + """ Get number of volumes for output data """ + slice_nos = self.image_defs['slice number'] + vol_nos = vol_numbers(slice_nos) + is_full = vol_is_full(slice_nos, self.general_info['max_slices']) + return len(set(np.array(vol_nos)[is_full])) + def get_data_shape_in_file(self): """Return the shape of the binary blob in the REC file. @@ -793,25 +795,10 @@ def get_data_shape_in_file(self): number of dynamic scans, number of directions in diffusion, or number of echoes """ - # there should not be more than one: multiple dynamics, DTI, echoes - lens = [self.general_info[x] for x in ['n_dynamics', 'n_dti_volumes', - 'n_echoes', 'n_seq']] - if sum(x > 1 for x in lens) > 1: - raise PARRECError('Cannot have multiple dynamics, dti volumes, ' - 'or echoes in the same file, found %s of each, ' - 'respectively' % lens) - inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) - shape = inplane_shape + (self.general_info['n_slices'],) - if self.general_info['n_dynamics'] > 1: - shape = shape + (self.general_info['n_dynamics'],) - elif self.general_info['n_dti_volumes'] > 1: - shape = shape + (self.general_info['n_dti_volumes'],) - elif self.general_info['n_echoes'] > 1: - shape = shape + (self.general_info['n_echoes'],) - elif self.general_info['n_seq'] > 1: - shape = shape + (self.general_info['n_seq'],) - return shape + shape = inplane_shape + (self._get_n_slices(),) + n_vols = self._get_n_vols() + return shape + (n_vols,) if n_vols > 1 else shape def get_data_scaling(self, method="dv"): """Returns scaling slope and intercept. From 65b92d65b770478f106d5e3d726a8e6489e612d6 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 19:52:15 -0400 Subject: [PATCH 40/55] RF: use `dyn_scan` general info to detect dynamic ``dyn_scan`` is 1 if the scan is dynamic; use this instead of the calculated ``n_dynamics`` value to determine if the scan is a dynamic scan. --- nibabel/parrec.py | 4 ++-- nibabel/tests/test_parrec.py | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index a49f218e14..0900e33a6a 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -703,8 +703,8 @@ def _get_zooms(self): # voxel size (inplaneX, inplaneY, slices) zooms[:3] = self.get_voxel_size() zooms[2] += slice_gap - if len(zooms) > 3 and self.general_info['n_dynamics'] > 1: - # Convert time from milliseconds to seconds + # If 4D dynamic scan, convert time from milliseconds to seconds + if len(zooms) > 3 and self.general_info['dyn_scan']: zooms[3] = self.general_info['repetition_time'] / 1000. return zooms diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index c5de88cf6a..535a13016f 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -254,3 +254,13 @@ def test_null_diffusion_params(): hdr = PARRECHeader(gen_info, slice_info) assert_equal(hdr.get_bvals_bvecs(), (None, None)) assert_equal(hdr.get_q_vectors(), None) + + +def test_epi_params(): + # Check EPI conversion + for par_root in ('T2_-interleaved', 'T2_', 'phantom_EPI_asc_CLEAR_2_1'): + epi_par = pjoin(DATA_PATH, par_root + '.PAR') + with open(epi_par, 'rt') as fobj: + epi_hdr = PARRECHeader.from_fileobj(fobj) + assert_equal(len(epi_hdr.get_data_shape()), 4) + assert_almost_equal(epi_hdr.get_zooms()[-1], 2.0) From f49693519d0f7872509b6ac0bdacc460aae01423 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 20:18:06 -0400 Subject: [PATCH 41/55] NF: add extra check for partial volumes Use result of vol_is_full to warn or error on presence of partial volumes. --- nibabel/parrec.py | 11 +++++++++-- nibabel/tests/test_parrec.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 0900e33a6a..730a435d65 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -381,7 +381,7 @@ def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): def _calc_extras(general_info, image_defs, permit_truncated): - """ Calculate, return values from `general info`, `image_defs` + """ Calculate, check, return values from `general info`, `image_defs` """ # DTI volumes (b-values-1 x directions) # there is some awkward exception to this rule for b-values > 2 @@ -394,7 +394,8 @@ def _calc_extras(general_info, image_defs, permit_truncated): # XXX TODO This needs to be a conditional! max_dti_volumes += 1 n_dti_volumes += 1 - n_slices = len(np.unique(image_defs['slice number'])) + slice_nos = image_defs['slice number'] + n_slices = len(set(slice_nos)) n_echoes = len(np.unique(image_defs['echo number'])) n_dynamics = len(np.unique(image_defs['dynamic scan number'])) n_seq = len(np.unique(image_defs['scanning sequence'])) @@ -407,6 +408,12 @@ def _calc_extras(general_info, image_defs, permit_truncated): general_info['max_dynamics'], pt, True) n_dti_volumes = _check_truncation('dti volumes', n_dti_volumes, max_dti_volumes, pt, True) + # Final check for partial volumes + if not np.all(vol_is_full(slice_nos, general_info['max_slices'])): + msg = "Found one or more partial volume(s)" + if not pt: + raise PARRECError(msg) + warnings.warn(msg) return dict(n_dti_volumes=n_dti_volumes, n_echoes=n_echoes, n_dynamics=n_dynamics, diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 535a13016f..176fa3a94e 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -3,6 +3,7 @@ from os.path import join as pjoin, dirname, basename from glob import glob +from warnings import catch_warnings import numpy as np from numpy import array as npa @@ -264,3 +265,20 @@ def test_epi_params(): epi_hdr = PARRECHeader.from_fileobj(fobj) assert_equal(len(epi_hdr.get_data_shape()), 4) assert_almost_equal(epi_hdr.get_zooms()[-1], 2.0) + + +def test_truncations(): + # Test tests for truncation + par = pjoin(DATA_PATH, 'T2_.PAR') + with open(par, 'rt') as fobj: + gen_info, slice_info = parse_PAR_header(fobj) + # Header is well-formed as is + hdr = PARRECHeader(gen_info, slice_info) + assert_equal(hdr.get_data_shape(), (80, 80, 10, 2)) + # Drop one line, raises error + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info[:-1]) + # When we are permissive, we raise a warning, and drop a volume + with catch_warnings(record=True) as wlist: + hdr = PARRECHeader(gen_info, slice_info[:-1], permit_truncated=True) + assert_equal(len(wlist), 1) + assert_equal(hdr.get_data_shape(), (80, 80, 10)) From 37ce2cfe812c5a64c4373eb7270c8874c0f23c67 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 21:29:19 -0400 Subject: [PATCH 42/55] RF+TST: refactor / rename / test truncation checks The function that was `_calc_extras` is now effectively just doing truncation checks; rename accordingly. The truncation tests were being used to knock volumes off the end of the data shape, but I'm using the ``vol_is_full`` function for this now. Refactor the checks to reflect fact that the check is not doing this calculation. Add checks for the number of b values and gradient directions. Remove check for number of diffusion slices for now; sorry Eric - I don't understand the check, and the comments worried me. Can we wait for test data to put that back? In general I suppose it's possible to have any nomber of DTI volumes with a given b value and gradient id number, so I don't at the moment see how we can easily use these to check the predicted number of volumes, but it's quite possible I didn't understand the check properly. I have no idea why, but I found it very difficult to grasp what the ``must_exceed_one`` function parameter was for. I think it has the effect, if True, of disabling the expected vs actual number check if the number of expected values is 0 or 1. Because I found it confusing and because all the files I had did have one value when the expected number of values was one, I didn't port that check - maybe add it back if we find a counter-example? --- nibabel/parrec.py | 75 ++++++++++++------------------------ nibabel/tests/test_parrec.py | 25 ++++++++++++ 2 files changed, 50 insertions(+), 50 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 730a435d65..1117c84f52 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -365,60 +365,35 @@ def vol_is_full(slice_nos, slice_max, slice_min=1): return is_full -def _check_truncation(name, n_have, n_expected, permit, must_exceed_one): - """Helper to alert user about truncated files and adjust computation""" - extra = (not must_exceed_one) or (n_expected > 1) - if extra and n_have != n_expected: - msg = ("Header inconsistency: Found <= %i %s, but expected %i." - % (n_have, name, n_expected)) - if not permit: +def _truncation_checks(general_info, image_defs, permit_truncated): + """ Check for presence of truncation in PAR file parameters + + Raise error if truncation present and `permit_truncated` is False. + """ + def _err_or_warn(msg): + if not permit_truncated: raise PARRECError(msg) - msg += " Assuming %i valid %s." % (n_have - 1, name) warnings.warn(msg) - # we assume up to the penultimate data line is correct - n_have -= 1 - return n_have + def _chk_trunc(idef_name, gdef_max_name): + id_values = image_defs[idef_name + ' number'] + n_have = len(set(id_values)) + n_expected = general_info[gdef_max_name] + if n_have != n_expected: + _err_or_warn( + "Header inconsistency: Found {0} {1} values, " + "but expected {2}".format(n_have, idef_name, n_expected)) + + _chk_trunc('slice', 'max_slices') + _chk_trunc('echo', 'max_echoes') + _chk_trunc('dynamic scan', 'max_dynamics') + _chk_trunc('diffusion b value', 'max_diffusion_values') + _chk_trunc('gradient orientation', 'max_gradient_orient') -def _calc_extras(general_info, image_defs, permit_truncated): - """ Calculate, check, return values from `general info`, `image_defs` - """ - # DTI volumes (b-values-1 x directions) - # there is some awkward exception to this rule for b-values > 2 - # XXX need to get test image... - max_dti_volumes = ((general_info['max_diffusion_values'] - 1) - * general_info['max_gradient_orient']) - n_b = len(np.unique(image_defs['diffusion b value number'])) - n_grad = len(np.unique(image_defs['gradient orientation number'])) - n_dti_volumes = (n_b - 1) * n_grad - # XXX TODO This needs to be a conditional! - max_dti_volumes += 1 - n_dti_volumes += 1 - slice_nos = image_defs['slice number'] - n_slices = len(set(slice_nos)) - n_echoes = len(np.unique(image_defs['echo number'])) - n_dynamics = len(np.unique(image_defs['dynamic scan number'])) - n_seq = len(np.unique(image_defs['scanning sequence'])) - pt = permit_truncated - n_slices = _check_truncation('slices', n_slices, - general_info['max_slices'], pt, False) - n_echoes = _check_truncation('echoes', n_echoes, - general_info['max_echoes'], pt, True) - n_dynamics = _check_truncation('dynamics', n_dynamics, - general_info['max_dynamics'], pt, True) - n_dti_volumes = _check_truncation('dti volumes', n_dti_volumes, - max_dti_volumes, pt, True) # Final check for partial volumes - if not np.all(vol_is_full(slice_nos, general_info['max_slices'])): - msg = "Found one or more partial volume(s)" - if not pt: - raise PARRECError(msg) - warnings.warn(msg) - return dict(n_dti_volumes=n_dti_volumes, - n_echoes=n_echoes, - n_dynamics=n_dynamics, - n_slices=n_slices, - n_seq=n_seq) + if not np.all(vol_is_full(image_defs['slice number'], + general_info['max_slices'])): + _err_or_warn("Found one or more partial volume(s)") def one_line(long_str): @@ -540,7 +515,7 @@ def __init__(self, info, image_defs, permit_truncated=False): """ self.general_info = info self.image_defs = image_defs - self.general_info.update(_calc_extras(info, image_defs, permit_truncated)) + _truncation_checks(info, image_defs, permit_truncated) self._slice_orientation = None # charge with basic properties to be able to use base class # functionality diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 176fa3a94e..08793a3eea 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -282,3 +282,28 @@ def test_truncations(): hdr = PARRECHeader(gen_info, slice_info[:-1], permit_truncated=True) assert_equal(len(wlist), 1) assert_equal(hdr.get_data_shape(), (80, 80, 10)) + # Increase max slices to raise error + gen_info['max_slices'] = 11 + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + gen_info['max_slices'] = 10 + hdr = PARRECHeader(gen_info, slice_info) + # Increase max_echoes + gen_info['max_echoes'] = 2 + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + gen_info['max_echoes'] = 1 + hdr = PARRECHeader(gen_info, slice_info) + # dyamics + gen_info['max_dynamics'] = 3 + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + gen_info['max_dynamics'] = 2 + hdr = PARRECHeader(gen_info, slice_info) + # number of b values + gen_info['max_diffusion_values'] = 2 + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + gen_info['max_diffusion_values'] = 1 + hdr = PARRECHeader(gen_info, slice_info) + # number of unique gradients + gen_info['max_gradient_orient'] = 2 + assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) + gen_info['max_gradient_orient'] = 1 + hdr = PARRECHeader(gen_info, slice_info) From aa50749387a73aee885b4da40c8ca78c7dd59eb2 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 6 Oct 2014 22:41:17 -0400 Subject: [PATCH 43/55] RF: rename init helper routines for clarity Rename routines used by __init__ to fill super-class attributes and add docstrings. Add underscore to ``get_data_shape_in_file`` and rename to show it is private and does not compete with ``get_data_shape``. --- nibabel/parrec.py | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 1117c84f52..ac9d79743c 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -524,8 +524,8 @@ def __init__(self, info, image_defs, permit_truncated=False): 'int' + str(self._get_unique_image_prop('image pixel size')[0])] Header.__init__(self, data_dtype=dtype, - shape=self.get_data_shape_in_file(), - zooms=self._get_zooms()) + shape=self._calc_data_shape(), + zooms=self._calc_zooms()) @classmethod def from_header(klass, header=None): @@ -671,10 +671,20 @@ def set_data_offset(self, offset): if offset != 0: raise PARRECError("PAR header assumes offset 0") - def _get_zooms(self): + def _calc_zooms(self): """Compute image zooms from header data. Spatial axis are first three. + + Returns + ------- + zooms : array + Length 3 array for 3D image, length 4 array for 4D image. + + Notes + ----- + This routine called in ``__init__``, so may not be able to use + some attributes available in the fully initalized object. """ # slice orientation for the whole image series slice_gap = self._get_unique_image_prop('slice gap')[0] @@ -762,20 +772,26 @@ def _get_n_vols(self): is_full = vol_is_full(slice_nos, self.general_info['max_slices']) return len(set(np.array(vol_nos)[is_full])) - def get_data_shape_in_file(self): - """Return the shape of the binary blob in the REC file. + def _calc_data_shape(self): + """ Calculate the output shape of the image data + + Returns length 3 tuple for 3D image, length 4 tuple for 4D. Returns ------- n_inplaneX : int - number of voxels in X direction + number of voxels in X direction. n_inplaneY : int - number of voxels in Y direction + number of voxels in Y direction. n_slices : int - number of slices + number of slices. n_vols : int - number of dynamic scans, number of directions in diffusion, or - number of echoes + number of volumes or absent for 3D image. + + Notes + ----- + This routine called in ``__init__``, so may not be able to use + some attributes available in the fully initalized object. """ inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) shape = inplane_shape + (self._get_n_slices(),) From f16c3bc0b1eb5dbd26c1fa1dc68f85617c1c3579 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Tue, 7 Oct 2014 18:28:25 -0400 Subject: [PATCH 44/55] RF: refactor setting of header dtype Slightly easier to read, I think. --- nibabel/parrec.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index ac9d79743c..96f09aec0f 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -520,10 +520,9 @@ def __init__(self, info, image_defs, permit_truncated=False): # charge with basic properties to be able to use base class # functionality # dtype - dtype = np.typeDict[ - 'int' + str(self._get_unique_image_prop('image pixel size')[0])] + bitpix = self._get_unique_image_prop('image pixel size')[0] Header.__init__(self, - data_dtype=dtype, + data_dtype=np.dtype('int' + str(bitpix)).type, shape=self._calc_data_shape(), zooms=self._calc_zooms()) From d8f541bcab9810adba18c5478206d553df31878d Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Tue, 7 Oct 2014 19:37:09 -0400 Subject: [PATCH 45/55] RF: generator to provide PAR files for tests Refactor PAR file fetching and opening into a generator. --- nibabel/tests/test_parrec.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 08793a3eea..31622ae53f 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -211,11 +211,16 @@ def test_vol_is_full(): [True, True, True, False, False]) -def test_vol_calculations(): - # Test vol_is_full on sample data +def gen_par_fobj(): for par in glob(pjoin(DATA_PATH, '*.PAR')): with open(par, 'rt') as fobj: - gen_info, slice_info = parse_PAR_header(fobj) + yield par, fobj + + +def test_vol_calculations(): + # Test vol_is_full on sample data + for par, fobj in gen_par_fobj(): + gen_info, slice_info = parse_PAR_header(fobj) slice_nos = slice_info['slice number'] max_slice = gen_info['max_slices'] assert_equal(set(slice_nos), set(range(1, max_slice + 1))) @@ -247,11 +252,10 @@ def test_diffusion_parameters(): def test_null_diffusion_params(): # Test non-diffusion PARs return None for diffusion params - for par in glob(pjoin(DATA_PATH, '*.PAR')): + for par, fobj in gen_par_fobj(): if basename(par) in ('DTI.PAR', 'NA.PAR'): continue - with open(par, 'rt') as fobj: - gen_info, slice_info = parse_PAR_header(fobj) + gen_info, slice_info = parse_PAR_header(fobj) hdr = PARRECHeader(gen_info, slice_info) assert_equal(hdr.get_bvals_bvecs(), (None, None)) assert_equal(hdr.get_q_vectors(), None) From 275b0eb29da6a4048326549725a0bf01096821f8 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 9 Oct 2014 18:48:30 -0400 Subject: [PATCH 46/55] RF: deprecate PARREC get_voxel_size method ``get_voxel_size` only being used in ``_calc_zooms`` method; fold code into that method, deprecate ``get_voxel_size``. --- nibabel/parrec.py | 16 ++++++++++++---- nibabel/tests/test_parrec.py | 9 ++++++++- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 96f09aec0f..eefa9d694f 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -649,10 +649,18 @@ def get_voxel_size(self): Does not include the slice gap in the slice extent. + This function is deprecated and we will remove it in future versions of + nibabel. Please use ``get_zooms`` instead. If you need the slice + thickness not including the slice gap, use ``self.image_defs['slice + thickness']``. + Returns ------- vox_size: shape (3,) ndarray """ + warnings.warn('Please use "get_zooms" instead of "get_voxel_size"', + DeprecationWarning, + stacklevel=2) # slice orientation for the whole image series slice_thickness = self._get_unique_image_prop('slice thickness')[0] voxsize_inplane = self._get_unique_image_prop('pixel spacing') @@ -690,10 +698,10 @@ def _calc_zooms(self): # scaling per image axis n_dim = 4 if self._get_n_vols() > 1 else 3 zooms = np.ones(n_dim) - # spatial axes correspond to voxelsize + inter slice gap - # voxel size (inplaneX, inplaneY, slices) - zooms[:3] = self.get_voxel_size() - zooms[2] += slice_gap + # spatial sizes are inplane X mm, inplane Y mm + inter slice gap + zooms[:2] = self._get_unique_image_prop('pixel spacing') + slice_thickness = self._get_unique_image_prop('slice thickness')[0] + zooms[2] = slice_thickness + slice_gap # If 4D dynamic scan, convert time from milliseconds to seconds if len(zooms) > 3 and self.general_info['dyn_scan']: zooms[3] = self.general_info['repetition_time'] / 1000. diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 31622ae53f..e7c8756267 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -3,7 +3,7 @@ from os.path import join as pjoin, dirname, basename from glob import glob -from warnings import catch_warnings +from warnings import catch_warnings, simplefilter import numpy as np from numpy import array as npa @@ -182,6 +182,13 @@ def test_affine_regression(): assert_almost_equal(hdr.get_affine(), exp_affine) +def test_get_voxel_size_deprecated(): + hdr = PARRECHeader(HDR_INFO, HDR_DEFS) + with catch_warnings(): + simplefilter('error') + assert_raises(DeprecationWarning, hdr.get_voxel_size) + + def test_vol_number(): # Test algorithm for calculating volume number assert_array_equal(vol_numbers([1, 3, 0]), [0, 0, 0]) From 3e771535f1953af6cc1668334ab13702723b9884 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Thu, 9 Oct 2014 18:53:52 -0400 Subject: [PATCH 47/55] BF: fix typo in PARREC general header field name --- nibabel/parrec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index eefa9d694f..be427a94cf 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -135,7 +135,7 @@ 'Preparation direction': ('prep_direction',), 'Technique': ('tech',), 'Scan resolution (x, y)': ('scan_resolution', int, (2,)), - 'Scan mode': ('san_mode',), + 'Scan mode': ('scan_mode',), 'Repetition time [ms]': ('repetition_time', float), 'FOV (ap,fh,rl) [mm]': ('fov', float, (3,)), 'Water Fat shift [pixels]': ('water_fat_shift', float), From e68a98a9fc4c19a1f97b22940034acd194e8cb79 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Fri, 10 Oct 2014 01:29:28 -0400 Subject: [PATCH 48/55] RF+TST: refactor / test ``_get_unique_image_prop`` I think the previous code was incorrect comparing 2D arrays, and I found it hard to follow. Refactor to do simple diff to find any differing rows in the array; return scalar if selected array is 1D. --- nibabel/parrec.py | 39 +++++++++++++++++------------------- nibabel/tests/test_parrec.py | 17 ++++++++++++++++ 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index be427a94cf..58069942cd 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -520,7 +520,7 @@ def __init__(self, info, image_defs, permit_truncated=False): # charge with basic properties to be able to use base class # functionality # dtype - bitpix = self._get_unique_image_prop('image pixel size')[0] + bitpix = self._get_unique_image_prop('image pixel size') Header.__init__(self, data_dtype=np.dtype('int' + str(bitpix)).type, shape=self._calc_data_shape(), @@ -614,35 +614,32 @@ def get_bvals_bvecs(self): return bvals, bvecs def _get_unique_image_prop(self, name): - """Scan image definitions and return unique value of a property. + """ Scan image definitions and return unique value of a property. - If the requested property is an array this method does _not_ behave - like `np.unique`. It will return the unique combination of all array - elements for any image definition, and _not_ the unique element values. + * Get array for named field of ``self.image_defs``; + * Check that all rows in the array are the same and raise error + otherwise; + * Return the row. Parameters ---------- name : str - Name of the property + Name of the property in ``self.image_defs`` Returns ------- - unique_value : array + unique_value : scalar or array Raises ------ - If there is more than a single unique value a `PARRECError` is raised. + PARRECError - if the rows of ``self.image_defs[name]`` do not all + compare equal """ - prop = self.image_defs[name] - if len(prop.shape) > 1: - uprops = [np.unique(prop[i]) for i in range(len(prop.shape))] - else: - uprops = [np.unique(prop)] - if not np.prod([len(uprop) for uprop in uprops]) == 1: - raise PARRECError('Varying %s in image sequence (%s). This is not ' - 'suppported.' % (name, uprops)) - else: - return np.array([uprop[0] for uprop in uprops]) + props = self.image_defs[name] + if np.any(np.diff(props, axis=0)): + raise PARRECError('Varying {0} in image sequence ({1}). This is ' + 'not suppported.'.format(name, props)) + return props[0] def get_voxel_size(self): """Returns the spatial extent of a voxel. @@ -694,13 +691,13 @@ def _calc_zooms(self): some attributes available in the fully initalized object. """ # slice orientation for the whole image series - slice_gap = self._get_unique_image_prop('slice gap')[0] + slice_gap = self._get_unique_image_prop('slice gap') # scaling per image axis n_dim = 4 if self._get_n_vols() > 1 else 3 zooms = np.ones(n_dim) # spatial sizes are inplane X mm, inplane Y mm + inter slice gap zooms[:2] = self._get_unique_image_prop('pixel spacing') - slice_thickness = self._get_unique_image_prop('slice thickness')[0] + slice_thickness = self._get_unique_image_prop('slice thickness') zooms[2] = slice_thickness + slice_gap # If 4D dynamic scan, convert time from milliseconds to seconds if len(zooms) > 3 and self.general_info['dyn_scan']: @@ -860,7 +857,7 @@ def get_slice_orientation(self): orientation : {'transverse', 'sagittal', 'coronal'} """ if self._slice_orientation is None: - lab = self._get_unique_image_prop('slice orientation')[0] + lab = self._get_unique_image_prop('slice orientation') self._slice_orientation = slice_orientation_codes.label[lab] return self._slice_orientation diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index e7c8756267..d879f31bb4 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -318,3 +318,20 @@ def test_truncations(): assert_raises(PARRECError, PARRECHeader, gen_info, slice_info) gen_info['max_gradient_orient'] = 1 hdr = PARRECHeader(gen_info, slice_info) + + +def test__get_uniqe_image_defs(): + hdr = PARRECHeader(HDR_INFO, HDR_DEFS.copy()) + uip = hdr._get_unique_image_prop + assert_equal(uip('image pixel size'), 16) + # Make values not same - raise error + hdr.image_defs['image pixel size'][3] = 32 + assert_raises(PARRECError, uip, 'image pixel size') + assert_array_equal(uip('recon resolution'), [64, 64]) + hdr.image_defs['recon resolution'][4, 1] = 32 + assert_raises(PARRECError, uip, 'recon resolution') + assert_array_equal(uip('image angulation'), [-13.26, 0, 0]) + hdr.image_defs['image angulation'][5, 2] = 1 + assert_raises(PARRECError, uip, 'image angulation') + # This one differs from the outset + assert_raises(PARRECError, uip, 'slice number') From 83aa7e154ac43d6d636cd382a2102983dc253e7e Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 10:41:40 -0700 Subject: [PATCH 49/55] RF: fix futurewarning about None, None comparison --- nibabel/parrec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 58069942cd..9954261123 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -578,7 +578,7 @@ def get_q_vectors(self): acquisition. """ bvals, bvecs = self.get_bvals_bvecs() - if (bvals, bvecs) == (None, None): + if bvals is None and bvecs is None: return None return bvecs * bvals[:, np.newaxis] From 779f1400c290d41bce00fedb229016cf4729d594 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 12:06:29 -0700 Subject: [PATCH 50/55] NF: add context manager to clear warnings registry Testing warnings can be really annoying, because once the warning has been raises, by default, it cannot be raised again, because it is already in the warnings registry. Here is a context manager to reset the warnings registry inside the context manager. The general idea was inspired by the conversation here: https://stackoverflow.com/questions/2390766/how-do-i-disable-and-then-re-enable-a-warning --- nibabel/testing/__init__.py | 26 ++++++++++++++++++++++++++ nibabel/tests/test_testing.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 nibabel/tests/test_testing.py diff --git a/nibabel/testing/__init__.py b/nibabel/testing/__init__.py index a2b966335e..b3f421535e 100644 --- a/nibabel/testing/__init__.py +++ b/nibabel/testing/__init__.py @@ -8,6 +8,7 @@ ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ## ''' Utilities for testing ''' from os.path import dirname, abspath, join as pjoin +from warnings import catch_warnings import numpy as np @@ -49,3 +50,28 @@ def assert_allclose_safely(a, b, match_nans=True): if b.dtype.kind in 'ui': b = b.astype(float) assert_true(np.allclose(a, b)) + + +class catch_warn_reset(catch_warnings): + """ Version of ``catch_warnings`` class that resets warning registry + """ + def __init__(self, *args, **kwargs): + self.modules = kwargs.pop('modules', []) + self._warnreg_copies = {} + super(catch_warn_reset, self).__init__(*args, **kwargs) + + def __enter__(self): + for mod in self.modules: + if hasattr(mod, '__warningregistry__'): + mod_reg = mod.__warningregistry__ + self._warnreg_copies[mod] = mod_reg.copy() + mod_reg.clear() + return super(catch_warn_reset, self).__enter__() + + def __exit__(self, *exc_info): + super(catch_warn_reset, self).__exit__(*exc_info) + for mod in self.modules: + if hasattr(mod, '__warningregistry__'): + mod.__warningregistry__.clear() + if mod in self._warnreg_copies: + mod.__warningregistry__.update(self._warnreg_copies[mod]) diff --git a/nibabel/tests/test_testing.py b/nibabel/tests/test_testing.py new file mode 100644 index 0000000000..476f048ba6 --- /dev/null +++ b/nibabel/tests/test_testing.py @@ -0,0 +1,32 @@ +""" Test testing utilties +""" + +import sys + +from warnings import warn, simplefilter + +from ..testing import catch_warn_reset + +from nose.tools import assert_equal + + +def test_catch_warn_reset(): + # Initial state of module, no warnings + my_mod = sys.modules[__name__] + assert_equal(getattr(my_mod, '__warningregistry__', None), None) + with catch_warn_reset(modules=[my_mod]): + simplefilter('ignore') + warn('Some warning') + assert_equal(my_mod.__warningregistry__, {}) + with catch_warn_reset(): + simplefilter('ignore') + warn('Some warning') + assert_equal(len(my_mod.__warningregistry__), 1) + with catch_warn_reset(modules=[my_mod]): + simplefilter('ignore') + warn('Another warning') + assert_equal(len(my_mod.__warningregistry__), 1) + with catch_warn_reset(): + simplefilter('ignore') + warn('Another warning') + assert_equal(len(my_mod.__warningregistry__), 2) From ff2a7f7777d18b5f81f09bac1ed1bca11183c446 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 12:09:01 -0700 Subject: [PATCH 51/55] RF+TST: rename and test sorted_slice_indices Rename the ``sorted_slice_indices`` property to a ``get_sorted_slice_indices`` method, and test. --- nibabel/parrec.py | 9 ++++----- nibabel/tests/test_parrec.py | 28 +++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 9954261123..50e06928f2 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -469,7 +469,7 @@ def __init__(self, file_like, header, scaling): # Copies of values needed to read array self._shape = header.get_data_shape() self._dtype = header.get_data_dtype() - self._slice_indices = header.sorted_slice_indices + self._slice_indices = header.get_sorted_slice_indices() self._slice_scaling = header.get_data_scaling(scaling) self._rec_shape = header.get_rec_shape() @@ -596,7 +596,7 @@ def get_bvals_bvecs(self): """ if self.general_info['diffusion'] == 0: return None, None - reorder = self.sorted_slice_indices + reorder = self.get_sorted_slice_indices() n_slices, n_vols = self.get_data_shape()[-2:] bvals = self.image_defs['diffusion_b_factor'][reorder].reshape( (n_slices, n_vols), order='F') @@ -841,7 +841,7 @@ def get_data_scaling(self, method="dv"): intercept = rescale_intercept / (rescale_slope * scale_slope) else: raise ValueError("Unknown scling method '%s'." % method) - reorder = self.sorted_slice_indices + reorder = self.get_sorted_slice_indices() slope = slope[reorder] intercept = intercept[reorder] shape = (1, 1) + self.get_data_shape()[2:] @@ -865,8 +865,7 @@ def get_rec_shape(self): inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) return inplane_shape + (len(self.image_defs),) - @property - def sorted_slice_indices(self): + def get_sorted_slice_indices(self): """Indices to sort (and maybe discard) slices in REC file Returns list for indexing into the last (third) dimension of the REC diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index d879f31bb4..fd82498d1d 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -3,11 +3,12 @@ from os.path import join as pjoin, dirname, basename from glob import glob -from warnings import catch_warnings, simplefilter +from warnings import simplefilter import numpy as np from numpy import array as npa +from .. import parrec from ..parrec import (parse_PAR_header, PARRECHeader, PARRECError, vol_numbers, vol_is_full) from ..openers import Opener @@ -18,6 +19,8 @@ from nose.tools import (assert_true, assert_false, assert_raises, assert_equal, assert_not_equal) +from ..testing import catch_warn_reset + DATA_PATH = pjoin(dirname(__file__), 'data') EG_PAR = pjoin(DATA_PATH, 'phantom_EPI_asc_CLEAR_2_1.PAR') @@ -184,11 +187,29 @@ def test_affine_regression(): def test_get_voxel_size_deprecated(): hdr = PARRECHeader(HDR_INFO, HDR_DEFS) - with catch_warnings(): + with catch_warn_reset(modules=[parrec]): simplefilter('error') assert_raises(DeprecationWarning, hdr.get_voxel_size) +def test_get_sorted_slice_indices(): + # Test sorted slice indices + hdr = PARRECHeader(HDR_INFO, HDR_DEFS) + n_slices = len(HDR_DEFS) + assert_array_equal(hdr.get_sorted_slice_indices(), range(n_slices)) + # Reverse - volume order preserved + hdr = PARRECHeader(HDR_INFO, HDR_DEFS[::-1]) + assert_array_equal(hdr.get_sorted_slice_indices(), + [8, 7, 6, 5, 4, 3, 2, 1, 0, + 17, 16, 15, 14, 13, 12, 11, 10, 9, + 26, 25, 24, 23, 22, 21, 20, 19, 18]) + # Omit last slice, only two volumes + with catch_warn_reset(modules=[parrec]): + simplefilter('ignore') + hdr = PARRECHeader(HDR_INFO, HDR_DEFS[:-1], permit_truncated=True) + assert_array_equal(hdr.get_sorted_slice_indices(), range(n_slices - 9)) + + def test_vol_number(): # Test algorithm for calculating volume number assert_array_equal(vol_numbers([1, 3, 0]), [0, 0, 0]) @@ -289,7 +310,8 @@ def test_truncations(): # Drop one line, raises error assert_raises(PARRECError, PARRECHeader, gen_info, slice_info[:-1]) # When we are permissive, we raise a warning, and drop a volume - with catch_warnings(record=True) as wlist: + with catch_warn_reset(modules=[parrec], record=True) as wlist: + simplefilter('always') hdr = PARRECHeader(gen_info, slice_info[:-1], permit_truncated=True) assert_equal(len(wlist), 1) assert_equal(hdr.get_data_shape(), (80, 80, 10)) From 539c1c1c44411b3a52b60320d13462c32c142dbc Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 12:20:34 -0700 Subject: [PATCH 52/55] RF: do not cache slice_orientation We were caching slice_orientation, but the other ``get_`` methods do not cache and get their values from the current values of ``self.general_info`` and ``self.image_defs``. Do the same for ``get_slice_orientation`` so that the values are always up to date with the image attributes. --- nibabel/parrec.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 50e06928f2..5132c39e17 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -516,7 +516,6 @@ def __init__(self, info, image_defs, permit_truncated=False): self.general_info = info self.image_defs = image_defs _truncation_checks(info, image_defs, permit_truncated) - self._slice_orientation = None # charge with basic properties to be able to use base class # functionality # dtype @@ -856,10 +855,8 @@ def get_slice_orientation(self): ------- orientation : {'transverse', 'sagittal', 'coronal'} """ - if self._slice_orientation is None: - lab = self._get_unique_image_prop('slice orientation') - self._slice_orientation = slice_orientation_codes.label[lab] - return self._slice_orientation + lab = self._get_unique_image_prop('slice orientation') + return slice_orientation_codes.label[lab] def get_rec_shape(self): inplane_shape = tuple(self._get_unique_image_prop('recon resolution')) From c3e7d97389a4d86e33bc49021a69db27e60e76f7 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 12:22:27 -0700 Subject: [PATCH 53/55] RF: copy input parameters on header creation The input parameters are small, and it is easy to get confused when using the mutable input parameters elsewhere, so copy the input dict, array so each header has their own copy. --- nibabel/parrec.py | 4 ++-- nibabel/tests/test_parrec.py | 17 ++++++++++++++--- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index 5132c39e17..bc69e62d7a 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -513,8 +513,8 @@ def __init__(self, info, image_defs, permit_truncated=False): If True, a warning is emitted instead of an error when a truncated recording is detected. """ - self.general_info = info - self.image_defs = image_defs + self.general_info = info.copy() + self.image_defs = image_defs.copy() _truncation_checks(info, image_defs, permit_truncated) # charge with basic properties to be able to use base class # functionality diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index fd82498d1d..7f7c7da25c 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -140,12 +140,10 @@ def test_orientation(): hdr = PARRECHeader(HDR_INFO, HDR_DEFS) assert_array_equal(HDR_DEFS['slice orientation'], 1) assert_equal(hdr.get_slice_orientation(), 'transverse') - hdr_defc = HDR_DEFS.copy() - hdr = PARRECHeader(HDR_INFO, hdr_defc) + hdr_defc = hdr.image_defs hdr_defc['slice orientation'] = 2 assert_equal(hdr.get_slice_orientation(), 'sagittal') hdr_defc['slice orientation'] = 3 - hdr = PARRECHeader(HDR_INFO, hdr_defc) assert_equal(hdr.get_slice_orientation(), 'coronal') @@ -357,3 +355,16 @@ def test__get_uniqe_image_defs(): assert_raises(PARRECError, uip, 'image angulation') # This one differs from the outset assert_raises(PARRECError, uip, 'slice number') + + +def test_copy_on_init(): + # Test that input dict / array gets copied when making header + hdr = PARRECHeader(HDR_INFO, HDR_DEFS) + assert_false(hdr.general_info is HDR_INFO) + hdr.general_info['max_slices'] = 10 + assert_equal(hdr.general_info['max_slices'], 10) + assert_equal(HDR_INFO['max_slices'], 9) + assert_false(hdr.image_defs is HDR_DEFS) + hdr.image_defs['image pixel size'] = 8 + assert_array_equal(hdr.image_defs['image pixel size'], 8) + assert_array_equal(HDR_DEFS['image pixel size'], 16) From 5da513006d597cf5541bcddc647839796cb319ca Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 15:42:29 -0700 Subject: [PATCH 54/55] BF: fix voxel_size code for new unique props ``get_voxel_size`` needs to use new API for get_unique_image_props. --- nibabel/parrec.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nibabel/parrec.py b/nibabel/parrec.py index bc69e62d7a..407e02e9b0 100644 --- a/nibabel/parrec.py +++ b/nibabel/parrec.py @@ -658,7 +658,7 @@ def get_voxel_size(self): DeprecationWarning, stacklevel=2) # slice orientation for the whole image series - slice_thickness = self._get_unique_image_prop('slice thickness')[0] + slice_thickness = self._get_unique_image_prop('slice thickness') voxsize_inplane = self._get_unique_image_prop('pixel spacing') voxsize = np.array((voxsize_inplane[0], voxsize_inplane[1], From efd215e1ab351f848d73fbbd282bbed9983ae126 Mon Sep 17 00:00:00 2001 From: Matthew Brett Date: Mon, 13 Oct 2014 15:43:21 -0700 Subject: [PATCH 55/55] RF: refactor catch_warns calls for Eric's comments Check wlist instead of raising an error. --- nibabel/tests/test_parrec.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/nibabel/tests/test_parrec.py b/nibabel/tests/test_parrec.py index 7f7c7da25c..1ec0374177 100644 --- a/nibabel/tests/test_parrec.py +++ b/nibabel/tests/test_parrec.py @@ -185,9 +185,10 @@ def test_affine_regression(): def test_get_voxel_size_deprecated(): hdr = PARRECHeader(HDR_INFO, HDR_DEFS) - with catch_warn_reset(modules=[parrec]): - simplefilter('error') - assert_raises(DeprecationWarning, hdr.get_voxel_size) + with catch_warn_reset(modules=[parrec], record=True) as wlist: + simplefilter('always') + hdr.get_voxel_size() + assert_equal(wlist[0].category, DeprecationWarning) def test_get_sorted_slice_indices(): @@ -202,8 +203,7 @@ def test_get_sorted_slice_indices(): 17, 16, 15, 14, 13, 12, 11, 10, 9, 26, 25, 24, 23, 22, 21, 20, 19, 18]) # Omit last slice, only two volumes - with catch_warn_reset(modules=[parrec]): - simplefilter('ignore') + with catch_warn_reset(modules=[parrec], record=True): hdr = PARRECHeader(HDR_INFO, HDR_DEFS[:-1], permit_truncated=True) assert_array_equal(hdr.get_sorted_slice_indices(), range(n_slices - 9)) @@ -309,7 +309,6 @@ def test_truncations(): assert_raises(PARRECError, PARRECHeader, gen_info, slice_info[:-1]) # When we are permissive, we raise a warning, and drop a volume with catch_warn_reset(modules=[parrec], record=True) as wlist: - simplefilter('always') hdr = PARRECHeader(gen_info, slice_info[:-1], permit_truncated=True) assert_equal(len(wlist), 1) assert_equal(hdr.get_data_shape(), (80, 80, 10))