Ejemplo n.º 1
0
    def __apply_transform__(self, image_info, transformation_files, temp_path):
        """Apply transform(s) to moving image using Transformix.

        Args:
            image_info (tuple[str, int]): File path, echo index (eg. 'scans/000.nii.gz, 0).
            transformation_files (list[str]): Ordered collection of paths to elastix transformation files.
            temp_path (str): Directory path to store temporary data.

        Returns:
            str: File path to warped file in NIfTI format.
        """
        filename, image_id = image_info
        logging.info("Applying transform {}".format(filename))
        warped_file = ''
        for f in transformation_files:
            reg = ApplyWarp()
            reg.inputs.moving_image = filename if len(warped_file) == 0 else warped_file

            reg.inputs.transform_file = f
            reg.inputs.output_path = io_utils.mkdirs(os.path.join(temp_path,
                                                                     "{:03d}".format(image_id)))
            reg.terminal_output = fc.NIPYPE_LOGGING
            reg_output = reg.run()

            warped_file = reg_output.outputs.warped_file
            assert warped_file != ""

        return warped_file
Ejemplo n.º 2
0
    def __save_dir__(self, dir_path: str, create_dir: bool = True):
        """Returns directory path specific to this scan.

        Formatted as '`base_load_dirpath`/`scan.NAME`'.

        Args:
            dir_path (str): Directory path where all data is stored.
            create_dir (`bool`, optional): If `True`, creates directory if it doesn't exist.

        Returns:
            str: Data directory path for this scan.
        """
        # folder_id = '%s-%03d' % (self.NAME, self.series_number)
        folder_id = self.NAME

        name_len = len(folder_id) + 2  # buffer
        if self.NAME in dir_path[-name_len:]:
            scan_dirpath = os.path.join(dir_path, folder_id)
        else:
            scan_dirpath = dir_path

        scan_dirpath = os.path.join(scan_dirpath, folder_id)

        if create_dir:
            scan_dirpath = io_utils.mkdirs(scan_dirpath)

        return scan_dirpath
Ejemplo n.º 3
0
    def __interregister_base_file__(self, base_image_info: tuple, target_path: str, temp_path: str,
                                    mask_path: str = None,
                                    parameter_files=(fc.ELASTIX_RIGID_PARAMS_FILE, fc.ELASTIX_AFFINE_PARAMS_FILE)):
        """Interregister the base moving image to the target image.

        Args:
            base_image_info (tuple[str, int]): File path, echo index (eg. 'scans/000.nii.gz, 0).
            target_path (str): File path to target scan. Must be in nifti (.nii.gz) format.
            temp_path (str): Directory path to store temporary data.
            mask_path (str): Path to mask to use to use as focus points for registration. Mask must be binary. Recommend
                using dilated mask.
            parameter_files (list[str]): Transformix parameter files to use for transformations.

        Returns:
            tuple[str, list[str]): File path to the transformed moving image and a list of file paths to elastix
                transformations (e.g. '/result.nii.gz', ['/tranformation0.txt', '/transformation1.txt']).
        """
        base_image_path, base_time_id = base_image_info

        # Register base image to the target image.
        logging.info("Registering %s (base image)".format(base_image_path))
        transformation_files = []

        use_mask_arr = [False, True]
        reg_output = None
        moving_image = base_image_path

        for i in range(len(parameter_files)):
            use_mask = use_mask_arr[i]
            pfile = parameter_files[i]

            reg = Registration()
            reg.inputs.fixed_image = target_path
            reg.inputs.moving_image = moving_image
            reg.inputs.output_path = io_utils.mkdirs(os.path.join(temp_path,
                                                                     '{:03d}_param{}'.format(base_time_id, i)))
            reg.inputs.parameters = pfile

            if use_mask and mask_path is not None:
                fixed_mask_filepath = self.__dilate_mask__(mask_path, temp_path)
                reg.inputs.fixed_mask = fixed_mask_filepath

            reg.terminal_output = fc.NIPYPE_LOGGING

            reg_output = reg.run()
            reg_output = reg_output.outputs
            assert reg_output is not None

            # Update moving image to output.
            moving_image = reg_output.warped_file

            transformation_files.append(reg_output.transform[0])

        return reg_output.warped_file, transformation_files
