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))
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
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
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 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
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)
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
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
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
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