Beispiel #1
0
 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
Beispiel #2
0
 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)
Beispiel #3
0
    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
Beispiel #4
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
Beispiel #5
0
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)
Beispiel #6
0
    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)
Beispiel #7
0
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)
Beispiel #8
0
    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)
Beispiel #9
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