Ejemplo n.º 4
0
    def save(self, volume: MedicalVolume, file_path: str):
        """Save volume in NIfTI format,

        Args:
            volume (MedicalVolume): Volume to save.
            file_path (str): File path to NIfTI file.

        Raises:
            ValueError: If `file_path` does not end in a supported NIfTI extension.
        """
        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))

        # Create dir if does not exist
        io_utils.mkdirs(os.path.dirname(file_path))

        nib_affine = volume.affine
        np_im = volume.volume
        nib_img = nib.Nifti1Image(np_im, nib_affine)

        nib.save(nib_img, file_path)
Ejemplo n.º 5
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)
Ejemplo n.º 6
0
    def __save_dirpath__(self, dirpath):
        """Tissue-specific subdirectory to store data.

        Subdirectory will have path '`dirpath`/`self.STR_ID`/'.

        If directory does not exist, it will be created.

        Args:
            dirpath (str): Directory path where all data is stored.

        Returns:
            str: Tissue-specific data directory.
        """
        return io_utils.mkdirs(os.path.join(dirpath, self.STR_ID))
Ejemplo n.º 7
0
    def interregister(self, target_path: str, target_mask_path: str = None):
        base_spin_lock_time, base_image = self.intraregistered_data['BASE']
        files = self.intraregistered_data['FILES']

        temp_interregistered_dirpath = io_utils.mkdirs(
            os.path.join(self.temp_path, 'interregistered'))

        logging.info("")
        logging.info("==" * 40)
        logging.info("Interregistering...")
        logging.info("Target: {}".format(target_path))
        if target_mask_path is not None:
            logging.info("Mask: {}".format(target_mask_path))
        logging.info("==" * 40)

        if not target_mask_path:
            parameter_files = [
                fc.ELASTIX_RIGID_PARAMS_FILE, fc.ELASTIX_AFFINE_PARAMS_FILE
            ]
        else:
            parameter_files = [
                fc.ELASTIX_RIGID_INTERREGISTER_PARAMS_FILE,
                fc.ELASTIX_AFFINE_INTERREGISTER_PARAMS_FILE
            ]

        warped_file, transformation_files = self.__interregister_base_file__(
            (base_image, base_spin_lock_time),
            target_path,
            temp_interregistered_dirpath,
            mask_path=target_mask_path,
            parameter_files=parameter_files)
        warped_files = [(base_spin_lock_time, warped_file)]

        nifti_reader = NiftiReader()

        # Load the transformation file. Apply same transform to the remaining images
        for spin_lock_time, filename in files:
            warped_file = self.__apply_transform__(
                (filename, spin_lock_time), transformation_files,
                temp_interregistered_dirpath)
            # append the last warped file - this has all the transforms applied
            warped_files.append((spin_lock_time, warped_file))

        # copy each of the interregistered warped files to their own output
        subvolumes = dict()
        for spin_lock_time, warped_file in warped_files:
            subvolumes[spin_lock_time] = nifti_reader.load(warped_file)

        self.subvolumes = subvolumes
Ejemplo n.º 8
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 | MedicalVolume): File path for mask or 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 dil_threshold < 0 or dil_threshold > 1:
            raise ValueError("'dil_threshold' must be in range [0, 1]")

        if isinstance(mask_path, MedicalVolume):
            mask = mask_path
        elif os.path.isfile(mask_path):
            mask = fio_utils.generic_load(mask_path, expected_num_volumes=1)
        else:
            raise FileNotFoundError("File {} not found".format(mask_path))

        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
