Beispiel #1
0
    def check_orientations(self, mv: MedicalVolume, orientations):
        """
        Apply each orientation specified in orientations to the Medical Volume mv
        Assert if mv --> apply orientation --> apply original orientation != mv original position coordinates
        :param mv: a Medical Volume
        :param orientations: a list or tuple of orientation tuples
        """
        o_base, so_base, ps_base = mv.orientation, mv.scanner_origin, mv.pixel_spacing
        ps_affine = np.array(mv.affine)

        for o in orientations:
            # Reorient to some orientation
            mv.reformat(o)

            # Reorient to original orientation
            mv.reformat(o_base)

            assert mv.orientation == o_base, "Orientation mismatch: Expected %s, got %s" % (
                str(o_base), str(mv.orientation))
            assert mv.scanner_origin == so_base, "Scanner Origin mismatch: Expected %s, got %s" % (
                str(so_base), str(mv.scanner_origin))
            assert mv.pixel_spacing == ps_base, "Pixel Spacing mismatch: Expected %s, got %s" % (
                str(ps_base), str(mv.pixel_spacing))

            assert (mv.affine == ps_affine).all(
            ), "Affine matrix mismatch: Expected\n%s\ngot\n%s" % (
                str(ps_affine), str(mv.affine))
Beispiel #2
0
    def __calc_quant_vals__(self, quant_map: MedicalVolume, map_type: QuantitativeValueType):
        """Helper method to get quantitative values for tissue - implemented per tissue.

        Different tissues should override this as they see fit.

        Args:
            quant_map (MedicalVolume): 3D map of pixel-wise quantitative measures (T2, T2*, T1-rho, etc.). Volume should
                have `np.nan` values for all pixels unable to be calculated.
            map_type (QuantitativeValueType): Type of quantitative value to analyze.

        Raises:
            TypeError: If `quant_map` is not of type `MedicalVolume` or `map_type` is not of type
                `QuantitativeValueType`.
            ValueError: If no mask is found for tissue.
        """
        if not isinstance(quant_map, MedicalVolume):
            raise TypeError("`Expected type 'MedicalVolume' for `quant_map`")
        if not isinstance(map_type, QuantitativeValueType):
            raise TypeError("`Expected type 'QuantitativeValueType' for `map_type`")

        if self.__mask__ is None:
            raise ValueError("Please initialize mask for {}".format(self.FULL_NAME))

        quant_map.reformat(self.__mask__.orientation)
        pass
Beispiel #3
0
    def set_mask(self, mask: MedicalVolume):
        """Set mask for tissue.

        Args:
            mask (MedicalVolume): Binary mask of segmented tissue.
        """
        assert type(mask) is MedicalVolume, "mask for tissue must be of type MedicalVolume"
        mask.reformat(SAGITTAL)
        self.__mask__ = mask
Beispiel #4
0
    def save(self, volume: MedicalVolume, dir_path: str):
        """Save `medical volume` in dicom format.

        Args:
            volume (MedicalVolume): Volume to save.
            dir_path: Directory path to store dicom files. Dicoms are stored in directories, as multiple files are
                needed to store the volume.

        Raises:
            ValueError: If `im` does not have initialized headers. Or if `im` was flipped across any axis. Flipping
                changes scanner origin, which is currently not handled.
        """
        # Get orientation indicated by headers.
        headers = volume.headers
        if headers is None:
            raise ValueError(
                "MedicalVolume headers must be initialized to save as a dicom")

        affine = LPSplus_to_RASplus(headers)
        orientation = stdo.orientation_nib_to_standard(nib.aff2axcodes(affine))

        # Currently do not support mismatch in scanner_origin.
        if tuple(affine[:3, 3]) != volume.scanner_origin:
            raise ValueError(
                "Scanner origin mismatch. "
                "Currently we do not handle mismatch in scanner origin (i.e. cannot flip across axis)"
            )

        # Reformat medical volume to expected orientation specified by dicom headers.
        # Store original orientation so we can undo the dicom-specific reformatting.
        original_orientation = volume.orientation

        volume.reformat(orientation)
        volume_arr = volume.volume
        assert volume_arr.shape[2] == len(headers), \
            "Dimension mismatch - {:d} slices but {:d} headers".format(volume_arr.shape[-1], len(headers))

        # Check if dir_path exists.
        dir_path = io_utils.mkdirs(dir_path)

        num_slices = len(headers)
        filename_format = "I%0" + str(max(4, ceil(
            log10(num_slices)))) + "d.dcm"

        for s in range(num_slices):
            s_filepath = os.path.join(dir_path, filename_format % (s + 1))
            self.__write_dicom_file__(volume_arr[..., s], headers[s],
                                      s_filepath)

        # Reformat image to original orientation (before saving).
        # We do this, because saving should not affect the existing state of any variable.
        volume.reformat(original_orientation)
