def __intraregister__(self, volumes: List[MedicalVolume]): """ Intraregister different subvolumes to ensure they are in the same space during computation :param volumes: a list of MedicalVolumes :return: """ 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) print('') print('==' * 40) print('Intraregistering...') print('==' * 40) # temporarily save subvolumes as nifti file raw_volumes_base_path = io_utils.check_dir(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' % echo_index + '.nii.gz') 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.check_dir(os.path.join(self.temp_path, 'intraregistered', '%03d' % echo_index)) reg.inputs.parameters = [fc.ELASTIX_AFFINE_PARAMS_FILE] reg.terminal_output = fc.NIPYPE_LOGGING print('Registering %s --> %s' % (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
def __apply_transform__(self, image_info, transformation_files, temp_path): """Apply transform(s) to moving image using transformix :param image_info: tuple of filepath, echo index (eg. 'scans/000.nii.gz, 0) :param transformation_files: list of filepaths to elastix transformations :param temp_path: path to store temporary data :return: filepath to warped file in nifti (.nii.gz) format """ filename, image_id = image_info print('Applying transform %s' % 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.check_dir( os.path.join(temp_path, '%03d' % 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 handle_mat(vargin): mat_filepath = vargin[FILE_KEY][0] var_name = vargin[VARIABLE_KEY][0] sample_dicom_path = vargin[SAMPLE_DICOM_KEY][0] save_path = vargin[SAVE_KEY] # extract array from mat file mat_contents = sio.loadmat(mat_filepath) arr = mat_contents[var_name] # extract pixel spacing from dicom pixel_spacing = dicom_utils.get_pixel_spacing(sample_dicom_path) v = MedicalVolume(arr, pixel_spacing) fs = os.path.splitext(os.path.basename(mat_filepath)) filename = fs[0] if save_path is None: save_path = os.path.dirname(mat_filepath) save_path = io_utils.check_dir(save_path) save_filepath = os.path.join(save_path, '%s.nii.gz' % filename) v.save_volume(save_filepath)
def __save_quant_data__(self, dirpath): """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 :param dirpath: base filepath to save data """ q_names = [] dfs = [] for quant_val in QuantitativeValues: 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.check_dir(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] is_picture_written = False if defaults.VISUALIZATION_HARD_BOUNDS: plt.imshow(data_map, cmap='jet', vmin=0.0, vmax=BOUNDS[quant_val]) is_picture_written = True if defaults.VISUALIZATION_SOFT_BOUNDS and not is_picture_written: if np.sum(data_map <= upper_bound) == 0: plt.imshow(data_map, cmap='jet', vmin=0.0, vmax=BOUNDS[quant_val]) is_picture_written = True else: warnings.warn('%s: Pixel value exceeded upper bound (%0.1f). Using normalized scale.' % (quant_val.name, upper_bound)) if not is_picture_written: plt.imshow(data_map, cmap='jet') plt.xlabel(xlabel) plt.ylabel(ylabel) plt.title(title) plt.colorbar() plt.savefig(filepath) if len(dfs) > 0: io_utils.save_tables(os.path.join(dirpath, 'data.xlsx'), dfs, q_names)
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 :param base_image_info: tuple of filepath, echo index (eg. 'scans/000.nii.gz, 0) :param target_path: filepath to target scan - should be in nifti (.nii.gz) format :param temp_path: path to store temporary data :param mask_path: path to mask to use to use as focus points for registration, mask must be binary recommend that this is the path to a dilated version of the mask for registration purposes :param parameter_files: list of filepaths to elastix parameter files to use for transformations :return: tuple of the path to the transformed moving image and a list of filepaths 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 print('Registering %s (base image)' % 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.check_dir( os.path.join(temp_path, '%03d_param%i' % (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, im: MedicalVolume, filepath: str): """ Save a MedicalVolume in NIfTI format :param im: a Medical Volume :param filepath: a string defining filepath to save image to :raises ValueError if filepath does not contain supported NIfTI extension """ if not self.data_format_code.is_filetype(filepath): raise ValueError( '%s must be a file with extension `.nii` or `.nii.gz`' % filepath) # Create dir if does not exist io_utils.check_dir(os.path.dirname(filepath)) nib_affine = im.affine np_im = im.volume nib_img = nib.Nifti1Image(np_im, nib_affine) nib.save(nib_img, filepath)
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.check_dir( os.path.join(self.temp_path, 'interregistered')) print('') print('==' * 40) print('Interregistering...') print('Target: %s' % target_path) if target_mask_path: print('Mask: %s' % target_mask_path) print('==' * 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 save(self, im, filepath): """ Save a medical volume in dicom format :param im: a Medical Volume :param filepath: a path to a directory to store dicom files :raises ValueError if im (MedicalVolume) does not have initialized headers :raises ValueError if im was flipped across any axis. Flipping changes scanner origin, which is currently not handled """ # Get orientation indicated by headers headers = im.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]) != im.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 = im.orientation im.reformat(orientation) volume = im.volume assert volume.shape[2] == len( headers), "Dimension mismatch - %d slices but %d headers" % ( volume.shape[-1], len(headers)) # check if filepath exists filepath = io_utils.check_dir(filepath) 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(filepath, filename_format % (s + 1)) self.__write_dicom_file__(volume[..., 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 im.reformat(original_orientation)
def __save_dir__(self, dirpath, create_dir=True): """Returns directory specific to this scan :param dirpath: base directory path to locate data directory for this scan :param create_dir: create the data directory :return: data directory for this scan type """ name_len = len(self.NAME) + 2 # buffer if self.NAME in dirpath[-name_len:]: scan_dirpath = os.path.join(dirpath, '%s_data' % self.NAME) else: scan_dirpath = dirpath scan_dirpath = os.path.join(scan_dirpath, '%s_data' % self.NAME) if create_dir: scan_dirpath = io_utils.check_dir(scan_dirpath) return scan_dirpath
def __dilate_mask__( self, mask_path: str, temp_path: str, dil_rate: float = defaults.DEFAULT_MASK_DIL_RATE, dil_threshold: float = defaults.DEFAULT_MASK_DIL_THRESHOLD): """Dilate mask using gaussian blur and write to disk to use with elastix :param mask_path: path to mask to use to use as focus points for registration, mask must be binary :param temp_path: path to store temporary data :param dil_rate: dilation rate (sigma) :param dil_threshold: threshold to binarize dilated mask - float between [0, 1] :return: the path to the dilated mask :raise FileNotFoundError: 1. filepath specified by mask_path is not found :raise ValueError: 1. dil_threshold not in range [0, 1] """ if not os.path.isfile(mask_path): raise FileNotFoundError('File %s not found' % 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.check_dir(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_dir__(self, dirpath: str, create_dir: bool = True): """Returns directory specific to this scan :param dirpath: base directory path to locate data directory for this scan :param create_dir: create the data directory :return: data directory 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 dirpath[-name_len:]: scan_dirpath = os.path.join(dirpath, folder_id) else: scan_dirpath = dirpath scan_dirpath = os.path.join(scan_dirpath, folder_id) if create_dir: scan_dirpath = io_utils.check_dir(scan_dirpath) return scan_dirpath
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.check_dir(cls.data_dirpath)
def interregister(self, target_path, mask_path=None): temp_raw_dirpath = io_utils.check_dir( 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' % 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.check_dir( os.path.join(self.temp_path, 'interregistered')) print('') print('==' * 40) print('Interregistering...') print('Target: %s' % target_path) if mask_path is not None: print('Mask: %s' % mask_path) print('==' * 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 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=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 subvolumes = dict() for echo_time, warped_file in warped_files: subvolumes[echo_time] = io_utils.load_nifti(warped_file) self.subvolumes = subvolumes
def setUpClass(cls): io_utils.check_dir(IO_UTILS_DATA)
def __save_dirpath__(self, dirpath): """Subdirectory to store data - save_dirpath/self.STR_ID/ :param dirpath: base dirpath :return: """ return io_utils.check_dir(os.path.join(dirpath, '%s' % self.STR_ID))
def __intraregister__(self, subvolumes): """Intraregister cubequant subvolumes to each other Patient could have moved between acquisition of different subvolumes, so different subvolumes 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 Subvolumes corresponding to the other spin lock times are registered to the target Affine registration is done using elastix :param subvolumes: dictionary of subvolumes mapping spin lock time index --> MedicalVolume (e.g. {0 --> MedicalVolume A, 1 --> MedicalVolume B} :return: a dictionary of base, other files spin-lock index --> nifti filepath """ if subvolumes is None: raise ValueError('subvolumes must be dict()') print('') print('==' * 40) print('Intraregistering...') print('==' * 40) # temporarily save subvolumes as nifti file ordered_spin_lock_time_indices = natsorted(list(subvolumes.keys())) raw_volumes_base_path = io_utils.check_dir( 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' % spin_lock_time_index + '.nii.gz') 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.check_dir( os.path.join(self.temp_path, 'intraregistered', '%03d' % spin_lock_time_index)) reg.inputs.parameters = [fc.ELASTIX_AFFINE_PARAMS_FILE] reg.terminal_output = fc.NIPYPE_LOGGING print('Registering %s --> %s' % (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 }