Example #1
0
    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
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 __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
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