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
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
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
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)
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)
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))
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
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
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)
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)
def setUpClass(cls): io_utils.mkdirs(IO_UTILS_DATA)
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
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)
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
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 }