Beispiel #5
0
    def fit(self):
        svs = []
        msk = None

        subvolumes = self.subvolumes
        for sv in subvolumes[1:]:
            assert subvolumes[0].is_same_dimensions(
                sv), "Dimension mismatch within subvolumes"

        if self.mask:
            assert subvolumes[0].is_same_dimensions(
                self.mask,
                defaults.AFFINE_DECIMAL_PRECISION), "Mask dimension mismatch"
            msk = self.mask.volume
            msk = msk.reshape(1, -1)

        original_shape = subvolumes[0].volume.shape
        affine = np.array(self.subvolumes[0].affine)

        for i in range(len(self.ts)):
            sv = subvolumes[i].volume

            svr = sv.reshape((1, -1))
            if msk is not None:
                svr = svr * msk

            svs.append(svr)

        svs = np.concatenate(svs)

        vals, r_squared = __fit_monoexp_tc__(self.ts, svs, self.tc0)

        map_unfiltered = vals.reshape(original_shape)
        r_squared = r_squared.reshape(original_shape)

        # All accepted values must meet an r-squared threshold of `DEFAULT_R2_THRESHOLD`.
        tc_map = map_unfiltered * (r_squared >=
                                   preferences.fitting_r2_threshold)

        # Filter calculated values that are below limit bounds.
        tc_map[tc_map <= self.bounds[0]] = np.nan
        tc_map = np.nan_to_num(tc_map)
        tc_map[tc_map > self.bounds[1]] = np.nan
        tc_map = np.nan_to_num(tc_map)

        tc_map = np.around(tc_map, self.decimal_precision)

        time_constant_volume = MedicalVolume(tc_map, affine=affine)
        rsquared_volume = MedicalVolume(r_squared, affine=affine)

        return time_constant_volume, rsquared_volume
Beispiel #6
0
    def load(self, file_path):
        """Load volume from NIfTI file path.

        A NIfTI file should only correspond to one volume.

        Args:
            file_path (str): File path to NIfTI file.

        Returns:
            MedicalVolume: Loaded volume.

        Raises:
            FileNotFoundError: If `file_path` not found.
            ValueError: If `file_path` does not end in a supported NIfTI extension.
        """
        if not os.path.isfile(file_path):
            raise FileNotFoundError("{} not found".format(file_path))

        if not self.data_format_code.is_filetype(file_path):
            raise ValueError("{} must be a file with extension '.nii' or '.nii.gz'".format(file_path))

        nib_img = nib.load(file_path)
        nib_img_affine = nib_img.affine
        nib_img_affine = self.__normalize_affine(nib_img_affine)

        np_img = nib_img.get_fdata()

        return MedicalVolume(np_img, nib_img_affine)
