def __init__(self, dcm_path, metadata_only=True): self.filepath = dcm_path try: if os.path.isfile(self.filepath) and tarfile.is_tarfile(self.filepath): # compressed tarball self.compressed = True with tarfile.open(self.filepath) as archive: archive.next() # skip over top-level directory self._hdr = dicom.read_file(cStringIO.StringIO(archive.extractfile(archive.next()).read()), stop_before_pixels=metadata_only) else: # directory of dicoms or single file self.compressed = False dcm_path = self.filepath if os.path.isfile(self.filepath) else os.path.join(self.filepath, os.listdir(self.filepath)[0]) self._hdr = dicom.read_file(dcm_path, stop_before_pixels=metadata_only) except Exception as e: raise NIMSDicomError(str(e)) self.exam_no = getelem(self._hdr, 'StudyID', int) self.patient_id = getelem(self._hdr, 'PatientID') super(NIMSDicom, self).__init__() def acq_date(hdr): if 'AcquisitionDate' in hdr: return hdr.AcquisitionDate elif 'StudyDate' in hdr: return hdr.StudyDate else: return '19000101' def acq_time(hdr): if 'AcquisitionTime' in hdr: return hdr.AcquisitionTime elif 'StudyTime' in hdr: return hdr.StudyTime else: return '000000' self.series_no = getelem(self._hdr, 'SeriesNumber', int) self.acq_no = getelem(self._hdr, 'AcquisitionNumber', int, 0) self.exam_uid = getelem(self._hdr, 'StudyInstanceUID') self.series_uid = getelem(self._hdr, 'SeriesInstanceUID') self.series_desc = getelem(self._hdr, 'SeriesDescription') self.subj_firstname, self.subj_lastname = self.parse_subject_name(getelem(self._hdr, 'PatientName', None, '')) self.subj_dob = self.parse_subject_dob(getelem(self._hdr, 'PatientBirthDate', None, '')) self.subj_sex = {'M': 'male', 'F': 'female'}.get(getelem(self._hdr, 'PatientSex')) self.psd_name = os.path.basename(getelem(self._hdr, TAG_PSD_NAME, None, 'unknown')) self.psd_type = nimsmrdata.infer_psd_type(self.psd_name) self.timestamp = datetime.datetime.strptime(acq_date(self._hdr) + acq_time(self._hdr)[:6], '%Y%m%d%H%M%S') self.ti = getelem(self._hdr, 'InversionTime', float, 0.) / 1000.0 self.te = getelem(self._hdr, 'EchoTime', float, 0.) / 1000.0 self.tr = getelem(self._hdr, 'RepetitionTime', float, 0.) / 1000.0 self.flip_angle = getelem(self._hdr, 'FlipAngle', float, 0.) self.pixel_bandwidth = getelem(self._hdr, 'PixelBandwidth', float, 0.) self.phase_encode = int(getelem(self._hdr, 'InPlanePhaseEncodingDirection', None, '') == 'COL') self.mt_offset_hz = getelem(self._hdr, TAG_MTOFF_HZ, float, 0.) self.images_in_mosaic = getelem(self._hdr, TAG_IMAGES_IN_MOSAIC, int, 0) self.total_num_slices = getelem(self._hdr, 'ImagesInAcquisition', int, 0) self.num_slices = getelem(self._hdr, TAG_SLICES_PER_VOLUME, int, 1) self.num_timepoints = getelem(self._hdr, 'NumberOfTemporalPositions', int, self.total_num_slices / self.num_slices) if self.total_num_slices == self.num_slices: self.total_num_slices = self.num_slices * self.num_timepoints self.num_averages = getelem(self._hdr, 'NumberOfAverages', int, 1) self.num_echos = getelem(self._hdr, 'EchoNumbers', int, 1) self.receive_coil_name = getelem(self._hdr, 'ReceiveCoilName', None, 'unknown') self.num_receivers = 0 # FIXME: where is this stored? self.prescribed_duration = self.tr * self.num_timepoints * self.num_averages # FIXME: probably need more hacks in here to compute the correct duration. self.duration = self.prescribed_duration # actual duration can only be computed after all data are loaded self.operator = getelem(self._hdr, 'OperatorsName', None, 'unknown') self.protocol_name = getelem(self._hdr, 'ProtocolName', None, 'unknown') self.scanner_name = '%s %s'.strip() % (getelem(self._hdr, 'InstitutionName', None, ''), getelem(self._hdr, 'StationName', None, '')) self.scanner_type = '%s %s'.strip() % (getelem(self._hdr, 'Manufacturer', None, ''), getelem(self._hdr, 'ManufacturerModelName', None, '')) self.acquisition_type = getelem(self._hdr, 'MRAcquisitionType', None, 'unknown') self.mm_per_vox = getelem(self._hdr, 'PixelSpacing', float, [1., 1.]) + [getelem(self._hdr, 'SpacingBetweenSlices', float, getelem(self._hdr, 'SliceThickness', float, 1.))] # FIXME: confirm that DICOM (Columns,Rows) = PFile (X,Y) self.size = [getelem(self._hdr, 'Columns', int, 0), getelem(self._hdr, 'Rows', int, 0)] self.fov = 2 * [getelem(self._hdr, 'ReconstructionDiameter', float, 0.)] # Dicom convention is ROW,COL. E.g., ROW is the first dim (index==0), COL is the second (index==1) if self.phase_encode == 1: # The Acquisition matrix field includes four values: [freq rows, freq columns, phase rows, phase columns]. # E.g., for a 64x64 image, it would be [64,0,0,64] if the image row axis was the frequency encoding axis or # [0,64,64,0] if the image row was the phase encoding axis. self.acquisition_matrix = getelem(self._hdr, 'AcquisitionMatrix', None, [0, 0, 0, 0])[0:4:3] self.fov[1] /= (getelem(self._hdr, 'PercentPhaseFieldOfView', float, 0.) / 100.) if 'PercentPhaseFieldOfView' in self._hdr else 1. else: # We want the acq matrix to always be ROWS,COLS, so we flip the order for the case where the phase encode is the first dim: self.acquisition_matrix = getelem(self._hdr, 'AcquisitionMatrix', None, [0, 0, 0, 0])[2:0:-1] self.fov[0] /= (getelem(self._hdr, 'PercentPhaseFieldOfView', float, 0.) / 100.) if 'PercentPhaseFieldOfView' in self._hdr else 1. r = getelem(self._hdr, TAG_PHASE_ENCODE_UNDERSAMPLE, None, [1., 1.]) self.phase_encode_undersample, self.slice_encode_undersample = [float(x) for x in (r.split('\\') if isinstance(r, basestring) else r)] self.num_bands = 1 # assume that dicoms are never multiband self.qto_xyz = None self.image_type = getelem(self._hdr, 'ImageType', None, []) self.effective_echo_spacing = getelem(self._hdr, TAG_EPI_EFFECTIVE_ECHO_SPACING, float, 0.) / 1e6 self.phase_encode_direction = None; # FINDME: 'pepolar'-- stored in bit 4 of rec.dacq_ctrl in pfiles. Probably in a private tag in DICOM. self.is_dwi = bool(self.image_type == TYPE_ORIGINAL and getelem(self._hdr, TAG_DIFFUSION_DIRS, int, 0) >= 6) self.bvals = None self.bvecs = None self.slice_order = None self.slice_duration = None self.reverse_slice_order = None self.notes = '' self.scan_type = self.infer_scan_type() self.dcm_list = None
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 __init__(self, dcm_path, metadata_only=True): self.filepath = dcm_path try: if os.path.isfile(self.filepath) and tarfile.is_tarfile( self.filepath): # compressed tarball self.compressed = True with tarfile.open(self.filepath) as archive: archive.next() # skip over top-level directory self._hdr = dicom.read_file( cStringIO.StringIO( archive.extractfile(archive.next()).read()), stop_before_pixels=metadata_only) else: # directory of dicoms or single file self.compressed = False dcm_path = self.filepath if os.path.isfile( self.filepath) else os.path.join( self.filepath, os.listdir(self.filepath)[0]) self._hdr = dicom.read_file(dcm_path, stop_before_pixels=metadata_only) except Exception as e: raise NIMSDicomError(str(e)) self.exam_no = getelem(self._hdr, 'StudyID', int) self.patient_id = getelem(self._hdr, 'PatientID') super(NIMSDicom, self).__init__() def acq_date(hdr): if 'AcquisitionDate' in hdr: return hdr.AcquisitionDate elif 'StudyDate' in hdr: return hdr.StudyDate else: return '19000101' def acq_time(hdr): if 'AcquisitionTime' in hdr: return hdr.AcquisitionTime elif 'StudyTime' in hdr: return hdr.StudyTime else: return '000000' self.series_no = getelem(self._hdr, 'SeriesNumber', int) self.acq_no = getelem(self._hdr, 'AcquisitionNumber', int, 0) self.exam_uid = getelem(self._hdr, 'StudyInstanceUID') self.series_uid = getelem(self._hdr, 'SeriesInstanceUID') self.series_desc = getelem(self._hdr, 'SeriesDescription') self.subj_firstname, self.subj_lastname = self.parse_subject_name( getelem(self._hdr, 'PatientName', None, '')) self.subj_dob = self.parse_subject_dob( getelem(self._hdr, 'PatientBirthDate', None, '')) self.subj_sex = { 'M': 'male', 'F': 'female' }.get(getelem(self._hdr, 'PatientSex')) self.psd_name = os.path.basename( getelem(self._hdr, TAG_PSD_NAME, None, 'unknown')) self.psd_type = nimsmrdata.infer_psd_type(self.psd_name) self.timestamp = datetime.datetime.strptime( acq_date(self._hdr) + acq_time(self._hdr)[:6], '%Y%m%d%H%M%S') self.ti = getelem(self._hdr, 'InversionTime', float, 0.) / 1000.0 self.te = getelem(self._hdr, 'EchoTime', float, 0.) / 1000.0 self.tr = getelem(self._hdr, 'RepetitionTime', float, 0.) / 1000.0 self.flip_angle = getelem(self._hdr, 'FlipAngle', float, 0.) self.pixel_bandwidth = getelem(self._hdr, 'PixelBandwidth', float, 0.) self.phase_encode = int( getelem(self._hdr, 'InPlanePhaseEncodingDirection', None, '') == 'COL') self.mt_offset_hz = getelem(self._hdr, TAG_MTOFF_HZ, float, 0.) self.images_in_mosaic = getelem(self._hdr, TAG_IMAGES_IN_MOSAIC, int, 0) self.total_num_slices = getelem(self._hdr, 'ImagesInAcquisition', int, 0) self.num_slices = getelem(self._hdr, TAG_SLICES_PER_VOLUME, int, 1) self.num_timepoints = getelem(self._hdr, 'NumberOfTemporalPositions', int, self.total_num_slices / self.num_slices) if self.total_num_slices == self.num_slices: self.total_num_slices = self.num_slices * self.num_timepoints self.num_averages = getelem(self._hdr, 'NumberOfAverages', int, 1) self.num_echos = getelem(self._hdr, 'EchoNumbers', int, 1) self.receive_coil_name = getelem(self._hdr, 'ReceiveCoilName', None, 'unknown') self.num_receivers = 0 # FIXME: where is this stored? self.prescribed_duration = self.tr * self.num_timepoints * self.num_averages # FIXME: probably need more hacks in here to compute the correct duration. self.duration = self.prescribed_duration # actual duration can only be computed after all data are loaded self.operator = getelem(self._hdr, 'OperatorsName', None, 'unknown') self.protocol_name = getelem(self._hdr, 'ProtocolName', None, 'unknown') self.scanner_name = '%s %s'.strip() % (getelem( self._hdr, 'InstitutionName', None, ''), getelem(self._hdr, 'StationName', None, '')) self.scanner_type = '%s %s'.strip() % (getelem( self._hdr, 'Manufacturer', None, ''), getelem(self._hdr, 'ManufacturerModelName', None, '')) self.acquisition_type = getelem(self._hdr, 'MRAcquisitionType', None, 'unknown') self.mm_per_vox = getelem( self._hdr, 'PixelSpacing', float, [1., 1.]) + [ getelem(self._hdr, 'SpacingBetweenSlices', float, getelem(self._hdr, 'SliceThickness', float, 1.)) ] # FIXME: confirm that DICOM (Columns,Rows) = PFile (X,Y) self.size = [ getelem(self._hdr, 'Columns', int, 0), getelem(self._hdr, 'Rows', int, 0) ] self.fov = 2 * [ getelem(self._hdr, 'ReconstructionDiameter', float, 0.) ] # Dicom convention is ROW,COL. E.g., ROW is the first dim (index==0), COL is the second (index==1) if self.phase_encode == 1: # The Acquisition matrix field includes four values: [freq rows, freq columns, phase rows, phase columns]. # E.g., for a 64x64 image, it would be [64,0,0,64] if the image row axis was the frequency encoding axis or # [0,64,64,0] if the image row was the phase encoding axis. self.acquisition_matrix = getelem(self._hdr, 'AcquisitionMatrix', None, [0, 0, 0, 0])[0:4:3] self.fov[1] /= ( getelem(self._hdr, 'PercentPhaseFieldOfView', float, 0.) / 100.) if 'PercentPhaseFieldOfView' in self._hdr else 1. else: # We want the acq matrix to always be ROWS,COLS, so we flip the order for the case where the phase encode is the first dim: self.acquisition_matrix = getelem(self._hdr, 'AcquisitionMatrix', None, [0, 0, 0, 0])[2:0:-1] self.fov[0] /= ( getelem(self._hdr, 'PercentPhaseFieldOfView', float, 0.) / 100.) if 'PercentPhaseFieldOfView' in self._hdr else 1. r = getelem(self._hdr, TAG_PHASE_ENCODE_UNDERSAMPLE, None, [1., 1.]) self.phase_encode_undersample, self.slice_encode_undersample = [ float(x) for x in (r.split('\\') if isinstance(r, basestring) else r) ] self.num_bands = 1 # assume that dicoms are never multiband self.qto_xyz = None self.image_type = getelem(self._hdr, 'ImageType', None, []) self.effective_echo_spacing = getelem( self._hdr, TAG_EPI_EFFECTIVE_ECHO_SPACING, float, 0.) / 1e6 self.phase_encode_direction = None # FINDME: 'pepolar'-- stored in bit 4 of rec.dacq_ctrl in pfiles. Probably in a private tag in DICOM. self.is_dwi = bool( self.image_type == TYPE_ORIGINAL and getelem(self._hdr, TAG_DIFFUSION_DIRS, int, 0) >= 6) self.bvals = None self.bvecs = None self.slice_order = None self.slice_duration = None self.reverse_slice_order = None self.notes = '' self.scan_type = self.infer_scan_type() self.dcm_list = None
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