def load_all_metadata(self): # TODO: make this less expensive. We can probably get away with a much more selective # loading of dicoms. E.g, maybe just the first volume and then the first slice of each subsequent vol? if self.dcm_list == None: self.load_dicoms() if self.is_dwi: self.bvals = np.array([getelem(dcm, TAG_BVALUE, float)[0] for dcm in self.dcm_list[0::self.num_slices]]) self.bvecs = np.array([[getelem(dcm, TAG_BVEC[i], float) for i in range(3)] for dcm in self.dcm_list[0::self.num_slices]]).transpose() # Try to carry on on incomplete datasets. Also, some weird scans like MRVs don't set the # number of slices correctly in the dicom header. (Or at least they set it in a weird way # that we don't understand.) if len(self.dcm_list) < self.num_slices: self.num_slices = len(self.dcm_list) if len(self.dcm_list) < self.total_num_slices: self.total_num_slices = len(self.dcm_list) image_position = [tuple(getelem(dcm, 'ImagePositionPatient', float, [0., 0., 0.])) for dcm in self.dcm_list] if self.num_timepoints == 1: unique_slice_pos = np.unique(image_position).astype(np.float) # crude check for a 3-plane localizer. When we get one of these, we actually # want each plane to be a different time point. slice_distance = np.sqrt((np.diff(unique_slice_pos,axis=0)**2).sum(1)) # ugly hack! The number of time points is the number of big (>10mm) jumps in slice-to-slice distance. self.num_timepoints = np.sum((slice_distance - np.median(slice_distance)) > 10) + 1 self.num_slices = self.total_num_slices / self.num_timepoints cosines = getelem(self._hdr, 'ImageOrientationPatient', float, 6 * [np.nan]) row_cosines = np.array(cosines[0:3]) col_cosines = np.array(cosines[3:6]) # Compute the slice_norm. From the NIFTI-1 header: # The third column of R will be either the cross-product of the first 2 columns or # its negative. It is possible to infer the sign of the 3rd column by examining # the coordinates in DICOM attribute (0020,0032) "Image Position (Patient)" for # successive slices. However, this method occasionally fails for reasons that I # (RW Cox) do not understand. # For Siemens data, it seems that looking at 'SliceNormalVector' can help resolve this. # dicom_slice_norm = getelem(self._hdr, 'SliceNormalVector', float, None) # if dicom_slice_norm != None and np.dot(self.slice_norm, dicom_slice_norm) < 0.: # self.slice_norm = -self.slice_norm # But otherwise, we'll have to fix this up after we load all the dicoms and check # the slice-to-slice position deltas. slice_norm = np.cross(row_cosines, col_cosines) # FIXME: the following could fail if the acquisition was before a full volume was aquired. if np.dot(slice_norm, image_position[0]) > np.dot(slice_norm, image_position[self.num_slices-1]): log.debug('flipping slice order') #slice_norm = -slice_norm self.reverse_slice_order = True self.origin = image_position[self.num_slices-1] * np.array([-1, -1, 1]) else: self.origin = image_position[0] * np.array([-1, -1, 1]) self.slice_order = nimsmrdata.SLICE_ORDER_UNKNOWN if self.total_num_slices >= self.num_slices and getelem(self.dcm_list[0], 'TriggerTime', float) is not None: trigger_times = np.array([getelem(dcm, 'TriggerTime', float) for dcm in self.dcm_list[0:self.num_slices]]) if self.reverse_slice_order: # the slice order will be flipped when the image is saved, so flip the trigger times trigger_times = trigger_times[::-1] trigger_times_from_first_slice = trigger_times[0] - trigger_times if self.num_slices > 2: self.slice_duration = float(min(abs(trigger_times_from_first_slice[1:]))) / 1000. # msec to sec if trigger_times_from_first_slice[1] < 0: # slice 1 happened after slice 0, so this must be either SEQ_INC or ALT_INC self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC if trigger_times[2] > trigger_times[1] else nimsmrdata.SLICE_ORDER_ALT_INC else: # slice 1 before slice 0, so must be ALT_DEC or SEQ_DEC self.slice_order = nimsmrdata.SLICE_ORDER_ALT_DEC if trigger_times[2] > trigger_times[1] else nimsmrdata.SLICE_ORDER_SEQ_DEC else: self.slice_duration = trigger_times[0] self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC rot = nimsmrdata.compute_rotation(row_cosines, col_cosines, slice_norm) if self.is_dwi: self.bvecs,self.bvals = nimsmrdata.adjust_bvecs(self.bvecs, self.bvals, self.scanner_type, rot) self.qto_xyz = nimsmrdata.build_affine(rot, self.mm_per_vox, self.origin) super(NIMSDicom, self).load_all_metadata()
def __init__(self, filepath, num_virtual_coils=16, notch_thresh=0, recon_type=None): try: self.compressed = is_compressed(filepath) self._hdr = pfile.parse(filepath, self.compressed) except (IOError, pfile.PFileError) as e: raise NIMSPFileError(str(e)) self.exam_no = self._hdr.exam.ex_no self.patient_id = self._hdr.exam.patidff.strip('\x00') super(NIMSPFile, self).__init__() self.filepath = os.path.abspath(filepath) self.dirpath = os.path.dirname(self.filepath) self.filename = os.path.basename(self.filepath) self.basename = self.filename[:-3] if self.compressed else self.filename self.imagedata = None self.fm_data = None self.num_vcoils = num_virtual_coils self.notch_thresh = notch_thresh self.recon_type = recon_type self.psd_name = os.path.basename( self._hdr.image.psdname.partition('\x00')[0]) self.psd_type = nimsmrdata.infer_psd_type(self.psd_name) self.pfilename = 'P%05d' % self._hdr.rec.run_int self.series_no = self._hdr.series.se_no self.acq_no = self._hdr.image.scanactno self.exam_uid = unpack_uid(self._hdr.exam.study_uid) self.series_uid = unpack_uid(self._hdr.series.series_uid) self.series_desc = self._hdr.series.se_desc.strip('\x00') self.subj_firstname, self.subj_lastname = self.parse_subject_name( self._hdr.exam.patnameff.strip('\x00')) self.subj_dob = self.parse_subject_dob( self._hdr.exam.dateofbirth.strip('\x00')) self.subj_sex = ('male', 'female')[self._hdr.exam.patsex - 1] if self._hdr.exam.patsex in [1, 2 ] else None if self._hdr.image.im_datetime > 0: self.timestamp = datetime.datetime.utcfromtimestamp( self._hdr.image.im_datetime) else: # HOShims don't have self._hdr.image.im_datetime month, day, year = map( int, self._hdr.rec.scan_date.strip('\x00').split('/')) hour, minute = map( int, self._hdr.rec.scan_time.strip('\x00').split(':')) self.timestamp = datetime.datetime( year + 1900, month, day, hour, minute) # GE's epoch begins in 1900 self.ti = self._hdr.image.ti / 1e6 self.te = self._hdr.image.te / 1e6 self.tr = self._hdr.image.tr / 1e6 # tr in seconds self.flip_angle = float(self._hdr.image.mr_flip) self.pixel_bandwidth = self._hdr.rec.bw # Note: the freq/phase dir isn't meaningful for spiral trajectories. # GE numbers the dims 1,2, so freq_dir==1 is the first dim. We'll use # the convention where first dim = 0, second dim = 1, etc. for phase_encode. self.phase_encode = 1 if self._hdr.image.freq_dir == 1 else 0 self.mt_offset_hz = self._hdr.image.offsetfreq self.num_slices = self._hdr.image.slquant self.num_averages = self._hdr.image.averages self.num_echos = self._hdr.rec.nechoes self.receive_coil_name = self._hdr.image.cname.strip('\x00') self.num_receivers = self._hdr.rec.dab[0].stop_rcv - self._hdr.rec.dab[ 0].start_rcv + 1 self.operator = self._hdr.exam.operator_new.strip('\x00') self.protocol_name = self._hdr.series.prtcl.strip('\x00') self.scanner_name = self._hdr.exam.hospname.strip( '\x00') + ' ' + self._hdr.exam.ex_sysid.strip('\x00') self.scanner_type = 'GE MEDICAL' # FIXME self.acquisition_type = '' self.size = [self._hdr.image.dim_X, self._hdr.image.dim_Y] # imatrix_Y self.fov = [self._hdr.image.dfov, self._hdr.image.dfov_rect] self.scan_type = self._hdr.image.psd_iname.strip('\x00') self.num_bands = 1 self.num_mux_cal_cycle = 0 self.num_timepoints = self._hdr.rec.npasses # Some sequences (e.g., muxepi) acuire more timepoints that will be available in the resulting data file. # The following will indicate how many to expect in the final image. self.num_timepoints_available = self.num_timepoints self.deltaTE = 0.0 self.scale_data = False # Compute the voxel size rather than use image.pixsize_X/Y self.mm_per_vox = [ self.fov[0] / self.size[0], self.fov[1] / self.size[1], self._hdr.image.slthick + self._hdr.image.scanspacing ] image_tlhc = np.array([ self._hdr.image.tlhc_R, self._hdr.image.tlhc_A, self._hdr.image.tlhc_S ]) image_trhc = np.array([ self._hdr.image.trhc_R, self._hdr.image.trhc_A, self._hdr.image.trhc_S ]) image_brhc = np.array([ self._hdr.image.brhc_R, self._hdr.image.brhc_A, self._hdr.image.brhc_S ]) # psd-specific params get set here if self.psd_type == 'spiral': self.num_timepoints = int( self._hdr.rec.user0) # not in self._hdr.rec.nframes for sprt self.deltaTE = self._hdr.rec.user15 self.band_spacing = 0 self.scale_data = True # spiral is always a square encode based on the frequency encode direction (size_x) # Atsushi also likes to round up to the next higher power of 2. # self.size_x = int(pow(2,ceil(log2(pf.size_x)))) # The rec.im_size field seems to have the correct reconned image size, but # this isn't guaranteed to be correct, as Atsushi's recon does whatever it # damn well pleases. Maybe we could add a check to infer the image size, # assuming it's square? self.size[0] = self.size[1] = self._hdr.rec.im_size self.mm_per_vox[0:2] = [self.fov[0] / self.size[0]] * 2 elif self.psd_type == 'basic': # first 6 are ref scans, so ignore those. Also, two acquired timepoints are used # to generate each reconned time point. self.num_timepoints = ( self._hdr.rec.npasses * self._hdr.rec.nechoes - 6) / 2 self.num_echos = 1 elif self.psd_type == 'muxepi': self.num_bands = int(self._hdr.rec.user6) self.num_mux_cal_cycle = int(self._hdr.rec.user7) self.band_spacing_mm = self._hdr.rec.user8 # When ARC is used with mux, the number of acquired TRs is greater than what's Rxed. # ARC calibration uses multi-shot, so the additional TRs = num_bands*(ileaves-1)*num_mux_cal_cycle self.num_timepoints = self._hdr.rec.npasses + self.num_bands * ( self._hdr.rec.ileaves - 1) * self.num_mux_cal_cycle # The actual number of images returned by the mux recon is npasses - num_calibration_passes + num_mux_cal_cycle self.num_timepoints_available = self._hdr.rec.npasses - self.num_bands * self.num_mux_cal_cycle + self.num_mux_cal_cycle # TODO: adjust the image.tlhc... fields to match the correct geometry. elif self.psd_type == 'mrs': self._hdr.image.scanspacing = 0. self.mm_per_vox = [ self._hdr.rec.roileny, self._hdr.rec.roilenx, self._hdr.rec.roilenz ] image_tlhc = np.array( (-self._hdr.rec.roilocx - self.mm_per_vox[0] / 2., self._hdr.rec.roilocy + self.mm_per_vox[1] / 2., self._hdr.rec.roilocz - self.mm_per_vox[1] / 2.)) image_trhc = image_tlhc - [self.mm_per_vox[0], 0., 0.] image_brhc = image_trhc + [0., self.mm_per_vox[1], 0.] # Tread carefully! Most of the stuff down here depends on various fields being corrected in the # sequence-specific set of hacks just above. So, move things with care! # Note: the following is true for single-shot planar acquisitions (EPI and 1-shot spiral). # For multishot sequences, we need to multiply by the # of shots. And for non-planar aquisitions, # we'd need to multiply by the # of phase encodes (accounting for any acceleration factors). # Even for planar sequences, this will be wrong (under-estimate) in case of cardiac-gating. self.prescribed_duration = self.num_timepoints * self.tr self.total_num_slices = self.num_slices * self.num_timepoints # The actual duration can only be computed after the data are loaded. Settled for rx duration for now. self.duration = self.prescribed_duration self.effective_echo_spacing = self._hdr.image.effechospace / 1e6 self.phase_encode_undersample = 1. / self._hdr.rec.ileaves # TODO: Set this correctly! (it's in the dicom at (0x0043, 0x1083)) self.slice_encode_undersample = 1. self.acquisition_matrix = [ self._hdr.rec.rc_xres, self._hdr.rec.rc_yres ] # TODO: it looks like the pfile now has a 'grad_data' field! # Diffusion params self.dwi_numdirs = self._hdr.rec.numdifdirs # You might think that the b-value for diffusion scans would be stored in self._hdr.image.b_value. # But alas, this is GE. Apparently, that var stores the b-value of the just the first image, which is # usually a non-dwi. So, we had to modify the PSD and stick the b-value into an rhuser CV. Sigh. # NOTE: pre-dv24, the bvalue was stored in rec.user22. self.dwi_bvalue = self._hdr.rec.user1 self.is_dwi = True if self.dwi_numdirs >= 6 else False # if bit 4 of rhtype(int16) is set, then fractional NEX (i.e., partial ky acquisition) was used. self.partial_ky = self._hdr.rec.scan_type & np.uint16(16) > 0 # was pepolar used to flip the phase encode direction? self.phase_encode_direction = 1 if np.bitwise_and( self._hdr.rec.dacq_ctrl, 4) == 4 else 0 self.caipi = self._hdr.rec.user13 # true: CAIPIRINHA-type acquisition; false: Direct aliasing of simultaneous slices. self.cap_blip_start = self._hdr.rec.user14 # Starting index of the kz blips. 0~(mux-1) correspond to -kmax~kmax. self.cap_blip_inc = self._hdr.rec.user15 # Increment of the kz blip index for adjacent acquired ky lines. self.mica = self._hdr.rec.user17 # MICA bit-reverse? self.slice_duration = self.tr / self.num_slices lr_diff = image_trhc - image_tlhc si_diff = image_trhc - image_brhc if not np.all(lr_diff == 0) and not np.all(si_diff == 0): row_cosines = lr_diff / np.sqrt(lr_diff.dot(lr_diff)) col_cosines = -si_diff / np.sqrt(si_diff.dot(si_diff)) else: row_cosines = np.array([1., 0, 0]) col_cosines = np.array([0, -1., 0]) self.slice_order = nimsmrdata.SLICE_ORDER_UNKNOWN # FIXME: check that this is correct. if self._hdr.series.se_sortorder == 0: self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC elif self._hdr.series.se_sortorder == 1: self.slice_order = nimsmrdata.SLICE_ORDER_ALT_INC # header geometry is LPS but we need RAS, so negate R and A. slice_norm = np.array([ -self._hdr.image.norm_R, -self._hdr.image.norm_A, self._hdr.image.norm_S ]) # This is either the first slice tlhc (image_tlhc) or the last slice tlhc. How to decide? # And is it related to wheather I have to negate the slice_norm? # Tuned this empirically by comparing spiral and EPI data with the same Rx. # Everything seems reasonable, except the test for axial orientation (start_ras==S|I). # I have no idea why I need that! But the flipping only seems necessary for axials, not # coronals or the few obliques I've tested. # FIXME: haven't tested sagittals! if (self._hdr.series.start_ras in 'SI' and self._hdr.series.start_loc > self._hdr.series.end_loc): self.reverse_slice_order = True slice_fov = np.abs(self._hdr.series.start_loc - self._hdr.series.end_loc) image_position = image_tlhc - slice_norm * slice_fov # FIXME: since we are reversing the slice order here, should we change the slice_order field below? else: image_position = image_tlhc self.reverse_slice_order = False # Not sure why the following is needed. # TODO: * test non-slice-reversed coronals-- do they also need l/r flip? # * test sagitals-- do they need any flipping? if (self._hdr.series.start_ras in 'AP' and self._hdr.series.start_loc > self._hdr.series.end_loc): slice_norm = -slice_norm self.flip_lr = True else: self.flip_lr = False if self.num_bands > 1: image_position = image_position - slice_norm * self.band_spacing_mm * ( self.num_bands - 1.0) / 2.0 #origin = image_position * np.array([-1, -1, 1]) # Fix the half-voxel offset. Apparently, the p-file convention specifies coords at the # corner of a voxel. But DICOM/NIFTI convention is the voxel center. So offset by a half-voxel. origin = image_position + (row_cosines + col_cosines) * ( np.array(self.mm_per_vox) / 2) # The DICOM standard defines these two unit vectors in an LPS coordinate frame, but we'll # need RAS (+x is right, +y is anterior, +z is superior) for NIFTI. So, we compute them # such that self.row_cosines points to the right and self.col_cosines points up. row_cosines[0:2] = -row_cosines[0:2] col_cosines[0:2] = -col_cosines[0:2] if self.is_dwi and self.dwi_bvalue == 0: log.warning( 'the data appear to be diffusion-weighted, but image.b_value is 0! Setting it to 10.' ) # Set it to something other than 0 so non-dwi's can be distinguished from dwi's self.dwi_bvalue = 10. # The bvals/bvecs will get set later self.bvecs, self.bvals = (None, None) self.image_rotation = nimsmrdata.compute_rotation( row_cosines, col_cosines, slice_norm) self.qto_xyz = nimsmrdata.build_affine(self.image_rotation, self.mm_per_vox, origin) self.scan_type = self.infer_scan_type() self.aux_files = None
def load_all_metadata(self): # TODO: make this less expensive. We can probably get away with a much more selective # loading of dicoms. E.g, maybe just the first volume and then the first slice of each subsequent vol? if self.dcm_list == None: self.load_dicoms() if self.is_dwi: self.bvals = np.array([ getelem(dcm, TAG_BVALUE, float)[0] for dcm in self.dcm_list[0::self.num_slices] ]) self.bvecs = np.array( [[getelem(dcm, TAG_BVEC[i], float) for i in range(3)] for dcm in self.dcm_list[0::self.num_slices]]).transpose() # Try to carry on on incomplete datasets. Also, some weird scans like MRVs don't set the # number of slices correctly in the dicom header. (Or at least they set it in a weird way # that we don't understand.) if len(self.dcm_list) < self.num_slices: self.num_slices = len(self.dcm_list) if len(self.dcm_list) < self.total_num_slices: self.total_num_slices = len(self.dcm_list) image_position = [ tuple(getelem(dcm, 'ImagePositionPatient', float, [0., 0., 0.])) for dcm in self.dcm_list ] if self.num_timepoints == 1: unique_slice_pos = np.unique(image_position).astype(np.float) # crude check for a 3-plane localizer. When we get one of these, we actually # want each plane to be a different time point. slice_distance = np.sqrt((np.diff(unique_slice_pos, axis=0)**2).sum(1)) # ugly hack! The number of time points is the number of big (>10mm) jumps in slice-to-slice distance. self.num_timepoints = np.sum( (slice_distance - np.median(slice_distance)) > 10) + 1 self.num_slices = self.total_num_slices / self.num_timepoints cosines = getelem(self._hdr, 'ImageOrientationPatient', float, 6 * [np.nan]) row_cosines = np.array(cosines[0:3]) col_cosines = np.array(cosines[3:6]) # Compute the slice_norm. From the NIFTI-1 header: # The third column of R will be either the cross-product of the first 2 columns or # its negative. It is possible to infer the sign of the 3rd column by examining # the coordinates in DICOM attribute (0020,0032) "Image Position (Patient)" for # successive slices. However, this method occasionally fails for reasons that I # (RW Cox) do not understand. # For Siemens data, it seems that looking at 'SliceNormalVector' can help resolve this. # dicom_slice_norm = getelem(self._hdr, 'SliceNormalVector', float, None) # if dicom_slice_norm != None and np.dot(self.slice_norm, dicom_slice_norm) < 0.: # self.slice_norm = -self.slice_norm # But otherwise, we'll have to fix this up after we load all the dicoms and check # the slice-to-slice position deltas. slice_norm = np.cross(row_cosines, col_cosines) # FIXME: the following could fail if the acquisition was before a full volume was aquired. if np.dot(slice_norm, image_position[0]) > np.dot( slice_norm, image_position[self.num_slices - 1]): log.debug('flipping slice order') #slice_norm = -slice_norm self.reverse_slice_order = True self.origin = image_position[self.num_slices - 1] * np.array( [-1, -1, 1]) else: self.origin = image_position[0] * np.array([-1, -1, 1]) self.slice_order = nimsmrdata.SLICE_ORDER_UNKNOWN if self.total_num_slices >= self.num_slices and getelem( self.dcm_list[0], 'TriggerTime', float) is not None: trigger_times = np.array([ getelem(dcm, 'TriggerTime', float) for dcm in self.dcm_list[0:self.num_slices] ]) if self.reverse_slice_order: # the slice order will be flipped when the image is saved, so flip the trigger times trigger_times = trigger_times[::-1] trigger_times_from_first_slice = trigger_times[0] - trigger_times if self.num_slices > 2: self.slice_duration = float( min(abs(trigger_times_from_first_slice[1:])) ) / 1000. # msec to sec if trigger_times_from_first_slice[1] < 0: # slice 1 happened after slice 0, so this must be either SEQ_INC or ALT_INC self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC if trigger_times[ 2] > trigger_times[ 1] else nimsmrdata.SLICE_ORDER_ALT_INC else: # slice 1 before slice 0, so must be ALT_DEC or SEQ_DEC self.slice_order = nimsmrdata.SLICE_ORDER_ALT_DEC if trigger_times[ 2] > trigger_times[ 1] else nimsmrdata.SLICE_ORDER_SEQ_DEC else: self.slice_duration = trigger_times[0] self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC rot = nimsmrdata.compute_rotation(row_cosines, col_cosines, slice_norm) if self.is_dwi: self.bvecs, self.bvals = nimsmrdata.adjust_bvecs( self.bvecs, self.bvals, self.scanner_type, rot) self.qto_xyz = nimsmrdata.build_affine(rot, self.mm_per_vox, self.origin) super(NIMSDicom, self).load_all_metadata()
def __init__(self, filepath, num_virtual_coils=16): try: self.compressed = is_compressed(filepath) self._hdr = pfile.parse(filepath, self.compressed) except (IOError, pfile.PFileError) as e: raise NIMSPFileError(str(e)) self.exam_no = self._hdr.exam.ex_no self.patient_id = self._hdr.exam.patidff.strip('\x00') super(NIMSPFile, self).__init__() self.filepath = os.path.abspath(filepath) self.dirpath = os.path.dirname(self.filepath) self.filename = os.path.basename(self.filepath) self.basename = self.filename[:-3] if self.compressed else self.filename self.imagedata = None self.fm_data = None self.num_vcoils = num_virtual_coils self.psd_name = os.path.basename(self._hdr.image.psdname.partition('\x00')[0]) self.psd_type = nimsmrdata.infer_psd_type(self.psd_name) self.pfilename = 'P%05d' % self._hdr.rec.run_int self.series_no = self._hdr.series.se_no self.acq_no = self._hdr.image.scanactno self.exam_uid = unpack_uid(self._hdr.exam.study_uid) self.series_uid = unpack_uid(self._hdr.series.series_uid) self.series_desc = self._hdr.series.se_desc.strip('\x00') self.subj_firstname, self.subj_lastname = self.parse_subject_name(self._hdr.exam.patnameff.strip('\x00')) self.subj_dob = self.parse_subject_dob(self._hdr.exam.dateofbirth.strip('\x00')) self.subj_sex = ('male', 'female')[self._hdr.exam.patsex-1] if self._hdr.exam.patsex in [1,2] else None if self._hdr.image.im_datetime > 0: self.timestamp = datetime.datetime.utcfromtimestamp(self._hdr.image.im_datetime) else: # HOShims don't have self._hdr.image.im_datetime month, day, year = map(int, self._hdr.rec.scan_date.strip('\x00').split('/')) hour, minute = map(int, self._hdr.rec.scan_time.strip('\x00').split(':')) self.timestamp = datetime.datetime(year + 1900, month, day, hour, minute) # GE's epoch begins in 1900 self.ti = self._hdr.image.ti / 1e6 self.te = self._hdr.image.te / 1e6 self.tr = self._hdr.image.tr / 1e6 # tr in seconds self.flip_angle = float(self._hdr.image.mr_flip) self.pixel_bandwidth = self._hdr.rec.bw # Note: the freq/phase dir isn't meaningful for spiral trajectories. # GE numbers the dims 1,2, so freq_dir==1 is the first dim. We'll use # the convention where first dim = 0, second dim = 1, etc. for phase_encode. self.phase_encode = 1 if self._hdr.image.freq_dir==1 else 0 self.mt_offset_hz = self._hdr.image.offsetfreq self.num_slices = self._hdr.image.slquant self.num_averages = self._hdr.image.averages self.num_echos = self._hdr.rec.nechoes self.receive_coil_name = self._hdr.image.cname.strip('\x00') self.num_receivers = self._hdr.rec.dab[0].stop_rcv - self._hdr.rec.dab[0].start_rcv + 1 self.operator = self._hdr.exam.operator_new.strip('\x00') self.protocol_name = self._hdr.series.prtcl.strip('\x00') self.scanner_name = self._hdr.exam.hospname.strip('\x00') + ' ' + self._hdr.exam.ex_sysid.strip('\x00') self.scanner_type = 'GE MEDICAL' # FIXME self.acquisition_type = '' self.size = [self._hdr.image.dim_X, self._hdr.image.dim_Y] # imatrix_Y self.fov = [self._hdr.image.dfov, self._hdr.image.dfov_rect] self.scan_type = self._hdr.image.psd_iname.strip('\x00') self.num_bands = 1 self.num_mux_cal_cycle = 0 self.num_timepoints = self._hdr.rec.npasses # Some sequences (e.g., muxepi) acuire more timepoints that will be available in the resulting data file. # The following will indicate how many to expect in the final image. self.num_timepoints_available = self.num_timepoints self.deltaTE = 0.0 self.scale_data = False # Compute the voxel size rather than use image.pixsize_X/Y self.mm_per_vox = [self.fov[0] / self.size[0], self.fov[1] / self.size[1], self._hdr.image.slthick + self._hdr.image.scanspacing] image_tlhc = np.array([self._hdr.image.tlhc_R, self._hdr.image.tlhc_A, self._hdr.image.tlhc_S]) image_trhc = np.array([self._hdr.image.trhc_R, self._hdr.image.trhc_A, self._hdr.image.trhc_S]) image_brhc = np.array([self._hdr.image.brhc_R, self._hdr.image.brhc_A, self._hdr.image.brhc_S]) # psd-specific params get set here if self.psd_type == 'spiral': self.num_timepoints = int(self._hdr.rec.user0) # not in self._hdr.rec.nframes for sprt self.num_timepoints_available = self.num_timepoints self.deltaTE = self._hdr.rec.user15 self.band_spacing = 0 self.scale_data = True # spiral is always a square encode based on the frequency encode direction (size_x) # Atsushi also likes to round up to the next higher power of 2. # self.size_x = int(pow(2,ceil(log2(pf.size_x)))) # The rec.im_size field seems to have the correct reconned image size, but # this isn't guaranteed to be correct, as Atsushi's recon does whatever it # damn well pleases. Maybe we could add a check to infer the image size, # assuming it's square? self.size[0] = self.size[1] = self._hdr.rec.im_size self.mm_per_vox[0:2] = [self.fov[0] / self.size[0]] * 2 elif self.psd_type == 'basic': # first 6 are ref scans, so ignore those. Also, two acquired timepoints are used # to generate each reconned time point. self.num_timepoints = (self._hdr.rec.npasses * self._hdr.rec.nechoes - 6) / 2 self.num_timepoints_available = self.num_timepoints self.num_echos = 1 elif self.psd_type == 'muxepi': self.num_bands = int(self._hdr.rec.user6) self.num_mux_cal_cycle = int(self._hdr.rec.user7) self.band_spacing_mm = self._hdr.rec.user8 self.num_timepoints = self._hdr.rec.npasses + self.num_bands * self._hdr.rec.ileaves * (self.num_mux_cal_cycle-1) self.num_timepoints_available = self._hdr.rec.npasses - self.num_bands * self._hdr.rec.ileaves * (self.num_mux_cal_cycle-1) + self.num_mux_cal_cycle # TODO: adjust the image.tlhc... fields to match the correct geometry. elif self.psd_type == 'mrs': self._hdr.image.scanspacing = 0. self.mm_per_vox = [self._hdr.rec.roileny, self._hdr.rec.roilenx, self._hdr.rec.roilenz] image_tlhc = np.array((-self._hdr.rec.roilocx - self.mm_per_vox[0]/2., self._hdr.rec.roilocy + self.mm_per_vox[1]/2., self._hdr.rec.roilocz - self.mm_per_vox[1]/2.)) image_trhc = image_tlhc - [self.mm_per_vox[0], 0., 0.] image_brhc = image_trhc + [0., self.mm_per_vox[1], 0.] # Tread carefully! Most of the stuff down here depends on various fields being corrected in the # sequence-specific set of hacks just above. So, move things with care! # Note: the following is true for single-shot planar acquisitions (EPI and 1-shot spiral). # For multishot sequences, we need to multiply by the # of shots. And for non-planar aquisitions, # we'd need to multiply by the # of phase encodes (accounting for any acceleration factors). # Even for planar sequences, this will be wrong (under-estimate) in case of cardiac-gating. self.prescribed_duration = self.num_timepoints * self.tr self.total_num_slices = self.num_slices * self.num_timepoints # The actual duration can only be computed after the data are loaded. Settled for rx duration for now. self.duration = self.prescribed_duration self.effective_echo_spacing = self._hdr.image.effechospace / 1e6 self.phase_encode_undersample = 1. / self._hdr.rec.ileaves # TODO: Set this correctly! (it's in the dicom at (0x0043, 0x1083)) self.slice_encode_undersample = 1. self.acquisition_matrix = [self._hdr.rec.rc_xres, self._hdr.rec.rc_yres] # Diffusion params self.dwi_numdirs = self._hdr.rec.numdifdirs # You might think that the b-valuei for diffusion scans would be stored in self._hdr.image.b_value. # But alas, this is GE. Apparently, that var stores the b-value of the just the first image, which is # usually a non-dwi. So, we had to modify the PSD and stick the b-value into an rhuser CV. Sigh. self.dwi_bvalue = self._hdr.rec.user22 self.is_dwi = True if self.dwi_numdirs >= 6 else False # if bit 4 of rhtype(int16) is set, then fractional NEX (i.e., partial ky acquisition) was used. self.partial_ky = self._hdr.rec.scan_type & np.uint16(16) > 0 self.caipi = self._hdr.rec.user13 # true: CAIPIRINHA-type acquisition; false: Direct aliasing of simultaneous slices. self.cap_blip_start = self._hdr.rec.user14 # Starting index of the kz blips. 0~(mux-1) correspond to -kmax~kmax. self.cap_blip_inc = self._hdr.rec.user15 # Increment of the kz blip index for adjacent acquired ky lines. self.mica = self._hdr.rec.user17 # MICA bit-reverse? self.slice_duration = self.tr / self.num_slices lr_diff = image_trhc - image_tlhc si_diff = image_trhc - image_brhc if not np.all(lr_diff==0) and not np.all(si_diff==0): row_cosines = lr_diff / np.sqrt(lr_diff.dot(lr_diff)) col_cosines = -si_diff / np.sqrt(si_diff.dot(si_diff)) else: row_cosines = np.array([1.,0,0]) col_cosines = np.array([0,-1.,0]) self.slice_order = nimsmrdata.SLICE_ORDER_UNKNOWN # FIXME: check that this is correct. if self._hdr.series.se_sortorder == 0: self.slice_order = nimsmrdata.SLICE_ORDER_SEQ_INC elif self._hdr.series.se_sortorder == 1: self.slice_order = nimsmrdata.SLICE_ORDER_ALT_INC slice_norm = np.array([-self._hdr.image.norm_R, -self._hdr.image.norm_A, self._hdr.image.norm_S]) # This is either the first slice tlhc (image_tlhc) or the last slice tlhc. How to decide? # And is it related to wheather I have to negate the slice_norm? # Tuned this empirically by comparing spiral and EPI data with the same Rx. # Everything seems reasonable, except the test for axial orientation (start_ras==S|I). # I have no idea why I need that! But the flipping only seems necessary for axials, not # coronals or the few obliques I've tested. # FIXME: haven't tested sagittals! if (self._hdr.series.start_ras=='S' or self._hdr.series.start_ras=='I') and self._hdr.series.start_loc > self._hdr.series.end_loc: self.reverse_slice_order = True slice_fov = np.abs(self._hdr.series.start_loc - self._hdr.series.end_loc) image_position = image_tlhc - slice_norm * slice_fov # FIXME: since we are reversing the slice order here, should we change the slice_order field below? else: image_position = image_tlhc self.reverse_slice_order = False if self.num_bands > 1: image_position = image_position - slice_norm * self.band_spacing_mm * (self.num_bands - 1.0) / 2.0 #origin = image_position * np.array([-1, -1, 1]) # Fix the half-voxel offset. Apparently, the p-file convention specifies coords at the # corner of a voxel. But DICOM/NIFTI convention is the voxel center. So offset by a half-voxel. origin = image_position + (row_cosines+col_cosines)*(np.array(self.mm_per_vox)/2) # The DICOM standard defines these two unit vectors in an LPS coordinate frame, but we'll # need RAS (+x is right, +y is anterior, +z is superior) for NIFTI. So, we compute them # such that self.row_cosines points to the right and self.col_cosines points up. row_cosines[0:2] = -row_cosines[0:2] col_cosines[0:2] = -col_cosines[0:2] if self.is_dwi and self.dwi_bvalue==0: log.warning('the data appear to be diffusion-weighted, but image.b_value is 0!') # The bvals/bvecs will get set later self.bvecs,self.bvals = (None,None) self.image_rotation = nimsmrdata.compute_rotation(row_cosines, col_cosines, slice_norm) self.qto_xyz = nimsmrdata.build_affine(self.image_rotation, self.mm_per_vox, origin) self.scan_type = self.infer_scan_type() self.aux_files = None