Beispiel #7
0
    def __dilate_mask__(self, mask_path: str, temp_path: str,
                        dil_rate: float = preferences.mask_dilation_rate,
                        dil_threshold: float = preferences.mask_dilation_threshold):
        """Dilate mask using gaussian blur and write to disk to use with Elastix.

        Args:
            mask_path (str): File path for mask to use to use as focus points for registration. Mask must be binary.
            temp_path (str): Directory path to store temporary data.
            dil_rate (`float`, optional): Dilation rate (sigma). Defaults to `preferences.mask_dilation_rate`.
            dil_threshold (`float`, optional): Threshold to binarize dilated mask. Must be between [0, 1]. Defaults to
                `preferences.mask_dilation_threshold`.

        Returns:
            str: File path of dilated mask.

        Raises:
            FileNotFoundError: If `mask_path` not valid file.
            ValueError: If `dil_threshold` not in range [0, 1].
        """

        if not os.path.isfile(mask_path):
            raise FileNotFoundError("File {} not found".format(mask_path))

        if dil_threshold < 0 or dil_threshold > 1:
            raise ValueError("'dil_threshold' must be in range [0, 1]")

        mask = fio_utils.generic_load(mask_path, expected_num_volumes=1)

        dilated_mask = sni.gaussian_filter(np.asarray(mask.volume, dtype=np.float32),
                                           sigma=dil_rate) > dil_threshold
        fixed_mask = np.asarray(dilated_mask,
                                dtype=np.int8)
        fixed_mask_filepath = os.path.join(io_utils.mkdirs(temp_path), "dilated-mask.nii.gz")

        dilated_mask_volume = MedicalVolume(fixed_mask,
                                            affine=mask.affine)
        dilated_mask_volume.save_volume(fixed_mask_filepath)

        return fixed_mask_filepath
Beispiel #8
0
    def load(self,
             dicom_dir_path: str,
             group_by: Union[str, tuple] = 'EchoNumbers',
             ignore_ext: bool = False):
        """Load dicoms into `MedicalVolume`s grouped by `group_by` tag.

        Args:
            dicom_dir_path (str): Path to directory with dicom files.
            group_by (`str` or `tuple`, optional): DICOM field tag name or tag number used to group dicoms. Defaults
                to `EchoNumbers`. Most DICOM headers encode different echo numbers as volumes acquired at different echo
                times or different phases.
            ignore_ext (`bool`, optional): Ignore extension (`.dcm`) when loading dicoms. Defaults to `False`.

        Returns:
            list[MedicalVolume]: Different volumes grouped by the `group_by` DICOM tag.

        Raises:
            NotADirectoryError: If `dicom_dir_path` does not exist or is not a directory.
            FileNotFoundError: If no dicom files found in directory.

        Note:
            This function sorts files using natsort, an intelligent sorting tool. Please verify dicoms are labeled in a
                sequenced manner (e.g.: dicom1,dicom2,dicom3,...).
        """
        if not os.path.isdir(dicom_dir_path):
            raise NotADirectoryError(
                "Directory {} does not exist".format(dicom_dir_path))

        if not group_by:
            raise ValueError(
                "`group_by` must be specified, even if there are not multiple volumes encoded in dicoms"
            )

        possible_files = os.listdir(dicom_dir_path)

        lstFilesDCM = []
        for f in possible_files:
            # If ignore extension, don't look for '.dcm' extension.
            match_ext = ignore_ext or (not ignore_ext and
                                       self.data_format_code.is_filetype(f))
            is_file = os.path.isfile(os.path.join(dicom_dir_path, f))
            is_hidden_file = f.startswith('.')
            if is_file and match_ext and not is_hidden_file:
                lstFilesDCM.append(os.path.join(dicom_dir_path, f))

        lstFilesDCM = natsorted(lstFilesDCM)
        if len(lstFilesDCM) == 0:
            raise FileNotFoundError(
                "No files found in directory {}".format(dicom_dir_path))

        # Check if dicom file has the group_by element specified
        temp_dicom = pydicom.read_file(lstFilesDCM[0], force=True)

        if not temp_dicom.get(group_by):
            raise ValueError("Tag {} does not exist in dicom".format(group_by))

        dicom_data = {}

        for dicom_filename in lstFilesDCM:
            # read the file
            ds = pydicom.read_file(dicom_filename, force=True)
            val_groupby = ds.get(group_by)
            if type(val_groupby) is pydicom.DataElement:
                val_groupby = val_groupby.value

            if val_groupby not in dicom_data.keys():
                dicom_data[val_groupby] = {"headers": [], "arr": []}

            dicom_data[val_groupby]["headers"].append(ds)
            dicom_data[val_groupby]["arr"].append(ds.pixel_array)

        vols = []
        for k in sorted(list(dicom_data.keys())):
            dd = dicom_data[k]
            headers = dd["headers"]
            if len(headers) == 0:
                continue
            arr = np.stack(dd["arr"], axis=-1)

            affine = LPSplus_to_RASplus(headers)

            vol = MedicalVolume(arr, affine, headers=headers)
            vols.append(vol)

        return vols
