def image_call(xyz_image, function, inaxis='t', outaxis='new'): """ Take an XYZImage, perform some operation on it, over a specified axis, and return a new XYZImage. For the sake of testing things out, we will assume that the operation can only operate on the first axis of the array. Parameters ---------- xyz_image : XYZImage function : callable An operation that does something over the first axis, such as lambda x: x[::2] inaxis : str or int Specification of axis of XYZImage outaxis : str Name of new axis in new XYZImage Returns ------- newim : XYZImage with axis inaxis replaced with outaxis """ if inaxis in xyz_image.reference.coord_names + \ xyz_image.axes.coord_names[:3] + tuple(range(3)): raise ValueError('cannot reduce over a spatial axis' + 'or we will not be able to output XYZImages') image = xyz_image.to_image() image = image_rollaxis(image, inaxis) inaxis = image.axes.coord_names[0] # now it's a string newdata = function(image.get_data()) newcoordmap = image.coordmap.renamed_domain({inaxis:outaxis}) newimage = Image(newdata, newcoordmap) # we have to roll the axis back axis_index = xyz_image.axes.index(inaxis) newimage = image_rollaxis(newimage, axis_index, inverse=True) return XYZImage(newimage.get_data(), xyz_image.affine, newimage.axes.coord_names)
def setup(): tmp_img = load_image(funcfile) # For now, img is an Image # instead of an XYZImage A = np.identity(4) A[:3,:3] = tmp_img.affine[:3,:3] A[:3,-1] = tmp_img.affine[:3,-1] xyz_data = tmp_img.get_data() xyz_img = XYZImage(xyz_data, A, tmp_img.axes.coord_names[:3] + ('t',)) # If load_image returns an XYZImage, I'd really be # starting from xyz_img, so from here # Here, I'm just doing this so I know that # img.shape[0] is the number of volumes img = image_rollaxis(Image(xyz_img._data, xyz_img.coordmap), 't') data_dict['nimages'] = img.shape[0] # It might be worth to make # data a public attribute of Image/XYZImage # and we might rename get_data-> data_as_array = np.asarray(self.data) # Then, the above would not access a private attribute # Below, I am just making a mask # because I already have img, I # know I can do this # In principle, though, the pca function # will just take another XYZImage as a mask img_data = img.get_data() first_frame = img_data[0] mask = XYZImage(np.greater(np.asarray(first_frame), 500).astype(np.float64), A, xyz_img.axes.coord_names[:3]) data_dict['fmridata'] = xyz_img data_dict['mask'] = mask data_dict['img'] = img print data_dict['mask'].shape, np.sum(np.array(data_dict['mask']))
def need_specific_axis_reduce(xyz_image, reduce_op): """ Take an XYZImage, perform some reduce operation on it, over the axis 'specific', and returns a new XYZImage. For the sake of testing things out, we will assume that the operation reduces over the first axis only. Parameters ---------- xyz_image : XYZImage reduce_op : callable An operation that reduces over the first axis, such as lambda x: x.sum(0) Returns ------- newim : XYZImage, missing axis """ image = xyz_image.to_image() image = image_rollaxis(image, 'specific') axis_name = image.axes.coord_names[0] output_axes = list(xyz_image.axes.coord_names) output_axes.remove(axis_name) newdata = reduce_op(image.get_data()) return XYZImage(newdata, xyz_image.affine, output_axes)
def image_modify(xyz_image, modify, axis='y+PA'): """ Take an XYZImage, perform some operation on it, over a specified axis, and return a new XYZImage. For this operation, we are allowed to iterate over spatial axes. For the sake of testing things out, we will assume that the operation modify can only operate by iterating over the first axis of an array. Parameters ---------- xyz_image : XYZImage modify : callable An operation that does modifies an array. Something like def f(x): x[:] = x.mean() axis : str or int Specification of axis of XYZImage Returns ------- newim : XYZImage with a modified copy of xyz_image._data. """ image = Image(xyz_image.get_data(), xyz_image.coordmap) image = image_rollaxis(image, axis) data = image.get_data().copy() for d in data: modify(d) import copy newimage = Image(data, copy.copy(image.coordmap)) # Now, we have to put the data back in order # with XYZImage newimage = synchronized_order(newimage, xyz_image) return XYZImage.from_image(newimage)
def image_reduce(xyz_image, reduce_op, axis='t'): """ Take an XYZImage, perform some reduce operation on it, over a specified axis, and return a new XYZImage. For the sake of testing things out, we will assume that the operation reduces over the first axis only. Parameters ---------- xyz_image : XYZImage reduce_op : callable An operation that reduces over the first axis, such as lambda x: x.sum(0) axis : str or int Specification of axis of XYZImage Returns ------- newim : XYZImage, missing axis """ if axis in xyz_image.reference.coord_names + \ xyz_image.axes.coord_names[:3] + tuple(range(3)): raise ValueError('cannot reduce over a spatial axis' + 'or we will not be able to output XYZImages') image = xyz_image.to_image() image = image_rollaxis(image, axis) axis_name = image.axes.coord_names[0] output_axes = list(xyz_image.axes.coord_names) output_axes.remove(axis_name) newdata = reduce_op(image.get_data()) return XYZImage(newdata, xyz_image.affine, output_axes)
def pca_image(xyz_image, axis='t', mask=None, ncomp=None, standardize=True, design_keep=None, design_resid='mean', tol_ratio=0.01): """ Compute the PCA of an image over a specified axis. Parameters ---------- data : XYZImage The image on which to perform PCA over its first axis. axis : str or int Axis over which to perform PCA. Cannot be a spatial axis because the results have to be XYZImages. Default is 't' mask : XYZImage An optional mask, should have shape == image.shape[:3] and the same XYZTransform. ncomp : {None, int}, optional How many component basis projections to return. If ncomp is None (the default) then the number of components is given by the calculated rank of the data, after applying `design_keep`, `design_resid` and `tol_ratio` below. We always return all the basis vectors and percent variance for each component; `ncomp` refers only to the number of basis_projections returned. standardize : bool, optional If True, standardize so each time series (after application of `design_keep` and `design_resid`) has the same standard deviation, as calculated by the ``np.std`` function. design_keep : None or ndarray, optional Data is projected onto the column span of design_keep. None (default) equivalent to ``np.identity(data.shape[axis])`` design_resid : str or None or ndarray, optional After projecting onto the column span of design_keep, data is projected perpendicular to the column span of this matrix. If None, we do no such second projection. If a string 'mean', then the mean of the data is removed, equivalent to passing a column vector matrix of 1s. tol_ratio : float, optional If ``XZ`` is the vector of singular values of the projection matrix from `design_keep` and `design_resid`, and S are the singular values of ``XZ``, then `tol_ratio` is the value used to calculate the effective rank of the projection of the design, as in ``rank = ((S / S.max) > tol_ratio).sum()`` Returns ------- results : dict $L$ is the number of non-trivial components found after applying `tol_ratio` to the projections of `design_keep` and `design_resid`. `results` has keys: * ``basis_vectors``: series over `axis`, shape (data.shape[axis], L) - the eigenvectors of the PCA * ``pcnt_var``: percent variance explained by component, shape (L,) * ``basis_projections``: PCA components, with components varying over axis `axis`; thus shape given by: ``s = list(data.shape); s[axis] = ncomp`` * ``axis``: axis over which PCA has been performed. """ if axis in xyz_image.reference.coord_names + \ xyz_image.axes.coord_names[:3] + tuple(range(3)): raise ValueError('cannot perform PCA over a spatial axis' + 'or we will not be able to output XYZImages') xyz_data = xyz_image.get_data() image = Image(xyz_data, xyz_image.coordmap) image = image_rollaxis(image, axis) if mask is not None: if mask.xyz_transform != xyz_image.xyz_transform: raise ValueError('mask and xyz_image have different coordinate systems') if mask.ndim != image.ndim - 1: raise ValueError('mask should have one less dimension than xyz_image') if mask.axes.coord_names != image.axes.coord_names[1:]: raise ValueError('mask should have axes %s' % str(image.axes.coord_names[1:])) data = image.get_data() if mask is not None: mask_data = mask.get_data() else: mask_data = None # do the PCA res = pca(data, 0, mask_data, ncomp, standardize, design_keep, design_resid, tol_ratio) # Clean up images after PCA img_first_axis = image.axes.coord_names[0] # Rename the axis. # # Because we started with XYZImage, all non-spatial # coordinates agree in the range and the domain # so this will work and the renamed_range # call is not even necessary because when we call # XYZImage, we only use the axisnames output_coordmap = image.coordmap.renamed_domain( {img_first_axis:'PCA components'}).renamed_range({img_first_axis:'PCA components'}) output_img = Image(res['basis_projections'], output_coordmap) # We have to roll the axis back roll_index = xyz_image.axes.index(img_first_axis) output_img = image_rollaxis(output_img, roll_index, inverse=True) output_xyz = XYZImage(output_img.get_data(), xyz_image.affine, output_img.axes.coord_names) key = 'basis_vectors over %s' % img_first_axis res[key] = res['basis_vectors'] res['basis_projections'] = output_xyz return res
def run_model(subj, run): """ Single subject fitting of FIAC model """ #---------------------------------------------------------------------- # Set initial parameters of the FIAC dataset #---------------------------------------------------------------------- # Number of volumes in the fMRI data nvol = 191 # The TR of the experiment TR = 2.5 # The time of the first volume Tstart = 0.0 # The array of times corresponding to each # volume in the fMRI data volume_times = np.arange(nvol)*TR + Tstart # This recarray of times has one column named 't' # It is used in the function design.event_design # to create the design matrices. volume_times_rec = make_recarray(volume_times, 't') # Get a path description dictionary that contains all the path data # relevant to this subject/run path_info = futil.path_info(subj,run) #---------------------------------------------------------------------- # Experimental design #---------------------------------------------------------------------- # Load the experimental description from disk. We have utilities in futil # that reformat the original FIAC-supplied format into something where the # factorial structure of the design is more explicit. This has already # been run once, and get_experiment_initial() will simply load the # newly-formatted design description files (.csv) into record arrays. experiment, initial = futil.get_experiment_initial(path_info) # Create design matrices for the "initial" and "experiment" factors, # saving the default contrasts. # The function event_design will create # design matrices, which in the case of "experiment" # will have num_columns = # (# levels of speaker) * (# levels of sentence) * len(delay.spectral) = # 2 * 2 * 2 = 8 # For "initial", there will be # (# levels of initial) * len([hrf.glover]) = 1 * 1 = 1 # Here, delay.spectral is a sequence of 2 symbolic HRFs that # are described in # # Liao, C.H., Worsley, K.J., Poline, J-B., Aston, J.A.D., Duncan, G.H., # Evans, A.C. (2002). \'Estimating the delay of the response in fMRI # data.\' NeuroImage, 16:593-606. # The contrasts, cons_exper, # is a dictionary with keys: ['constant_0', 'constant_1', 'speaker_0', # 'speaker_1', # 'sentence_0', 'sentence_1', 'sentence:speaker_0', 'sentence:speaker_1'] # representing the four default contrasts: constant, main effects + # interactions, # each convolved with 2 HRFs in delay.spectral. Its values # are matrices with 8 columns. # XXX use the hrf __repr__ for naming contrasts X_exper, cons_exper = design.event_design(experiment, volume_times_rec, hrfs=delay.spectral) # The contrasts for 'initial' are ignored # as they are "uninteresting" and are included # in the model as confounds. X_initial, _ = design.event_design(initial, volume_times_rec, hrfs=[hrf.glover]) # In addition to factors, there is typically a "drift" term # In this case, the drift is a natural cubic spline with # a not at the midpoint (volume_times.mean()) vt = volume_times # shorthand drift = np.array( [vt**i for i in range(4)] + [(vt-vt.mean())**3 * (np.greater(vt, vt.mean()))] ) for i in range(drift.shape[0]): drift[i] /= drift[i].max() # We transpose the drift so that its shape is (nvol,5) so that it will have # the same number of rows as X_initial and X_exper. drift = drift.T # There are helper functions to create these drifts: design.fourier_basis, # design.natural_spline. Therefore, the above is equivalent (except for # the normalization by max for numerical stability) to # # >>> drift = design.natural_spline(t, [volume_times.mean()]) # Stack all the designs, keeping the new contrasts which has the same keys # as cons_exper, but its values are arrays with 15 columns, with the # non-zero entries matching the columns of X corresponding to X_exper X, cons = design.stack_designs((X_exper, cons_exper), (X_initial, {}), (drift, {})) # Sanity check: delete any non-estimable contrasts # XXX - this seems to be broken right now, it's producing bogus warnings. ## for k in cons.keys(): ## if not isestimable(X, cons[k]): ## del(cons[k]) ## warnings.warn("contrast %s not estimable for this run" % k) # The default contrasts are all t-statistics. We may want to output # F-statistics for 'speaker', 'sentence', 'speaker:sentence' based on the # two coefficients, one for each HRF in delay.spectral cons['speaker'] = np.vstack([cons['speaker_0'], cons['speaker_1']]) cons['sentence'] = np.vstack([cons['sentence_0'], cons['sentence_1']]) cons['sentence:speaker'] = np.vstack([cons['sentence:speaker_0'], cons['sentence:speaker_1']]) #---------------------------------------------------------------------- # Data loading #---------------------------------------------------------------------- # Load in the fMRI data, saving it as an array # It is transposed to have time as the first dimension, # i.e. fmri[t] gives the t-th volume. fmri_lpi = futil.get_fmri(path_info) # an LPIImage fmri_im = Image(fmri_lpi._data, fmri_lpi.coordmap) fmri_im = image_rollaxis(fmri_im, 't') fmri = fmri_im.get_data() # now, it's an ndarray nvol, volshape = fmri.shape[0], fmri.shape[1:] nslice, sliceshape = volshape[0], volshape[1:] #---------------------------------------------------------------------- # Model fit #---------------------------------------------------------------------- # The model is a two-stage model, the first stage being an OLS (ordinary # least squares) fit, whose residuals are used to estimate an AR(1) # parameter for each voxel. m = OLSModel(X) ar1 = np.zeros(volshape) # Fit the model, storing an estimate of an AR(1) parameter at each voxel for s in range(nslice): d = np.array(fmri[:,s]) flatd = d.reshape((d.shape[0], -1)) result = m.fit(flatd) ar1[s] = ((result.resid[1:] * result.resid[:-1]).sum(0) / (result.resid**2).sum(0)).reshape(sliceshape) # We round ar1 to nearest one-hundredth # and group voxels by their rounded ar1 value, # fitting an AR(1) model to each batch of voxels. # XXX smooth here? # ar1 = smooth(ar1, 8.0) ar1 *= 100 ar1 = ar1.astype(np.int) / 100. # We split the contrasts into F-tests and t-tests. # XXX helper function should do this fcons = {}; tcons = {} for n, v in cons.items(): v = np.squeeze(v) if v.ndim == 1: tcons[n] = v else: fcons[n] = v # Setup a dictionary to hold all the output # XXX ideally these would be memmap'ed Image instances output = {} for n in tcons: tempdict = {} for v in ['sd', 't', 'effect']: tempdict[v] = np.memmap(NamedTemporaryFile(prefix='%s%s.nii' \ % (n,v)), dtype=np.float, shape=volshape, mode='w+') output[n] = tempdict for n in fcons: output[n] = np.memmap(NamedTemporaryFile(prefix='%s%s.nii' \ % (n,v)), dtype=np.float, shape=volshape, mode='w+') # Loop over the unique values of ar1 for val in np.unique(ar1): armask = np.equal(ar1, val) m = ARModel(X, val) d = fmri[:,armask] results = m.fit(d) # Output the results for each contrast for n in tcons: resT = results.Tcontrast(tcons[n]) output[n]['sd'][armask] = resT.sd output[n]['t'][armask] = resT.t output[n]['effect'][armask] = resT.effect for n in fcons: output[n][armask] = results.Fcontrast(fcons[n]).F # Dump output to disk odir = futil.output_dir(path_info,tcons,fcons) # The coordmap for a single volume in the time series vol0_map = fmri_im[0].coormap for n in tcons: for v in ['t', 'sd', 'effect']: im = Image(output[n][v], vol0_map) save_image(im, pjoin(odir, n, '%s.nii' % v)) for n in fcons: im = Image(output[n], vol0_map) save_image(im, pjoin(odir, n, "F.nii"))