class GaussianSmooth(Operation): """ Apply a gaussian kernel to the image. Specify FWHM and kernel rank. """ params = ( Parameter(name='fwhm', type='float', default=2.0, description=""" Full width at half max of gaussian dist in mm"""), Parameter(name='rank_factor', type='int', default=3, description=""" Make the rank of the smoothing kernel appx this many times the stdev{x,y}""" ), Parameter(name='SOS', type='bool', default=True, description=""" Act only on the sum-of-squares combined image if possible."""), ) @ChannelIndependentOperation def run(self, image): fwhm_x_pix = self.fwhm / image.isize fwhm_y_pix = self.fwhm / image.jsize fwhm_scale = (8 * np.log(2))**0.5 sx = fwhm_x_pix / fwhm_scale sy = fwhm_y_pix / fwhm_scale scale = self.rank_factor M = 2 * int(scale * sy + 0.99) N = 2 * int(scale * sx + 0.99) image[:] = gaussian_smooth(image[:], sy, sx, (M, N))
class UBPC_siemens_1shot(Operation): params = ( Parameter(name='coefs', type='tuple', default=(0, 0, 0, 0, 0)), Parameter(name='l', type='float', default=1.0), ) @ChannelAwareOperation def run(self, image): if image.N1 == image.n_pe and image.fov_x > image.fov_y: image.fov_y *= 2 image.jsize = image.isize elif image.fov_y == image.fov_x and image.n_pe > image.N1: image.fov_y *= (image.n_pe / image.N1) image.jsize = image.isize print image.fov_y grad = Gradient(image.T_ramp, image.T_flat, image.T0, image.n_pe, image.N1, image.fov_x) k = kernel(image, grad, self.coefs) for n2 in range(image.n_pe): sig = image.cdata[:, :, :, n2, :] sigshape = sig.shape srows = np.product(sigshape[:-1]) sig.shape = (srows, image.N1) scorr = regularized_solve(k[n2], sig.transpose(), self.l).transpose() ## scorr = half_solve(k[n2], sig.transpose()).transpose() scorr.shape = sigshape image.cdata[:, :, :, n2, :] = scorr image.use_membuffer(0)
class FlipSlices(Operation): """ Flip image slices up-down and left-right (in voxel space, not real space) """ params = (Parameter(name="flipud", type="bool", default=False, description="flip each slice up-down"), Parameter(name="fliplr", type="bool", default=False, description="flip each slice left-right")) #------------------------------------------------------------------------- @ChannelIndependentOperation def run(self, image): if not self.flipud and not self.fliplr: return new_xform = image.orientation_xform.tomatrix() if self.fliplr: new_xform[:, 0] = -new_xform[:, 0] if self.flipud: new_xform[:, 1] = -new_xform[:, 1] image.transform(new_mapping=new_xform)
class PlanarPhaseCorrection(Operation): """This is a fairly hacky operation to correct timing offsets in a possibly accelerated EPI acquisition, and ACS data when present. """ params = ( Parameter(name="fov_lim", type="tuple", default=None), Parameter(name="mask_noise", type="bool", default=True), ) @ChannelIndependentOperation def run(self, image): arrays = (image.data, ) refs = (image.ref_data, ) samps = (image.n2_sampling, slice(None)) if hasattr(image, 'acs_data'): arrays += (image.acs_data, ) refs += (image.acs_ref_data, ) a, b = grappa_sampling(image.shape[-2], int(image.accel), image.n_acs) if len(b): init_acs_dir = (-1.0)**(a.tolist().index(b[0])) else: init_acs_dir = 1 polarities = (1.0, init_acs_dir) nr = image.n_ramp nf = image.n_flat if image.pslabel == 'ep2d_bold_acs_test' or image.accel > 2: acs_xleave = int(image.accel) else: acs_xleave = 1 xleaves = (1, int(acs_xleave)) for arr, n2, ref_arr, r, x in zip(arrays, samps, refs, polarities, xleaves): if arr is None: continue util.ifft1(arr, inplace=True, shift=True) # enforce 4D arrays arr.shape = (1, ) * (4 - len(arr.shape)) + arr.shape ref_arr.shape = (1, ) * (4 - len(ref_arr.shape)) + ref_arr.shape sub_samp_slicing = [slice(None)] * len(arr.shape) sub_samp_slicing[-2] = n2 Q2, Q1 = arr[sub_samp_slicing].shape[-2:] q1_ax = np.linspace(-Q1 / 2., Q1 / 2., Q1, endpoint=False) q2_pattern = r * np.power(-1.0, np.arange(Q2) / x) for v in xrange(arr.shape[0]): sub_samp_slicing[0] = v for sl in range(arr.shape[1]): sub_samp_slicing[1] = sl m = simple_unbal_phase_ramp(ref_arr[v, sl].copy(), nr, nf, image.pref_polarity, fov_lim=self.fov_lim, mask_noise=self.mask_noise) soln_pln = (m * q1_ax[None, :]) * q2_pattern[:, None] phs = np.exp(-1j * soln_pln) arr[sub_samp_slicing] *= phs # correct for flat dimensions arr.shape = tuple([d for d in arr.shape if d > 1]) util.fft1(arr, inplace=True, shift=True)
class RotPlane(Operation): """ The orientations in the operation are taken from ANALYZE orient codes and are left-handed. However, if the final image is to be NIFTI type, the rotation transform is updated (in the right-handed system). """ params = ( Parameter(name="orient_target", type="str", default=None, description=""" Final orientation of the image, taken from ANALYZE orient codes. Can be: radiological, transverse, coronal, coronal_flipped, sagittal, and sagittal_flipped. Also may be recon_epi."""), Parameter(name="force", type="bool", default=False, description=""" If your image is scanned even slightly off axis from X, Y, or Z in scanner space, then the transformation will not proceed. You can, however, force the rotation, and imply that the new orient target is the true mapping"""), ) #@ChannelIndependentOperation @ChannelAwareOperation def run(self, image): if (self.orient_target not in xforms.keys() + [ "recon_epi", ]): self.log("no xform available for %s" % self.orient_target) return if self.orient_target == "recon_epi": # always swap -x to +y, and -y to +x, and reflect in z Ts = np.array([ [0., -1., 0.], [-1., 0., 0.], [0., 0., -1.], ]) dest_xform = np.dot(image.orientation_xform.tomatrix(), np.linalg.inv(Ts)) else: dest_xform = xforms.get(self.orient_target, None) image.transform(new_mapping=dest_xform, force=self.force)
class FixTimeSkew(Operation): """ Use sinc interpolation to shift slice data back-in-time to a point corresponding to the beginning of acquisition. """ params = (Parameter(name="data_space", type="str", default="kspace", description=""" name of space to run op: kspace or imspace."""), ) @ChannelIndependentOperation def run(self, image): ## if not verify_scanner_image(self, image): ## return if image.tdim < 2: self.log("Cannot interpolate with only one volume") return nslice = image.kdim # slice acquisition can be in some nonlinear order # eg: Varian data is acquired in an order like this: # [19,17,15,13,11, 9, 7, 5, 3, 1,18,16,14,12,10, 8, 6, 4, 2, 0,] # So the phase shift factor c for spatially ordered slices should go: # [19/20., 9/20., 18/20., 8/20., ...] # --- slices in order of acquistion --- acq_order = image.acq_order # --- shift factors indexed slice number --- shifts = np.array( [np.nonzero(acq_order == s)[0] for s in range(nslice)]) if self.data_space == "imspace": image.setData(np.abs(image[:]).astype(np.float32)) # image-space magnitude interpolation can't be # multisegment sensitive, so do it as if it's 1-seg if image.nseg == 1 or self.data_space == "imspace": for s in range(nslice): c = float(shifts[s]) / float(nslice) #sl = (slice(0,nvol), slice(s,s+1), slice(0,npe), slice(0,nfe)) subsampInterp(image[:, s, :, :], c, axis=0) else: # get the appropriate slicing for sampling type #sl1 = self.segn(image,0) #sl2 = self.segn(image,1) sl1 = image.seg_slicing(0) sl2 = image.seg_slicing(1) for s in range(nslice): # want to shift seg1 forward temporally and seg2 backwards-- # have them meet halfway (??) c1 = -(nslice - shifts[s] - 0.5) / float(nslice) c2 = (shifts[s] + 0.5) / float(nslice) # interpolate for each segment subsampInterp_regular(image[:, s, sl1, :], c1, axis=0) subsampInterp_regular(image[:, s, sl2, :], c2, axis=0)
class Template(Operation): """ A template for operations, with some pointers on Python math (does nothing to the data) @param fparm: a floating-point number @param iparm: an integer """ #the Parameter objects in params are all constructed with four #elements: Parameter(name, type, default, description). #If you're not using any Parameters, skip this step #This params list is actually a Python "tuple". Examples: # 3-tuple: (a, b, c); 2-tuple: (a, b); 1-tuple: (a,) #The notation is not 100% obvious: be careful to add the #trailing comma when defining only 1 Parameter! params = (Parameter(name="fparm", type="float", default=0.75, description="A fractional number"), Parameter(name="iparm", type="int", default=4, description="A whole number")) #note the definition of run: the declaration MUST be this way @ChannelIndependentOperation def run(self, image): # do something to the ReconImage "image" here... #Numeric arrays are indexed in C-order. For a time-series #of volumes, image[t,z,y,x] is the voxel at point (x,y,z,t) #Be aware that this is the opposite of MATLAB indexing. #So, the length of the y-dim and x-dim are always the last 2 dimensions (ySize, xSize) = image.shape[-2:] # a Python way of iterating (secretly using ReconImage's __iter__) for vol in image: #every "vol" in this iteration is a 3D DataChunk object; #a DataChunk also supports __iter__ and can slice into the data for slice in vol: #every slice is a 2D DataChunk slice[:] = doNothing(slice[:], fparm, iparm, xSize, ySize)
class Shift(Operation): """ Allows a user to make arbitrary shifts of the data. """ params = (Parameter(name="yshift", type="int", default=0, description="number of points to shift up and down"), Parameter(name="xshift", type="int", default=0, description="number of points to shift left to right")) @ChannelIndependentOperation def run(self, image): if self.xshift: shift(image[:], self.xshift, axis=-1) if self.yshift: shift(image[:], self.yshift, axis=-2)
class FermiFilter (Operation): """ Apply a Fermi filter to the image. """ params=( Parameter(name="cutoff", type="float", default=0.95, description=""" Distance from the center at which the filter drops to 0.5. Units for cutoff are percentage of radius."""), Parameter(name="trans_width", type="float", default=0.3, description=""" Transition width for rolloff. Smaller values will result in a sharper dropoff.""")) #------------------------------------------------------------------------- @ChannelIndependentOperation def run(self, image): rows, cols = image.shape[-2:] image *= fermi_filter(rows, cols, self.cutoff, self.trans_width)
class WriteImage(Operation): """ Write an image to the filesystem. """ params = (Parameter(name="filename", type="str", default="image", description=""" File name prefix for output (extension is determined by the format)."""), Parameter(name="suffix", type="str", default=None, description=""" Over-rides the default suffix behavior."""), Parameter(name="filedim", type="int", default=3, description=""" Number of dimensions per output file."""), Parameter(name="format", type="str", default="analyze", description=""" File format to write image as."""), Parameter(name="datatype", type="str", default="magnitude", description=""" Output datatype options: %s.""" % output_datatypes)) @ChannelAwareOperation def run(self, image): image.writeImage(self.filename, format_type=self.format, datatype=self.datatype, targetdim=self.filedim, suffix=self.suffix)
class ReadImage(Operation): "Read image from file." params = (Parameter(name="filename", type="str", default="image", description=""" File name prefix for output (extension will be determined by the format)."""), Parameter(name="format", type="str", default=None, description=""" File format to write image as."""), Parameter(name="datatype", type="str", default=None, description=""" Load incoming data as this data type (default is raw data type; only complex32 is supported for FID loading). Available datatypes: %s""" % recon_output2dtype.keys()), Parameter(name="vrange", type="tuple", default=(), description=""" Volume range over-ride"""), Parameter(name="N1", type="int", default=None, description=""" Number of freq. encode points to read""")) #------------------------------------------------------------------------- def run(self): return readImage(self.filename, self.format, datatype=self.datatype, vrange=self.vrange) #, N1=128)
class Window(Operation): """ Apodizes the k-space data based on a specified 2D window. """ params = (Parameter(name="win_name", type="str", default="hanning", description=""" Type of window. Can be blackman, hamming, or hanning."""), ) @ChannelIndependentOperation def run(self, image): # multiply the window by each slice of the image N.multiply(image[:], getWindow(self.win_name, image.idim, image.jdim), image[:])
class ViewImage (Operation): """ Run the sliceview volume viewer. """ params = ( Parameter(name="title", type="str", default="sliceview", description="optional name for the sliceview window"), ) #------------------------------------------------------------------------- @ChannelAwareOperation def run(self, image): # make this raise any runtime error at actual runtime, instead # of load-time from recon.visualization.sliceview import sliceview dimnames = (image.tdim and ("Time Point",) or ()) + \ ("Slice", "Row", "Column",) sliceview(image, dimnames, title=self.title)
class FillHalfSpace (Operation): """ Implements various methods for filling in a full k-space matrix after asymmetric EPI sampling. """ params=( Parameter(name="fill_size", type="int", default=0, description=""" The new number of rows to fill to in k-space"""), Parameter(name="win_size", type="int", default=8, description=""" Length of transition window between measured k-space and filled k-space; a window reduces Gibbs ringing"""), Parameter(name="iterations", type="int", default=0, description=""" Number of times to iterate the merge process"""), Parameter(name="converge_crit", type="float", default=0, description=""" Stop iteration when the summed absolute difference between sucessive reconstructed volumes equals this amount"""), Parameter(name="method", type="str", default="zero filled", description=""" Possible values: iterative, hermitian, or zero filled""") ) def phaseMap2D(self, slice): (_, nx) = slice.shape ny = self.fill_size y0 = self.fill_size/2 fill_slice = N.zeros((ny,nx), N.complex128) fill_slice[y0-self.over_fill:y0+self.over_fill,:] = \ slice[0:self.over_fill*2,:] phase_map = ifft2d(fill_slice) return phase_map def imageFromFill2D(self, slice): (_, nx) = slice.shape ny = self.fill_size fill_slice = N.zeros((ny,nx), N.complex128) embedIm(slice, fill_slice, self.fill_rows, 0) fill_slice[:] = ifft2d(fill_slice) return fill_slice def HermitianFill(self, Im, n_fill_rows): #volumewise for sl in Im: np, nf = sl.shape x1 = np/2+1 # this is where x=1 # catch x=0 with sub-matrix symmetric with A Acomp = sl[np-n_fill_rows+1:,x1-1:].copy() Bcomp = sl[np-n_fill_rows+1:,1:x1-1].copy() sl[1:n_fill_rows,1:x1] = N.conjugate(N.rot90(Acomp, k=2)) sl[1:n_fill_rows,x1:] = N.conjugate(N.rot90(Bcomp, k=2)) mag_fact = abs(sl[n_fill_rows+1]).sum()/abs(sl[n_fill_rows]).sum() #print mag_fact sl[1:n_fill_rows,:] *= mag_fact # fill in row -N/2 and col -M/2 where there's no conj. info sl[0] = 0 sl[0:n_fill_rows,0] = 0 def mergeFill2D(self, filled, measured, winsize=8): wsize_f = float(winsize) mergept = self.fill_rows fill_win = 0.5*(1 + N.cos(N.pi*(N.arange(wsize_f)/wsize_f))) measured_win = 0.5*(1 + N.cos(N.pi + N.pi*(N.arange(wsize_f)/wsize_f))) #filled[:mergept,:] = filled[:mergept,:] filled[mergept+winsize:,:] = measured[winsize:,:] # merge measured data with filled data in winsize merge region filled[mergept:mergept+winsize,:] = \ fill_win[:,N.newaxis]*filled[mergept:mergept+winsize,:] + \ measured_win[:,N.newaxis]*measured[:winsize,:] def cookImage2D(self, volData): out = sys.stdout (ns, _, nx) = volData.shape ny = self.fill_size cooked3D = N.zeros((ns,ny,nx), N.complex128) for s, slice in enumerate(volData): out.write("filling slice %d: "%(s,)) theta = self.phaseMap2D(slice) mag = abs(self.imageFromFill2D(slice)) cooked = N.zeros((ny, nx), N.complex128) prev_power = 0. c = self.criterion[1]=="converge" and 100000. or self.iterations while c > self.criterion[0]: prev_image = cooked.copy() cooked = mag*N.exp(1.j*N.angle(theta)) cooked[:] = fft2d(cooked) cooked[self.fill_rows:,:] = slice[:] cooked[:] = ifft2d(cooked) diff = abs(cooked-prev_image).sum() mag = abs(cooked) c = self.criterion[1]=="converge" and diff or c-1 cooked = mag*N.exp(1.j*N.angle(theta)) cooked[:] = fft2d(cooked) self.mergeFill2D(cooked, slice, winsize=self.win_size) cooked3D[s][:] = cooked[:] out.write("absolute difference=%f\n"%(diff)) return cooked3D.astype(volData.dtype) @ChannelIndependentOperation def run(self, image): ny = image.jdim self.over_fill = ny - self.fill_size/2 self.fill_rows = self.fill_size - ny if self.over_fill < 1: self.log("not enough measured data: this method needs a few " \ "over-scan lines (sampled past the middle of k-space)") return if self.fill_size <= ny: self.log("fill size is not longer than size of measured data "\ "(no filling to be done)") return if self.method not in ("hermitian", "zero filled"): if self.converge_crit > 0 and self.iterations > 0: self.log("you cannot specify the convergence criterion OR the"\ " number of iterations, NOT both: doing nothing") return elif self.converge_crit > 0: self.criterion = (self.converge_crit,"converge") elif self.iterations > 0: self.criterion = (0,"iterateN") else: self.log("no iterative criterion given, default to 5 loops") self.criterion = (0,"iterateN") self.iterations = 5 old_image = image._subimage(image[:].copy()) new_shape = list(image.shape) new_shape[-2] = self.fill_size image.resize(new_shape) # often the jsize is set wrong--as if the partial j-dim spans the FOV if image.jsize > image.isize: image.jsize = image.isize for new_vol, old_vol in zip(image, old_image): if self.method == "iterative": cooked = self.cookImage2D(old_vol[:]) elif self.method == "hermitian": new_vol[:,-ny:,:] = old_vol[:].copy() self.HermitianFill(new_vol[:], self.fill_rows) cooked = new_vol[:] else: cooked = self.kSpaceFill(old_vol[:]) new_vol[:] = cooked[:] def kSpaceFill(self, vol): (ns, _, nx) = vol.shape ny = self.fill_size fill_vol = N.zeros((ns,ny,nx), N.complex64) for s in range(ns): embedIm(vol[s], fill_vol[s], self.fill_rows, 0) return fill_vol.astype(vol.dtype)
class BalPhaseCorrection(Operation): """ Balanced Phase Correction attempts to reduce N/2 ghosting and other systematic phase errors by fitting referrence scan data to a system model. This can only be run on special balanced reference scan data. """ params = ( Parameter(name="percentile", type="float", default=90.0, description=""" Indicates what percentage of "good quality" points to use in the solution. """), Parameter(name="backplane_adj", type="bool", default=False, description=""" Try to keep data contaminated by backplane eddy currents out of solution. """), Parameter(name="fitmeans", type="bool", default=False, description=""" Fit evn/odd means rather than individual planes. """), ) @ChannelIndependentOperation def run(self, image): if not verify_scanner_image(self, image): return -1 if not hasattr(image, "ref_data") or image.ref_data.shape[0] < 2: self.log("Not enough reference volumes, quitting.") return -1 self.volShape = image.shape[-3:] inv_ref0 = ifft(image.ref_data[0]) inv_ref1 = ifft(reverse(image.ref_data[1], axis=-1)) inv_ref = inv_ref0 * N.conjugate(inv_ref1) n_slice, n_pe, n_fe = self.refShape = inv_ref0.shape #phs_vol comes back shaped (n_slice, n_pe, lin2-lin1) phs_vol = unwrap_ref_volume(inv_ref) q1_mask = N.zeros((n_slice, n_pe, n_fe)) # get slice positions (in order) so we can throw out the ones # too close to the backplane of the headcoil (or not ???) if self.backplane_adj: s_idx = tag_backplane_slices(image) else: s_idx = range(n_slice) q1_mask[s_idx] = 1.0 q1_mask[s_idx, 0::2, :] = qual_map_mask(phs_vol[s_idx, 0::2, :], self.percentile) q1_mask[s_idx, 1::2, :] = qual_map_mask(phs_vol[s_idx, 1::2, :], self.percentile) theta = N.empty(self.refShape, N.float64) s_line = N.arange(n_slice) r_line = N.arange(n_fe) - n_fe / 2 B1, B2, B3 = range(3) # planar solution nrows = n_slice * n_fe M = N.zeros((nrows, 3), N.float64) M[:, B1] = N.outer(N.ones(n_slice), r_line).flatten() M[:, B2] = N.repeat(s_line, n_fe) M[:, B3] = 1. A = N.empty((n_slice, 3), N.float64) B = N.empty((3, n_fe), N.float64) A[:, 0] = 1. A[:, 1] = s_line A[:, 2] = 1. if not self.fitmeans: for m in range(n_pe): P = N.reshape(0.5 * phs_vol[:, m, :], (nrows, )) pt_mask = N.reshape(q1_mask[:, m, :], (nrows, )) nz = pt_mask.nonzero()[0] Msub = M[nz] P = P[nz] [u, sv, vt] = N.linalg.svd(Msub, full_matrices=0) coefs = N.dot(vt.transpose(), N.dot(N.diag(1 / sv), N.dot(u.transpose(), P))) B[0, :] = coefs[B1] * r_line B[1, :] = coefs[B2] B[2, :] = coefs[B3] theta[:, m, :] = N.dot(A, B) else: for rows in ('evn', 'odd'): if rows is 'evn': slicing = (slice(None), slice(0, n_pe, 2), slice(None)) else: slicing = (slice(None), slice(1, n_pe, 2), slice(None)) P = N.reshape(0.5 * phs_vol[slicing].mean(axis=-2), (nrows, )) pt_mask = q1_mask[slicing].prod(axis=-2) pt_mask.shape = (nrows, ) nz = pt_mask.nonzero()[0] Msub = M[nz] P = P[nz] [u, sv, vt] = N.linalg.svd(Msub, full_matrices=0) coefs = N.dot(vt.transpose(), N.dot(N.diag(1 / sv), N.dot(u.transpose(), P))) B[0, :] = coefs[B1] * r_line B[1, :] = coefs[B2] B[2, :] = coefs[B3] theta[slicing] = N.dot(A, B)[:, None, :] phase = N.exp(-1.j * theta).astype(image[:].dtype) from recon.tools import Recon if Recon._FAST_ARRAY: apply_phase_correction(image[:], phase) else: for dvol in image: apply_phase_correction(dvol[:], phase)
class UnbalPhaseCorrection(Operation): """ Unbalanced Phase Correction attempts to reduce N/2 ghosting and other systematic phase errors by fitting referrence scan data to a system model. This can be run on Varian sequence EPI data acquired in 1 shot, multishot linear interleaved, or 2-shot centric sampling. """ params = ( Parameter(name="percentile", type="float", default=25.0, description=""" Indicates what percentage of "good quality" points to use in the solution. """), Parameter(name="shear_correct", type="bool", default=True, description=""" Attempt to correct shearing caused by Varian gradient DC offset """), Parameter(name="force_6p_soln", type="bool", default=False, description=""" Fit the 6 parameter model, even if not attempting shearing correction (ie, even if only using 3 parameters in the correction). """), ) @ChannelIndependentOperation def run(self, image): # basic tasks here: # 1: data preparation # 2: phase unwrapping # 3: find mean phase diff lines (2 means or 4, depending on sequence) # 4: solve for linear coefficients # 5: create correction matrix from coefs # 6: apply correction to all image volumes # # * all linearly-sampled data can be treated in a generalized way by # paying attention to the interleave factor (self.xleave) and # the sampling trajectory+timing of each row (image.epi_trajectory) # # * centric sampled data, whose k-space trajectory had opposite # directions, needs special treatment: basically the general case # is handled in separated parts if not verify_scanner_image(self, image): return -1 if not hasattr(image, "ref_data"): self.log("No reference volume, quitting") return -1 if len(image.ref_data.shape) > 3 and image.ref_data.shape[-4] > 1: self.log("Could be performing Balanced Phase Correction!") self.volShape = image.shape[-3:] refVol = image.ref_data[0] n_slice, n_ref_rows, n_fe = self.refShape = refVol.shape # iscentric says whether kspace is multishot centric; # xleave is the factor to which kspace data has been interleaved # (in the case of multishot interleave) iscentric = image.sampstyle is "centric" self.xleave = iscentric and 1 or image.nseg self.alpha, self.beta, _, self.ref_alpha = image.epi_trajectory() # get slice positions (in order) so we can throw out the ones # too close to the backplane of the headcoil #self.good_slices = tag_backplane_slices(image) self.good_slices = range(n_slice) # want to fork the code based on sampling style if iscentric: theta = self.run_centric(image) else: theta = self.run_linear(image) phase = N.exp(-1.j * theta).astype(image[:].dtype) from recon.tools import Recon ## apply_phase_correction(image[:], phase) # this is faster?? if Recon._FAST_ARRAY: apply_phase_correction(image[:], phase) else: for dvol in image: apply_phase_correction(dvol[:], phase) def run_linear(self, image): n_slice, n_ref_rows, n_fe = self.refShape N1 = image.shape[-1] n_conj_rows = n_ref_rows - self.xleave # form the S[u]S*[u+1] array: inv_ref = ifft(image.ref_data[0]) inv_ref = inv_ref[:,:-self.xleave,:] * \ N.conjugate(inv_ref[:,self.xleave:,:]) # Adjust the percentile parameter to reflect the percentage of # points that actually have data (not the pctage of all points). # Do this by determining the fraction of points that pass an # intensity threshold masking step. ir_mask = build_3Dmask(N.abs(inv_ref), 0.1) self.percentile *= ir_mask.sum() / (n_conj_rows * n_slice * n_fe) # partition the phase data based on acquisition order: # pos_order, neg_order define which rows in a slice are grouped # (remember not to count the lines contaminated by artifact!) pos_order = (self.ref_alpha[:n_conj_rows] > 0).nonzero()[0] neg_order = (self.ref_alpha[:n_conj_rows] < 0).nonzero()[0] # in Varian scans, the phase of the 0th product seems to be # contaminated.. so throw it out if there is at least one more # even-odd product # case < 3 ref rows: can't solve problem # case 3 ref rows: p0 from (0,1), n0 from (1,2) # case >=4 ref rows: p0 from (2,3), n0 from (1,2) (can kick line 0) # if the amount of data can support it, throw out p0 if len(pos_order) > 1: pos_order = pos_order[1:] phs_vol = unwrap_ref_volume(inv_ref) phs_mean, q1_mask = mean_and_mask(phs_vol[:, pos_order, :], phs_vol[:, neg_order, :], self.percentile, self.good_slices) ### SOLVE FOR THE SYSTEM PARAMETERS if not self.shear_correct: if self.force_6p_soln: # solve for a1,a2,a3,a4,a5,a6, keep (a1,a3,a5) coefs = solve_phase_6d(phs_mean, q1_mask) coefs = coefs[0::2] else: coefs = solve_phase_3d(phs_mean, q1_mask) print coefs return correction_volume_3d(self.volShape, self.alpha, *coefs) else: coefs = solve_phase_6d(phs_mean, q1_mask) print coefs return correction_volume_6d(self.volShape, self.alpha, self.beta, *coefs) def run_centric(self, image): # centric sampling for epidw goes [0,..,31] then [-1,..,-32] # in index terms this is [32,33,..,63] + [31,30,..,0] # solving for angle(S[u]S*[u+1]) is equal to the basic problem for u>=0 # for u<0: # angle(S[u]S*[u+1]) = 2[sign-flip-terms]*(-1)^(u+1) + [shear-terms] # = -(2[sign-flip-terms]*(-1)^u - [shear-terms]) # so by flipping the sign on the phs means data, we can solve for the # sign-flipping (raster) terms and the DC offset terms with the same # equations. n_slice, n_ref_rows, n_fe = self.refShape n_vol_rows = self.volShape[-2] n_conj_rows = n_ref_rows - 2 # this is S[u]S*[u+1].. now with n_ref_rows-1 rows inv_ref = ifft(image.ref_data[0]) inv_ref = inv_ref[:, :-1, :] * N.conjugate(inv_ref[:, 1:, :]) # Adjust the percentile parameter to reflect the percentage of # points that actually have data (not the pctage of all points). # Do this by determining the fraction of points that pass an # intensity threshold masking step. ir_mask = build_3Dmask(N.abs(inv_ref), 0.1) self.percentile *= ir_mask.sum() / (n_conj_rows * (n_slice * n_fe)) # in the lower segment, do NOT grab the n_ref_rows/2-th line.. # its product spans the two segments cnj_upper = inv_ref[:, n_ref_rows / 2:, :].copy() cnj_lower = inv_ref[:, :n_ref_rows / 2 - 1, :].copy() phs_evn_upper = unwrap_ref_volume(cnj_upper[:, 0::2, :]) phs_odd_upper = unwrap_ref_volume(cnj_upper[:, 1::2, :]) # 0th phase diff on the upper trajectory is contaminated by eddy curr, # throw it out if possible: if phs_evn_upper.shape[-2] > 1: phs_evn_upper = phs_evn_upper[:, 1:, :] phs_evn_lower = unwrap_ref_volume(cnj_lower[:, 0::2, :]) phs_odd_lower = unwrap_ref_volume(cnj_lower[:, 1::2, :]) # 0th phase diff on downward trajectory (== S[u]S*[u+1] for u=-30) # is contaminated too if phs_evn_lower.shape[-2] > 1: phs_evn_lower = phs_evn_lower[:, :-1, :] phs_mean_upper, q1_mask_upper = \ mean_and_mask(phs_evn_upper, phs_odd_upper, self.percentile, self.good_slices) phs_mean_lower, q1_mask_lower = \ mean_and_mask(phs_evn_lower, phs_odd_lower, self.percentile, self.good_slices) if not self.shear_correct: # for upper (u>=0), solve normal SVD if self.force_6p_soln: coefs = solve_phase_6d(phs_mean_upper, q1_mask_upper) coefs = coefs[0::2] else: coefs = solve_phase_3d(phs_mean_upper, q1_mask_upper) print coefs theta_upper = correction_volume_3d(self.volShape, self.alpha, *coefs) # for lower (u < 0), solve with negative data if self.force_6p_soln: coefs = solve_phase_6d(-phs_mean_lower, q1_mask_lower) coefs = coefs[0::2] else: coefs = solve_phase_3d(-phs_mean_lower, q1_mask_lower) print coefs theta_lower = correction_volume_3d(self.volShape, self.alpha, *coefs) theta_lower[:, n_vol_rows / 2:, :] = theta_upper[:, n_vol_rows / 2:, :] return theta_lower else: # for upper (u>=0), solve normal SVD coefs = solve_phase_6d(phs_mean_upper, q1_mask_upper) print coefs theta_upper = correction_volume_6d(self.volShape, self.alpha, self.beta, *coefs) # for lower (u < 0), solve with negative data coefs = solve_phase_6d(-phs_mean_lower, q1_mask_lower) print coefs theta_lower = correction_volume_6d(self.volShape, self.alpha, self.beta, *coefs) theta_lower[:, n_vol_rows / 2:, :] = theta_upper[:, n_vol_rows / 2:, :] return theta_lower
class PreWhitenChannels(Operation): params = (Parameter( name='go_slow', type='bool', default=False, description="""break up transform into smaller blocks"""), ) @staticmethod def cov_mat(s, full=False): from neuroimaging.timeseries import algorithms as alg m, n = s.shape cm = np.zeros((m, m), 'D') _, csd_list = alg.multi_taper_csd(s, BW=5.0 / n) for i in xrange(m): for j in xrange(i + 1): cm[i, j] = np.trapz(csd_list[i][j], dx=1.0 / n) if full: cm = cm + np.tril(cm, -1).T.conj() return cm @staticmethod def cov_mat_biased(s, full=False): m, n = s.shape cm = np.zeros((m, m), 'D') for i in xrange(m): for j in xrange(i + 1): cm[i, j] = (s[i].conjugate() * s[j]).mean() if full: cm = cm + np.tril(cm, -1).T.conj() return cm @ChannelAwareOperation def run(self, image): if not hasattr(image, 'n_chan'): return from recon.scanners import siemens n_chan = image.n_chan dat = siemens.MemmapDatFile(image.path, n_chan) nz_scan = np.empty((image.n_chan, image.M1), 'F') nz_scan[:] = dat[:]['data'] ## del dat ## image.nz_scan = nz_scan covariance = PreWhitenChannels.cov_mat(nz_scan) l, v = np.linalg.eigh(covariance, UPLO='L') W = np.dot(v, (l**(-1 / 2.))[:, None] * v.conjugate().T).astype('F') arr_names = ['cdata', 'cacs_data'] arrs = [ getattr(image, arr_name) for arr_name in arr_names if hasattr(image, arr_name) ] if self.go_slow: print 'foo' for arr in arrs: for s in xrange(image.n_slice): sl = arr[:, :, s, :, :].copy() sl_shape = sl.shape sl.shape = (sl_shape[0], np.prod(sl_shape[1:])) arr[:, :, s, :, :] = np.dot(W, sl).reshape(sl_shape) del sl else: for arr, arr_name in zip(arrs, arr_names): arr_shape = arr.shape arr.shape = (arr.shape[0], np.prod(arr.shape[1:])) setattr(image, arr_name, np.dot(W, arr).reshape(arr_shape)) del arr ## cd_shape = image.cdata.shape ## image.cdata.shape = (n_chan, np.prod(cd_shape[1:])) ## acs_shape = image.cacs_data.shape ## image.cacs_data.shape = (n_chan, np.prod(acs_shape[1:])) ## cd = np.dot(W, image.cdata) ## cd.shape = cd_shape ## del image.cdata ## image.cdata = cd ## acs = np.dot(W, image.cacs_data) ## acs.shape = acs_shape ## del image.cacs_data ## image.cacs_data = acs image.use_membuffer(0)
class GrappaSynthesize(Operation): params = ( Parameter(name='nblocks', type='int', default=4), Parameter(name='sliding', type='bool', default=False), Parameter(name='n1_window', type='int', default=None), Parameter(name='floating_window', type='bool', default=False, description=""" Toggles whether the readout neighborhood window is always centered on the current column (floating), or whether the neighborhoods are fixed segments. """), Parameter(name='ft', type='bool', default=False, description=""" fourier transform readout direction prior to GRAPPA fitting/synthesis """), Parameter(name='beloud', type='bool', default=False, description=""" Allow copious information about the GRAPPA process to be printed on screen """), ) @ChannelAwareOperation def run(self, image): accel = int(image.accel) sub_data = image.cdata # shape (nc, nsl, n2, n1) acs = image.cacs_data # cheap fix for new shape of potentially multi-acquisition acs if len(acs.shape) > 4: acs = acs[:, 0, :, :, :] # transpose to (nsl, nc, n2, n1) acs = acs.transpose(1, 0, 2, 3).copy() if self.ft: print "transforming readout..." util.ifft1(sub_data, inplace=True, shift=True) util.ifft1(acs, inplace=True, shift=True) n2_sampling = image.pe_sampling[:] n2_sampling.sort() for s in range(image.n_slice): N, e = grappa_coefs(acs[s], accel, self.nblocks, sliding=self.sliding, n1_window=self.n1_window, loud=self.beloud, fixed_window=not self.floating_window) # find weightings for each slide reconstruction based on the # errors of their fits (slide enumeration in axis=-2) w = find_weights(e, axis=-2) if self.beloud: print e print w.sum(axis=-2) Ssub = sub_data[:, :, s, :, :] Ssub[:] = grappa_synthesize(Ssub, N, n2_sampling, weights=w, loud=self.beloud, fixed_window=not self.floating_window) if self.ft: util.fft1(sub_data, inplace=True, shift=True) util.fft1(acs, inplace=True, shift=True) # by convention, set view to channel 0 image.use_membuffer(0)
class GeometricUndistortionK(Operation): """ Uses a fieldmap to calculate the geometric distortion kernel for each PE line in an image, then applies the inverse operator on k-space data to correct for field inhomogeneity distortions. """ params = ( Parameter(name="fmap_file", type="str", default="fieldmap-0", description=""" Name of the field map file."""), Parameter(name="lmbda", type="float", default=8.0, description=""" Inverse regularization factor."""), ) @ChannelIndependentOperation def run(self, image): if not verify_scanner_image(self, image): return fmap_file = clean_name(self.fmap_file)[0] ## if hasattr(image, 'n_chan'): ## fmap_file += '.c%02d'%image.chan try: fmapIm = readImage(fmap_file) except: self.log("fieldmap not found: " + fmap_file) return -1 (nslice, npe, nfe) = image.shape[-3:] # make sure that the length of the q1 columns of the fmap # are AT LEAST equal to that of the image regrid_fac = max(npe, fmapIm.shape[-2]) # fmap and chi-mask are swapped to be of shape (Q1,Q2) fmap = np.swapaxes( regrid_bilinear(fmapIm[0], regrid_fac, axis=-2).astype(np.float64), -1, -2) chi = np.swapaxes(regrid_bilinear(fmapIm[1], regrid_fac, axis=-2), -1, -2) Q1, Q2 = fmap.shape[-2:] # compute T_n2 vector Tl = image.T_pe delT = image.delT a, b, n2, _ = image.epi_trajectory() K = get_kernel(Q2, Tl, b, n2, fmap, chi) for s in range(nslice): # dchunk is shaped (nvol, npe, nfe) # inverse transform along nfe (can't do in-place) dchunk = ifft1(image[:, s, :, :]) # now shape is (nfe, npe, nvol) dchunk = np.swapaxes(dchunk, 0, 2) for fe in range(nfe): # want to solve Kx = y for x # K is (npe,npe), and y is (npe,nvol) # # There seems to be a trade-off here as nvol changes... # Doing this in two steps is faster for large nvol; I think # it takes advantage of the faster BLAS matrix-product in dot # as opposed to LAPACK's linear solver. For smaller values # of nvol, the overhead seems to outweigh the benefit. iK = regularized_inverse(K[s, fe], self.lmbda) dchunk[fe] = np.dot(iK, dchunk[fe]) dchunk = np.swapaxes(dchunk, 0, 2) # fft x back to kx, can do inplace here fft1(dchunk, inplace=True) image[:, s, :, :] = dchunk
class ComputeFieldMap(Operation): """ Perform phase unwrapping and calculate B0 inhomogeneity field map. """ params = ( Parameter(name="fmap_file", type="str", default="fieldmap", description=""" Name of the field map file to store"""), Parameter(name="threshfactor", type="float", default=0.1, description=""" Adjust this factor to raise or lower the masking threshold for the ASEMS signal."""), ) #------------------------------------------------------------------------- @ChannelAwareOperation def run(self, image): # Make sure it's an asems image if not hasattr(image, "asym_times") and not hasattr(image, "te"): self.log("No asym_time, can't compute field map.") return asym_times = image.asym_times # Make sure there are at least two volumes if image.tdim < 2: self.log("Cannot calculate field map from only a single volume."\ " Must have at least two volumes.") return diffshape = (image.tdim - 1, ) + image.shape[-3:] diffshape = image.tdim > 2 and diffshape or diffshape[-3:] phase_map = image._subimage(np.zeros(diffshape, np.float32)) bytemask = image._subimage(np.zeros(diffshape, np.float32)) # Get phase difference between scan n+1 and scan n # Then find the mask and unwrapped phase, and compute # the field strength in terms of rad/s fwhm = max(image.isize, image.jsize) * 1.5 for psub, msub in zip(phase_map, bytemask): del_te = asym_times[msub.num + 1] - asym_times[msub.num] if self.haschans: for c in range(image.n_chan): image.load_chan(c) dphs = image[msub.num + 1] * np.conjugate(image[msub.num]) msk = build_3Dmask(np.power(np.abs(dphs), 0.5), self.threshfactor) msub[:] += msk ## psub[:] += (unwrap3D(np.angle(dphs)) * msk) #/ del_te ## psub[:] += gaussian_smooth(np.angle(dphs), 1.5, 3) ## smooth_phs = unwrap3D(gaussian_smooth(np.angle(dphs), ## sigma_y, sigma_x, ## gaussian_dims))*msk smooth_phs = ReconImage(np.angle(dphs), isize=image.isize, jsize=image.jsize) GaussianSmooth(fwhm=fwhm).run(smooth_phs) psub[:] += smooth_phs[:] #psub[:] = np.where(msub[:], psub[:]/msub[:], 0) psub[:] /= image.n_chan msub[:] = np.where(msub[:], 1, 0) #psub[:] = (unwrap3D(psub[:]) * msub[:]) #/ del_te image.use_membuffer(0) else: dphs = image[msub.num + 1] * np.conjugate(image[msub.num]) msub[:] = build_3Dmask(np.power(np.abs(dphs), 0.5), self.threshfactor) #psub[:] = (unwrap3D(np.angle(dphs)) * msub[:]) #/ del_te psub[:] = np.angle(dphs) GaussianSmooth(fwhm=fwhm).run(psub) psub[:] = unwrap3D(psub[:]) * msub[:] psub[:] /= del_te # for each diff vol, write a file with vol0 = fmap, vol1 = mask fmap_file = clean_name(self.fmap_file)[0] if phase_map.ndim > 3: for index in range(phase_map.tdim): catIm = phase_map.subImage(index).concatenate( bytemask.subImage(index), newdim=True) catIm.writeImage(fmap_file + "-%d" % index, format_type="nifti-single") else: catIm = phase_map.concatenate(bytemask, newdim=True) catIm.writeImage(fmap_file, format_type="nifti-single")
class ReadoutResampling(Operation): """ This operation performs all resampling in the read-out direction. These resampling operations are a combination of ramp-sampling correction and positive/negative echo timing mis-matches. It does not YET reduce oversampling. It runs fairly slowly. """ params = ( Parameter(name="fov_lim", type="tuple", default=None), Parameter(name="mask_noise", type="bool", default=True), ) @ChannelAwareOperation def run(self, image): Tr = image.T_ramp Tf = image.T_flat T0 = image.T0 N1 = image.N1 Tg = 2 * Tr + Tf dt = (Tg - 2 * T0) / (N1 - 1) As = Tf + Tr - T0**2 / Tr t_axis = np.arange(N1) * dt + T0 nr = image.n_ramp nf = image.n_flat pe_rev = image.pe_reflected pe_sampling = image.pe_sampling ref_rev = image.ref_reflected acs_sampling = image.acs_sampling acs_rev = image.acs_reflected acs_ref_rev = image.acs_ref_reflected # tn_regrid and neg_tn_regrid are the destination time points # for positive and negative lobe readouts tn_regrid = image.t_n1() neg_tn_regrid = Tg - tn_regrid ## print t_axis ## print tn_regrid, neg_tn_regrid ## tn_regrid = np.empty((N1,), 'd') ## neg_tn_regrid = np.empty_like(tn_regrid) ## idx = np.arange(N1) ## r_up = idx < (nr+1) ## tn_regrid[r_up] = ( T0**2 + 2*idx[r_up]*Tr*As/(N1-1) )**0.5 ## r_fl = (idx >= (nr+1)) & (idx < (nf+1)) ## tn_regrid[r_fl] = idx[r_fl]*As/(N1-1) + Tr/2 + T0**2/(2*Tr) ## r_dn = idx >= (nf+1) ## tn_regrid[r_dn] = Tg - ( 2*Tr*(Tf+Tr-T0**2/(2*Tr) - idx[r_dn]*As/(N1-1)) )**.5 ## neg_tn_regrid = Tg - tn_regrid same_grad_diff = pe_rev[1] - pe_rev[0] neg_slicing = slice(pe_rev[0], pe_rev[-1] + 1, same_grad_diff) pos_sampling = pe_sampling[:] for pe in pe_rev: pos_sampling.remove(pe) pos_slicing = slice(pos_sampling[0], pos_sampling[-1] + 1, same_grad_diff) for c in xrange(image.n_chan): for v in xrange(image.n_vol): for s in xrange(image.n_slice): r = image.cref_data[c, v, s].copy() # polarity is -1 if the first ref was on a neg gradient polarity = ref_rev[0] == 0 and -1 or +1 m = sm_utils.simple_unbal_phase_ramp( r, nr, nf, polarity, self.fov_lim, self.mask_noise) Tau = m * dt * N1 / (2 * np.pi) # these are the time points of the acquired sampled tn = (t_axis - Tau) ## print Tau, '('+str(dt)+')' # compute these in transpose op_pos = np.sinc((tn_regrid[None, :] - tn[:, None]) / dt) op_neg = np.sinc( (neg_tn_regrid[None, :] - tn[:, None]) / dt) neg_block = image.cdata[c, v, s, neg_slicing].copy() pos_block = image.cdata[c, v, s, pos_slicing].copy() # these are L x N1, to be transformed by N1 x N1 operators # such that S <-- [(N1 x N1)*(L x N1)^T]^T = (L x N1)*(N1 x N1)^T image.cdata[c, v, s, neg_slicing] = np.dot(neg_block, op_neg) image.cdata[c, v, s, pos_slicing] = np.dot(pos_block, op_pos) if not acs_sampling: return acs_offset = -acs_sampling[0] # refs in the EPI section are never interleaved n_refs = image.n_refs n_acs_seg = image.cacs_ref_data.shape[-2] / n_refs g = 0 # transform the acs indicators to be segment-wise l = len(acs_sampling) acs_sampling = np.array(acs_sampling).reshape(n_acs_seg, l / n_acs_seg) l = len(acs_rev) acs_rev = np.array(acs_rev).reshape(n_acs_seg, l / n_acs_seg) l = len(acs_ref_rev) acs_ref_rev = np.array(acs_ref_rev).reshape(n_acs_seg, l / n_acs_seg) if len(image.cacs_data.shape) < 5: # pad with a rep dimension ashape = list(image.cacs_data.shape) rshape = list(image.cacs_ref_data.shape) ashape.insert(1, 1) rshape.insert(1, 1) image.cacs_data.shape = tuple(ashape) image.cacs_ref_data.shape = tuple(rshape) n_acs_rep = image.cacs_data.shape[1] for c in xrange(image.n_chan): for r in xrange(n_acs_rep): for s in xrange(image.n_slice): for g in xrange(n_acs_seg): acs_rev_g = acs_rev[g] sampling_g = acs_sampling[g].tolist() same_grad_diff = acs_rev_g[1] - acs_rev_g[0] for acs in acs_rev_g: sampling_g.remove(acs) neg_slicing = slice(acs_rev_g[0] + acs_offset, acs_rev_g[-1] + acs_offset, same_grad_diff) pos_slicing = slice(sampling_g[0] + acs_offset, sampling_g[-1] + acs_offset, same_grad_diff) rd = image.cacs_ref_data[c, r, s, g * n_refs:(g + 1) * n_refs].copy() polarity = acs_ref_rev[g, 0] == 0 and -1 or +1 m = sm_utils.simple_unbal_phase_ramp( rd, nr, nf, polarity, self.fov_lim, self.mask_noise) Tau = m * dt * N1 / (2 * np.pi) ## print Tau, '('+str(dt)+')' # these are the time points of the acquired sampled tn = t_axis - Tau # compute these in transpose op_pos = np.sinc( (tn_regrid[None, :] - tn[:, None]) / dt) op_neg = np.sinc( (neg_tn_regrid[None, :] - tn[:, None]) / dt) neg_block = image.cacs_data[c, r, s, neg_slicing].copy() pos_block = image.cacs_data[c, r, s, pos_slicing].copy() # these are L x N1, to be multiplied by N1 x N1 # resampling operators such that # S <-- [(N1 x N1)*(L x N1)^T]^T = (L x N1)*(N1 x N1)^T image.cacs_data[c, r, s, neg_slicing] = np.dot( neg_block, op_neg) image.cacs_data[c, r, s, pos_slicing] = np.dot( pos_block, op_pos) image.use_membuffer(0)