Beispiel #9
0
    def generate_t2_map(self,
                        tissue: Tissue,
                        suppress_fat: bool = False,
                        suppress_fluid: bool = False,
                        beta: float = 1.2,
                        gl_area: float = None,
                        tg: float = None):
        """Generate 3D T2 map.

        Method is detailed in this `paper <https://www.ncbi.nlm.nih.gov/pubmed/28017730>`_.

        Args:
            tissue (Tissue): Tissue to generate T2 map for.
            suppress_fat (`bool`, optional): Suppress fat region in T2 computation. Helps reduce noise.
            suppress_fluid (`bool`, optional): Suppress fluid region in T2 computation. Fluid-nulled image is calculated
                as `S1 - beta*S2`.
            beta (`float`, optional): Beta value used for suppressing fluid. Defaults to 1.2.
            gl_area (`float`, optional): GL Area. Required if not provided in the dicom. Defaults to value in dicom
                tag '0x001910b6'.
            tg: tg value (in microseconds). Required if not provided in the dicom. Defaults to value in dicom tag
                '0x001910b7'.

        Returns:
            qv.T2: T2 fit for tissue.
        """

        if not self.__validate_scan__() and (not gl_area or not tg):
            raise ValueError(
                'dicoms in \'%s\' do not contain GL_Area and Tg tags. Please input manually'
                % self.dicom_path)

        if self.volumes is None or self.ref_dicom is None:
            raise ValueError(
                'volumes and ref_dicom fields must be initialized')

        ref_dicom = self.ref_dicom

        r, c, num_slices = self.volumes[0].volume.shape
        subvolumes = self.volumes

        # Split echos
        echo_1 = subvolumes[0].volume
        echo_2 = subvolumes[1].volume

        # All timing in seconds
        TR = float(ref_dicom.RepetitionTime) * 1e-3
        TE = float(ref_dicom.EchoTime) * 1e-3
        Tg = tg * 1e-6 if tg else float(
            ref_dicom[self.__TG_TAG__].value) * 1e-6
        T1 = float(tissue.T1_EXPECTED) * 1e-3

        # Flip Angle (degree -> radians)
        alpha = math.radians(float(ref_dicom.FlipAngle))

        GlArea = gl_area if gl_area else float(
            ref_dicom[self.__GL_AREA_TAG__].value)

        Gl = GlArea / (Tg * 1e6) * 100
        gamma = 4258 * 2 * math.pi  # Gamma, Rad / (G * s).
        dkL = gamma * Gl * Tg

        # Simply math
        k = math.pow(
            (math.sin(alpha / 2)),
            2) * (1 + math.exp(-TR / T1 - TR * math.pow(dkL, 2) * self.__D__)
                  ) / (1 - math.cos(alpha) *
                       math.exp(-TR / T1 - TR * math.pow(dkL, 2) * self.__D__))

        c1 = (TR - Tg / 3) * (math.pow(dkL, 2)) * self.__D__

        # T2 fit
        mask = np.ones([r, c, num_slices])

        ratio = mask * echo_2 / echo_1
        ratio = np.nan_to_num(ratio)

        # have to divide division into steps to avoid overflow error
        t2map = (-2000 * (TR - TE) / (np.log(abs(ratio) / k) + c1))

        t2map = np.nan_to_num(t2map)

        # Filter calculated T2 values that are below 0ms and over 100ms
        t2map[t2map <= self.__T2_LOWER_BOUND__] = np.nan
        t2map = np.nan_to_num(t2map)
        t2map[t2map > self.__T2_UPPER_BOUND__] = np.nan
        t2map = np.nan_to_num(t2map)

        t2map = np.around(t2map, self.__T2_DECIMAL_PRECISION__)

        if suppress_fat:
            t2map = t2map * (echo_1 > 0.15 * np.max(echo_1))

        if suppress_fluid:
            vol_null_fluid = echo_1 - beta * echo_2
            t2map = t2map * (vol_null_fluid > 0.1 * np.max(vol_null_fluid))

        t2_map_wrapped = MedicalVolume(t2map,
                                       affine=subvolumes[0].affine,
                                       headers=deepcopy(subvolumes[0].headers))
        t2_map_wrapped = T2(t2_map_wrapped)

        tissue.add_quantitative_value(t2_map_wrapped)

        return t2_map_wrapped
