def apply_mask(brain_img, mask_img): """ Load brain image, matching mask; apply and return masked image Parameters ---------- brain_img : str or image string giving image filename or image object mask_img : str or image string giving mask image filename or image object. Mask must match `brain_img` after mapping with image and mask affine. Returns ------- mask_img : image object image object where data is data from `brain_img` multiplied elementwise by the data of `mask_img`, where the mask data has[ been resampled into the voxel space of `brain_img` if necessary. """ brain_img = as_image(brain_img) b_aff = brain_img.affine b_hdr = brain_img.header mask_img = as_image(mask_img) brain_data = brain_img.get_data() mask_data = mask_img.get_data() if not np.allclose(b_aff, mask_img.affine): # Mask and brain have different affines - we need to resample brain2mask = npl.inv(mask_img.affine).dot(brain_img.affine) mat, vec = to_matvec(brain2mask) mask_data = affine_transform(mask_data, mat, vec, output_shape=brain_data.shape, order=0) # nearest neighbor return nib.Nifti1Image(brain_data * mask_data, b_aff, b_hdr)
def resample_img2img(img_to, img_from, order=1, out_class=nib.Nifti1Image): vox2vox = npl.inv(img_from.affine).dot(img_to.affine) rzs, trans = to_matvec(vox2vox) data = spnd.affine_transform(img_from.get_data(), rzs, trans, img_to.shape, order = order) return out_class(data, img_to.affine)
def resample_img2img(img_to, img_from, order=1, out_class=nib.Nifti1Image): if not have_scipy: raise Exception('Scipy must be installed to run resample_img2img.') from scipy import ndimage as spnd vox2vox = npl.inv(img_from.affine).dot(img_to.affine) rzs, trans = to_matvec(vox2vox) data = spnd.affine_transform(img_from.get_data(), rzs, trans, img_to.shape, order=order) return out_class(data, img_to.affine)
def apply_rotate(matrix, rad_x=0, rad_y=0, rad_z=0): ''' axis = x or y or z ''' rmat = dict(x=np.array([[1, 0, 0], [0, np.cos(rad_x), -np.sin(rad_x)], [0, np.sin(rad_x), np.cos(rad_x)]]).astype('float'), y=np.array([[np.cos(rad_y), 0, np.sin(rad_y)], [0, 1, 0], [-np.sin(rad_y), 0, np.cos(rad_y)]]).astype('float'), z=np.array([[np.cos(rad_z), -np.sin(rad_z), 0], [np.sin(rad_z), np.cos(rad_z), 0], [0, 0, 1]]).astype('float')) af_mat, af_vec = to_matvec(matrix) rotated_mat = rmat['z'].dot(rmat['y'].dot(rmat['x'].dot(af_mat))) rotated_vec = rmat['z'].dot(rmat['y'].dot(rmat['x'].dot(af_vec))) return from_matvec(rotated_mat, rotated_vec)
def apply_flip(matrix, axis, mat=True, vec=True): '''axis = x or y or z''' flip_idx = dict(x=0, y=1, z=2) orig_mat, orig_vec = to_matvec(matrix) aff_mat = np.ones(3) aff_mat[flip_idx[axis]] = -1 aff_mat = np.diag(aff_mat) if mat: flip_mat = aff_mat.dot(orig_mat) else: flip_mat = orig_mat if vec: flip_vec = aff_mat.dot(orig_vec) else: flip_vec = orig_vec return from_matvec(flip_mat, flip_vec)
def resample(image, target, mapping, shape, order=3): """ Resample `image` to `target` CoordinateMap Use a "world-to-world" mapping `mapping` and spline interpolation of a `order`. Here, "world-to-world" refers to the fact that mapping should be a callable that takes a physical coordinate in "target" and gives a physical coordinate in "image". Parameters ---------- image : Image instance image that is to be resampled target : CoordinateMap coordinate map for output image mapping : callable or tuple or array transformation from target.function_range to image.coordmap.function_range, i.e. 'world-to-world mapping'. Can be specified in three ways: a callable, a tuple (A, b) representing the mapping y=dot(A,x)+b or a representation of this mapping as an affine array, in homogeneous coordinates. shape : sequence of int shape of output array, in target.function_domain order : int, optional what order of interpolation to use in `scipy.ndimage` Returns ------- output : Image instance with interpolated data and output.coordmap == target """ if not callable(mapping): if type(mapping) is type(()): mapping = from_matvec(*mapping) # image world to target world mapping TW2IW = AffineTransform(target.function_range, image.coordmap.function_range, mapping) else: if isinstance(mapping, AffineTransform): TW2IW = mapping else: TW2IW = CoordinateMap(target.function_range, image.coordmap.function_range, mapping) # target voxel to image world mapping TV2IW = compose(TW2IW, target) # CoordinateMap describing mapping from target voxel to # image world coordinates if not isinstance(TV2IW, AffineTransform): # interpolator evaluates image at values image.coordmap.function_range, # i.e. physical coordinates rather than voxel coordinates grid = ArrayCoordMap.from_shape(TV2IW, shape) interp = ImageInterpolator(image, order=order) idata = interp.evaluate(grid.transposed_values) del (interp) else: # it is an affine transform, but, what if we compose? TV2IV = compose(image.coordmap.inverse(), TV2IW) if isinstance(TV2IV, AffineTransform): # still affine A, b = to_matvec(TV2IV.affine) idata = affine_transform(image.get_data(), A, offset=b, output_shape=shape, order=order) else: # not affine anymore interp = ImageInterpolator(image, order=order) grid = ArrayCoordMap.from_shape(TV2IV, shape) idata = interp.evaluate(grid.values) del (interp) return Image(idata, copy.copy(target))
def proc_file(infile, opts): # figure out the output filename, and see if it exists basefilename = splitext_addext(os.path.basename(infile))[0] if opts.outdir is not None: # set output path basefilename = os.path.join(opts.outdir, basefilename) # prep a file if opts.compressed: verbose('Using gzip compression') outfilename = basefilename + '.nii.gz' else: outfilename = basefilename + '.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 and data scaling = 'dv' if opts.scaling == 'off' else opts.scaling infile = fname_ext_ul_case(infile) pr_img = pr.load(infile, permit_truncated=opts.permit_truncated, scaling=scaling, strict_sort=opts.strict_sort) pr_hdr = pr_img.header affine = pr_hdr.get_affine(origin=opts.origin) slope, intercept = pr_hdr.get_data_scaling(scaling) if opts.scaling != 'off': verbose('Using data scaling "%s"' % opts.scaling) # get original scaling, and decide if we scale in-place or not if opts.scaling == 'off': slope = np.array([1.]) intercept = np.array([0.]) in_data = pr_img.dataobj.get_unscaled() out_dtype = pr_hdr.get_data_dtype() elif not np.any(np.diff(slope)) and not np.any(np.diff(intercept)): # Single scalefactor case slope = slope.ravel()[0] intercept = intercept.ravel()[0] in_data = pr_img.dataobj.get_unscaled() out_dtype = pr_hdr.get_data_dtype() else: # Multi scalefactor case slope = np.array([1.]) intercept = np.array([0.]) in_data = np.array(pr_img.dataobj) out_dtype = np.float64 # Reorient data block to LAS+ if necessary ornt = io_orientation(np.diag([-1, 1, 1, 1]).dot(affine)) if np.all(ornt == [[0, 1], [1, 1], [2, 1]]): # already in LAS+ t_aff = np.eye(4) else: # Not in LAS+ t_aff = inv_ornt_aff(ornt, pr_img.shape) affine = np.dot(affine, t_aff) in_data = apply_orientation(in_data, ornt) bvals, bvecs = pr_hdr.get_bvals_bvecs() if not opts.keep_trace: # discard Philips DTI trace if present if bvecs is not None: bad_mask = np.logical_and(bvals != 0, (bvecs == 0).all(axis=1)) if bad_mask.sum() > 0: pl = 's' if bad_mask.sum() != 1 else '' verbose('Removing %s DTI trace volume%s' % (bad_mask.sum(), pl)) good_mask = ~bad_mask in_data = in_data[..., good_mask] bvals = bvals[good_mask] bvecs = bvecs[good_mask] # Make corresponding NIfTI image nimg = nifti1.Nifti1Image(in_data, affine, pr_hdr) nhdr = nimg.header nhdr.set_data_dtype(out_dtype) nhdr.set_slope_inter(slope, intercept) nhdr.set_sform(affine, code=1) nhdr.set_qform(affine, code=1) if 'parse' in opts.minmax: # need to get the scaled data verbose('Loading (and scaling) the data to determine value range') if opts.minmax[0] == 'parse': nhdr['cal_min'] = in_data.min() * slope + intercept else: nhdr['cal_min'] = float(opts.minmax[0]) if opts.minmax[1] == 'parse': nhdr['cal_max'] = in_data.max() * slope + intercept else: nhdr['cal_max'] = float(opts.minmax[1]) # container for potential NIfTI1 header extensions if opts.store_header: # dump the full PAR header content into an extension with open(infile, 'rb') as fobj: # contents must be bytes hdr_dump = fobj.read() dump_ext = nifti1.Nifti1Extension('comment', hdr_dump) nhdr.extensions.append(dump_ext) verbose('Writing %s' % outfilename) nibabel.save(nimg, outfilename) # write out bvals/bvecs if requested if opts.bvs: if bvals is None and bvecs is None: verbose('No DTI volumes detected, bvals and bvecs not written') elif bvecs is None: verbose('DTI volumes detected, but no diffusion direction info was' 'found. Writing .bvals file only.') 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') else: verbose('Writing .bvals and .bvecs files') # Transform bvecs with reorientation affine orig2new = npl.inv(t_aff) bv_reorient = from_matvec(to_matvec(orig2new)[0], [0, 0, 0]) bvecs = apply_affine(bv_reorient, 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: fid.write('%s ' % val) fid.write('\n') # export data labels varying along the 4th dimensions if requested if opts.vol_info: labels = pr_img.header.get_volume_labels() if len(labels) > 0: vol_keys = list(labels.keys()) with open(basefilename + '.ordering.csv', 'w') as csvfile: csvwriter = csv.writer(csvfile, delimiter=',') csvwriter.writerow(vol_keys) for vals in zip(*[labels[k] for k in vol_keys]): csvwriter.writerow(vals) # write out dwell time if requested if opts.dwell_time: 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 ' 'magnet' % (dwell_time, opts.field_strength)) with open(basefilename + '.dwell_time', 'w') as fid: fid.write('%r\n' % dwell_time)
def resample(image, target, mapping, shape, order=3, mode='constant', cval=0.0): """ Resample `image` to `target` CoordinateMap Use a "world-to-world" mapping `mapping` and spline interpolation of a `order`. Here, "world-to-world" refers to the fact that mapping should be a callable that takes a physical coordinate in "target" and gives a physical coordinate in "image". Parameters ---------- image : Image instance image that is to be resampled. target : CoordinateMap coordinate map for output image. mapping : callable or tuple or array transformation from target.function_range to image.coordmap.function_range, i.e. 'world-to-world mapping'. Can be specified in three ways: a callable, a tuple (A, b) representing the mapping y=dot(A,x)+b or a representation of this mapping as an affine array, in homogeneous coordinates. shape : sequence of int shape of output array, in target.function_domain. order : int, optional what order of interpolation to use in ``scipy.ndimage``. mode : str, optional Points outside the boundaries of the input are filled according to the given mode ('constant', 'nearest', 'reflect' or 'wrap'). Default is 'constant'. cval : scalar, optional Value used for points outside the boundaries of the input if mode='constant'. Default is 0.0. Returns ------- output : Image instance Image has interpolated data and output.coordmap == target. """ if not callable(mapping): if type(mapping) is type(()): mapping = from_matvec(*mapping) # image world to target world mapping TW2IW = AffineTransform(target.function_range, image.coordmap.function_range, mapping) else: if isinstance(mapping, AffineTransform): TW2IW = mapping else: TW2IW = CoordinateMap(target.function_range, image.coordmap.function_range, mapping) # target voxel to image world mapping TV2IW = compose(TW2IW, target) # CoordinateMap describing mapping from target voxel to # image world coordinates if not isinstance(TV2IW, AffineTransform): # interpolator evaluates image at values image.coordmap.function_range, # i.e. physical coordinates rather than voxel coordinates grid = ArrayCoordMap.from_shape(TV2IW, shape) interp = ImageInterpolator(image, order=order, mode=mode, cval=cval) idata = interp.evaluate(grid.transposed_values) del(interp) else: # it is an affine transform, but, what if we compose? TV2IV = compose(image.coordmap.inverse(), TV2IW) if isinstance(TV2IV, AffineTransform): # still affine A, b = to_matvec(TV2IV.affine) idata = affine_transform(image.get_data(), A, offset=b, output_shape=shape, order=order, mode=mode, cval=cval) else: # not affine anymore interp = ImageInterpolator(image, order=order, mode=mode, cval=cval) grid = ArrayCoordMap.from_shape(TV2IV, shape) idata = interp.evaluate(grid.values) del(interp) return Image(idata, copy.copy(target))
def test_resample_to_output(): # Test routine to sample iamges to output space # Image aligned to output axes - no-op data = np.arange(24).reshape((2, 3, 4)) img = Nifti1Image(data, np.eye(4)) # Check default resampling img2 = resample_to_output(img) assert_array_equal(img2.shape, (2, 3, 4)) assert_array_equal(img2.affine, np.eye(4)) assert_array_equal(img2.dataobj, data) # Check resampling with different voxel size specifications for vox_sizes in (None, 1, [1, 1, 1]): img2 = resample_to_output(img, vox_sizes) assert_array_equal(img2.shape, (2, 3, 4)) assert_array_equal(img2.affine, np.eye(4)) assert_array_equal(img2.dataobj, data) img2 = resample_to_output(img, vox_sizes) # Check 2D works img_2d = Nifti1Image(data[0], np.eye(4)) for vox_sizes in (None, 1, (1, 1), (1, 1, 1)): img3 = resample_to_output(img_2d, vox_sizes) assert_array_equal(img3.shape, (3, 4, 1)) assert_array_equal(img3.affine, np.eye(4)) assert_array_equal(img3.dataobj, data[0][..., None]) # Even 1D img_1d = Nifti1Image(data[0, 0], np.eye(4)) img3 = resample_to_output(img_1d) assert_array_equal(img3.shape, (4, 1, 1)) assert_array_equal(img3.affine, np.eye(4)) assert_array_equal(img3.dataobj, data[0, 0][..., None, None]) # But 4D does not img_4d = Nifti1Image(data.reshape(2, 3, 2, 2), np.eye(4)) with pytest.raises(ValueError): resample_to_output(img_4d) # Run vox2vox_out tests, checking output shape, coordinate transform for in_shape, in_aff, vox, out_shape, out_aff in get_outspace_params(): # Allow for expansion of image shape from < 3D in_n_dim = len(in_shape) if in_n_dim < 3: in_shape = in_shape + (1,) * (3 - in_n_dim) if not vox is None: vox = vox + (1,) * (3 - in_n_dim) assert len(out_shape) == in_n_dim out_shape = out_shape + (1,) * (3 - in_n_dim) img = Nifti1Image(np.ones(in_shape), in_aff) out_img = resample_to_output(img, vox) assert_all_in(in_shape, in_aff, out_img.shape, out_img.affine) assert out_img.shape == out_shape assert_almost_equal(out_img.affine, out_aff) # Check data is as expected with some transforms # Flip first axis out_img = resample_to_output(Nifti1Image(data, np.diag([-1, 1, 1, 1]))) assert_array_equal(out_img.dataobj, np.flipud(data)) # Subsample voxels out_img = resample_to_output(Nifti1Image(data, np.diag([4, 5, 6, 1]))) exp_out = spnd.affine_transform(data, [1/4, 1/5, 1/6], output_shape = (5, 11, 19)) assert_array_equal(out_img.dataobj, exp_out) # Unsubsample with voxel sizes out_img = resample_to_output(Nifti1Image(data, np.diag([4, 5, 6, 1])), [4, 5, 6]) assert_array_equal(out_img.dataobj, data) # A rotation to test nearest, order, cval rot_3 = from_matvec(euler2mat(np.pi / 4), [0, 0, 0]) rot_3_img = Nifti1Image(data, rot_3) out_img = resample_to_output(rot_3_img) exp_shape = (4, 4, 4) assert out_img.shape == exp_shape exp_aff = np.array([[1, 0, 0, -2 * np.cos(np.pi / 4)], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) assert_almost_equal(out_img.affine, exp_aff) rzs, trans = to_matvec(np.dot(npl.inv(rot_3), exp_aff)) exp_out = spnd.affine_transform(data, rzs, trans, exp_shape) assert_almost_equal(out_img.dataobj, exp_out) # Order assert_almost_equal( resample_to_output(rot_3_img, order=0).dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, order=0)) # Cval assert_almost_equal( resample_to_output(rot_3_img, cval=99).dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, cval=99)) # Mode assert_almost_equal( resample_to_output(rot_3_img, mode='nearest').dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, mode='nearest')) # out_class img_ni1 = Nifti2Image(data, np.eye(4)) img_ni2 = Nifti2Image(data, np.eye(4)) # Default is Nifti1Image assert resample_to_output(img_ni2).__class__ == Nifti1Image # Can be overriden assert resample_to_output(img_ni1, out_class=Nifti2Image).__class__ == Nifti2Image # None specifies out_class from input assert resample_to_output(img_ni2, out_class=None).__class__ == Nifti2Image
def test_resample_to_output(): # Test routine to sample iamges to output space # Image aligned to output axes - no-op data = np.arange(24).reshape((2, 3, 4)) img = Nifti1Image(data, np.eye(4)) # Check default resampling img2 = resample_to_output(img) assert_array_equal(img2.shape, (2, 3, 4)) assert_array_equal(img2.affine, np.eye(4)) assert_array_equal(img2.dataobj, data) # Check resampling with different voxel size specifications for vox_sizes in (None, 1, [1, 1, 1]): img2 = resample_to_output(img, vox_sizes) assert_array_equal(img2.shape, (2, 3, 4)) assert_array_equal(img2.affine, np.eye(4)) assert_array_equal(img2.dataobj, data) img2 = resample_to_output(img, vox_sizes) # Check 2D works img_2d = Nifti1Image(data[0], np.eye(4)) for vox_sizes in (None, 1, (1, 1), (1, 1, 1)): img3 = resample_to_output(img_2d, vox_sizes) assert_array_equal(img3.shape, (3, 4, 1)) assert_array_equal(img3.affine, np.eye(4)) assert_array_equal(img3.dataobj, data[0][..., None]) # Even 1D img_1d = Nifti1Image(data[0, 0], np.eye(4)) img3 = resample_to_output(img_1d) assert_array_equal(img3.shape, (4, 1, 1)) assert_array_equal(img3.affine, np.eye(4)) assert_array_equal(img3.dataobj, data[0, 0][..., None, None]) # But 4D does not img_4d = Nifti1Image(data.reshape(2, 3, 2, 2), np.eye(4)) assert_raises(ValueError, resample_to_output, img_4d) # Run vox2vox_out tests, checking output shape, coordinate transform for in_shape, in_aff, vox, out_shape, out_aff in get_outspace_params(): # Allow for expansion of image shape from < 3D in_n_dim = len(in_shape) if in_n_dim < 3: in_shape = in_shape + (1,) * (3 - in_n_dim) if not vox is None: vox = vox + (1,) * (3 - in_n_dim) assert len(out_shape) == in_n_dim out_shape = out_shape + (1,) * (3 - in_n_dim) img = Nifti1Image(np.ones(in_shape), in_aff) out_img = resample_to_output(img, vox) assert_all_in(in_shape, in_aff, out_img.shape, out_img.affine) assert_equal(out_img.shape, out_shape) assert_almost_equal(out_img.affine, out_aff) # Check data is as expected with some transforms # Flip first axis out_img = resample_to_output(Nifti1Image(data, np.diag([-1, 1, 1, 1]))) assert_array_equal(out_img.dataobj, np.flipud(data)) # Subsample voxels out_img = resample_to_output(Nifti1Image(data, np.diag([4, 5, 6, 1]))) exp_out = spnd.affine_transform(data, [1/4, 1/5, 1/6], output_shape = (5, 11, 19)) assert_array_equal(out_img.dataobj, exp_out) # Unsubsample with voxel sizes out_img = resample_to_output(Nifti1Image(data, np.diag([4, 5, 6, 1])), [4, 5, 6]) assert_array_equal(out_img.dataobj, data) # A rotation to test nearest, order, cval rot_3 = from_matvec(euler2mat(np.pi / 4), [0, 0, 0]) rot_3_img = Nifti1Image(data, rot_3) out_img = resample_to_output(rot_3_img) exp_shape = (4, 4, 4) assert_equal(out_img.shape, exp_shape) exp_aff = np.array([[1, 0, 0, -2 * np.cos(np.pi / 4)], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]) assert_almost_equal(out_img.affine, exp_aff) rzs, trans = to_matvec(np.dot(npl.inv(rot_3), exp_aff)) exp_out = spnd.affine_transform(data, rzs, trans, exp_shape) assert_almost_equal(out_img.dataobj, exp_out) # Order assert_almost_equal( resample_to_output(rot_3_img, order=0).dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, order=0)) # Cval assert_almost_equal( resample_to_output(rot_3_img, cval=99).dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, cval=99)) # Mode assert_almost_equal( resample_to_output(rot_3_img, mode='nearest').dataobj, spnd.affine_transform(data, rzs, trans, exp_shape, mode='nearest')) # out_class img_ni1 = Nifti2Image(data, np.eye(4)) img_ni2 = Nifti2Image(data, np.eye(4)) # Default is Nifti1Image assert_equal( resample_to_output(img_ni2).__class__, Nifti1Image) # Can be overriden assert_equal( resample_to_output(img_ni1, out_class=Nifti2Image).__class__, Nifti2Image) # None specifies out_class from input assert_equal( resample_to_output(img_ni2, out_class=None).__class__, Nifti2Image)
def proc_file(infile, opts): # figure out the output filename, and see if it exists basefilename = splitext_addext(os.path.basename(infile))[0] if opts.outdir is not None: # set output path basefilename = os.path.join(opts.outdir, basefilename) # prep a file if opts.compressed: verbose("Using gzip compression") outfilename = basefilename + ".nii.gz" else: outfilename = basefilename + ".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 and data scaling = "dv" if opts.scaling == "off" else opts.scaling infile = fname_ext_ul_case(infile) pr_img = pr.load(infile, permit_truncated=opts.permit_truncated, scaling=scaling, strict_sort=opts.strict_sort) pr_hdr = pr_img.header affine = pr_hdr.get_affine(origin=opts.origin) slope, intercept = pr_hdr.get_data_scaling(scaling) if opts.scaling != "off": verbose('Using data scaling "%s"' % opts.scaling) # get original scaling, and decide if we scale in-place or not if opts.scaling == "off": slope = np.array([1.0]) intercept = np.array([0.0]) in_data = pr_img.dataobj.get_unscaled() out_dtype = pr_hdr.get_data_dtype() elif not np.any(np.diff(slope)) and not np.any(np.diff(intercept)): # Single scalefactor case slope = slope.ravel()[0] intercept = intercept.ravel()[0] in_data = pr_img.dataobj.get_unscaled() out_dtype = pr_hdr.get_data_dtype() else: # Multi scalefactor case slope = np.array([1.0]) intercept = np.array([0.0]) in_data = np.array(pr_img.dataobj) out_dtype = np.float64 # Reorient data block to LAS+ if necessary ornt = io_orientation(np.diag([-1, 1, 1, 1]).dot(affine)) if np.all(ornt == [[0, 1], [1, 1], [2, 1]]): # already in LAS+ t_aff = np.eye(4) else: # Not in LAS+ t_aff = inv_ornt_aff(ornt, pr_img.shape) affine = np.dot(affine, t_aff) in_data = apply_orientation(in_data, ornt) bvals, bvecs = pr_hdr.get_bvals_bvecs() if not opts.keep_trace: # discard Philips DTI trace if present if bvecs is not None: bad_mask = np.logical_and(bvals != 0, (bvecs == 0).all(axis=1)) if bad_mask.sum() > 0: pl = "s" if bad_mask.sum() != 1 else "" verbose("Removing %s DTI trace volume%s" % (bad_mask.sum(), pl)) good_mask = ~bad_mask in_data = in_data[..., good_mask] bvals = bvals[good_mask] bvecs = bvecs[good_mask] # Make corresponding NIfTI image nimg = nifti1.Nifti1Image(in_data, affine, pr_hdr) nhdr = nimg.header nhdr.set_data_dtype(out_dtype) nhdr.set_slope_inter(slope, intercept) nhdr.set_sform(affine, code=1) nhdr.set_qform(affine, code=1) if "parse" in opts.minmax: # need to get the scaled data verbose("Loading (and scaling) the data to determine value range") if opts.minmax[0] == "parse": nhdr["cal_min"] = in_data.min() * slope + intercept else: nhdr["cal_min"] = float(opts.minmax[0]) if opts.minmax[1] == "parse": nhdr["cal_max"] = in_data.max() * slope + intercept else: nhdr["cal_max"] = float(opts.minmax[1]) # container for potential NIfTI1 header extensions if opts.store_header: # dump the full PAR header content into an extension with open(infile, "rb") as fobj: # contents must be bytes hdr_dump = fobj.read() dump_ext = nifti1.Nifti1Extension("comment", hdr_dump) nhdr.extensions.append(dump_ext) verbose("Writing %s" % outfilename) nibabel.save(nimg, outfilename) # write out bvals/bvecs if requested if opts.bvs: if bvals is None and bvecs is None: verbose("No DTI volumes detected, bvals and bvecs not written") elif bvecs is None: verbose("DTI volumes detected, but no diffusion direction info was" "found. Writing .bvals file only.") 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") else: verbose("Writing .bvals and .bvecs files") # Transform bvecs with reorientation affine orig2new = npl.inv(t_aff) bv_reorient = from_matvec(to_matvec(orig2new)[0], [0, 0, 0]) bvecs = apply_affine(bv_reorient, 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: fid.write("%s " % val) fid.write("\n") # export data labels varying along the 4th dimensions if requested if opts.vol_info: labels = pr_img.header.get_volume_labels() if len(labels) > 0: vol_keys = list(labels.keys()) with open(basefilename + ".ordering.csv", "w") as csvfile: csvwriter = csv.writer(csvfile, delimiter=",") csvwriter.writerow(vol_keys) for vals in zip(*[labels[k] for k in vol_keys]): csvwriter.writerow(vals) # write out dwell time if requested if opts.dwell_time: 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 " "magnet" % (dwell_time, opts.field_strength)) with open(basefilename + ".dwell_time", "w") as fid: fid.write("%r\n" % dwell_time)
def nipy2nifti(img, data_dtype=None, strict=None, fix0=False): """ Return NIFTI image from nipy image `img` Parameters ---------- img : object An object, usually a NIPY ``Image``, having attributes `coordmap` and `shape` data_dtype : None or dtype specifier None means try and use header dtype, otherwise try and use data dtype, otherwise use np.float32. A dtype specifier means set the header output data dtype using ``np.dtype(data_dtype)``. strict : bool, optional Whether to use strict checking of input image for creating NIFTI fix0: bool, optional Whether to fix potential 0 column / row in affine. This option only used when trying to find time etc axes in the coordmap output names. In order to find matching input names, we need to use the corresponding rows and columns in the affine. Sometimes time, in particular, has 0 scaling, and thus all 0 in the corresponding row / column. In that case it's hard to work out which input corresponds. If `fix0` is True, and there is only one all zero (matrix part of the) affine row, and only one all zero (matrix part of the) affine column, fix scaling for that combination to zero, assuming this a zero scaling for time. Returns ------- ni_img : ``nibabel.Nifti1Image`` NIFTI image Raises ------ NiftiError: if space axes not orthogonal to non-space axes NiftiError: if non-space axes not orthogonal to each other NiftiError: if `img` output space does not match named spaces in NIFTI NiftiError: if input image has more than 7 dimensions NiftiError: if input image has 7 dimensions, but no time dimension, because we need to add an extra 1 length axis at position 3 NiftiError: if we find a time-like input axis but the matching output axis is a different time-like. NiftiError: if we find a time-like output axis but the matching input axis is a different time-like. NiftiError: if we find a time output axis and there are non-zero non-spatial offsets in the affine, but we can't find a corresponding input axis. Notes ----- First, we need to create a valid XYZ Affine. We check if this can be done by checking if there are recognizable X, Y, Z output axes and corresponding input (voxel) axes. This requires the input image to be at least 3D. If we find these requirements, we reorder the image axes to have XYZ output axes and 3 spatial input axes first, and get the corresponding XYZ affine. If the spatial dimensions are not orthogonal to the non-spatial dimensions, raise a NiftiError. If the non-spatial dimensions are not orthogonal to each other, raise a NiftiError. We check if the XYZ output fits with the NIFTI named spaces of scanner, aligned, Talairach, MNI. If so, set the NIFTI code and qform, sform accordingly. If the space corresponds to 'unknown' then we must set the NIFTI transform codes to 0, and the affine must match the affine we will get from loading the NIFTI with no qform, sform. If not, we're going to lose information in the affine, and raise an error. If any of the first three input axes are named ('slice', 'freq', 'phase') set the ``dim_info`` field accordingly. Set the ``xyzt_units`` field to indicate millimeters and seconds, if there is a 't' axis, otherwise millimeters and 0 (unknown). We look to see if we have a time-like axis in the inputs or the outputs. A time-like axis has labels 't', 'hz', 'ppm', 'rads'. If we have an axis 't' in the inputs *and* the outputs, check they either correspond, or both inputs and output correspond with no other axis, otherwise raise NiftiError. Do the same check for 'hz', then 'ppm', then 'rads'. If we do have a time-like axis, roll that axis to be the 4th axis. If this axis is actually time, take the ``affine[3, -1]`` and put into the ``toffset`` field. If there's no time-like axis, but there are other non-spatial axes, make a length 1 4th array axis to indicate this. If the resulting NIFTI image has more than 7 dimensions, raise a NiftiError. Set ``pixdim`` for axes >= 3 using vector length of corresponding affine columns. We don't set the intent-related fields for now. """ strict_none = strict is None if strict_none: warnings.warn('Default `strict` currently False; this will change to ' 'True in a future version of nipy', FutureWarning, stacklevel = 2) strict = False known_names = ncrs.known_names if not strict: # add simple 'xyz' to acceptable spatial names known_names = copy(known_names) # copy module global dict for c in 'xyz': known_names[c] = c try: img = as_xyz_image(img, known_names) except (ncrs.AxesError, ncrs.AffineError): # Python 2.5 / 3 compatibility e = sys.exc_info()[1] raise NiftiError('Image cannot be reordered to XYZ because: "%s"' % e) coordmap = img.coordmap # Get useful information from old header in_hdr = img.metadata.get('header', None) hdr = nib.Nifti1Header.from_header(in_hdr) # Default behavior is to take datatype from old header, unless there was no # header, in which case we try to use the data dtype. data = None if data_dtype is None: if in_hdr is None: data = img.get_data() data_dtype = data.dtype else: data_dtype = in_hdr.get_data_dtype() else: data_dtype = np.dtype(data_dtype) hdr.set_data_dtype(data_dtype) # Remaining axes orthogonal? rzs, trans = to_matvec(coordmap.affine) if (not np.allclose(rzs[3:, :3], 0) or not np.allclose(rzs[:3, 3:], 0)): raise NiftiError('Non space axes not orthogonal to space') # And to each other? nsp_affine = rzs[3:,3:] nsp_nzs = np.abs(nsp_affine) > TINY n_in_col = np.sum(nsp_nzs, axis=0) n_in_row = np.sum(nsp_nzs, axis=1) if np.any(n_in_col > 1) or np.any(n_in_row > 1): raise NiftiError('Non space axes not orthogonal to each other') # Affine seems OK, check for space xyz_affine = ncrs.xyz_affine(coordmap, known_names) spatial_output_names = coordmap.function_range.coord_names[:3] out_space = CS(spatial_output_names) for name, space in XFORM2SPACE.items(): if out_space in space: hdr.set_sform(xyz_affine, name) hdr.set_qform(xyz_affine, name) break else: if not strict and spatial_output_names == ('x', 'y', 'z'): warnings.warn('Default `strict` currently False; ' 'this will change to True in a future version of ' 'nipy; output names of "x", "y", "z" will raise ' 'an error. Please use canonical output names from ' 'nipy.core.reference.spaces', FutureWarning, stacklevel = 2) hdr.set_sform(xyz_affine, 'scanner') hdr.set_qform(xyz_affine, 'scanner') elif not out_space in ncrs.unknown_space: # no space we recognize raise NiftiError('Image world not a NIFTI world') else: # unknown space requires affine that matches if not np.allclose(xyz_affine, hdr.get_base_affine()): raise NiftiError("Image world is 'unknown' but affine not " "compatible; please reset image world or " "affine") hdr.set_qform(None) hdr.set_sform(None) # Use list() to get .index method for python < 2.6 input_names = list(coordmap.function_domain.coord_names) spatial_names = input_names[:3] dim_infos = [] for fps in 'freq', 'phase', 'slice': dim_infos.append( spatial_names.index(fps) if fps in spatial_names else None) hdr.set_dim_info(*dim_infos) # Set units without knowing time hdr.set_xyzt_units(xyz='mm') # Done if we only have 3 input dimensions n_ns = coordmap.ndims[0] - 3 if n_ns == 0: # No non-spatial dimensions return nib.Nifti1Image(img.get_data(), xyz_affine, hdr) elif n_ns > 4: raise NiftiError("Too many dimensions to convert") # Go now to data, pixdims if data is None: data = img.get_data() rzs, trans = to_matvec(img.coordmap.affine) ns_pixdims = list(np.sqrt(np.sum(rzs[3:, 3:] ** 2, axis=0))) in_ax, out_ax, tl_name = _find_time_like(coordmap, fix0) if in_ax is None: # No time-like axes # add new 1-length axis if n_ns == 4: raise NiftiError("Too many dimensions to convert") n_ns += 1 data = data[:, :, :, None, ...] # xyzt_units hdr.set_xyzt_units(xyz='mm') # shift pixdims ns_pixdims.insert(0, 0) else: # Time-like hdr.set_xyzt_units(xyz='mm', t=TIME_LIKE_AXES[tl_name]['units']) # If this is really time, set toffset if tl_name == 't' and np.any(trans[3:]): # Which output axis corresponds to time? if out_ax is None: raise NiftiError('Time input and output do not match') hdr['toffset'] = trans[out_ax] # Make sure this time-like axis is first non-space axis if in_ax != 3: data = np.rollaxis(data, in_ax, 3) order = range(n_ns) order.pop(in_ax - 3) order.insert(0, in_ax - 3) ns_pixdims = [ns_pixdims[i] for i in order] hdr['pixdim'][4:(4 + n_ns)] = ns_pixdims return nib.Nifti1Image(data, xyz_affine, hdr)
def nipy2nifti(img, data_dtype=None, strict=None, fix0=True): """ Return NIFTI image from nipy image `img` Parameters ---------- img : object An object, usually a NIPY ``Image``, having attributes `coordmap` and `shape` data_dtype : None or dtype specifier None means try and use header dtype, otherwise try and use data dtype, otherwise use np.float32. A dtype specifier means set the header output data dtype using ``np.dtype(data_dtype)``. strict : bool, optional Whether to use strict checking of input image for creating NIFTI fix0: bool, optional Whether to fix potential 0 column / row in affine. This option only used when trying to find time etc axes in the coordmap output names. In order to find matching input names, we need to use the corresponding rows and columns in the affine. Sometimes time, in particular, has 0 scaling, and thus all 0 in the corresponding row / column. In that case it's hard to work out which input corresponds. If `fix0` is True, and there is only one all zero (matrix part of the) affine row, and only one all zero (matrix part of the) affine column, fix scaling for that combination to zero, assuming this a zero scaling for time. Returns ------- ni_img : ``nibabel.Nifti1Image`` NIFTI image Raises ------ NiftiError: if space axes not orthogonal to non-space axes NiftiError: if non-space axes not orthogonal to each other NiftiError: if `img` output space does not match named spaces in NIFTI NiftiError: if input image has more than 7 dimensions NiftiError: if input image has 7 dimensions, but no time dimension, because we need to add an extra 1 length axis at position 3 NiftiError: if we find a time-like input axis but the matching output axis is a different time-like. NiftiError: if we find a time-like output axis but the matching input axis is a different time-like. NiftiError: if we find a time output axis and there are non-zero non-spatial offsets in the affine, but we can't find a corresponding input axis. Notes ----- First, we need to create a valid XYZ Affine. We check if this can be done by checking if there are recognizable X, Y, Z output axes and corresponding input (voxel) axes. This requires the input image to be at least 3D. If we find these requirements, we reorder the image axes to have XYZ output axes and 3 spatial input axes first, and get the corresponding XYZ affine. If the spatial dimensions are not orthogonal to the non-spatial dimensions, raise a NiftiError. If the non-spatial dimensions are not orthogonal to each other, raise a NiftiError. We check if the XYZ output fits with the NIFTI named spaces of scanner, aligned, Talairach, MNI. If so, set the NIFTI code and qform, sform accordingly. If the space corresponds to 'unknown' then we must set the NIFTI transform codes to 0, and the affine must match the affine we will get from loading the NIFTI with no qform, sform. If not, we're going to lose information in the affine, and raise an error. If any of the first three input axes are named ('slice', 'freq', 'phase') set the ``dim_info`` field accordingly. Set the ``xyzt_units`` field to indicate millimeters and seconds, if there is a 't' axis, otherwise millimeters and 0 (unknown). We look to see if we have a time-like axis in the inputs or the outputs. A time-like axis has labels 't', 'hz', 'ppm', 'rads'. If we have an axis 't' in the inputs *and* the outputs, check they either correspond, or both inputs and output correspond with no other axis, otherwise raise NiftiError. Do the same check for 'hz', then 'ppm', then 'rads'. If we do have a time-like axis, roll that axis to be the 4th axis. If this axis is actually time, take the ``affine[3, -1]`` and put into the ``toffset`` field. If there's no time-like axis, but there are other non-spatial axes, make a length 1 4th array axis to indicate this. If the resulting NIFTI image has more than 7 dimensions, raise a NiftiError. Set ``pixdim`` for axes >= 3 using vector length of corresponding affine columns. We don't set the intent-related fields for now. """ strict_none = strict is None if strict_none: warnings.warn('Default `strict` currently False; this will change to ' 'True in a future version of nipy', FutureWarning, stacklevel = 2) strict = False known_names = ncrs.known_names if not strict: # add simple 'xyz' to acceptable spatial names known_names = copy(known_names) # copy module global dict for c in 'xyz': known_names[c] = c try: img = as_xyz_image(img, known_names) except (ncrs.AxesError, ncrs.AffineError): # Python 2.5 / 3 compatibility e = sys.exc_info()[1] raise NiftiError('Image cannot be reordered to XYZ because: "%s"' % e) coordmap = img.coordmap # Get useful information from old header in_hdr = img.metadata.get('header', None) hdr = nib.Nifti1Header.from_header(in_hdr) # Default behavior is to take datatype from old header, unless there was no # header, in which case we try to use the data dtype. data = None if data_dtype is None: if in_hdr is None: data = img.get_data() data_dtype = data.dtype else: data_dtype = in_hdr.get_data_dtype() else: data_dtype = np.dtype(data_dtype) hdr.set_data_dtype(data_dtype) # Remaining axes orthogonal? rzs, trans = to_matvec(coordmap.affine) if (not np.allclose(rzs[3:, :3], 0) or not np.allclose(rzs[:3, 3:], 0)): raise NiftiError('Non space axes not orthogonal to space') # And to each other? nsp_affine = rzs[3:,3:] nsp_nzs = np.abs(nsp_affine) > TINY n_in_col = np.sum(nsp_nzs, axis=0) n_in_row = np.sum(nsp_nzs, axis=1) if np.any(n_in_col > 1) or np.any(n_in_row > 1): raise NiftiError('Non space axes not orthogonal to each other') # Affine seems OK, check for space xyz_affine = ncrs.xyz_affine(coordmap, known_names) spatial_output_names = coordmap.function_range.coord_names[:3] out_space = CS(spatial_output_names) for name, space in XFORM2SPACE.items(): if out_space in space: hdr.set_sform(xyz_affine, name) hdr.set_qform(xyz_affine, name) break else: if not strict and spatial_output_names == ('x', 'y', 'z'): warnings.warn('Default `strict` currently False; ' 'this will change to True in a future version of ' 'nipy; output names of "x", "y", "z" will raise ' 'an error. Please use canonical output names from ' 'nipy.core.reference.spaces', FutureWarning, stacklevel = 2) hdr.set_sform(xyz_affine, 'scanner') hdr.set_qform(xyz_affine, 'scanner') elif not out_space in ncrs.unknown_space: # no space we recognize raise NiftiError('Image world not a NIFTI world') else: # unknown space requires affine that matches if not np.allclose(xyz_affine, hdr.get_base_affine()): raise NiftiError("Image world is 'unknown' but affine not " "compatible; please reset image world or " "affine") hdr.set_qform(None) hdr.set_sform(None) # Use list() to get .index method for python < 2.6 input_names = list(coordmap.function_domain.coord_names) spatial_names = input_names[:3] dim_infos = [] for fps in 'freq', 'phase', 'slice': dim_infos.append( spatial_names.index(fps) if fps in spatial_names else None) hdr.set_dim_info(*dim_infos) # Set units without knowing time hdr.set_xyzt_units(xyz='mm') # Done if we only have 3 input dimensions n_ns = coordmap.ndims[0] - 3 if n_ns == 0: # No non-spatial dimensions return nib.Nifti1Image(img.get_data(), xyz_affine, hdr) elif n_ns > 4: raise NiftiError("Too many dimensions to convert") # Go now to data, pixdims if data is None: data = img.get_data() rzs, trans = to_matvec(img.coordmap.affine) ns_pixdims = list(np.sqrt(np.sum(rzs[3:, 3:] ** 2, axis=0))) in_ax, out_ax, tl_name = _find_time_like(coordmap, fix0) if in_ax is None: # No time-like axes # add new 1-length axis if n_ns == 4: raise NiftiError("Too many dimensions to convert") n_ns += 1 data = data[:, :, :, None, ...] # xyzt_units hdr.set_xyzt_units(xyz='mm') # shift pixdims ns_pixdims.insert(0, 0) else: # Time-like hdr.set_xyzt_units(xyz='mm', t=TIME_LIKE_AXES[tl_name]['units']) # If this is really time, set toffset if tl_name == 't' and np.any(trans[3:]): # Which output axis corresponds to time? if out_ax is None: raise NiftiError('Time input and output do not match') hdr['toffset'] = trans[out_ax] # Make sure this time-like axis is first non-space axis if in_ax != 3: data = np.rollaxis(data, in_ax, 3) order = range(n_ns) order.pop(in_ax - 3) order.insert(0, in_ax - 3) ns_pixdims = [ns_pixdims[i] for i in order] hdr['pixdim'][4:(4 + n_ns)] = ns_pixdims return nib.Nifti1Image(data, xyz_affine, hdr)