def calculate_transform(self): """Form the linear transform matrix. Returns: Transform: a Transform """ image_orientation = np.array( self.get_parameter('ImageOrientationPatient')) flip = np.array([-1, -1, 1]) R = np.vstack( (image_orientation[0:3] * flip, image_orientation[3:6] * flip)) R = np.vstack((R, -np.cross(R[0, :], R[1, :]))).T T_matrix = np.eye(4) T_matrix[0:3, 0:3] = R T_matrix = np.dot( T_matrix, np.diag([ self.get_parameter('VoiReadoutFoV'), self.get_parameter('VoiPhaseFoV'), self.get_parameter('VoiThickness'), 1 ])) T_matrix[0:3, 3] = np.array(self.get_parameter('VoiPosition')) * flip self.T_matrix = T_matrix self.qform = Transform(T_matrix) return (self.qform)
def make_voi(t1_nifti, voi_tform): """ Creates a volumetric ROI for SVS data. Parameters: t1_nifti (Nifti1Image): A Nifti1Image with an affine transform voi_tform (Transform): A Transform with the affine for the voxel Returns: img (Nifti1Image): A binary image of the voxel in T1 space """ t1_aff = Transform(t1_nifti.get_qform()) t1_aff_inv = t1_aff.get_inverse() composed_affine = t1_aff_inv * voi_tform mrs_corners = [[-0.5, -0.5, -0.5, 1], #0 [-0.5, -0.5, 0.5, 1], #1 [-0.5, 0.5, -0.5, 1], #2 [-0.5, 0.5, 0.5, 1], #3 [ 0.5, -0.5, -0.5, 1], #4 [ 0.5, -0.5, 0.5, 1], #5 [ 0.5, 0.5, -0.5, 1], #6 [ 0.5, 0.5, 0.5, 1]] #7 #don't round off here t1_corners = np.array([(np.dot(composed_affine.get_matrix(), c)) for c in mrs_corners]).squeeze() t1_data = t1_nifti.get_data().squeeze() mrs_roi = np.ones_like(t1_data) * 0 #exhaustive search for points in the voxel tri = spatial.Delaunay(t1_corners[:,0:3]) for i in range(np.floor(min(t1_corners[:,0])).astype(int),np.ceil(max(t1_corners[:,0])).astype(int)): for j in range(np.floor(min(t1_corners[:,1])).astype(int),np.ceil(max(t1_corners[:,1])).astype(int)): for k in range(np.floor(min(t1_corners[:,2])).astype(int),np.ceil(max(t1_corners[:,2])).astype(int)): mrs_roi[i,j,k] = tri.find_simplex([i,j,k]) >=0 img = nib.Nifti1Image(mrs_roi, t1_aff.get_matrix()) return(img)
class Siemens(object): """Utility class for manipulating Siemens SVS DICOM files. Attributes: path (str): Full path to the originating DICOM csa (obj): CSA header dictionary meta (obj): Selected CSA fields T (:obj:`Transform`): NIfTI compatible transform """ csa = None qform = None path = None meta = {} data = None files = None def __init__(self, path): """Initialize a Siemens object from a DICOM file or directory. If a directory is given, read the first DICOM file in the directory. Args: path (str): The path to a Siemens SVS DICOM file or directory of DICOM files. """ self.path = os.path.realpath(path) dcmfile = None if os.path.isdir(self.path): # find the first dicom file to read meta data from files = os.listdir(self.path) # exclude any non dicoms files = [ f for f in files if (os.path.isfile(os.path.join(self.path, f)) and f.endswith(( '.DCM', '.dcm', '.ima', '.IMA')) and not f.startswith('.')) ] if len(files) == 0: raise FileNotFoundError('No DICOM files found') self.files = sorted(files) dcmfile = os.path.join(self.path, files[0]) # a single file, change the path if len(files) == 1: path = os.path.join(self.path, files[0]) self.path = path if not (dcmfile): raise FileNotFoundError('No DICOM files found') else: dcmfile = path dcm = dcmwrapper.wrapper_from_file(dcmfile) self.csa = csareader.get_csa_header(dcm.dcm_data) scalar_fields = [ 'MagneticFieldStrength', 'ImagingFrequency', 'MixingTime', 'EchoTime', 'RepetitionTime', 'ImaCoilString', 'SequenceName', 'VoiReadoutFoV', 'VoiPhaseFoV', 'VoiThickness', 'VoiInPlaneRotation', 'DataPointColumns', 'RealDwellTime', 'PixelBandwidth', 'ImagedNucleus' ] for k in scalar_fields: self.meta[k] = csareader.get_scalar(self.csa, k) self.meta['ImagePositionPatient'] = \ csareader.get_vector(self.csa, 'ImagePositionPatient', 3) self.meta['VoiPosition'] = csareader.get_vector( self.csa, 'VoiPosition', 3) self.meta['VoiOrientation'] = csareader.get_vector( self.csa, 'VoiOrientation', 3) self.meta['ImageOrientationPatient'] = \ csareader.get_vector(self.csa, 'ImageOrientationPatient', 6) def get_parameter(self, k): """Get the value for the specified CSA key, with attempts to cast Args: k (str): a selected CSA key Returns: The value of the key """ if k not in self.meta.keys(): raise KeyError() # try to cast v = self.meta[k] # return non scalar values directly if isinstance(v, (list, tuple, np.ndarray)): return (v) # int iv = None try: iv = int(v) except ValueError: pass # float fv = None try: fv = float(v) except ValueError: pass if (iv is not None) and (fv is not None): if iv == fv: return (iv) else: return (fv) else: if iv is not None: return (iv) if fv is not None: return (fv) # a string, if not leave the exception unhandled return (str(v)) def calculate_transform(self): """Form the linear transform matrix. Returns: Transform: a Transform """ image_orientation = np.array( self.get_parameter('ImageOrientationPatient')) flip = np.array([-1, -1, 1]) R = np.vstack( (image_orientation[0:3] * flip, image_orientation[3:6] * flip)) R = np.vstack((R, -np.cross(R[0, :], R[1, :]))).T T_matrix = np.eye(4) T_matrix[0:3, 0:3] = R T_matrix = np.dot( T_matrix, np.diag([ self.get_parameter('VoiReadoutFoV'), self.get_parameter('VoiPhaseFoV'), self.get_parameter('VoiThickness'), 1 ])) T_matrix[0:3, 3] = np.array(self.get_parameter('VoiPosition')) * flip self.T_matrix = T_matrix self.qform = Transform(T_matrix) return (self.qform) def read_data(self, conj=True): """Read the associated fids. If the instance was initialized with a directory, the directory is assumed to contain a single series in order. Dimensions are channel x rep x mega x isis x t """ print('Reading data...') if os.path.isfile(self.path): dcm = dcmwrapper.wrapper_from_file(self.path) data = np.array(_read_fid(dcm), ndmin=5) self.dcm_data = dcm.dcm_data elif os.path.isdir(self.path): # read a directory of DICOMS, each containing one fid # the directory must contain more than one file, as this is checked # in the class constructor channels = [] # find the instance number of the last dicom to calculate data size # this assumes different interleaved acquisitions have the same # (0020, 0012) Acquisition Number # true for eja sequences lastdcmfile = os.path.join(self.path, self.files[-1]) lastdcm = dcmwrapper.wrapper_from_file(lastdcmfile) self.dcm_data = lastdcm.dcm_data lastinstance = int(lastdcm.get((0x0020, 0x0013)).value) # figure out which channels are on csa_series = csareader.get_csa_header(lastdcm.dcm_data, 'series') csa_image = csareader.get_csa_header(lastdcm.dcm_data, 'image') siemens_hdr = csa_series['tags']['MrPhoenixProtocol']['items'][0] m = re.findall( r"""sCoilSelectMeas.aRxCoilSelectData\[0\].asList\[(?P<coilnum>\d+)\].sCoilElementID.tElement\t = \t""(?P<coilname>[HENC]+\d+)""" "", siemens_hdr) channels = dict(m) channels = dict(zip(channels.values(), channels.keys())) n_channels = len(channels) self.channels = channels # is the data combined over channels? # mri_probedicom reports ucUncombImages, but where is this in the CSA? # the first two instances of uncombined eja sequences are single channels # TODO: figure out what they are # Assume the first match the last two, which are missing the same channel n_reps = lastinstance - 2 is_combined = False # TODO: handle channel combined data if len(self.files) != (n_reps * (n_channels + 1)): # not enough files for uncombined data if len(self.files) == lastinstance: warnings.warn('Assuming channels are combined') n_reps = lastinstance n_channels = 1 is_combined = True else: raise Exception( 'Expected n_reps[%d] * (n_channels[%d] + 1 files' % (n_reps, n_channels)) data = np.zeros( (n_channels, n_reps, 1, 1, int(csareader.get_scalar(csa_image, 'DataPointColumns'))), dtype=complex) for fi in range(len(self.files)): dcmfile = os.path.join(self.path, self.files[fi]) dcm = dcmwrapper.wrapper_from_file(dcmfile) csa = csareader.get_csa_header(dcm.dcm_data) fid = np.array(_read_fid(dcm), ndmin=5) channel = csareader.get_scalar(csa, 'ImaCoilString') inst = int(dcm.get((0x0020, 0x0013)).value) if not is_combined: if inst <= 2: ri = n_reps - inst else: ri = inst - 2 - 1 else: ri = inst - 1 # there are combined coils (HC1-7) in the dicoms? # make sure this channel is one that is turned on if not is_combined: if channel in channels.keys(): ci = int(channels[channel]) data[ci, ri, 0, 0, :] = fid else: data[0, ri, 0, 0, :] = fid print('Read %d acquisitions from %d channels' % (n_reps, n_channels)) print(channels.keys()) # take the complex conjugate, which Tarquin seems to expect if conj: data = np.conj(data) # permute data for SPECIAL and MEGA if self.get_parameter('SequenceName') in [ 'eja_svs_mpress', 'eja_svs_mslaser' ]: data_on = data[:, 0:int(n_reps / 2.), 0, ::] data_off = data[:, int(n_reps / 2.):n_reps, 0, ::] data = np.stack((data_off, data_on), 2) #data = np.reshape(data, (n_channels, int(n_reps/2.), 2, 1, self.get_parameter('DataPointColumns'))) self.data = data return (data) def get_svsdata(self): if self.data is None: self.read_data() if self.qform is None: self.calculate_transform() data = svsdata.SVSData() data.fid = self.data data.transform = self.qform data.sequence_name = self.get_parameter('SequenceName') data.tr = self.get_parameter('RepetitionTime') / 1000.0 data.te = self.get_parameter('EchoTime') / 1000.0 data.tm = self.get_parameter('MixingTime') / 1000.0 data.nucleus = self.get_parameter('ImagedNucleus') data.larmor = self.get_parameter('ImagingFrequency') * 1.0e6 dt = self.get_parameter('RealDwellTime') * 1.0e-9 sw = 1.0 / dt data.sw = sw npts = self.get_parameter('DataPointColumns') data.f = np.arange(-sw / 2.0 + sw / (2 * npts), sw / 2, sw / npts) data.t = np.arange(0, npts * dt, dt) return (data) def get_sidecar(self): """Returns a BIDS-style sidecar """ json_dict = _csa_to_dict(self.csa) # data has to be read in to count channels if self.data is None: self.read_data() json_dict['ReceiveCoilActiveElements'] = list(self.channels.keys()) json_dict.pop('ImaCoilString', None) if self.qform is None: self.calculate_transform() json_dict['Transform'] = np.squeeze(np.asarray( self.qform.get_matrix())).tolist() dicom_tags = [((0x0008, 0x0060), 'Modality'), ((0x0008, 0x0008), 'ImageType'), ((0x0008, 0x0070), 'Manufacturer'), ((0x0008, 0x0080), 'InstitutionName'), ((0x0008, 0x0081), 'InstitutionAddress'), ((0x0008, 0x1010), 'StationName'), ((0x0008, 0x1030), 'StudyDescription'), ((0x0008, 0x103e), 'SeriesDescription'), ((0x0008, 0x1090), 'ManufacturersModelName'), ((0x0018, 0x0015), 'BodyPartExamined'), ((0x0018, 0x1000), 'DeviceSerialNumber'), ((0x0018, 0x1020), 'SoftwareVersions'), ((0x0018, 0x1030), 'ProtocolName'), ((0x0018, 0x5100), 'PatientPosition'), ((0x0020, 0x0011), 'SeriesNumber'), ((0x0020, 0x0012), 'AcquisitionNumber'), ((0x0020, 0x4000), 'ImageComments')] for tag in dicom_tags: json_dict[tag[1]] = self.dcm_data.get(tag[0]).value json_dict.pop('ReferencedImageSequence', None) json_dict.pop('ScanningSequence') json_dict.update({'ImageType': list(json_dict['ImageType'])}) json_dict.update({'SeriesNumber': int(json_dict['SeriesNumber'])}) json_dict.update( {'AcquisitionNumber': int(json_dict['AcquisitionNumber'])}) return (json_dict)
def test_rotx(): I = Transform(np.eye(4)) I.rotx(45) assert (approx(I.tform) == [[1, 0, 0, 0], [0, SQRT2, -SQRT2, 0], [0, SQRT2, SQRT2, 0], [0, 0, 0, 1]])
def test_concat(): targ = [[SQRT2, 0, SQRT2, 0], [.5, SQRT2, -.5, 0], [-.5, SQRT2, .5, 0], [0, 0, 0, 1]] I = Transform(np.eye(4)) rx = Transform(np.eye(4)) rx.rotx(45) ry = Transform(np.eye(4)) ry.roty(45) I.concatenate_matrix(rx.tform) I.concatenate_matrix(ry.tform) assert approx(I.tform) == targ # overload I2 = Transform(np.eye(4)) X = I2 * rx * ry assert approx(X.tform) == targ
def test_scale(): I = Transform(np.eye(4)) I.scaleXYZ(2, 3, 4) assert (I.tform == np.diag([2, 3, 4, 1])).all() assert (I.get_scale() == [2, 3, 4]).all() I2 = Transform(np.eye(4)) I2.scale([2, 3, 4]) assert (I.get_scale() == I2.get_scale()).all()
def test_rotz(): I = Transform(np.eye(4)) I.rotz(45) assert approx(I.tform) == [[SQRT2, -SQRT2, 0, 0], [SQRT2, SQRT2, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
for i in range(3): if pos[i] < 0: d = directions_neg[i] else: d = directions_pos[i] direction_string.append('%s%0.0f' % (d, np.abs(pos[i]))) return (' '.join(direction_string)) # load the MNI template FSLDIR = os.getenv('FSLDIR', '/usr/local/fsl') mni_nifti = nib.load(os.path.join(FSLDIR, 'data/standard/MNI152_T1_1mm.nii.gz')) mni_aff = Transform(mni_nifti.get_qform()) # load T1 t1_nifti = nib.load(args.t1_nifti) t1_aff = t1_nifti.get_qform() t1_inv = np.linalg.pinv(t1_aff) # load native2mni FLIRT transform native2mni = np.loadtxt(args.native2mni) # load the template voxel spec, in mm mrs_aff_orig = np.loadtxt(args.svs_transform) mrs_mm_tform = Transform(mrs_aff_orig) # get scaling factor of native2mni transform and scale the voxel to preserve size # this is mm, so do before voxel conversion