Ejemplo n.º 9
0
    def __save_quant_data__(self, dirpath: str):
        """Save quantitative data and 2D visualizations of femoral cartilage.

        Check which quantitative values (T2, T1rho, etc) are defined for femoral cartilage and analyze these
            1. Save 2D total, superficial, and deep visualization maps.
            2. Save {'medial', 'lateral'}, {'anterior', 'central', 'posterior'}, {'deep', 'superficial'} data to excel
                file

        Args:
            dirpath (str): Directory path to tissue data.
        """
        q_names = []
        dfs = []

        for quant_val in QuantitativeValueType:
            if quant_val.name not in self.quant_vals.keys():
                continue

            q_names.append(quant_val.name)
            q_val = self.quant_vals[quant_val.name]
            dfs.append(q_val[1])

            q_name_dirpath = io_utils.mkdirs(
                os.path.join(dirpath, quant_val.name.lower()))
            for q_map_data in q_val[0]:
                filepath = os.path.join(q_name_dirpath, q_map_data['filename'])
                xlabel = 'Slice'
                ylabel = 'Angle (binned)'
                title = q_map_data['title']
                data_map = q_map_data['data']

                plt.clf()

                upper_bound = BOUNDS[quant_val]

                if preferences.visualization_use_vmax:
                    # Hard bounds - clipping
                    plt.imshow(data_map,
                               cmap='jet',
                               vmin=0.0,
                               vmax=BOUNDS[quant_val])
                else:
                    # Try to use a soft bounds
                    if np.sum(data_map <= upper_bound) == 0:
                        plt.imshow(data_map,
                                   cmap='jet',
                                   vmin=0.0,
                                   vmax=BOUNDS[quant_val])
                    else:
                        warnings.warn(
                            '%s: Pixel value exceeded upper bound (%0.1f). Using normalized scale.'
                            % (quant_val.name, upper_bound))
                        plt.imshow(data_map, cmap='jet')

                plt.xlabel(xlabel)
                plt.ylabel(ylabel)
                plt.title(title)
                clb = plt.colorbar()
                clb.ax.set_title('(ms)')

                plt.savefig(filepath)

                # Save data
                raw_data_filepath = os.path.join(
                    q_name_dirpath, 'raw_data',
                    q_map_data['raw_data_filename'])
                io_utils.save_pik(raw_data_filepath, data_map)

        if len(dfs) > 0:
            io_utils.save_tables(os.path.join(dirpath, 'data.xlsx'), dfs,
                                 q_names)
Ejemplo n.º 10
0
    def __save_quant_data__(self, dirpath):
        """Save quantitative data and 2D visualizations of patellar cartilage

        Check which quantitative values (T2, T1rho, etc) are defined for
        patellar cartilage and analyze these:

        1. Save 2D total, superficial, and deep visualization maps
        2. Save {'medial', 'lateral'}, {'anterior', 'posterior'},
            {'superior', 'inferior', 'total'} data to excel file

        :param dirpath: base filepath to save data
        """
        q_names = []
        dfs = []

        for quant_val in QuantitativeValueType:
            if quant_val.name not in self.quant_vals.keys():
                continue

            q_names.append(quant_val.name)
            q_val = self.quant_vals[quant_val.name]
            dfs.append(q_val[1])

            q_name_dirpath = io_utils.mkdirs(
                os.path.join(dirpath, quant_val.name.lower()))
            for q_map_data in q_val[0]:
                filepath = os.path.join(q_name_dirpath, q_map_data["filename"])
                xlabel = ""
                ylabel = ""
                title = q_map_data["title"]
                data_map = q_map_data["data"]

                axs_bounds = self.__get_axis_bounds__(data_map,
                                                      leave_buffer=True)

                plt.clf()

                upper_bound = BOUNDS[quant_val]
                if preferences.visualization_use_vmax:
                    # Hard bounds - clipping
                    plt.imshow(data_map,
                               cmap="jet",
                               vmin=0.0,
                               vmax=BOUNDS[quant_val])
                else:
                    # Try to use a soft bounds
                    if np.sum(data_map <= upper_bound) == 0:
                        plt.imshow(data_map,
                                   cmap="jet",
                                   vmin=0.0,
                                   vmax=BOUNDS[quant_val])
                    else:
                        warnings.warn(
                            "%s: Pixel value exceeded upper bound (%0.1f). Using normalized scale."
                            % (quant_val.name, upper_bound))
                        plt.imshow(data_map, cmap="jet")

                plt.xlabel(xlabel)
                plt.ylabel(ylabel)
                plt.title(title)
                plt.ylim(axs_bounds[0])
                plt.gca().invert_yaxis()
                plt.xlim(axs_bounds[1])
                # plt.axis('tight')
                clb = plt.colorbar()
                clb.ax.set_ylabel("(ms)")
                plt.savefig(filepath)

                # Save data
                raw_data_filepath = os.path.join(
                    q_name_dirpath, "raw_data",
                    q_map_data["raw_data_filename"])
                io_utils.save_pik(raw_data_filepath, data_map)

        if len(dfs) > 0:
            io_utils.save_tables(os.path.join(dirpath, "data.xlsx"), dfs,
                                 q_names)
Ejemplo n.º 11
0
 def setUpClass(cls):
     io_utils.mkdirs(IO_UTILS_DATA)
Ejemplo n.º 12
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 = preferences.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.volumes = intraregistered_volumes
Ejemplo n.º 13
0
 def setUpClass(cls):
     cls.dicom_dirpath = get_dicoms_path(os.path.join(UNITTEST_SCANDATA_PATH, cls.SCAN_TYPE.NAME))
     cls.data_dirpath = get_data_path(os.path.join(UNITTEST_SCANDATA_PATH, cls.SCAN_TYPE.NAME))
     io_utils.mkdirs(cls.data_dirpath)
