import nibabel as nib import numpy as np from scipy.interpolate import interp1d def nyul_apply_standard_scale(input_image, standard_hist, input_mask=None, interp_type='linear'): """ Based on J.Reinhold code: https://github.com/jcreinhold/intensity-normalization Use Nyul and Udupa method ([1,2]) to normalize the intensities of a MRI image passed as input. Args: input_image (np.ndarray): input image to normalize standard_hist (str): path to output or use standard histogram landmarks input_mask (nii): optional brain mask Returns: normalized (np.ndarray): normalized input image References: [1] N. Laszlo G and J. K. Udupa, “On Standardizing the MR Image Intensity Scale,” Magn. Reson. Med., vol. 42, pp. 1072–1081, 1999. [2] M. Shah, Y. Xiao, N. Subbanna, S. Francis, D. L. Arnold, D. L. Collins, and T. Arbel, “Evaluating intensity normalization on MRIs of human brain with multiple sclerosis,” Med. Image Anal., vol. 15, no. 2, pp. 267–282, 2011. """ # load learned standard scale and the percentiles standard_scale, percs = np.load(standard_hist) # apply transformation to image return do_hist_normalization(input_image, percs, standard_scale, input_mask, interp_type=interp_type) def do_hist_normalization(input_image, landmark_percs, standard_scale, mask=None, interp_type='linear'): """ do the Nyul and Udupa histogram normalization routine with a given set of learned landmarks Based on J.Reinhold code: https://github.com/jcreinhold/intensity-normalization Args: input_image (np.ndarray): image on which to find landmarks landmark_percs (np.ndarray): corresponding landmark points of standard scale standard_scale (np.ndarray): landmarks on the standard scale mask (np.ndarray): foreground mask for img interp_type (str): type of interpolation Returns: normalized (np.ndarray): normalized image """ mask_data = input_image > input_image.mean() if mask is None else mask masked = input_image[mask_data > 0] # extract only part of image where mask is non-emtpy landmarks = get_landmarks(masked, landmark_percs) f = interp1d(landmarks, standard_scale, kind=interp_type, fill_value='extrapolate') # define interpolating function # apply transformation to input image return f(input_image) def get_landmarks(img, percs): """ get the landmarks for the Nyul and Udupa norm method for a specific image Based on J.Reinhold code: https://github.com/jcreinhold/intensity-normalization Args: img (nibabel.nifti1.Nifti1Image): image on which to find landmarks percs (np.ndarray): corresponding landmark percentiles to extract Returns: landmarks (np.ndarray): intensity values corresponding to percs in img """ landmarks = np.percentile(img, percs) return landmarks def nyul_train_standard_scale(img_fns, mask_fns=None, i_min=1, i_max=99, i_s_min=1, i_s_max=100, l_percentile=10, u_percentile=90, step=10): """ determine the standard scale for the set of images Based on J.Reinhold code: https://github.com/jcreinhold/intensity-normalization Args: img_fns (list): set of NifTI MR image paths which are to be normalized mask_fns (list): set of corresponding masks (if not provided, estimated) i_min (float): minimum percentile to consider in the images i_max (float): maximum percentile to consider in the images i_s_min (float): minimum percentile on the standard scale i_s_max (float): maximum percentile on the standard scale l_percentile (int): middle percentile lower bound (e.g., for deciles 10) u_percentile (int): middle percentile upper bound (e.g., for deciles 90) step (int): step for middle percentiles (e.g., for deciles 10) Returns: standard_scale (np.ndarray): average landmark intensity for images percs (np.ndarray): array of all percentiles used """ # compute masks is those are not entered as a parameters mask_fns = [None] * len(img_fns) if mask_fns is None else mask_fns percs = np.concatenate(([i_min], np.arange(l_percentile, u_percentile+1, step), [i_max])) standard_scale = np.zeros(len(percs)) # process each image in order to build the standard scale for i, (img_fn, mask_fn) in enumerate(zip(img_fns, mask_fns)): print('processing scan ', img_fn) img_data = nib.load(img_fn).get_data() # extract image as numpy array mask = nib.load(mask_fn) if mask_fn is not None else None # load mask as nibabel object mask_data = img_data > img_data.mean() \ if mask is None else mask.get_data() # extract mask as numpy array masked = img_data[mask_data > 0] # extract only part of image where mask is non-emtpy landmarks = get_landmarks(masked, percs) min_p = np.percentile(masked, i_min) max_p = np.percentile(masked, i_max) f = interp1d([min_p, max_p], [i_s_min, i_s_max]) # create interpolating function landmarks = np.array(f(landmarks)) # interpolate landmarks standard_scale += landmarks # add landmark values of this volume to standard_scale standard_scale = standard_scale / len(img_fns) # get mean values return standard_scale, percs