Example #1
0
    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()
Example #2
0
    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
Example #3
0
    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()
Example #4
0
    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