Ejemplo n.º 14
0
    def interregister(self, target_path: str, target_mask_path: str = None):
        temp_raw_dirpath = io_utils.mkdirs(os.path.join(self.temp_path, "raw"))
        subvolumes = self.subvolumes

        raw_filepaths = dict()

        echo_time_inds = natsorted(list(subvolumes.keys()))

        for i in range(len(echo_time_inds)):
            raw_filepath = os.path.join(temp_raw_dirpath,
                                        "{:03d}.nii.gz".format(i))
            subvolumes[i].save_volume(raw_filepath)
            raw_filepaths[i] = raw_filepath

        # last echo should be base
        base_echo_time, base_image = len(echo_time_inds) - 1, raw_filepaths[
            len(echo_time_inds) - 1]

        temp_interregistered_dirpath = io_utils.mkdirs(
            os.path.join(self.temp_path, "interregistered"))

        logging.info("")
        logging.info("==" * 40)
        logging.info("Interregistering...")
        logging.info("Target: {}".format(target_path))
        if target_mask_path is not None:
            logging.info("Mask: {}".format(target_mask_path))
        logging.info("==" * 40)

        files_to_warp = []
        for echo_time_ind in raw_filepaths.keys():
            if echo_time_ind == base_echo_time:
                continue
            filepath = raw_filepaths[echo_time_ind]
            files_to_warp.append((echo_time_ind, filepath))

        if not target_mask_path:
            parameter_files = [
                fc.ELASTIX_RIGID_PARAMS_FILE, fc.ELASTIX_AFFINE_PARAMS_FILE
            ]
        else:
            parameter_files = [
                fc.ELASTIX_RIGID_INTERREGISTER_PARAMS_FILE,
                fc.ELASTIX_AFFINE_INTERREGISTER_PARAMS_FILE
            ]

        warped_file, transformation_files = self.__interregister_base_file__(
            (base_image, base_echo_time),
            target_path,
            temp_interregistered_dirpath,
            mask_path=target_mask_path,
            parameter_files=parameter_files)
        warped_files = [(base_echo_time, warped_file)]

        # Load the transformation file. Apply same transform to the remaining images
        for echo_time, filename in files_to_warp:
            warped_file = self.__apply_transform__(
                (filename, echo_time), transformation_files,
                temp_interregistered_dirpath)
            # append the last warped file - this has all the transforms applied
            warped_files.append((echo_time, warped_file))

        # copy each of the interregistered warped files to their own output
        nifti_reader = NiftiReader()
        subvolumes = dict()
        for echo_time, warped_file in warped_files:
            subvolumes[echo_time] = nifti_reader.load(warped_file)

        self.subvolumes = subvolumes
Ejemplo n.º 15
0
    def __intraregister__(self, subvolumes):
        """Intra-register volumes.

        Patient could have moved between acquisition of different volumes, so different volumes of CubeQuant scan
            have to be registered with each other.

        The first spin lock time has the highest SNR, so it is used as the target. Volumes corresponding to the other
            spin lock times are registered to the target.

        Affine registration is done using Elastix.

        Args:
            subvolumes (dict[int, MedicalVolume]): Dictionary of spin lock time index -> volume. (e.g.
                {0 --> MedicalVolume A, 1 --> MedicalVolume B}).

        Returns:
            dict[int, str]: Dictionary of base, other files spin-lock index -> output nifti file path.
        """

        if subvolumes is None:
            raise TypeError("subvolumes must be dict")

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

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

        # Use first spin lock time as a basis for registration
        spin_lock_nii_files = []
        for spin_lock_time_index in ordered_spin_lock_time_indices:
            filepath = os.path.join(
                raw_volumes_base_path,
                "{:03d}.nii.gz".format(spin_lock_time_index))
            spin_lock_nii_files.append(filepath)

            subvolumes[spin_lock_time_index].save_volume(filepath)

        target_filepath = spin_lock_nii_files[0]

        intraregistered_files = []
        for i in range(1, len(spin_lock_nii_files)):
            spin_file = spin_lock_nii_files[i]
            spin_lock_time_index = ordered_spin_lock_time_indices[i]

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

            warped_file = tmp.outputs.warped_file
            intraregistered_files.append((spin_lock_time_index, warped_file))

        return {
            "BASE":
            (ordered_spin_lock_time_indices[0], spin_lock_nii_files[0]),
            "FILES": intraregistered_files
        }