Beispiel #10
0
    def __intraregister__(self, volumes: List[MedicalVolume]):
        """Intraregister volumes.

        Sets `self.volumes` to intraregistered volumes.

        Args:
            volumes (list[MedicalVolume]): Volumes to register.

        Raises:
            TypeError: If `volumes` is not `list[MedicalVolume]`.
        """
        if (not volumes) or (type(volumes) is not list) or (
                len(volumes) != __EXPECTED_NUM_ECHO_TIMES__):
            raise TypeError("`volumes` must be of type List[MedicalVolume]")

        num_echos = len(volumes)

        logging.info("")
        logging.info("==" * 40)
        logging.info("Intraregistering...")
        logging.info("==" * 40)

        # temporarily save subvolumes as nifti file
        raw_volumes_base_path = io_utils.mkdirs(
            os.path.join(self.temp_path, "raw"))

        # Use first subvolume as a basis for registration - save in nifti format to use with elastix/transformix
        volume_files = []
        for echo_index in range(num_echos):
            filepath = os.path.join(raw_volumes_base_path,
                                    "{:03d}.nii.gz".format(echo_index))
            volume_files.append(filepath)

            volumes[echo_index].save_volume(filepath,
                                            data_format=ImageDataFormat.nifti)

        target_echo_index = 0
        target_image_filepath = volume_files[target_echo_index]

        nr = NiftiReader()
        intraregistered_volumes = [deepcopy(volumes[target_echo_index])]
        for echo_index in range(1, num_echos):
            moving_image = volume_files[echo_index]

            reg = Registration()
            reg.inputs.fixed_image = target_image_filepath
            reg.inputs.moving_image = moving_image
            reg.inputs.output_path = io_utils.mkdirs(
                os.path.join(self.temp_path, "intraregistered",
                             "{:03d}".format(echo_index)))
            reg.inputs.parameters = [fc.ELASTIX_AFFINE_PARAMS_FILE]
            reg.terminal_output = fc.NIPYPE_LOGGING
            logging.info("Registering {} -> {}".format(str(echo_index),
                                                       str(target_echo_index)))
            tmp = reg.run()

            warped_file = tmp.outputs.warped_file
            intrareg_vol = nr.load(warped_file)

            # copy affine from original volume, because nifti changes loading accuracy
            intrareg_vol = MedicalVolume(volume=intrareg_vol.volume,
                                         affine=volumes[echo_index].affine,
                                         headers=deepcopy(
                                             volumes[echo_index].headers))

            intraregistered_volumes.append(intrareg_vol)

        self.raw_volumes = deepcopy(volumes)
        self.volumes = intraregistered_volumes