def _get_xform(self): "forms a Quaternion object and the r0 vector given a hdr dictionary" # The hdr dictionary is guaranteed to be full, if not specific if self.qform_code: qb, qc, qd, qfac = (self.quatern_b, self.quatern_c, self.quatern_d, self.qfac) quat = Quaternion(i=qb, j=qc, k=qd, qfac=qfac) offset = (self.qoffset_x, self.qoffset_y, self.qoffset_z) elif self.sform_code: M = [[ self.srow_x0,self.srow_x1,self.srow_x2 ], [ self.srow_y0,self.srow_y1,self.srow_y2 ], [ self.srow_z0,self.srow_z1,self.srow_z2 ]] quat = Quaternion(M=M) offset = (self.srow_x3, self.srow_y3, self.srow_z3) else: quat = Quaternion(M=N.identity(3)) offset = (0,0,0) return quat, offset
def toImage(self): orient_name = orientcode2orientname.get(self.orient, "") M = xforms.get(orient_name, np.identity(3)) quat = Quaternion(M=M) offset_ana = np.array( [self.x0 * self.isize, self.y0 * self.jsize, self.z0 * self.ksize]) offset = -np.dot(M, offset_ana) dimlengths = np.array([self.ksize, self.jsize, self.isize]) * \ np.array(self.data.shape[-3:]) ## # sanity check: if the offset values were funky, don't try to set it ## if not (np.abs(offset*2) < dimlengths).all(): ## offset = None return ReconImage(self.data.copy(), self.isize, self.jsize, self.ksize, self.tsize, offset=offset, scaling=(self.scale_factor or 1.0), orient_xform=quat)
def __init__(self, data, isize=1., jsize=1., ksize=1., tsize=1., offset=None, scaling=None, orient_xform=None): """ Construct a ReconImage with at least data, isize, jsize, ksize, and tsize known. Optional information are an offset 3-tuple specifying (x0,y0,z0), a Quaternion object representing the transformation of this data to neurological orientation (+X,+Y,+Z) = (Right,Anterior,Superior), and a name for the data's orientation (used for ANALYZE format output). """ self.setData(data) self.isize, self.jsize, self.ksize, self.tsize = \ (isize, jsize, ksize, tsize) self.orientation_xform = orient_xform or Quaternion() # offset should be the (x,y,z) offset in xyz-space xform = self.orientation_xform.tomatrix() if offset is not None: (self.x0, self.y0, self.z0) = offset else: # assume that vox at (idim/2, jdim/2, kdim/2) is the origin # thus Trans*(i0,j0,k0)^T + (x0,y0,z0)^T = (0,0,0)^T (self.x0, self.y0, self.z0) = \ -np.dot(xform, np.array([self.isize*self.idim/2., self.jsize*self.jdim/2., self.ksize*self.kdim/2.])) self.scaling = scaling or 1.0
def parse_siemens_hdr(fname): """ lRepetitions = 19 (= nvol - 1) sKSpace.lBaseResolution = 64 sKSpace.lPhaseEncodingLines = 128 sKSpace.ucMultiSliceMode = 0x2 (interleaved) tSequenceFileName = "%CustomerSeq%\ep2d_bold_bigfov" sRXSPEC.alDwellTime[0] = 2800 alTR[0] = 2500000 lContrasts = 2 alTE[0] = 4920 alTE[1] = 7380 sSliceArray.asSlice[0].sPosition.dTra = -39.375 sSliceArray.asSlice[0].sNormal.dTra = 1 sSliceArray.asSlice[0].dThickness = 3 sSliceArray.asSlice[0].dPhaseFOV = 240 sSliceArray.asSlice[0].dReadoutFOV = 240 sSliceArray.asSlice[0].dInPlaneRot = 2.051034897e-010 ... sSliceArray.lSize = 22 sFastImaging.lEchoSpacing = 420 sPat.lAccelFactPE = 1 sPat.lRefLinesPE = 24 asCoilSelectMeas[0].aFFT_SCALE[0].flFactor = 1.26665 asCoilSelectMeas[0].aFFT_SCALE[0].bValid = 1 asCoilSelectMeas[0].aFFT_SCALE[0].lRxChannel = 1 ... more info... sSliceArray.ucMode (I THINK this is slice acq orde -- can it be OR'd??) enum SeriesMode { ASCENDING = 0x01, DESCENDING = 0x02, INTERLEAVED = 0x04 }; sKSpace.unReordering enum Reordering { REORDERING_LINEAR = 0x01, REORDERING_CENTRIC = 0x02, REORDERING_LINE_SEGM = 0x04, REORDERING_PART_SEGM = 0x08, REORDERING_FREE_0 = 0x10, REORDERING_FREE_1 = 0x20, REORDERING_FREE_2 = 0x40, REORDERING_FREE_3 = 0x80 }; sKSpace.ucPhasePartialFourier sKSpace.ucSlicePartialFourier enum PartialFourierFactor { PF_HALF = 0x01, PF_5_8 = 0x02, PF_6_8 = 0x04, PF_7_8 = 0x08, PF_OFF = 0x10 }; sKSpace.ucMultiSliceMode enum MultiSliceMode { MSM_SEQUENTIAL = 0x01, MSM_INTERLEAVED = 0x02, MSM_SINGLESHOT = 0x04 }; slice spacing ( can do with position arrays ) slice order / acq order missing: n_pe_acq (can this be derived from n_pe and accel? may be 31 for accel=2) n_ref == nsegmeas + 1 (or is that a coincidence???) n_part -- do I care? rampsamp info """ hdr_dict = {} ## hdrlen = header_length(fname) ## hdr_str = open(fname, 'r').read(4+hdrlen) hdr_str = header_string(fname) asc_dict = strip_ascconv(hdr_str) chan_scales = condense_array(asc_dict, 'asCoilSelectMeas[0].aFFT_SCALE') gains = chan_scales['flFactor'] chans = chan_scales['lRxChannel'].astype('i') # usually [1, 2, 3, 4, ...] hdr_dict['n_chan'] = gains.shape[0] hdr_dict['channel_gains'] = gains[chans - 1] hdr_dict['n_echo'] = int(asc_dict['lContrasts']) hdr_dict['n_vol'] = int(1 + asc_dict.get('lRepetitions', 0)) hdr_dict['n_slice'] = int(asc_dict.get('sSliceArray.lSize', 1)) hdr_dict['n_partition'] = int(asc_dict.get('sKSpace.lPartitions', 1)) # I've seen set down as 127.. I don't think it could hurt to enforce 2**n # .. doing ceil(log2(n_pe)) is much more strong than round(log2(n_pe)) # .. which one?? n_pe = int(asc_dict['sKSpace.lPhaseEncodingLines']) n_pe = int(2**np.ceil(np.log2(n_pe))) hdr_dict['n_pe'] = n_pe hdr_dict['N1'] = int(asc_dict['sKSpace.lBaseResolution']) hdr_dict['M1'] = hdr_dict['n_fe'] = 2 * hdr_dict['N1'] hdr_dict['fov_x'] = asc_dict['sSliceArray.asSlice[0].dReadoutFOV'] hdr_dict['fov_y'] = asc_dict['sSliceArray.asSlice[0].dPhaseFOV'] hdr_dict['tr'] = asc_dict['alTR[0]'] hdr_dict['te'] = [] for e in range(hdr_dict['n_echo']): hdr_dict['te'].append(asc_dict['alTE[%d]' % e]) hdr_dict['dwell_time'] = asc_dict['sRXSPEC.alDwellTime[0]'] hdr_dict['echo_spacing'] = asc_dict.get('sFastImaging.lEchoSpacing', 0.0) hdr_dict['accel'] = asc_dict.get('sPat.lAccelFactPE', 1) hdr_dict['n_acs'] = asc_dict.get('sPat.lRefLinesPE', 0) pslabel = asc_dict['tSequenceFileName'].split('\\')[-1] hdr_dict['isepi'] = (pslabel.find('ep2d') >= 0) hdr_dict['isgre'] = (pslabel == 'gre') hdr_dict['isagems'] = (pslabel == 'gre_field_mapping') hdr_dict['isgrs'] = (pslabel.find('grs3d') >= 0) hdr_dict['pslabel'] = pslabel # now decode some bit encoded fields slices = np.arange(hdr_dict['n_slice']) slicing_mode = asc_dict['sSliceArray.ucMode'] if slicing_mode == 4: # interleaved if hdr_dict['n_slice'] % 2: hdr_dict['acq_order'] = np.concatenate( (slices[0::2], slices[1::2])) else: hdr_dict['acq_order'] = np.concatenate( (slices[1::2], slices[0::2])) elif slicing_mode == 2: # descending hdr_dict['acq_order'] = slices[::-1] else: # ascending hdr_dict['acq_order'] = slices sampstyle = asc_dict['sKSpace.unReordering'] hdr_dict['sampstyle'] = { 1: 'linear', 2: 'centric' }.get(sampstyle, 'unknown') partial_fourier = asc_dict['sKSpace.ucPhasePartialFourier'] # factors are encoded in increments of n/8 starting at 4 partial_numerator = {1: 4, 2: 5, 4: 6, 8: 7, 16: 8}.get(partial_fourier) hdr_dict['pe0'] = int( round(hdr_dict['n_pe'] * (1 / 2. - partial_numerator / 8.))) # Now get rampsamp info, n_pe_acq (in case of accel>1), n_ref fields_by_section = { '<ParamFunctor."rawobjprovider">': [( '<ParamLong."RawLin">', # raw str 'n_pe_acq', # our name int)], # how to convert '<ParamFunctor."adjroftregrid">': [('<ParamLong."RampupTime">', 'T_ramp', float), ('<ParamLong."FlattopTime">', 'T_flat', float), ('<ParamLong."DelaySamplesTime">', 'T0', float), ('<ParamDouble."ADCDuration">', 'adc_period', float)], '<ParamFunctor."EPIPhaseCorrPE">': [('<ParamLong."NSeg">', 'n_refs', int)] } for section, parse_info in fields_by_section.items(): p = hdr_str.find(section) valid = p >= 0 if valid: s = hdr_str[p:] for (field, val_name, evalfunc) in parse_info: if not valid: hdr_dict[val_name] = evalfunc('0') continue p = s.find(field) s = s[(p + len(field)):] p = s.find('\n') val = s[:p].split()[-2] hdr_dict[val_name] = evalfunc(val) # see if we have should forge a plausible gradient shape if not hdr_dict['adc_period'] and (hdr_dict['isepi'] or hdr_dict['isgrs']): # time resolution for gradient events is 5 microsec.. # calculate flat time as dt * M1 # cut echo spacing time into 3 even parts: ramp_up + flat + ramp_dn adc = (hdr_dict['M1'] - 1) * hdr_dict['dwell_time'] / 1e3 Tpe = hdr_dict['echo_spacing'] # 1) make flat time long enough to support adc period # 2) make echo-spacing - flat_time be evenly split in two npts = int(adc / 5) if npts % 2: npts += 1 else: npts += 2 flat = npts * 5 ramps = (Tpe - flat) / 2 hdr_dict['T_flat'] = flat hdr_dict['T_ramp'] = ramps hdr_dict['T0'] = ramps hdr_dict['adc_period'] = adc hdr_dict['ramp_samp'] = (hdr_dict['T0'] < hdr_dict['T_ramp']) # now get slice thickness order, x0, y0, z0 # (or is it i0, j0, k0 ??) slice_arrays = condense_array(asc_dict, 'sSliceArray.asSlice') ns = hdr_dict['n_slice'] pos_tra = slice_arrays.get('sPosition.dTra', np.zeros(ns)) pos_cor = slice_arrays.get('sPosition.dCor', np.zeros(ns)) pos_sag = slice_arrays.get('sPosition.dSag', np.zeros(ns)) if ns > 1: hdr_dict['dSL'] = np.sqrt( (pos_tra[1]-pos_tra[0])**2 + \ (pos_cor[1]-pos_cor[0])**2 + \ (pos_sag[1]-pos_sag[0])**2 ) hdr_dict['slice_thick'] = slice_arrays['dThickness'][0] hdr_dict['slice_gap'] = hdr_dict['dSL'] - hdr_dict['slice_thick'] else: hdr_dict['dSL'] = hdr_dict['slice_thick'] = hdr_dict['slice_gap'] = 1. ## norm_tra = slice_arrays.get('sNormal.dTra', np.zeros(ns)) ## norm_cor = slice_arrays.get('sNormal.dCor', np.zeros(ns)) ## norm_sag = slice_arrays.get('sNormal.dSag', np.zeros(ns)) ## # the normal tells us the angles phi and theta in the rotation, ## # psi is found literally in the header ## normal = np.array([norm_sag[0], norm_cor[0], norm_tra[0]]) ## theta = np.arccos(normal[2]) ## phi = np.arccos(-normal[1]/np.sin(theta)) ## psi = slice_arrays.get('dInPlaneRot', np.zeros(ns))[0] ## m = real_euler_rot(phi=phi, theta=theta, psi=psi) dat = MemmapDatFile(fname, nblocks=1) mdh = MDH(dat[0]['hdr']) in_plane_rot = slice_arrays.get('dInPlaneRot', np.zeros(ns))[0] # this is the "rotated" voxel to world mapping xform = Quaternion(i=mdh.quatI, j=mdh.quatJ, k=mdh.quatK) # this is the voxel transform in the vox coordinates m = xform.tomatrix() # maybe this is irrelevant? maybe the rotation matrix is just # x pe fe sl # L x x x # P x x x # S x x x ## rot = eulerRot(phi=in_plane_rot) ## m = np.dot(m, rot) # m is now the transform from (j,i,k) to (L,P,S) (DICOM coords) # since (R,A,S) = (-L,-P,S), just multiply two axes by -1 and swap them m_ijk = np.zeros_like(m) m_ijk[:, 0] = -m[:, 1] m_ijk[:, 1] = -m[:, 0] m_ijk[:, 2] = m[:, 2] m_ijk[:2] *= -1 # now m*(n_fe/2, n_pe/2, 0) + r0 = (-pos_sag[0], -pos_cor[0], pos_tra[0]) kdim = hdr_dict['n_slice'] ksize = hdr_dict['dSL'] jdim = hdr_dict['n_pe'] jsize = hdr_dict['fov_y'] / jdim # account for oversampling.. n_fe is already 2X base resolution idim = hdr_dict['n_fe'] isize = 2.0 * hdr_dict['fov_x'] / idim dim_scale = np.array([isize, jsize, ksize]) sl0_center = np.array([-pos_sag[0], -pos_cor[0], pos_tra[0]]) # want sl0_center = M*(i=idim/2,j=jdim/2,k=0) + r0 sl0_vox_center = np.array([idim / 2, jdim / 2, 0]) r0 = sl0_center - np.dot(m_ijk * dim_scale, sl0_vox_center) ## print m*dim_scale, sl0_vox_center ## print sl0_center, r0 # would have to re-adjust one of the r0 components when oversampling # is eliminated hdr_dict['orientation_xform'] = Quaternion(M=m_ijk) hdr_dict['x0'] = r0[0] hdr_dict['y0'] = r0[1] hdr_dict['z0'] = r0[2] # faking this for now.. hdr_dict['nseg'] = 1 return hdr_dict
class ReconImage (object): """ Interface definition for any image in Recon Tools. This class of images will be able to go through many of the available ops. This class of images can be exported to some medical imaging formats. Attributes: _data: 2, 3, or 4 dimensional matrix representing a slice, single volume, or a timecourse of volumes. ndim: number of dimensions tdim: number of volumes in a timecourse kdim: number of slices per volume jdim: number of rows per slice idim: number of columns per row isize: spacial width of array columns jsize: spacial height of array rows ksize: spacial slice thickness (3rd dim of array) tsize: duration of each time-series volume (4th dim of array) x0: x coordinate of xyz offset y0: y coordinate of xyz offset z0: z coordinate of xyz offset orientation: name of the orientaion (coronal, axial, etc) orientation_xform: quaternion describing the orientation capabilities provided: volume/slice slicing fe/pe slicing __getitem__, __setitem__ data xform (abs, real, imag, etc) """ #------------------------------------------------------------------------- def __init__(self, data, isize=1., jsize=1., ksize=1., tsize=1., offset=None, scaling=None, orient_xform=None): """ Construct a ReconImage with at least data, isize, jsize, ksize, and tsize known. Optional information are an offset 3-tuple specifying (x0,y0,z0), a Quaternion object representing the transformation of this data to neurological orientation (+X,+Y,+Z) = (Right,Anterior,Superior), and a name for the data's orientation (used for ANALYZE format output). """ self.setData(data) self.isize, self.jsize, self.ksize, self.tsize = \ (isize, jsize, ksize, tsize) self.orientation_xform = orient_xform or Quaternion() # offset should be the (x,y,z) offset in xyz-space xform = self.orientation_xform.tomatrix() if offset is not None: (self.x0, self.y0, self.z0) = offset else: # assume that vox at (idim/2, jdim/2, kdim/2) is the origin # thus Trans*(i0,j0,k0)^T + (x0,y0,z0)^T = (0,0,0)^T (self.x0, self.y0, self.z0) = \ -np.dot(xform, np.array([self.isize*self.idim/2., self.jsize*self.jdim/2., self.ksize*self.kdim/2.])) self.scaling = scaling or 1.0 #------------------------------------------------------------------------- def info(self): print "ndim =",self.ndim print "idim =",self.idim print "jdim =",self.jdim print "kdim =",self.kdim print "tdim =",self.tdim print "isize =",self.isize print "jsize =",self.jsize print "ksize =",self.ksize print "x0 =",self.x0 print "y0 =",self.y0 print "z0 =",self.z0 print "data.shape =",self.data.shape print "data.dtype =",self.data.dtype #------------------------------------------------------------------------- def setData(self, data): """Inform self about dimension info from the data array. Assuming that new data is centered at the same location as the old data, update the origin. """ from recon.slicerimage import SlicerImage if hasattr(self, 'shape'): old_shape = self.shape else: old_shape = data.shape[-3:] self.data = data self.ndim, self.tdim, self.kdim, self.jdim, self.idim = get_dims(data) self.shape = (self.tdim, self.kdim, self.jdim, self.idim) while self.shape[0] < 1: self.shape = self.shape[1:] self.data.shape = self.shape ## try: ## # this fails if isize,jsize,ksize reset before resizing ## # the old_ctr_vox is mis-calculated ## std_img = SlicerImage(self) ## old_ctr_vox = std_img.zyx2vox((0,0,0)) ## ctr_ratio0 = old_ctr_vox/np.array(old_shape, dtype=np.float32) ## new_shape = data.shape[-3:] ## ctr_ratio1 = old_ctr_vox/np.array(new_shape, dtype=np.float32) ## new_ctr_vox = old_ctr_vox * (ctr_ratio0 / ctr_ratio1) ## off_center = std_img.zyx_coords(vox_coords=new_ctr_vox) ## origin = [self.z0, self.y0, self.x0] ## origin = map(lambda (x1,x2): x1 - x2, zip(origin, off_center)) ## self.z0, self.y0, self.x0 = origin ## except: ## pass #------------------------------------------------------------------------- def concatenate(self, image, axis=0, newdim=False): """Stitch together two images along a given axis, possibly creating a new dimension """ self_sizes = (self.isize, self.jsize, self.ksize) image_sizes = (image.isize, image.jsize, image.ksize) # pixel sizes must match if self_sizes != image_sizes: raise ValueError( "won't concatenate images with different pixel sizes: %s != %s"%\ (self_sizes, image_sizes)) if newdim: newdata = np.asarray((self[:], image.data)) else: if len(self.shape) > len(image.shape): newdata = np.concatenate((self[:], image[(None,)])) else: newdata = np.concatenate((self[:], image.data), axis) return self._subimage(newdata) #------------------------------------------------------------------------- def transform(self, new_mapping=None, transform=None, force=False): """Updates the voxel to real-space transform. There are two modes of usage-- 1) supply a new voxel to real mapping. In this case a voxel to voxel transform is found, and the image is rotated in-plane around the origin. Only transposes and reflections are supported. Image info is updated appropriately 2) supply a real to real transform to apply to the current mapping. In this case the data is not updated, but the mapping is updated. """ if new_mapping is None and transform is None: return if new_mapping is not None and transform is not None: print """ The user must specify either a new mapping to convert to, or a transform to apply, but cannot specify both.""" return if transform is not None: # this doesn't change the image array, it just updates the # transformation old_xform = self.orientation_xform.tomatrix() if isinstance(transform, Quaternion): transform = transform.tomatrix() dim_scale = np.array([self.isize, self.jsize, self.ksize]) r0 = np.array([self.x0, self.y0, self.z0]) origin_voxels = np.round(np.linalg.solve(old_xform*dim_scale, -r0)) # now derive r0 again.. Tmap*(i,j,k)^T + r0^T = (x,y,z)^T r0 = -np.dot(transform*dim_scale, origin_voxels) self.x0, self.y0, self.z0 = r0 self.orientation_xform = Quaternion(M=transform) return # else handle the new mapping from recon.slicerimage import nearest_simple_transform # Tr already maps [i,j,k]^T into [R,A,S] ... # Here I want to change coordinates with this relationship: # Tr*[i,j,k]^T = Tnew*[i',j',k']^T # so Tnew*(Tvx*[i,j,k]^T) = Tr*[i,j,k]^T # So inv(Tnew)*Tr*[i,j,k] = [i',j',k'] = orientation of choice! # The task is to analyze Tvx = (Tnew^-1 * Tr) to get rotation Tr = self.orientation_xform.tomatrix() Tvx = np.linalg.solve(new_mapping, Tr) Tvxp = nearest_simple_transform(Tvx) # allow a goodly amount of wiggle room for each element of the # rotation matrix if not np.allclose(Tvxp, Tvx, atol=1e-4): # Tvxp might be a good choice in this case.. so could suggest # Tnew'*Tvxp = Tr # (Tvxp^T * Tnew'^T) = Tr^T # Tnew' = solve(Tvxp^T, Tr^T)^T Tnew_suggest = np.linalg.solve(Tvxp.T, Tr.T).T if not force: raise ValueError("""This method will not transform the data to the stated new mapping because the transformation cannot be accomplished through transposes and reflections. The closest new mapping you can perform is:\n"""+str(Tnew_suggest)) else: print """It is not possible to rotate simply to the stated mapping; proceeding with this mapping:\n"""+str(Tnew_suggest)+\ """\nbut lying about the final mapping.""" #new_mapping = Tnew_suggest Tvx = Tvxp else: # if Tvx is adequately close to its trimmed version, # let's go ahead and use the simple transform Tvx = Tvxp if not Tvx[-1,-1]: raise ValueError("This operation only makes in-plane rotations. "\ "EG you cannot use the sagittal transform for "\ "an image in the coronal plane.") if (Tvx==np.identity(3)).all(): print "Already in place, not transforming" return # this is for simple mixing of indices Tvx_abs = np.abs(Tvx) r0 = np.array([self.x0, self.y0, self.z0]) dvx_0 = np.array([self.isize, self.jsize, self.ksize]) # mix up the voxel sizes dvx_1 = np.dot(Tvx_abs, dvx_0) (self.isize, self.jsize, self.ksize) = dvx_1 dim_sizes = np.array(self.shape[-3:][::-1]) # solve for (i,j,k) where Tr*(i,j,k)^T + r0 = (0,0,0) # columns are i,j,k space, so scale columns by vox sizes vx_0 = np.linalg.solve(Tr*dvx_0, -r0) # transform the voxels -- # can normalize to {0..I-1},{0..J-1},{0..K-1} due to periodicity vx_1 = (np.dot(Tvx, vx_0) + dim_sizes) % dim_sizes r0_prime = -np.dot(new_mapping*dvx_1, vx_1) (self.x0, self.y0, self.z0) = r0_prime if self.shape[-1] != self.shape[-2]: func = compose_xform(Tvx, view=False, square=False) if self.tdim: new_shape = (self.tdim,) else: new_shape = () new_shape += tuple(np.dot(Tvx_abs, self.shape[::-1]).astype('i'))[::-1] temp = func(self[:]) self.resize(new_shape) self[:] = temp.copy() del temp else: func = compose_xform(Tvx, view=False) func(self[:]) self.orientation_xform = Quaternion(M=new_mapping) #------------------------------------------------------------------------- def __iter__(self): "Handles iteration over the image--always yields a 3D DataChunk" # want to iterate over volumes, if tdim=0, then nvol = 1 if len(self.shape) > 3: for t in range(self.tdim): yield DataChunk(self[t], t) raise StopIteration else: yield DataChunk(self[:], 0) raise StopIteration #------------------------------------------------------------------------- def __getitem__(self, slicer): if type(slicer) is type(()) and len(slicer) > self.ndim: nfakes = len(slicer)-self.ndim slicer = (None,)*(nfakes) + slicer[nfakes:] return self.data[slicer] #------------------------------------------------------------------------- def __setitem__(self, slicer, newdata): ndata = np.asarray(newdata) if ndata.dtype.char.isupper() and self.data.dtype.char.islower(): print "warning: losing information on complex->real cast!" if type(slicer) is type(()) and len(slicer) > self.ndim: nfakes = len(slicer)-self.ndim slicer = (None,)*(nfakes) + slicer[nfakes:] self.data[slicer] = ndata.astype(self.data.dtype) #------------------------------------------------------------------------- def __mul__(self, a): self[:] = self[:]*a #------------------------------------------------------------------------- def __div__(self, a): self[:] = self[:]/a #------------------------------------------------------------------------- def _subimage(self, data): return ReconImage(data, self.isize, self.jsize, self.ksize, self.tsize, offset=(self.x0, self.y0, self.z0), scaling=self.scaling, orient_xform=self.orientation_xform) #------------------------------------------------------------------------- def subImage(self, subnum): "returns subnum-th sub-image with dimension ndim-1" return self._subimage(self.data[subnum]) #------------------------------------------------------------------------- def subImages(self): "yeilds all images of dimension self.ndim-1" if len(self.shape) < 2: raise StopIteration("can't iterate subdimensions of a 2D image") for subnum in xrange(self.shape[0]): yield self.subImage(subnum) #------------------------------------------------------------------------- def resize(self, newsize): """ resize/reshape the data, non-destructively if the number of elements doesn't change """ if np.product(newsize) == np.product(self.shape): self.data.shape = tuple(newsize) else: self.data.resize(tuple(newsize), refcheck=False) self.setData(self[:]) #------------------------------------------------------------------------- def runOperations(self, opchain, logger=None): """ This method runs the image object through a pipeline of operations, which are ordered inside the opchain list. ReconImage's method is a basic operations driver, and could be expanded in subclasses. """ for operation in opchain: operation.log("Running") if operation.run(self) == -1: raise RuntimeError("critical operation failure") if logger is not None: logger.logop(operation) #------------------------------------------------------------------------- def writeImage(self, filestem, format_type="analyze", datatype="magnitude", **kwargs): """ Export the image object in a medical file format (ANALYZE or NIFTI). format_type is one of the internal file format specifiers, which are currently %s. possible keywords are: datatype -- a datatype identifier, supported by the given format targetdim -- number of dimensions per file filetype -- differentiates single + dual formats for NIFTI suffix -- over-ride default suffix style (eg volume0001) If necessary, a scaling is found for integer types """%(" ; ".join(available_writers)) new_dtype = recon_output2dtype.get(datatype.lower(), None) if new_dtype is None: raise ValueError("Unsupported data type: %s"%datatype) # The image writing tool does scaling only to preserve dynamic range # when saving an as integer data type. Therefore specifying a scale # in the writeImage kwargs is not appropriate, since it would # cause the image writing logic to "unscale" the data first--it # is not to be used as a "gain" knob. try: kwargs.pop('scale') except KeyError: pass if new_dtype in integer_ranges.keys(): scale = float(scale_data(self[:], new_dtype)) else: scale = float(1.0) _write(self, filestem, format_type, scale=scale, dtype=new_dtype,**kwargs)
def transform(self, new_mapping=None, transform=None, force=False): """Updates the voxel to real-space transform. There are two modes of usage-- 1) supply a new voxel to real mapping. In this case a voxel to voxel transform is found, and the image is rotated in-plane around the origin. Only transposes and reflections are supported. Image info is updated appropriately 2) supply a real to real transform to apply to the current mapping. In this case the data is not updated, but the mapping is updated. """ if new_mapping is None and transform is None: return if new_mapping is not None and transform is not None: print """ The user must specify either a new mapping to convert to, or a transform to apply, but cannot specify both.""" return if transform is not None: # this doesn't change the image array, it just updates the # transformation old_xform = self.orientation_xform.tomatrix() if isinstance(transform, Quaternion): transform = transform.tomatrix() dim_scale = np.array([self.isize, self.jsize, self.ksize]) r0 = np.array([self.x0, self.y0, self.z0]) origin_voxels = np.round(np.linalg.solve(old_xform*dim_scale, -r0)) # now derive r0 again.. Tmap*(i,j,k)^T + r0^T = (x,y,z)^T r0 = -np.dot(transform*dim_scale, origin_voxels) self.x0, self.y0, self.z0 = r0 self.orientation_xform = Quaternion(M=transform) return # else handle the new mapping from recon.slicerimage import nearest_simple_transform # Tr already maps [i,j,k]^T into [R,A,S] ... # Here I want to change coordinates with this relationship: # Tr*[i,j,k]^T = Tnew*[i',j',k']^T # so Tnew*(Tvx*[i,j,k]^T) = Tr*[i,j,k]^T # So inv(Tnew)*Tr*[i,j,k] = [i',j',k'] = orientation of choice! # The task is to analyze Tvx = (Tnew^-1 * Tr) to get rotation Tr = self.orientation_xform.tomatrix() Tvx = np.linalg.solve(new_mapping, Tr) Tvxp = nearest_simple_transform(Tvx) # allow a goodly amount of wiggle room for each element of the # rotation matrix if not np.allclose(Tvxp, Tvx, atol=1e-4): # Tvxp might be a good choice in this case.. so could suggest # Tnew'*Tvxp = Tr # (Tvxp^T * Tnew'^T) = Tr^T # Tnew' = solve(Tvxp^T, Tr^T)^T Tnew_suggest = np.linalg.solve(Tvxp.T, Tr.T).T if not force: raise ValueError("""This method will not transform the data to the stated new mapping because the transformation cannot be accomplished through transposes and reflections. The closest new mapping you can perform is:\n"""+str(Tnew_suggest)) else: print """It is not possible to rotate simply to the stated mapping; proceeding with this mapping:\n"""+str(Tnew_suggest)+\ """\nbut lying about the final mapping.""" #new_mapping = Tnew_suggest Tvx = Tvxp else: # if Tvx is adequately close to its trimmed version, # let's go ahead and use the simple transform Tvx = Tvxp if not Tvx[-1,-1]: raise ValueError("This operation only makes in-plane rotations. "\ "EG you cannot use the sagittal transform for "\ "an image in the coronal plane.") if (Tvx==np.identity(3)).all(): print "Already in place, not transforming" return # this is for simple mixing of indices Tvx_abs = np.abs(Tvx) r0 = np.array([self.x0, self.y0, self.z0]) dvx_0 = np.array([self.isize, self.jsize, self.ksize]) # mix up the voxel sizes dvx_1 = np.dot(Tvx_abs, dvx_0) (self.isize, self.jsize, self.ksize) = dvx_1 dim_sizes = np.array(self.shape[-3:][::-1]) # solve for (i,j,k) where Tr*(i,j,k)^T + r0 = (0,0,0) # columns are i,j,k space, so scale columns by vox sizes vx_0 = np.linalg.solve(Tr*dvx_0, -r0) # transform the voxels -- # can normalize to {0..I-1},{0..J-1},{0..K-1} due to periodicity vx_1 = (np.dot(Tvx, vx_0) + dim_sizes) % dim_sizes r0_prime = -np.dot(new_mapping*dvx_1, vx_1) (self.x0, self.y0, self.z0) = r0_prime if self.shape[-1] != self.shape[-2]: func = compose_xform(Tvx, view=False, square=False) if self.tdim: new_shape = (self.tdim,) else: new_shape = () new_shape += tuple(np.dot(Tvx_abs, self.shape[::-1]).astype('i'))[::-1] temp = func(self[:]) self.resize(new_shape) self[:] = temp.copy() del temp else: func = compose_xform(Tvx, view=False) func(self[:]) self.orientation_xform = Quaternion(M=new_mapping)
class ReconImage(object): """ Interface definition for any image in Recon Tools. This class of images will be able to go through many of the available ops. This class of images can be exported to some medical imaging formats. Attributes: _data: 2, 3, or 4 dimensional matrix representing a slice, single volume, or a timecourse of volumes. ndim: number of dimensions tdim: number of volumes in a timecourse kdim: number of slices per volume jdim: number of rows per slice idim: number of columns per row isize: spacial width of array columns jsize: spacial height of array rows ksize: spacial slice thickness (3rd dim of array) tsize: duration of each time-series volume (4th dim of array) x0: x coordinate of xyz offset y0: y coordinate of xyz offset z0: z coordinate of xyz offset orientation: name of the orientaion (coronal, axial, etc) orientation_xform: quaternion describing the orientation capabilities provided: volume/slice slicing fe/pe slicing __getitem__, __setitem__ data xform (abs, real, imag, etc) """ #------------------------------------------------------------------------- def __init__(self, data, isize=1., jsize=1., ksize=1., tsize=1., offset=None, scaling=None, orient_xform=None): """ Construct a ReconImage with at least data, isize, jsize, ksize, and tsize known. Optional information are an offset 3-tuple specifying (x0,y0,z0), a Quaternion object representing the transformation of this data to neurological orientation (+X,+Y,+Z) = (Right,Anterior,Superior), and a name for the data's orientation (used for ANALYZE format output). """ self.setData(data) self.isize, self.jsize, self.ksize, self.tsize = \ (isize, jsize, ksize, tsize) self.orientation_xform = orient_xform or Quaternion() # offset should be the (x,y,z) offset in xyz-space xform = self.orientation_xform.tomatrix() if offset is not None: (self.x0, self.y0, self.z0) = offset else: # assume that vox at (idim/2, jdim/2, kdim/2) is the origin # thus Trans*(i0,j0,k0)^T + (x0,y0,z0)^T = (0,0,0)^T (self.x0, self.y0, self.z0) = \ -np.dot(xform, np.array([self.isize*self.idim/2., self.jsize*self.jdim/2., self.ksize*self.kdim/2.])) self.scaling = scaling or 1.0 #------------------------------------------------------------------------- def info(self): print "ndim =", self.ndim print "idim =", self.idim print "jdim =", self.jdim print "kdim =", self.kdim print "tdim =", self.tdim print "isize =", self.isize print "jsize =", self.jsize print "ksize =", self.ksize print "x0 =", self.x0 print "y0 =", self.y0 print "z0 =", self.z0 print "data.shape =", self.data.shape print "data.dtype =", self.data.dtype #------------------------------------------------------------------------- def setData(self, data): """Inform self about dimension info from the data array. Assuming that new data is centered at the same location as the old data, update the origin. """ from recon.slicerimage import SlicerImage if hasattr(self, 'shape'): old_shape = self.shape else: old_shape = data.shape[-3:] self.data = data self.ndim, self.tdim, self.kdim, self.jdim, self.idim = get_dims(data) self.shape = (self.tdim, self.kdim, self.jdim, self.idim) while self.shape[0] < 1: self.shape = self.shape[1:] self.data.shape = self.shape ## try: ## # this fails if isize,jsize,ksize reset before resizing ## # the old_ctr_vox is mis-calculated ## std_img = SlicerImage(self) ## old_ctr_vox = std_img.zyx2vox((0,0,0)) ## ctr_ratio0 = old_ctr_vox/np.array(old_shape, dtype=np.float32) ## new_shape = data.shape[-3:] ## ctr_ratio1 = old_ctr_vox/np.array(new_shape, dtype=np.float32) ## new_ctr_vox = old_ctr_vox * (ctr_ratio0 / ctr_ratio1) ## off_center = std_img.zyx_coords(vox_coords=new_ctr_vox) ## origin = [self.z0, self.y0, self.x0] ## origin = map(lambda (x1,x2): x1 - x2, zip(origin, off_center)) ## self.z0, self.y0, self.x0 = origin ## except: ## pass #------------------------------------------------------------------------- def concatenate(self, image, axis=0, newdim=False): """Stitch together two images along a given axis, possibly creating a new dimension """ self_sizes = (self.isize, self.jsize, self.ksize) image_sizes = (image.isize, image.jsize, image.ksize) # pixel sizes must match if self_sizes != image_sizes: raise ValueError( "won't concatenate images with different pixel sizes: %s != %s"%\ (self_sizes, image_sizes)) if newdim: newdata = np.asarray((self[:], image.data)) else: if len(self.shape) > len(image.shape): newdata = np.concatenate((self[:], image[(None, )])) else: newdata = np.concatenate((self[:], image.data), axis) return self._subimage(newdata) #------------------------------------------------------------------------- def transform(self, new_mapping=None, transform=None, force=False): """Updates the voxel to real-space transform. There are two modes of usage-- 1) supply a new voxel to real mapping. In this case a voxel to voxel transform is found, and the image is rotated in-plane around the origin. Only transposes and reflections are supported. Image info is updated appropriately 2) supply a real to real transform to apply to the current mapping. In this case the data is not updated, but the mapping is updated. """ if new_mapping is None and transform is None: return if new_mapping is not None and transform is not None: print """ The user must specify either a new mapping to convert to, or a transform to apply, but cannot specify both.""" return if transform is not None: # this doesn't change the image array, it just updates the # transformation old_xform = self.orientation_xform.tomatrix() if isinstance(transform, Quaternion): transform = transform.tomatrix() dim_scale = np.array([self.isize, self.jsize, self.ksize]) r0 = np.array([self.x0, self.y0, self.z0]) origin_voxels = np.round( np.linalg.solve(old_xform * dim_scale, -r0)) # now derive r0 again.. Tmap*(i,j,k)^T + r0^T = (x,y,z)^T r0 = -np.dot(transform * dim_scale, origin_voxels) self.x0, self.y0, self.z0 = r0 self.orientation_xform = Quaternion(M=transform) return # else handle the new mapping from recon.slicerimage import nearest_simple_transform # Tr already maps [i,j,k]^T into [R,A,S] ... # Here I want to change coordinates with this relationship: # Tr*[i,j,k]^T = Tnew*[i',j',k']^T # so Tnew*(Tvx*[i,j,k]^T) = Tr*[i,j,k]^T # So inv(Tnew)*Tr*[i,j,k] = [i',j',k'] = orientation of choice! # The task is to analyze Tvx = (Tnew^-1 * Tr) to get rotation Tr = self.orientation_xform.tomatrix() Tvx = np.linalg.solve(new_mapping, Tr) Tvxp = nearest_simple_transform(Tvx) # allow a goodly amount of wiggle room for each element of the # rotation matrix if not np.allclose(Tvxp, Tvx, atol=1e-4): # Tvxp might be a good choice in this case.. so could suggest # Tnew'*Tvxp = Tr # (Tvxp^T * Tnew'^T) = Tr^T # Tnew' = solve(Tvxp^T, Tr^T)^T Tnew_suggest = np.linalg.solve(Tvxp.T, Tr.T).T if not force: raise ValueError("""This method will not transform the data to the stated new mapping because the transformation cannot be accomplished through transposes and reflections. The closest new mapping you can perform is:\n""" + str(Tnew_suggest)) else: print """It is not possible to rotate simply to the stated mapping; proceeding with this mapping:\n"""+str(Tnew_suggest)+\ """\nbut lying about the final mapping.""" #new_mapping = Tnew_suggest Tvx = Tvxp else: # if Tvx is adequately close to its trimmed version, # let's go ahead and use the simple transform Tvx = Tvxp if not Tvx[-1, -1]: raise ValueError("This operation only makes in-plane rotations. "\ "EG you cannot use the sagittal transform for "\ "an image in the coronal plane.") if (Tvx == np.identity(3)).all(): print "Already in place, not transforming" return # this is for simple mixing of indices Tvx_abs = np.abs(Tvx) r0 = np.array([self.x0, self.y0, self.z0]) dvx_0 = np.array([self.isize, self.jsize, self.ksize]) # mix up the voxel sizes dvx_1 = np.dot(Tvx_abs, dvx_0) (self.isize, self.jsize, self.ksize) = dvx_1 dim_sizes = np.array(self.shape[-3:][::-1]) # solve for (i,j,k) where Tr*(i,j,k)^T + r0 = (0,0,0) # columns are i,j,k space, so scale columns by vox sizes vx_0 = np.linalg.solve(Tr * dvx_0, -r0) # transform the voxels -- # can normalize to {0..I-1},{0..J-1},{0..K-1} due to periodicity vx_1 = (np.dot(Tvx, vx_0) + dim_sizes) % dim_sizes r0_prime = -np.dot(new_mapping * dvx_1, vx_1) (self.x0, self.y0, self.z0) = r0_prime if self.shape[-1] != self.shape[-2]: func = compose_xform(Tvx, view=False, square=False) if self.tdim: new_shape = (self.tdim, ) else: new_shape = () new_shape += tuple(np.dot(Tvx_abs, self.shape[::-1]).astype('i'))[::-1] temp = func(self[:]) self.resize(new_shape) self[:] = temp.copy() del temp else: func = compose_xform(Tvx, view=False) func(self[:]) self.orientation_xform = Quaternion(M=new_mapping) #------------------------------------------------------------------------- def __iter__(self): "Handles iteration over the image--always yields a 3D DataChunk" # want to iterate over volumes, if tdim=0, then nvol = 1 if len(self.shape) > 3: for t in range(self.tdim): yield DataChunk(self[t], t) raise StopIteration else: yield DataChunk(self[:], 0) raise StopIteration #------------------------------------------------------------------------- def __getitem__(self, slicer): if type(slicer) is type(()) and len(slicer) > self.ndim: nfakes = len(slicer) - self.ndim slicer = (None, ) * (nfakes) + slicer[nfakes:] return self.data[slicer] #------------------------------------------------------------------------- def __setitem__(self, slicer, newdata): ndata = np.asarray(newdata) if ndata.dtype.char.isupper() and self.data.dtype.char.islower(): print "warning: losing information on complex->real cast!" if type(slicer) is type(()) and len(slicer) > self.ndim: nfakes = len(slicer) - self.ndim slicer = (None, ) * (nfakes) + slicer[nfakes:] self.data[slicer] = ndata.astype(self.data.dtype) #------------------------------------------------------------------------- def __mul__(self, a): self[:] = self[:] * a #------------------------------------------------------------------------- def __div__(self, a): self[:] = self[:] / a #------------------------------------------------------------------------- def _subimage(self, data): return ReconImage(data, self.isize, self.jsize, self.ksize, self.tsize, offset=(self.x0, self.y0, self.z0), scaling=self.scaling, orient_xform=self.orientation_xform) #------------------------------------------------------------------------- def subImage(self, subnum): "returns subnum-th sub-image with dimension ndim-1" return self._subimage(self.data[subnum]) #------------------------------------------------------------------------- def subImages(self): "yeilds all images of dimension self.ndim-1" if len(self.shape) < 2: raise StopIteration("can't iterate subdimensions of a 2D image") for subnum in xrange(self.shape[0]): yield self.subImage(subnum) #------------------------------------------------------------------------- def resize(self, newsize): """ resize/reshape the data, non-destructively if the number of elements doesn't change """ if np.product(newsize) == np.product(self.shape): self.data.shape = tuple(newsize) else: self.data.resize(tuple(newsize), refcheck=False) self.setData(self[:]) #------------------------------------------------------------------------- def runOperations(self, opchain, logger=None): """ This method runs the image object through a pipeline of operations, which are ordered inside the opchain list. ReconImage's method is a basic operations driver, and could be expanded in subclasses. """ for operation in opchain: operation.log("Running") if operation.run(self) == -1: raise RuntimeError("critical operation failure") if logger is not None: logger.logop(operation) #------------------------------------------------------------------------- def writeImage(self, filestem, format_type="analyze", datatype="magnitude", **kwargs): """ Export the image object in a medical file format (ANALYZE or NIFTI). format_type is one of the internal file format specifiers, which are currently %s. possible keywords are: datatype -- a datatype identifier, supported by the given format targetdim -- number of dimensions per file filetype -- differentiates single + dual formats for NIFTI suffix -- over-ride default suffix style (eg volume0001) If necessary, a scaling is found for integer types """ % (" ; ".join(available_writers)) new_dtype = recon_output2dtype.get(datatype.lower(), None) if new_dtype is None: raise ValueError("Unsupported data type: %s" % datatype) # The image writing tool does scaling only to preserve dynamic range # when saving an as integer data type. Therefore specifying a scale # in the writeImage kwargs is not appropriate, since it would # cause the image writing logic to "unscale" the data first--it # is not to be used as a "gain" knob. try: kwargs.pop('scale') except KeyError: pass if new_dtype in integer_ranges.keys(): scale = float(scale_data(self[:], new_dtype)) else: scale = float(1.0) _write(self, filestem, format_type, scale=scale, dtype=new_dtype, **kwargs)
def transform(self, new_mapping=None, transform=None, force=False): """Updates the voxel to real-space transform. There are two modes of usage-- 1) supply a new voxel to real mapping. In this case a voxel to voxel transform is found, and the image is rotated in-plane around the origin. Only transposes and reflections are supported. Image info is updated appropriately 2) supply a real to real transform to apply to the current mapping. In this case the data is not updated, but the mapping is updated. """ if new_mapping is None and transform is None: return if new_mapping is not None and transform is not None: print """ The user must specify either a new mapping to convert to, or a transform to apply, but cannot specify both.""" return if transform is not None: # this doesn't change the image array, it just updates the # transformation old_xform = self.orientation_xform.tomatrix() if isinstance(transform, Quaternion): transform = transform.tomatrix() dim_scale = np.array([self.isize, self.jsize, self.ksize]) r0 = np.array([self.x0, self.y0, self.z0]) origin_voxels = np.round( np.linalg.solve(old_xform * dim_scale, -r0)) # now derive r0 again.. Tmap*(i,j,k)^T + r0^T = (x,y,z)^T r0 = -np.dot(transform * dim_scale, origin_voxels) self.x0, self.y0, self.z0 = r0 self.orientation_xform = Quaternion(M=transform) return # else handle the new mapping from recon.slicerimage import nearest_simple_transform # Tr already maps [i,j,k]^T into [R,A,S] ... # Here I want to change coordinates with this relationship: # Tr*[i,j,k]^T = Tnew*[i',j',k']^T # so Tnew*(Tvx*[i,j,k]^T) = Tr*[i,j,k]^T # So inv(Tnew)*Tr*[i,j,k] = [i',j',k'] = orientation of choice! # The task is to analyze Tvx = (Tnew^-1 * Tr) to get rotation Tr = self.orientation_xform.tomatrix() Tvx = np.linalg.solve(new_mapping, Tr) Tvxp = nearest_simple_transform(Tvx) # allow a goodly amount of wiggle room for each element of the # rotation matrix if not np.allclose(Tvxp, Tvx, atol=1e-4): # Tvxp might be a good choice in this case.. so could suggest # Tnew'*Tvxp = Tr # (Tvxp^T * Tnew'^T) = Tr^T # Tnew' = solve(Tvxp^T, Tr^T)^T Tnew_suggest = np.linalg.solve(Tvxp.T, Tr.T).T if not force: raise ValueError("""This method will not transform the data to the stated new mapping because the transformation cannot be accomplished through transposes and reflections. The closest new mapping you can perform is:\n""" + str(Tnew_suggest)) else: print """It is not possible to rotate simply to the stated mapping; proceeding with this mapping:\n"""+str(Tnew_suggest)+\ """\nbut lying about the final mapping.""" #new_mapping = Tnew_suggest Tvx = Tvxp else: # if Tvx is adequately close to its trimmed version, # let's go ahead and use the simple transform Tvx = Tvxp if not Tvx[-1, -1]: raise ValueError("This operation only makes in-plane rotations. "\ "EG you cannot use the sagittal transform for "\ "an image in the coronal plane.") if (Tvx == np.identity(3)).all(): print "Already in place, not transforming" return # this is for simple mixing of indices Tvx_abs = np.abs(Tvx) r0 = np.array([self.x0, self.y0, self.z0]) dvx_0 = np.array([self.isize, self.jsize, self.ksize]) # mix up the voxel sizes dvx_1 = np.dot(Tvx_abs, dvx_0) (self.isize, self.jsize, self.ksize) = dvx_1 dim_sizes = np.array(self.shape[-3:][::-1]) # solve for (i,j,k) where Tr*(i,j,k)^T + r0 = (0,0,0) # columns are i,j,k space, so scale columns by vox sizes vx_0 = np.linalg.solve(Tr * dvx_0, -r0) # transform the voxels -- # can normalize to {0..I-1},{0..J-1},{0..K-1} due to periodicity vx_1 = (np.dot(Tvx, vx_0) + dim_sizes) % dim_sizes r0_prime = -np.dot(new_mapping * dvx_1, vx_1) (self.x0, self.y0, self.z0) = r0_prime if self.shape[-1] != self.shape[-2]: func = compose_xform(Tvx, view=False, square=False) if self.tdim: new_shape = (self.tdim, ) else: new_shape = () new_shape += tuple(np.dot(Tvx_abs, self.shape[::-1]).astype('i'))[::-1] temp = func(self[:]) self.resize(new_shape) self[:] = temp.copy() del temp else: func = compose_xform(Tvx, view=False) func(self[:]) self.orientation_xform = Quaternion(M=new_mapping)
def parse_siemens_hdr(fname): """ lRepetitions = 19 (= nvol - 1) sKSpace.lBaseResolution = 64 sKSpace.lPhaseEncodingLines = 128 sKSpace.ucMultiSliceMode = 0x2 (interleaved) tSequenceFileName = "%CustomerSeq%\ep2d_bold_bigfov" sRXSPEC.alDwellTime[0] = 2800 alTR[0] = 2500000 lContrasts = 2 alTE[0] = 4920 alTE[1] = 7380 sSliceArray.asSlice[0].sPosition.dTra = -39.375 sSliceArray.asSlice[0].sNormal.dTra = 1 sSliceArray.asSlice[0].dThickness = 3 sSliceArray.asSlice[0].dPhaseFOV = 240 sSliceArray.asSlice[0].dReadoutFOV = 240 sSliceArray.asSlice[0].dInPlaneRot = 2.051034897e-010 ... sSliceArray.lSize = 22 sFastImaging.lEchoSpacing = 420 sPat.lAccelFactPE = 1 sPat.lRefLinesPE = 24 asCoilSelectMeas[0].aFFT_SCALE[0].flFactor = 1.26665 asCoilSelectMeas[0].aFFT_SCALE[0].bValid = 1 asCoilSelectMeas[0].aFFT_SCALE[0].lRxChannel = 1 ... more info... sSliceArray.ucMode (I THINK this is slice acq orde -- can it be OR'd??) enum SeriesMode { ASCENDING = 0x01, DESCENDING = 0x02, INTERLEAVED = 0x04 }; sKSpace.unReordering enum Reordering { REORDERING_LINEAR = 0x01, REORDERING_CENTRIC = 0x02, REORDERING_LINE_SEGM = 0x04, REORDERING_PART_SEGM = 0x08, REORDERING_FREE_0 = 0x10, REORDERING_FREE_1 = 0x20, REORDERING_FREE_2 = 0x40, REORDERING_FREE_3 = 0x80 }; sKSpace.ucPhasePartialFourier sKSpace.ucSlicePartialFourier enum PartialFourierFactor { PF_HALF = 0x01, PF_5_8 = 0x02, PF_6_8 = 0x04, PF_7_8 = 0x08, PF_OFF = 0x10 }; sKSpace.ucMultiSliceMode enum MultiSliceMode { MSM_SEQUENTIAL = 0x01, MSM_INTERLEAVED = 0x02, MSM_SINGLESHOT = 0x04 }; slice spacing ( can do with position arrays ) slice order / acq order missing: n_pe_acq (can this be derived from n_pe and accel? may be 31 for accel=2) n_ref == nsegmeas + 1 (or is that a coincidence???) n_part -- do I care? rampsamp info """ hdr_dict = {} ## hdrlen = header_length(fname) ## hdr_str = open(fname, 'r').read(4+hdrlen) hdr_str = header_string(fname) asc_dict = strip_ascconv(hdr_str) chan_scales = condense_array(asc_dict, 'asCoilSelectMeas[0].aFFT_SCALE') gains = chan_scales['flFactor'] chans = chan_scales['lRxChannel'].astype('i') # usually [1, 2, 3, 4, ...] hdr_dict['n_chan'] = gains.shape[0] hdr_dict['channel_gains'] = gains[chans-1] hdr_dict['n_echo'] = int(asc_dict['lContrasts']) hdr_dict['n_vol'] = int(1 + asc_dict.get('lRepetitions', 0)) hdr_dict['n_slice'] = int(asc_dict.get('sSliceArray.lSize', 1)) hdr_dict['n_partition'] = int(asc_dict.get('sKSpace.lPartitions', 1)) # I've seen set down as 127.. I don't think it could hurt to enforce 2**n # .. doing ceil(log2(n_pe)) is much more strong than round(log2(n_pe)) # .. which one?? n_pe = int(asc_dict['sKSpace.lPhaseEncodingLines']) n_pe = int(2**np.ceil(np.log2(n_pe))) hdr_dict['n_pe'] = n_pe hdr_dict['N1'] = int(asc_dict['sKSpace.lBaseResolution']) hdr_dict['M1'] = hdr_dict['n_fe'] = 2*hdr_dict['N1'] hdr_dict['fov_x'] = asc_dict['sSliceArray.asSlice[0].dReadoutFOV'] hdr_dict['fov_y'] = asc_dict['sSliceArray.asSlice[0].dPhaseFOV'] hdr_dict['tr'] = asc_dict['alTR[0]'] hdr_dict['te'] = [] for e in range(hdr_dict['n_echo']): hdr_dict['te'].append(asc_dict['alTE[%d]'%e]) hdr_dict['dwell_time'] = asc_dict['sRXSPEC.alDwellTime[0]'] hdr_dict['echo_spacing'] = asc_dict.get('sFastImaging.lEchoSpacing', 0.0) hdr_dict['accel'] = asc_dict.get('sPat.lAccelFactPE', 1) hdr_dict['n_acs'] = asc_dict.get('sPat.lRefLinesPE', 0) pslabel = asc_dict['tSequenceFileName'].split('\\')[-1] hdr_dict['isepi'] = (pslabel.find('ep2d') >= 0) hdr_dict['isgre'] = (pslabel=='gre') hdr_dict['isagems'] = (pslabel=='gre_field_mapping') hdr_dict['isgrs'] = (pslabel.find('grs3d') >= 0) hdr_dict['pslabel'] = pslabel # now decode some bit encoded fields slices = np.arange(hdr_dict['n_slice']) slicing_mode = asc_dict['sSliceArray.ucMode'] if slicing_mode == 4: # interleaved if hdr_dict['n_slice']%2: hdr_dict['acq_order'] = np.concatenate((slices[0::2], slices[1::2])) else: hdr_dict['acq_order'] = np.concatenate((slices[1::2], slices[0::2])) elif slicing_mode == 2: # descending hdr_dict['acq_order'] = slices[::-1] else: # ascending hdr_dict['acq_order'] = slices sampstyle = asc_dict['sKSpace.unReordering'] hdr_dict['sampstyle'] = {1:'linear',2:'centric'}.get(sampstyle, 'unknown') partial_fourier = asc_dict['sKSpace.ucPhasePartialFourier'] # factors are encoded in increments of n/8 starting at 4 partial_numerator = {1: 4, 2: 5, 4: 6, 8: 7, 16: 8 }.get(partial_fourier) hdr_dict['pe0'] = int(round(hdr_dict['n_pe']*(1/2. - partial_numerator/8.))) # Now get rampsamp info, n_pe_acq (in case of accel>1), n_ref fields_by_section = { '<ParamFunctor."rawobjprovider">': [ ('<ParamLong."RawLin">', # raw str 'n_pe_acq', # our name int) ], # how to convert '<ParamFunctor."adjroftregrid">': [ ('<ParamLong."RampupTime">', 'T_ramp', float), ('<ParamLong."FlattopTime">', 'T_flat', float), ('<ParamLong."DelaySamplesTime">', 'T0', float), ('<ParamDouble."ADCDuration">', 'adc_period', float) ], '<ParamFunctor."EPIPhaseCorrPE">': [ ('<ParamLong."NSeg">', 'n_refs', int) ] } for section, parse_info in fields_by_section.items(): p = hdr_str.find(section) valid = p >= 0 if valid: s = hdr_str[p:] for (field, val_name, evalfunc) in parse_info: if not valid: hdr_dict[val_name] = evalfunc('0') continue p = s.find(field) s = s[(p + len(field)):] p = s.find('\n') val = s[:p].split()[-2] hdr_dict[val_name] = evalfunc(val) # see if we have should forge a plausible gradient shape if not hdr_dict['adc_period'] and (hdr_dict['isepi'] or hdr_dict['isgrs']): # time resolution for gradient events is 5 microsec.. # calculate flat time as dt * M1 # cut echo spacing time into 3 even parts: ramp_up + flat + ramp_dn adc = (hdr_dict['M1']-1)*hdr_dict['dwell_time']/1e3 Tpe = hdr_dict['echo_spacing'] # 1) make flat time long enough to support adc period # 2) make echo-spacing - flat_time be evenly split in two npts = int(adc/5) if npts%2: npts += 1 else: npts += 2 flat = npts*5 ramps = (Tpe-flat)/2 hdr_dict['T_flat'] = flat hdr_dict['T_ramp'] = ramps hdr_dict['T0'] = ramps hdr_dict['adc_period'] = adc hdr_dict['ramp_samp'] = (hdr_dict['T0'] < hdr_dict['T_ramp']) # now get slice thickness order, x0, y0, z0 # (or is it i0, j0, k0 ??) slice_arrays = condense_array(asc_dict, 'sSliceArray.asSlice') ns = hdr_dict['n_slice'] pos_tra = slice_arrays.get('sPosition.dTra', np.zeros(ns)) pos_cor = slice_arrays.get('sPosition.dCor', np.zeros(ns)) pos_sag = slice_arrays.get('sPosition.dSag', np.zeros(ns)) if ns > 1: hdr_dict['dSL'] = np.sqrt( (pos_tra[1]-pos_tra[0])**2 + \ (pos_cor[1]-pos_cor[0])**2 + \ (pos_sag[1]-pos_sag[0])**2 ) hdr_dict['slice_thick'] = slice_arrays['dThickness'][0] hdr_dict['slice_gap'] = hdr_dict['dSL'] - hdr_dict['slice_thick'] else: hdr_dict['dSL'] = hdr_dict['slice_thick'] = hdr_dict['slice_gap'] = 1. ## norm_tra = slice_arrays.get('sNormal.dTra', np.zeros(ns)) ## norm_cor = slice_arrays.get('sNormal.dCor', np.zeros(ns)) ## norm_sag = slice_arrays.get('sNormal.dSag', np.zeros(ns)) ## # the normal tells us the angles phi and theta in the rotation, ## # psi is found literally in the header ## normal = np.array([norm_sag[0], norm_cor[0], norm_tra[0]]) ## theta = np.arccos(normal[2]) ## phi = np.arccos(-normal[1]/np.sin(theta)) ## psi = slice_arrays.get('dInPlaneRot', np.zeros(ns))[0] ## m = real_euler_rot(phi=phi, theta=theta, psi=psi) dat = MemmapDatFile(fname, nblocks=1) mdh = MDH(dat[0]['hdr']) in_plane_rot = slice_arrays.get('dInPlaneRot', np.zeros(ns))[0] # this is the "rotated" voxel to world mapping xform = Quaternion(i=mdh.quatI, j=mdh.quatJ, k=mdh.quatK) # this is the voxel transform in the vox coordinates m = xform.tomatrix() # maybe this is irrelevant? maybe the rotation matrix is just # x pe fe sl # L x x x # P x x x # S x x x ## rot = eulerRot(phi=in_plane_rot) ## m = np.dot(m, rot) # m is now the transform from (j,i,k) to (L,P,S) (DICOM coords) # since (R,A,S) = (-L,-P,S), just multiply two axes by -1 and swap them m_ijk = np.zeros_like(m) m_ijk[:,0] = -m[:,1] m_ijk[:,1] = -m[:,0] m_ijk[:,2] = m[:,2] m_ijk[:2] *= -1 # now m*(n_fe/2, n_pe/2, 0) + r0 = (-pos_sag[0], -pos_cor[0], pos_tra[0]) kdim = hdr_dict['n_slice']; ksize = hdr_dict['dSL'] jdim = hdr_dict['n_pe']; jsize = hdr_dict['fov_y']/jdim # account for oversampling.. n_fe is already 2X base resolution idim = hdr_dict['n_fe']; isize = 2.0*hdr_dict['fov_x']/idim dim_scale = np.array([isize, jsize, ksize]) sl0_center = np.array([-pos_sag[0], -pos_cor[0], pos_tra[0]]) # want sl0_center = M*(i=idim/2,j=jdim/2,k=0) + r0 sl0_vox_center = np.array([idim/2, jdim/2, 0]) r0 = sl0_center - np.dot(m_ijk*dim_scale, sl0_vox_center) ## print m*dim_scale, sl0_vox_center ## print sl0_center, r0 # would have to re-adjust one of the r0 components when oversampling # is eliminated hdr_dict['orientation_xform'] = Quaternion(M=m_ijk) hdr_dict['x0'] = r0[0]; hdr_dict['y0'] = r0[1]; hdr_dict['z0'] = r0[2] # faking this for now.. hdr_dict['nseg'] = 1 return hdr_dict