def label_discs(fname_seg, list_disc_z, list_disc_value, verbose=1): """ Create file with single voxel label in the middle of the spinal cord for each disc. :param fname_seg: fname of the segmentation, no orientation expected :param list_disc_z: list of z that correspond to a disc :param list_disc_value: list of associated disc values :param verbose: :return: """ seg = Image(fname_seg) init_orientation = seg.orientation seg.change_orientation("RPI") disc_data = np.zeros_like(seg.data) nx, ny, nz = seg.data.shape for i in range(len(list_disc_z)): if list_disc_z[i] < nz: try: slices = seg.data[:, :, list_disc_z[i]] cx, cy = [int(x) for x in np.round(center_of_mass(slices)).tolist()] except EmptyArrayError: logger.warning("During disc labeling, center of mass calculation failed due to discontinuities in " "segmented spinal cord; please check the quality of your segmentation. Using " "interpolated centerline as a fallback.") interpolated_centerline, _, _, _ = get_centerline(seg) slices = interpolated_centerline.data[:, :, list_disc_z[i]] cx, cy = [int(x) for x in np.round(center_of_mass(slices)).tolist()] # Disc value are offset by one due to legacy code disc_data[cx, cy, list_disc_z[i]] = list_disc_value[i] + 1 seg.data = disc_data seg.change_orientation(init_orientation).save(add_suffix(fname_seg, '_labeled_disc'))
def label_discs(fname_seg, list_disc_z, list_disc_value, verbose=1): """ Create file with single voxel label in the middle of the spinal cord for each disc. :param fname_seg: fname of the segmentation, no orientation expected :param list_disc_z: list of z that correspond to a disc :param list_disc_value: list of associated disc values :param verbose: :return: """ seg = Image(fname_seg) init_orientation = seg.orientation seg.change_orientation("RPI") disc_data = np.zeros_like(seg.data) nx, ny, nz = seg.data.shape for i in range(len(list_disc_z)): if list_disc_z[i] < nz: slices = seg.data[:, :, list_disc_z[i]] cx, cy = [ int(x) for x in np.round(center_of_mass(slices)).tolist() ] # Disc value are offset by one due to legacy code disc_data[cx, cy, list_disc_z[i]] = list_disc_value[i] + 1 seg.data = disc_data seg.change_orientation(init_orientation).save( add_suffix(fname_seg, '_labeled_disc'))
def increment_z_inverse(img: Image) -> Image: """ Take all non-zero values, sort them along the inverse z direction, and attributes the values 1, 2, 3, etc. :param img: source image :returns: image with non-zero values sorted along inverse z """ og_orientation = img.orientation if og_orientation != "RPI": img.change_orientation("RPI") out = zeros_like(img) coordinates_input = img.getNonZeroCoordinates(sorting='z', reverse_coord=True) # for all points with non-zeros neighbors, force the neighbors to 0 for i, (x, y, z, _) in enumerate(coordinates_input): out.data[int(x), int(y), int(z)] = i + 1 if out.orientation != og_orientation: out.change_orientation(og_orientation) img.change_orientation(og_orientation) return out
def run_crop(fname_in, fname_out, nb_slice_average=1.0): img = Image(fname_in).change_orientation('RPI') if len(list(np.where(img.data == 3)[2])) == 1: # if label file x_start, x_end = str(np.where(img.data == 3)[0][0]), str( np.where(img.data == 3)[0][0]) nb_slice_average_each_side = 0 img.data[np.where(img.data != 3)] = 0 img.data[np.where(img.data == 3)] = 1 img.change_orientation('PIR') img.save(fname_out) del img else: # if grayscale image file x_med = int(np.rint(img.dim[0] * 1.0 / 2)) nb_slice_average_each_side = int(nb_slice_average / 2 / img.dim[4]) x_start, x_end = str(x_med - nb_slice_average_each_side), str( x_med + nb_slice_average_each_side) del img cmd_orient = [ 'sct_image', '-i', fname_in, '-setorient', 'PIR', '-o', fname_out ] sct.run(cmd_orient) cmd_crop = [ 'sct_crop_image', '-i', fname_out, '-zmin', x_start, '-zmax', x_end, '-o', fname_out ] sct.run(cmd_crop) if nb_slice_average_each_side: cmd_mean = [ 'sct_maths', '-i', fname_out, '-mean', 'z', '-o', fname_out ] sct.run(cmd_mean)
def main(fname_anat, fname_centerline, degree_poly, centerline_fitting, interp, remove_temp_files, verbose): # load input image im_anat = Image(fname_anat) nx, ny, nz, nt, px, py, pz, pt = im_anat.dim # re-oriente to RPI orientation_native = im_anat.orientation im_anat.change_orientation("RPI") # load centerline im_centerline = Image(fname_centerline).change_orientation("RPI") # smooth centerline and return fitted coordinates in voxel space x_centerline_fit, y_centerline_fit, z_centerline, x_centerline_deriv, y_centerline_deriv, z_centerline_deriv = smooth_centerline( im_centerline, algo_fitting=centerline_fitting, type_window='hanning', window_length=50, nurbs_pts_number=3000, phys_coordinates=False, verbose=verbose, all_slices=True) # compute translation for each slice, such that the flattened centerline is centered in the medial plane (R-L) and # avoid discontinuity in slices where there is no centerline (in which case, simply copy the translation of the # closest Z). # first, get zmin and zmax spanned by the centerline (i.e. with non-zero values) indz_centerline = np.where( [np.sum(im_centerline.data[:, :, iz]) for iz in range(nz)])[0] zmin, zmax = indz_centerline[0], indz_centerline[-1] # then, extend the centerline by copying values below zmin and above zmax x_centerline_extended = np.concatenate([ np.ones(zmin) * x_centerline_fit[0], x_centerline_fit, np.ones(nz - zmax) * x_centerline_fit[-1] ]) # loop across slices and apply translation im_anat_flattened = msct_image.change_type(im_anat, np.float32) # change type to float32 because of subsequent conversion (img_as_float). See #1790 for iz in range(nz): # compute translation along x (R-L) translation_x = x_centerline_extended[iz] - np.round(nx / 2.0) # apply transformation to 2D image with linear interpolation # tform = tf.SimilarityTransform(scale=1, rotation=0, translation=(translation_x, 0)) tform = transform.SimilarityTransform(translation=(0, translation_x)) # important to force input in float to skikit image, because it will output float values img = img_as_float(im_anat.data[:, :, iz]) img_reg = transform.warp(img, tform) im_anat_flattened.data[:, :, iz] = img_reg # img_as_uint(img_reg) # change back to native orientation im_anat_flattened.change_orientation(orientation_native) # save output fname_out = sct.add_suffix(fname_anat, '_flatten') im_anat_flattened.save(fname_out) sct.display_viewer_syntax([fname_anat, fname_out])
def detect_centerline(img, contrast): """Detect spinal cord centerline using OptiC. :param img: input Image() object. :param contrast: str: The type of contrast. Will define the path to Optic model. :returns: Image(): Output centerline """ # Fetch path to Optic model based on contrast optic_models_path = os.path.join(sct.__sct_dir__, 'data', 'optic_models', '{}_model'.format(contrast)) sct.log.debug('Detecting the spinal cord using OptiC') img_orientation = img.orientation temp_folder = sct.TempFolder() temp_folder.chdir() # convert image data type to int16, as required by opencv (backend in OptiC) img_int16 = img.copy() # Replace non-numeric values by zero img_data = img.data img_data[np.where(np.isnan(img_data))] = 0 img_data[np.where(np.isinf(img_data))] = 0 img_int16.data[np.where(np.isnan(img_int16.data))] = 0 img_int16.data[np.where(np.isinf(img_int16.data))] = 0 # rescale intensity min_out = np.iinfo('uint16').min max_out = np.iinfo('uint16').max min_in = np.nanmin(img_data) max_in = np.nanmax(img_data) data_rescaled = img_data.astype('float') * (max_out - min_out) / (max_in - min_in) img_int16.data = data_rescaled - (data_rescaled.min() - min_out) # change data type img_int16.change_type(np.uint16) # reorient the input image to RPI + convert to .nii img_int16.change_orientation('RPI') file_img = 'img_rpi_uint16' img_int16.save(file_img + '.nii') # call the OptiC method to generate the spinal cord centerline optic_input = file_img optic_filename = file_img + '_optic' os.environ["FSLOUTPUTTYPE"] = "NIFTI_PAIR" cmd_optic = 'isct_spine_detect -ctype=dpdt -lambda=1 "%s" "%s" "%s"' % \ (optic_models_path, optic_input, optic_filename) # TODO: output coordinates, for each slice, in continuous (not discrete) values. sct.run(cmd_optic, verbose=0) # convert .img and .hdr files to .nii.gz img_ctl = Image(file_img + '_optic_ctr.hdr') img_ctl.change_orientation(img_orientation) # return to initial folder temp_folder.chdir_undo() return img_ctl
def dummy_centerline(size_arr=(9, 9, 9), subsampling=1, dilate_ctl=0, hasnan=False, zeroslice=[], outlier=[], orientation='RPI', debug=False): """ Create a dummy Image centerline of small size. Return the full and sub-sampled version along z. :param size_arr: tuple: (nx, ny, nz) :param subsampling: int >=1. Subsampling factor along z. 1: no subsampling. 2: centerline defined every other z. :param dilate_ctl: Dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. if dilate_ctl=0, result will be a single pixel per slice. :param hasnan: Bool: Image has non-numerical values: nan, inf. In this case, do not subsample. :param zeroslice: list int: zero all slices listed in this param :param outlier: list int: replace the current point with an outlier at the corner of the image for the slices listed :param orientation: :param debug: Bool: Write temp files :return: """ from numpy import poly1d, polyfit nx, ny, nz = size_arr # define array based on a polynomial function, within X-Z plane, located at y=ny/4, based on the following points: x = np.array([round(nx/4.), round(nx/2.), round(3*nx/4.)]) z = np.array([0, round(nz/2.), nz-1]) p = poly1d(polyfit(z, x, deg=3)) data = np.zeros((nx, ny, nz)) arr_ctl = np.array([p(range(nz)).astype(np.int), [round(ny / 4.)] * len(range(nz)), range(nz)], dtype=np.uint16) # Loop across dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. for ixiy_ctl in itertools.product(range(-dilate_ctl, dilate_ctl+1, 1), range(-dilate_ctl, dilate_ctl+1, 1)): data[(arr_ctl[0] + ixiy_ctl[0]).tolist(), (arr_ctl[1] + ixiy_ctl[1]).tolist(), arr_ctl[2].tolist()] = 1 # Zero specified slices if zeroslice is not []: data[:, :, zeroslice] = 0 # Add outlier if outlier is not []: # First, zero all the slice data[:, :, outlier] = 0 # Then, add point in the corner data[0, 0, outlier] = 1 # Create image with default orientation LPI affine = np.eye(4) nii = nib.nifti1.Nifti1Image(data, affine) img = Image(data, hdr=nii.header, dim=nii.header.get_data_shape()) # subsample data img_sub = img.copy() img_sub.data = np.zeros((nx, ny, nz)) for iz in range(0, nz, subsampling): img_sub.data[..., iz] = data[..., iz] # Add non-numerical values at the top corner of the image if hasnan: img.data[0, 0, 0] = np.nan img.data[1, 0, 0] = np.inf # Update orientation img.change_orientation(orientation) img_sub.change_orientation(orientation) if debug: img_sub.save('tmp_dummy_seg_'+datetime.now().strftime("%Y%m%d%H%M%S%f")+'.nii.gz') return img, img_sub, arr_ctl
def extract_slices(self): # open image and re-orient it to RPI if needed im, seg = Image(self.param.fname_im), Image(self.param.fname_seg) if self.orientation_im != self.orientation_extraction: im.change_orientation(self.orientation_extraction) seg.change_orientation(self.orientation_extraction) # extract axial slices in self.dct_im_seg self.dct_im_seg['im'], self.dct_im_seg['seg'] = [im.data[:, :, z] for z in range(im.dim[2])], [seg.data[:, :, z] for z in range(im.dim[2])]
def z_brainstem(image_fold): brainstem_path = os.path.join(image_fold, 'label', 'brainstem_CST.nii.gz') brainstem_im = Image(brainstem_path) brainstem_im.change_orientation('RPI') z_brainstem_lst = list(set(np.where(brainstem_im.data)[2])) del brainstem_im return np.min(z_brainstem_lst), np.max(z_brainstem_lst)
def template_lpi(tmp_path_factory): """Change orientation of test data template to LPI.""" path_out = str(tmp_path_factory.mktemp("tmp_data")/'template_lpi') # tmp_path_factory is needed for module scope shutil.copytree('sct_testing_data/template', path_out) for file in glob.glob('sct_testing_data/template_lpi/template/*.nii.gz'): nii = Image(file) nii.change_orientation('LPI') nii.save(file) return path_out
def resample_image(fname, suffix='_resampled.nii.gz', binary=False, npx=0.3, npy=0.3, thr=0.0, interpolation='spline'): """ Resampling function: add a padding, resample, crop the padding :param fname: name of the image file to be resampled :param suffix: suffix added to the original fname after resampling :param binary: boolean, image is binary or not :param npx: new pixel size in the x direction :param npy: new pixel size in the y direction :param thr: if the image is binary, it will be thresholded at thr (default=0) after the resampling :param interpolation: type of interpolation used for the resampling :return: file name after resampling (or original fname if it was already in the correct resolution) """ im_in = Image(fname) orientation = im_in.orientation if orientation != 'RPI': im_in.change_orientation('RPI') fname = add_suffix(im_in.absolutepath, "_rpi") im_in.save(path=fname, mutable=True) nx, ny, nz, nt, px, py, pz, pt = im_in.dim if np.round(px, 2) != np.round(npx, 2) or np.round(py, 2) != np.round(npy, 2): name_resample = extract_fname(fname)[1] + suffix if binary: interpolation = 'nn' if nz == 1: # when data is 2d: we convert it to a 3d image in order to avoid conversion problem with 2d data # TODO: check if this above problem is still present (now that we are using nibabel instead of nipy) run_proc(['sct_image', '-i', ','.join([fname, fname]), '-concat', 'z', '-o', fname]) run_proc(['sct_resample', '-i', fname, '-mm', str(npx) + 'x' + str(npy) + 'x' + str(pz), '-o', name_resample, '-x', interpolation]) if nz == 1: # when input data was 2d: re-convert data 3d-->2d run_proc(['sct_image', '-i', name_resample, '-split', 'z']) im_split = Image(name_resample.split('.nii.gz')[0] + '_Z0000.nii.gz') im_split.save(name_resample) if binary: img = Image(name_resample) img.data = binarize(img.data, thr) img.save() if orientation != 'RPI': img = Image(name_resample) img.change_orientation(orientation) name_resample = add_suffix(img.absolutepath, "_{}".format(orientation.lower())) img.save(path=name_resample, mutable=True) return name_resample else: if orientation != 'RPI': fname = add_suffix(fname, "_RPI") im_in = change_orientation(im_in, orientation).save(fname) printv('Image resolution already ' + str(npx) + 'x' + str(npy) + 'xpz') return fname
def create_labels_along_segmentation( img: Image, labels: Sequence[Tuple[int, int]]) -> Image: """ Create an image with labels defined along the spinal cord segmentation (or centerline). Input image does **not** need to be RPI (re-orientation is done within this function). :param img: source segmentation :param labels: list of label tuples as (z_value, label_value) :returns: labeled segmentation (Image) """ og_orientation = img.orientation if og_orientation != "RPI": img.change_orientation("RPI") out = zeros_like(img) for idx_label, label in enumerate(labels): z, value = label # update z based on native image orientation (z should represent superior-inferior axis) coord = Coordinate( [z, z, z] ) # since we don't know which dimension corresponds to the superior-inferior # axis, we put z in all dimensions (we don't care about x and y here) _, _, z_rpi = coord.permute(img, 'RPI') # if z=-1, replace with nz/2 if z == -1: z_rpi = int(np.round(out.dim[2] / 2.0)) # get center of mass of segmentation at given z x, y = ndimage.measurements.center_of_mass( np.array(img.data[:, :, z_rpi])) # round values to make indices x, y = int(np.round(x)), int(np.round(y)) # display info logger.debug(f"Label # {idx_label}: {x}, {y}. {z_rpi} --> {value}") if len(out.data.shape) == 3: out.data[x, y, z_rpi] = value elif len(out.data.shape) == 2: if z != 0: raise ValueError( f"2D coordinates should have a Z value of 0! Current value: {coord.z}" ) out.data[x, y] = value if out.orientation != og_orientation: out.change_orientation(og_orientation) img.change_orientation(og_orientation) return out
def label_segmentation(fname_seg, list_disc_z, list_disc_value, verbose=1): """ Label segmentation image :param fname_seg: fname of the segmentation, no orientation expected :param list_disc_z: list of z that correspond to a disc :param list_disc_value: list of associated disc values :param verbose: :return: """ # open segmentation seg = Image(fname_seg) init_orientation = seg.orientation seg.change_orientation("RPI") dim = seg.dim ny = dim[1] nz = dim[2] # loop across z for iz in range(nz): # get index of the disc right above iz try: ind_above_iz = max( [i for i in range(len(list_disc_z)) if list_disc_z[i] > iz]) except ValueError: # if ind_above_iz is empty, attribute value 0 vertebral_level = 0 else: # assign vertebral level (add one because iz is BELOW the disk) vertebral_level = list_disc_value[ind_above_iz] + 1 # sct.printv(vertebral_level) # get voxels in mask ind_nonzero = np.nonzero(seg.data[:, :, iz]) seg.data[ind_nonzero[0], ind_nonzero[1], iz] = vertebral_level # if verbose == 2: # # move to OO. No time to finish... (JCA) # from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas # from matplotlib.figure import Figure # fig = Figure() # FigureCanvas(fig) # ax = fig.add_subplot(111) # ax.scatter(int(np.round(ny / 2)), iz, c=vertebral_level, vmin=min(list_disc_value), # vmax=max(list_disc_value), cmap='prism', marker='_', s=200) # # # TODO: the thing below crashes with the py3k move. Fix it when i have time... # import matplotlib # matplotlib.use('Agg') # import matplotlib.pyplot as plt # plt.figure(50) # plt.scatter(int(np.round(ny / 2)), iz, c=vertebral_level, vmin=min(list_disc_value), vmax=max(list_disc_value), cmap='prism', marker='_', s=200) # write file seg.change_orientation(init_orientation).save( sct.add_suffix(fname_seg, '_labeled'))
def z_slice_levels(levels_path): levels_im = Image(levels_path) levels_im.change_orientation('RPI') lvl_dct = {} for lvl in list(np.unique(levels_im.data)): if lvl: lvl_dct[lvl] = list(set(np.where(levels_im.data == lvl)[2])) del levels_im return lvl_dct
def project_labels_on_spinalcord(fname_label, fname_seg): """ Project labels orthogonally on the spinal cord centerline. The algorithm works by finding the smallest distance between each label and the spinal cord center of mass. :param fname_label: file name of labels :param fname_seg: file name of cord segmentation (could also be of centerline) :return: file name of projected labels """ # build output name fname_label_projected = sct.add_suffix(fname_label, "_projected") # open labels and segmentation im_label = Image(fname_label).change_orientation("RPI") im_seg = Image(fname_seg) native_orient = im_seg.orientation im_seg.change_orientation("RPI") # smooth centerline and return fitted coordinates in voxel space _, arr_ctl, _ = get_centerline(im_seg, algo_fitting='bspline') x_centerline_fit, y_centerline_fit, z_centerline = arr_ctl # convert pixel into physical coordinates centerline_xyz_transposed = \ [im_seg.transfo_pix2phys([[x_centerline_fit[i], y_centerline_fit[i], z_centerline[i]]])[0] for i in range(len(x_centerline_fit))] # transpose list centerline_phys_x = [i[0] for i in centerline_xyz_transposed] centerline_phys_y = [i[1] for i in centerline_xyz_transposed] centerline_phys_z = [i[2] for i in centerline_xyz_transposed] # get center of mass of label labels = im_label.getCoordinatesAveragedByValue() # initialize image of projected labels. Note that we use the space of the seg (not label). im_label_projected = msct_image.zeros_like(im_seg, dtype=np.uint8) # loop across label values for label in labels: # convert pixel into physical coordinates for the label label_phys_x, label_phys_y, label_phys_z = im_label.transfo_pix2phys([[label.x, label.y, label.z]])[0] # calculate distance between label and each point of the centerline distance_centerline = [np.linalg.norm([centerline_phys_x[i] - label_phys_x, centerline_phys_y[i] - label_phys_y, centerline_phys_z[i] - label_phys_z]) for i in range(len(x_centerline_fit))] # get the index corresponding to the min distance ind_min_distance = np.argmin(distance_centerline) # get centerline coordinate (in physical space) [min_phy_x, min_phy_y, min_phy_z] = [centerline_phys_x[ind_min_distance], centerline_phys_y[ind_min_distance], centerline_phys_z[ind_min_distance]] # convert coordinate to voxel space minx, miny, minz = im_seg.transfo_phys2pix([[min_phy_x, min_phy_y, min_phy_z]])[0] # use that index to assign projected label in the centerline im_label_projected.data[minx, miny, minz] = label.value # re-orient projected labels to native orientation and save im_label_projected.change_orientation(native_orient).save(fname_label_projected) return fname_label_projected
def project_labels_on_spinalcord(fname_label, fname_seg, param_centerline): """ Project labels orthogonally on the spinal cord centerline. The algorithm works by finding the smallest distance between each label and the spinal cord center of mass. :param fname_label: file name of labels :param fname_seg: file name of cord segmentation (could also be of centerline) :return: file name of projected labels """ # build output name fname_label_projected = sct.add_suffix(fname_label, "_projected") # open labels and segmentation im_label = Image(fname_label).change_orientation("RPI") im_seg = Image(fname_seg) native_orient = im_seg.orientation im_seg.change_orientation("RPI") # smooth centerline and return fitted coordinates in voxel space _, arr_ctl, _, _ = get_centerline(im_seg, param_centerline) x_centerline_fit, y_centerline_fit, z_centerline = arr_ctl # convert pixel into physical coordinates centerline_xyz_transposed = \ [im_seg.transfo_pix2phys([[x_centerline_fit[i], y_centerline_fit[i], z_centerline[i]]])[0] for i in range(len(x_centerline_fit))] # transpose list centerline_phys_x = [i[0] for i in centerline_xyz_transposed] centerline_phys_y = [i[1] for i in centerline_xyz_transposed] centerline_phys_z = [i[2] for i in centerline_xyz_transposed] # get center of mass of label labels = im_label.getCoordinatesAveragedByValue() # initialize image of projected labels. Note that we use the space of the seg (not label). im_label_projected = msct_image.zeros_like(im_seg, dtype=np.uint8) # loop across label values for label in labels: # convert pixel into physical coordinates for the label label_phys_x, label_phys_y, label_phys_z = im_label.transfo_pix2phys([[label.x, label.y, label.z]])[0] # calculate distance between label and each point of the centerline distance_centerline = [np.linalg.norm([centerline_phys_x[i] - label_phys_x, centerline_phys_y[i] - label_phys_y, centerline_phys_z[i] - label_phys_z]) for i in range(len(x_centerline_fit))] # get the index corresponding to the min distance ind_min_distance = np.argmin(distance_centerline) # get centerline coordinate (in physical space) [min_phy_x, min_phy_y, min_phy_z] = [centerline_phys_x[ind_min_distance], centerline_phys_y[ind_min_distance], centerline_phys_z[ind_min_distance]] # convert coordinate to voxel space minx, miny, minz = im_seg.transfo_phys2pix([[min_phy_x, min_phy_y, min_phy_z]])[0] # use that index to assign projected label in the centerline im_label_projected.data[minx, miny, minz] = label.value # re-orient projected labels to native orientation and save im_label_projected.change_orientation(native_orient).save(fname_label_projected) return fname_label_projected
def get_midNifti(path_im, ind): """ Retrieve the input images for the network. This images are generated by averaging the 7 slices in the middle of the volume :param path_im: path to image :param ind: index of the middle :return: """ a = Image(path_im) a.change_orientation('RPI') arr = np.array(a.data) return np.mean(arr[ind - 3:ind + 3, :, :], 0)
def _preprocess_segment(fname_t2, fname_t2_seg, contrast_test, dim_3=False): tmp_folder = sct.TempFolder() tmp_folder_path = tmp_folder.get_path() tmp_folder.chdir() img = Image(fname_t2) gt = Image(fname_t2_seg) fname_t2_RPI, fname_t2_seg_RPI = 'img_RPI.nii.gz', 'seg_RPI.nii.gz' img.change_orientation('RPI').save(fname_t2_RPI) gt.change_orientation('RPI').save(fname_t2_seg_RPI) input_resolution = gt.dim[4:7] del img, gt fname_res, fname_ctr = deepseg_sc.find_centerline(algo='svm', image_fname=fname_t2_RPI, contrast_type=contrast_test, brain_bool=False, folder_output=tmp_folder_path, remove_temp_files=1, centerline_fname=None) fname_t2_seg_RPI_res = 'seg_RPI_res.nii.gz' new_resolution = 'x'.join(['0.5', '0.5', str(input_resolution[2])]) resample_file(fname_t2_seg_RPI, fname_t2_seg_RPI_res, new_resolution, 'mm', 'linear', verbose=0) img, ctr, gt = Image(fname_res), Image(fname_ctr), Image(fname_t2_seg_RPI_res) _, _, _, img = deepseg_sc.crop_image_around_centerline(im_in=img, ctr_in=ctr, crop_size=64) _, _, _, gt = deepseg_sc.crop_image_around_centerline(im_in=gt, ctr_in=ctr, crop_size=64) del ctr img = deepseg_sc.apply_intensity_normalization(im_in=img) if dim_3: # If 3D kernels fname_t2_RPI_res_crop, fname_t2_seg_RPI_res_crop = 'img_RPI_res_crop.nii.gz', 'seg_RPI_res_crop.nii.gz' img.save(fname_t2_RPI_res_crop) gt.save(fname_t2_seg_RPI_res_crop) del img, gt fname_t2_RPI_res_crop_res = 'img_RPI_res_crop_res.nii.gz' fname_t2_seg_RPI_res_crop_res = 'seg_RPI_res_crop_res.nii.gz' resample_file(fname_t2_RPI_res_crop, fname_t2_RPI_res_crop_res, new_resolution, 'mm', 'linear', verbose=0) resample_file(fname_t2_seg_RPI_res_crop, fname_t2_seg_RPI_res_crop_res, new_resolution, 'mm', 'linear', verbose=0) img, gt = Image(fname_t2_RPI_res_crop_res), Image(fname_t2_seg_RPI_res_crop_res) tmp_folder.chdir_undo() tmp_folder.cleanup() return img, gt
def _preprocess_segment(fname_t2, fname_t2_seg, contrast_test, dim_3=False): tmp_folder = sct.TempFolder() tmp_folder_path = tmp_folder.get_path() tmp_folder.chdir() img = Image(fname_t2) gt = Image(fname_t2_seg) fname_t2_RPI, fname_t2_seg_RPI = 'img_RPI.nii.gz', 'seg_RPI.nii.gz' img.change_orientation('RPI').save(fname_t2_RPI) gt.change_orientation('RPI').save(fname_t2_seg_RPI) input_resolution = gt.dim[4:7] del img, gt fname_res, fname_ctr, _ = deepseg_sc.find_centerline(algo='svm', image_fname=fname_t2_RPI, contrast_type=contrast_test, brain_bool=False, folder_output=tmp_folder_path, remove_temp_files=1, centerline_fname=None) fname_t2_seg_RPI_res = 'seg_RPI_res.nii.gz' new_resolution = 'x'.join(['0.5', '0.5', str(input_resolution[2])]) resample_file(fname_t2_seg_RPI, fname_t2_seg_RPI_res, new_resolution, 'mm', 'linear', verbose=0) img, ctr, gt = Image(fname_res), Image(fname_ctr), Image(fname_t2_seg_RPI_res) _, _, _, img = deepseg_sc.crop_image_around_centerline(im_in=img, ctr_in=ctr, crop_size=64) _, _, _, gt = deepseg_sc.crop_image_around_centerline(im_in=gt, ctr_in=ctr, crop_size=64) del ctr img = deepseg_sc.apply_intensity_normalization(im_in=img) if dim_3: # If 3D kernels fname_t2_RPI_res_crop, fname_t2_seg_RPI_res_crop = 'img_RPI_res_crop.nii.gz', 'seg_RPI_res_crop.nii.gz' img.save(fname_t2_RPI_res_crop) gt.save(fname_t2_seg_RPI_res_crop) del img, gt fname_t2_RPI_res_crop_res = 'img_RPI_res_crop_res.nii.gz' fname_t2_seg_RPI_res_crop_res = 'seg_RPI_res_crop_res.nii.gz' resample_file(fname_t2_RPI_res_crop, fname_t2_RPI_res_crop_res, new_resolution, 'mm', 'linear', verbose=0) resample_file(fname_t2_seg_RPI_res_crop, fname_t2_seg_RPI_res_crop_res, new_resolution, 'mm', 'linear', verbose=0) img, gt = Image(fname_t2_RPI_res_crop_res), Image(fname_t2_seg_RPI_res_crop_res) tmp_folder.chdir_undo() tmp_folder.cleanup() return img, gt
def label_segmentation(fname_seg, list_disc_z, list_disc_value, verbose=1): """ Label segmentation image :param fname_seg: fname of the segmentation, no orientation expected :param list_disc_z: list of z that correspond to a disc :param list_disc_value: list of associated disc values :param verbose: :return: """ # open segmentation seg = Image(fname_seg) init_orientation = seg.orientation seg.change_orientation("RPI") dim = seg.dim ny = dim[1] nz = dim[2] # loop across z for iz in range(nz): # get index of the disc right above iz try: ind_above_iz = max( [i for i in range(len(list_disc_z)) if list_disc_z[i] > iz]) except ValueError: # if ind_above_iz is empty, attribute value 0 vertebral_level = 0 else: # assign vertebral level (add one because iz is BELOW the disk) vertebral_level = list_disc_value[ind_above_iz] + 1 # sct.printv(vertebral_level) # get voxels in mask ind_nonzero = np.nonzero(seg.data[:, :, iz]) seg.data[ind_nonzero[0], ind_nonzero[1], iz] = vertebral_level if verbose == 2: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt plt.figure(50) plt.scatter(int(np.round(ny / 2)), iz, c=vertebral_level, vmin=min(list_disc_value), vmax=max(list_disc_value), cmap='prism', marker='_', s=200) # write file seg.change_orientation(init_orientation).save( sct.add_suffix(fname_seg, '_labeled'))
def compute_texture(self): offset = int(self.param_glcm.distance) printv('\nCompute texture metrics...', self.param.verbose, 'normal') # open image and re-orient it to RPI if needed im_tmp = Image(self.param.fname_im) if self.orientation_im != self.orientation_extraction: im_tmp.change_orientation(self.orientation_extraction) dct_metric = {} for m in self.metric_lst: im_2save = zeros_like(im_tmp, dtype='float64') dct_metric[m] = im_2save # dct_metric[m] = Image(self.fname_metric_lst[m]) with sct_progress_bar() as pbar: for im_z, seg_z, zz in zip(self.dct_im_seg['im'], self.dct_im_seg['seg'], range(len(self.dct_im_seg['im']))): for xx in range(im_z.shape[0]): for yy in range(im_z.shape[1]): if not seg_z[xx, yy]: continue if xx < offset or yy < offset: continue if xx > (im_z.shape[0] - offset - 1) or yy > (im_z.shape[1] - offset - 1): continue # to check if the whole glcm_window is in the axial_slice if False in np.unique(seg_z[xx - offset: xx + offset + 1, yy - offset: yy + offset + 1]): continue # to check if the whole glcm_window is in the mask of the axial_slice glcm_window = im_z[xx - offset: xx + offset + 1, yy - offset: yy + offset + 1] glcm_window = glcm_window.astype(np.uint8) dct_glcm = {} for a in self.param_glcm.angle.split(','): # compute the GLCM for self.param_glcm.distance and for each self.param_glcm.angle dct_glcm[a] = greycomatrix(glcm_window, [self.param_glcm.distance], [np.radians(int(a))], symmetric=self.param_glcm.symmetric, normed=self.param_glcm.normed) for m in self.metric_lst: # compute the GLCM property (m.split('_')[0]) of the voxel xx,yy,zz dct_metric[m].data[xx, yy, zz] = greycoprops(dct_glcm[m.split('_')[2]], m.split('_')[0])[0][0] pbar.set_postfix(pos="{}/{}".format(zz, len(self.dct_im_seg["im"]))) pbar.update(1) for m in self.metric_lst: fname_out = add_suffix("".join(extract_fname(self.param.fname_im)[1:]), '_' + m) dct_metric[m].save(fname_out) self.fname_metric_lst[m] = fname_out
def compute_texture(self): offset = int(self.param_glcm.distance) sct.printv('\nCompute texture metrics...', self.param.verbose, 'normal') # open image and re-orient it to RPI if needed im_tmp = Image(self.param.fname_im) if self.orientation_im != self.orientation_extraction: im_tmp.change_orientation(self.orientation_extraction) dct_metric = {} for m in self.metric_lst: im_2save = msct_image.zeros_like(im_tmp, dtype='float64') dct_metric[m] = im_2save # dct_metric[m] = Image(self.fname_metric_lst[m]) with tqdm.tqdm() as pbar: for im_z, seg_z, zz in zip(self.dct_im_seg['im'], self.dct_im_seg['seg'], range(len(self.dct_im_seg['im']))): for xx in range(im_z.shape[0]): for yy in range(im_z.shape[1]): if not seg_z[xx, yy]: continue if xx < offset or yy < offset: continue if xx > (im_z.shape[0] - offset - 1) or yy > (im_z.shape[1] - offset - 1): continue # to check if the whole glcm_window is in the axial_slice if False in np.unique(seg_z[xx - offset: xx + offset + 1, yy - offset: yy + offset + 1]): continue # to check if the whole glcm_window is in the mask of the axial_slice glcm_window = im_z[xx - offset: xx + offset + 1, yy - offset: yy + offset + 1] glcm_window = glcm_window.astype(np.uint8) dct_glcm = {} for a in self.param_glcm.angle.split(','): # compute the GLCM for self.param_glcm.distance and for each self.param_glcm.angle dct_glcm[a] = greycomatrix(glcm_window, [self.param_glcm.distance], [np.radians(int(a))], symmetric=self.param_glcm.symmetric, normed=self.param_glcm.normed) for m in self.metric_lst: # compute the GLCM property (m.split('_')[0]) of the voxel xx,yy,zz dct_metric[m].data[xx, yy, zz] = greycoprops(dct_glcm[m.split('_')[2]], m.split('_')[0])[0][0] pbar.set_postfix(pos="{}/{}".format(zz, len(self.dct_im_seg["im"]))) pbar.update(1) for m in self.metric_lst: fname_out = sct.add_suffix(''.join(sct.extract_fname(self.param.fname_im)[1:]), '_' + m) dct_metric[m].save(fname_out) self.fname_metric_lst[m] = fname_out
def label_discs(fname_seg_labeled, verbose=1): """ Label discs from labeled_segmentation. The convention is C2/C3-->3, C3/C4-->4, etc. :param fname_seg_labeld: fname of the labeled segmentation :param verbose: :return: """ # open labeled segmentation im_seg_labeled = Image(fname_seg_labeled) orientation_native = im_seg_labeled.orientation im_seg_labeled.change_orientation("RPI") nx, ny, nz = im_seg_labeled.dim[0], im_seg_labeled.dim[ 1], im_seg_labeled.dim[2] data_disc = np.zeros([nx, ny, nz]) vertebral_level_previous = np.max( im_seg_labeled.data) # initialize with the max label value # loop across z in the superior direction (i.e. starts with the bottom slice), and each time the i/i+1 interface # between two levels is found, create a label at the center of the cord with the value corresponding to the # vertebral level below the point. E.g., at interface C3/C4, the value would be 4. for iz in range(nz): # get 2d slice slice = im_seg_labeled.data[:, :, iz] # check if at least one voxel is non-zero if np.any(slice): slice_one = np.copy(slice) # set all non-zero values to 1 (to compute center of mass) # Note: the reason we do this is because if one slice includes part of vertebral level i and i+1, the # center of mass will be shifted towards the i+1 level.We don't want that here (i.e. the goal is to be at # the center of the cord) slice_one[slice.nonzero()] = 1 # compute center of mass cx, cy = [ int(x) for x in np.round(center_of_mass(slice_one)).tolist() ] # retrieve vertebral level vertebral_level = slice[cx, cy] # if smaller than previous level, then labeled as a disc if vertebral_level < vertebral_level_previous: # label disc data_disc[cx, cy, iz] = vertebral_level + 1 # update variable vertebral_level_previous = vertebral_level # save disc labeled file im_seg_labeled.data = data_disc im_seg_labeled.change_orientation(orientation_native).save( add_suffix(fname_seg_labeled, '_disc'))
def create_qc(fname_in, fname_gt, fname_out): img, gt = Image(fname_in), Image(fname_gt) img.change_orientation('RPI') gt.change_orientation('RPI') coord_c2c3 = np.where(gt.data == 1) y_c2c3, z_c2c3 = coord_c2c3[1][0], coord_c2c3[2][0] sag_slice = img.data[0, :, :] del img, gt ax = plt.gca() ax.imshow(sag_slice, interpolation='nearest', cmap='gray', aspect='auto') circ = Circle((z_c2c3, y_c2c3), 2, facecolor='chartreuse') ax.add_patch(circ) ax.get_xaxis().set_visible(False) ax.get_yaxis().set_visible(False) plt.savefig(fname_out) plt.close()
def run_crop(fname_in, fname_out): img = Image(fname_in).change_orientation('RPI') if len(list(np.where(img.data == 3)[2])) == 1: # if label file x_med = str(np.where(img.data == 3)[0][0]) img.data[np.where(img.data != 3)] = 0 img.data[np.where(img.data == 3)] = 1 img.change_orientation('PIR') img.save(fname_out) del img else: # if grayscale image file x_med = str(int(np.rint(img.dim[0] * 1.0 / 2))) del img cmd_orient = ['sct_image', '-i', fname_in, '-setorient', 'PIR', '-o', fname_out] sct.run(cmd_orient) cmd_orient = ['sct_crop_image', '-i', fname_out, '-start', x_med, '-end', x_med, '-dim', '2', '-o', fname_out] sct.run(cmd_orient)
def launch_manual_label_gui(img: Image, input_labels_img: Image, labels: Sequence[int], msg): # the input image is reoriented to 'SAL' when open by the GUI input_labels_img.change_orientation('SAL') mid = int(np.round(input_labels_img.data.shape[2] / 2)) previous_points = input_labels_img.getNonZeroCoordinates() # boolean used to mark first element to initiate the list. first = True previous_label = None for i in range(len(previous_points)): if int(previous_points[i].value) in labels: pass else: labels.append(int(previous_points[i].value)) if first: points = np.array([ previous_points[i].x, previous_points[i].y, previous_points[i].z, previous_points[i].value ]) points = np.reshape(points, (1, 4)) previous_label = points first = False else: points = np.array([ previous_points[i].x, previous_points[i].y, previous_points[i].z, previous_points[i].value ]) points = np.reshape(points, (1, 4)) previous_label = np.append(previous_label, points, axis=0) labels.sort() # check if variable was created which means the file was not empty and contains some points asked in labels if previous_label is not None: # project onto mid sagittal plane for i in range(len(previous_label)): previous_label[i][2] = mid out = launch_sagittal_viewer(img, labels, msg, previous_points=previous_label) return out
def create_label_z(fname_seg, z, value, fname_labelz='labelz.nii.gz'): """ Create a label at coordinates x_center, y_center, z :param fname_seg: segmentation :param z: int :param fname_labelz: string file name of output label :return: fname_labelz """ nii = Image(fname_seg) orientation_origin = nii.orientation nii = nii.change_orientation("RPI") nx, ny, nz, nt, px, py, pz, pt = nii.dim # Get dimensions # find x and y coordinates of the centerline at z using center of mass x, y = center_of_mass(np.array(nii.data[:, :, z])) x, y = int(np.round(x)), int(np.round(y)) nii.data[:, :, :] = 0 nii.data[x, y, z] = value # dilate label to prevent it from disappearing due to nearestneighbor interpolation nii.data = dilate(nii.data, 3, 'ball') nii.change_orientation(orientation_origin) # put back in original orientation nii.save(fname_labelz) return fname_labelz
def resample_image(fname, suffix='_resampled.nii.gz', binary=False, npx=0.3, npy=0.3, thr=0.0, interpolation='spline'): """ Resampling function: add a padding, resample, crop the padding :param fname: name of the image file to be resampled :param suffix: suffix added to the original fname after resampling :param binary: boolean, image is binary or not :param npx: new pixel size in the x direction :param npy: new pixel size in the y direction :param thr: if the image is binary, it will be thresholded at thr (default=0) after the resampling :param interpolation: type of interpolation used for the resampling :return: file name after resampling (or original fname if it was already in the correct resolution) """ im_in = Image(fname) orientation = im_in.orientation if orientation != 'RPI': fname = im_in.change_orientation(im_in, 'RPI', generate_path=True).save().absolutepath nx, ny, nz, nt, px, py, pz, pt = im_in.dim if np.round(px, 2) != np.round(npx, 2) or np.round(py, 2) != np.round(npy, 2): name_resample = sct.extract_fname(fname)[1] + suffix if binary: interpolation = 'nn' if nz == 1: # when data is 2d: we convert it to a 3d image in order to avoid nipy problem of conversion nifti-->nipy with 2d data sct.run(['sct_image', '-i', ','.join([fname, fname]), '-concat', 'z', '-o', fname]) sct.run(['sct_resample', '-i', fname, '-mm', str(npx) + 'x' + str(npy) + 'x' + str(pz), '-o', name_resample, '-x', interpolation]) if nz == 1: # when input data was 2d: re-convert data 3d-->2d sct.run(['sct_image', '-i', name_resample, '-split', 'z']) im_split = Image(name_resample.split('.nii.gz')[0] + '_Z0000.nii.gz') im_split.save(name_resample) if binary: sct.run(['sct_maths', '-i', name_resample, '-bin', str(thr), '-o', name_resample]) if orientation != 'RPI': name_resample = Image(name_resample) \ .change_orientation(orientation, generate_path=True) \ .save() \ .absolutepath return name_resample else: if orientation != 'RPI': fname = sct.add_suffix(fname, "_RPI") im_in = msct_image.change_orientation(im_in, orientation).save(fname) sct.printv('Image resolution already ' + str(npx) + 'x' + str(npy) + 'xpz') return fname
def label_segmentation(fname_seg, list_disc_z, list_disc_value, verbose=1): """ Label segmentation image :param fname_seg: fname of the segmentation, no orientation expected :param list_disc_z: list of z that correspond to a disc :param list_disc_value: list of associated disc values :param verbose: :return: """ # open segmentation seg = Image(fname_seg) init_orientation = seg.orientation seg.change_orientation("RPI") dim = seg.dim ny = dim[1] nz = dim[2] # loop across z for iz in range(nz): # get index of the disc right above iz try: ind_above_iz = max( [i for i in range(len(list_disc_z)) if list_disc_z[i] > iz]) except ValueError: # if ind_above_iz is empty, attribute value 0 vertebral_level = 0 else: # assign vertebral level (add one because iz is BELOW the disk) vertebral_level = list_disc_value[ind_above_iz] + 1 # get voxels in mask ind_nonzero = np.nonzero(seg.data[:, :, iz]) seg.data[ind_nonzero[0], ind_nonzero[1], iz] = vertebral_level # write file seg.change_orientation(init_orientation).save( add_suffix(fname_seg, '_labeled'))
def dummy_centerline_small(size_arr=(9, 9, 9), subsampling=1, dilate_ctl=0, hasnan=False, orientation='RPI'): """ Create a dummy Image centerline of small size. Return the full and sub-sampled version along z. :param size_arr: tuple: (nx, ny, nz) :param subsampling: int >=1. Subsampling factor along z. 1: no subsampling. 2: centerline defined every other z. :param dilate_ctl: Dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. if dilate_ctl=0, result will be a single pixel per slice. :param hasnan: Bool: Image has non-numerical values: nan, inf. In this case, do not subsample. :param orientation: :return: """ from numpy import poly1d, polyfit nx, ny, nz = size_arr # define polynomial-based centerline within X-Z plane, located at y=ny/4 x = np.array([round(nx/4.), round(nx/2.), round(3*nx/4.)]) z = np.array([0, round(nz/2.), nz-1]) p = poly1d(polyfit(z, x, deg=3)) data = np.zeros((nx, ny, nz)) # Loop across dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. for ixiy_ctl in itertools.product(range(-dilate_ctl, dilate_ctl+1, 1), range(-dilate_ctl, dilate_ctl+1, 1)): data[p(range(nz)).astype(np.int) + ixiy_ctl[0], round(ny / 4.) + ixiy_ctl[1], range(nz)] = 1 # generate Image object with RPI orientation affine = np.eye(4) nii = nib.nifti1.Nifti1Image(data, affine) img = Image(data, hdr=nii.header, dim=nii.header.get_data_shape()) # subsample data img_sub = img.copy() img_sub.data = np.zeros((nx, ny, nz)) for iz in range(0, nz, subsampling): img_sub.data[..., iz] = data[..., iz] # Add non-numerical values at the top corner of the image if hasnan: img.data[0, 0, 0] = np.nan img.data[1, 0, 0] = np.inf # Update orientation img.change_orientation(orientation) img_sub.change_orientation(orientation) return img, img_sub
def mask2label(path_label, aim='full'): """ Convert nifti image to an array of coordinates :param path_label: path of nifti image :return: """ a = Image(path_label) a.change_orientation('RPI') arr = np.array(a.data) print(path_label) list_label_image = [] for i in range(len(arr.nonzero()[0])): x = arr.nonzero()[0][i] y = arr.nonzero()[1][i] z = arr.nonzero()[2][i] if aim == 'full': if arr[x, y, z] < 30 and arr[x, y, z] != 1: list_label_image.append([x, y, z, arr[x, y, z]]) elif aim == 'c2': if arr[x, y, z] == 3: list_label_image.append([x, y, z, arr[x, y, z]]) list_label_image.sort(key=lambda x: x[3]) return (list_label_image)
def label_vertebrae(img: Image, vertebral_levels: Sequence[int] = None) -> Image: """ Find the center of mass of vertebral levels specified by the user. :param img: source image :param vertebral_levels: list of vertebral levels :returns: image with labels """ og_orientation = img.orientation if og_orientation != "RPI": img.change_orientation("RPI") # get center of mass of each vertebral level out = cubic_to_point(img) # get list of coordinates for each label list_coordinates = out.getNonZeroCoordinates(sorting='value') # if user did not specify levels, include all: if not vertebral_levels: vertebral_levels = [int(i.value) for i in list_coordinates] # loop across labels and remove those that are not listed by the user for i in range(len(list_coordinates)): # check if this level is NOT in vertebral_levels. if not set value to zero if not vertebral_levels.count(int(list_coordinates[i].value)): out.data[int(list_coordinates[i].x), int(list_coordinates[i].y), int(list_coordinates[i].z)] = 0 if out.orientation != og_orientation: out.change_orientation(og_orientation) img.change_orientation(og_orientation) return out
def extract_pmj_symmetrical_sagittal_slice(self): """Extract a slice that is symmetrical about the estimated PMJ location.""" # Here, detection is used just as a way to determine the ROI for the sliding window approach self.extract_sagittal_slice() self.detect() self.get_max_position() image = Image(self.fname_im) # img in PIR orientation self.rl_coord = compute_cross_corr_3d(image.change_orientation('RPI'), [ self.rl_coord, self.pa_coord, self.is_coord, ]) # Find R-L symmetry # Replace the mid-sagittal slice, to be used for the "main" PMJ detection run_proc([ 'sct_crop_image', '-i', self.fname_im, '-zmin', str(self.rl_coord), '-zmax', str(self.rl_coord + 1), '-o', self.slice2D_im ])
class ProcessLabels(object): def __init__(self, fname_label, fname_output=None, fname_ref=None, cross_radius=5, dilate=False, coordinates=None, verbose=1, vertebral_levels=None, value=None, msg=""): """ Collection of processes that deal with label creation/modification. :param fname_label: :param fname_output: :param fname_ref: :param cross_radius: :param dilate: # TODO: remove dilate (does not seem to be used) :param coordinates: :param verbose: :param vertebral_levels: :param value: :param msg: string. message to display to the user. """ self.image_input = Image(fname_label, verbose=verbose) self.image_ref = None if fname_ref is not None: self.image_ref = Image(fname_ref, verbose=verbose) if isinstance(fname_output, list): if len(fname_output) == 1: self.fname_output = fname_output[0] else: self.fname_output = fname_output else: self.fname_output = fname_output self.cross_radius = cross_radius self.vertebral_levels = vertebral_levels self.dilate = dilate self.coordinates = coordinates self.verbose = verbose self.value = value self.msg = msg self.output_image = None def process(self, type_process): # for some processes, change orientation of input image to RPI change_orientation = False if type_process in ['vert-body', 'vert-disc', 'vert-continuous']: # get orientation of input image input_orientation = self.image_input.orientation # change orientation self.image_input.change_orientation('RPI') change_orientation = True if type_process == 'add': self.output_image = self.add(self.value) if type_process == 'plan': self.output_image = self.plan(self.cross_radius, 100, 5) if type_process == 'plan_ref': self.output_image = self.plan_ref() if type_process == 'increment': self.output_image = self.increment_z_inverse() if type_process == 'disks': self.output_image = self.labelize_from_disks() if type_process == 'MSE': self.MSE() self.fname_output = None if type_process == 'remove-reference': self.output_image = self.remove_label() if type_process == 'remove-symm': self.output_image = self.remove_label(symmetry=True) if type_process == 'create': self.output_image = self.create_label() if type_process == 'create-add': self.output_image = self.create_label(add=True) if type_process == 'create-seg': self.output_image = self.create_label_along_segmentation() if type_process == 'display-voxel': self.display_voxel() self.fname_output = None if type_process == 'diff': self.diff() self.fname_output = None if type_process == 'dist-inter': # second argument is in pixel distance self.distance_interlabels(5) self.fname_output = None if type_process == 'cubic-to-point': self.output_image = self.cubic_to_point() if type_process == 'vert-body': self.output_image = self.label_vertebrae(self.vertebral_levels) if type_process == 'vert-continuous': self.output_image = self.continuous_vertebral_levels() if type_process == 'create-viewer': self.output_image = self.launch_sagittal_viewer(self.value) if type_process in ['remove', 'keep']: self.output_image = self.remove_or_keep_labels(self.value, action=type_process) # TODO: do not save here. Create another function save() for that if self.fname_output is not None: if change_orientation: self.output_image.change_orientation(input_orientation) self.output_image.absolutepath = self.fname_output if type_process == 'vert-continuous': self.output_image.save(dtype='float32') elif type_process != 'plan_ref': self.output_image.save(dtype='minimize_int') else: self.output_image.save() return self.output_image def add(self, value): """ This function add a specified value to all non-zero voxels. """ image_output = self.image_input.copy() # image_output.data *= 0 coordinates_input = self.image_input.getNonZeroCoordinates() # for all points with non-zeros neighbors, force the neighbors to 0 for i, coord in enumerate(coordinates_input): image_output.data[int(coord.x), int(coord.y), int(coord.z)] = image_output.data[int(coord.x), int(coord.y), int(coord.z)] + float(value) return image_output def create_label(self, add=False): """ Create an image with labels listed by the user. This method works only if the user inserted correct coordinates. self.coordinates is a list of coordinates (class in msct_types). a Coordinate contains x, y, z and value. If only one label is to be added, coordinates must be completed with '[]' examples: For one label: object_define=ProcessLabels( fname_label, coordinates=[coordi]) where coordi is a 'Coordinate' object from msct_types For two labels: object_define=ProcessLabels( fname_label, coordinates=[coordi1, coordi2]) where coordi1 and coordi2 are 'Coordinate' objects from msct_types """ image_output = self.image_input.copy() if add else msct_image.zeros_like(self.image_input) # loop across labels for i, coord in enumerate(self.coordinates): if len(image_output.data.shape) == 3: image_output.data[int(coord.x), int(coord.y), int(coord.z)] = coord.value elif len(image_output.data.shape) == 2: assert str(coord.z) == '0', "ERROR: 2D coordinates should have a Z value of 0. Z coordinate is :" + str(coord.z) image_output.data[int(coord.x), int(coord.y)] = coord.value else: sct.printv('ERROR: Data should be 2D or 3D. Current shape is: ' + str(image_output.data.shape), 1, 'error') # display info sct.printv('Label #' + str(i) + ': ' + str(coord.x) + ',' + str(coord.y) + ',' + str(coord.z) + ' --> ' + str(coord.value), 1) return image_output def create_label_along_segmentation(self): """ Create an image with labels defined along the spinal cord segmentation (or centerline). Input image does **not** need to be RPI (re-orientation is done within this function). Example: object_define=ProcessLabels(fname_segmentation, coordinates=[coord_1, coord_2, coord_i]), where coord_i='z,value'. If z=-1, then use z=nz/2 (i.e. center of FOV in superior-inferior direction) Returns """ # reorient input image to RPI im_rpi = self.image_input.copy().change_orientation('RPI') im_output_rpi = zeros_like(im_rpi) # loop across labels for ilabel, coord in enumerate(self.coordinates): # split coord string list_coord = coord.split(',') # convert to int() and assign to variable z, value = [int(i) for i in list_coord] # update z based on native image orientation (z should represent superior-inferior axis) coord = Coordinate([z, z, z]) # since we don't know which dimension corresponds to the superior-inferior # axis, we put z in all dimensions (we don't care about x and y here) _, _, z_rpi = coord.permute(self.image_input, 'RPI') # if z=-1, replace with nz/2 if z == -1: z_rpi = int(np.round(im_output_rpi.dim[2] / 2.0)) # get center of mass of segmentation at given z x, y = ndimage.measurements.center_of_mass(np.array(im_rpi.data[:, :, z_rpi])) # round values to make indices x, y = int(np.round(x)), int(np.round(y)) # display info sct.printv('Label #' + str(ilabel) + ': ' + str(x) + ',' + str(y) + ',' + str(z_rpi) + ' --> ' + str(value), 1) if len(im_output_rpi.data.shape) == 3: im_output_rpi.data[x, y, z_rpi] = value elif len(im_output_rpi.data.shape) == 2: assert str(z) == '0', "ERROR: 2D coordinates should have a Z value of 0. Z coordinate is :" + str(z) im_output_rpi.data[x, y] = value # change orientation back to native return im_output_rpi.change_orientation(self.image_input.orientation) def plan(self, width, offset=0, gap=1): """ Create a plane of thickness="width" and changes its value with an offset and a gap between labels. """ image_output = msct_image.zeros_like(self.image_input) coordinates_input = self.image_input.getNonZeroCoordinates() # for all points with non-zeros neighbors, force the neighbors to 0 for coord in coordinates_input: image_output.data[:, :, int(coord.z) - width:int(coord.z) + width] = offset + gap * coord.value return image_output def plan_ref(self): """ Generate a plane in the reference space for each label present in the input image """ image_output = msct_image.zeros_like(Image(self.image_ref)) image_input_neg = msct_image.zeros_like(Image(self.image_input)) image_input_pos = msct_image.zeros_like(Image(self.image_input)) X, Y, Z = (self.image_input.data < 0).nonzero() for i in range(len(X)): image_input_neg.data[X[i], Y[i], Z[i]] = -self.image_input.data[X[i], Y[i], Z[i]] # in order to apply getNonZeroCoordinates X_pos, Y_pos, Z_pos = (self.image_input.data > 0).nonzero() for i in range(len(X_pos)): image_input_pos.data[X_pos[i], Y_pos[i], Z_pos[i]] = self.image_input.data[X_pos[i], Y_pos[i], Z_pos[i]] coordinates_input_neg = image_input_neg.getNonZeroCoordinates() coordinates_input_pos = image_input_pos.getNonZeroCoordinates() image_output.change_type('float32') for coord in coordinates_input_neg: image_output.data[:, :, int(coord.z)] = -coord.value # PB: takes the int value of coord.value for coord in coordinates_input_pos: image_output.data[:, :, int(coord.z)] = coord.value return image_output def cubic_to_point(self): """ Calculate the center of mass of each group of labels and returns a file of same size with only a label by group at the center of mass of this group. It is to be used after applying homothetic warping field to a label file as the labels will be dilated. Be careful: this algorithm computes the center of mass of voxels with same value, if two groups of voxels with the same value are present but separated in space, this algorithm will compute the center of mass of the two groups together. :return: image_output """ # 0. Initialization of output image output_image = msct_image.zeros_like(self.image_input) # 1. Extraction of coordinates from all non-null voxels in the image. Coordinates are sorted by value. coordinates = self.image_input.getNonZeroCoordinates(sorting='value') # 2. Separate all coordinates into groups by value groups = dict() for coord in coordinates: if coord.value in groups: groups[coord.value].append(coord) else: groups[coord.value] = [coord] # 3. Compute the center of mass of each group of voxels and write them into the output image for value, list_coord in groups.items(): center_of_mass = sum(list_coord) / float(len(list_coord)) sct.printv("Value = " + str(center_of_mass.value) + " : (" + str(center_of_mass.x) + ", " + str(center_of_mass.y) + ", " + str(center_of_mass.z) + ") --> ( " + str(np.round(center_of_mass.x)) + ", " + str(np.round(center_of_mass.y)) + ", " + str(np.round(center_of_mass.z)) + ")", verbose=self.verbose) output_image.data[int(np.round(center_of_mass.x)), int(np.round(center_of_mass.y)), int(np.round(center_of_mass.z))] = center_of_mass.value return output_image def increment_z_inverse(self): """ Take all non-zero values, sort them along the inverse z direction, and attributes the values 1, 2, 3, etc. This function assuming RPI orientation. """ image_output = msct_image.zeros_like(self.image_input) coordinates_input = self.image_input.getNonZeroCoordinates(sorting='z', reverse_coord=True) # for all points with non-zeros neighbors, force the neighbors to 0 for i, coord in enumerate(coordinates_input): image_output.data[int(coord.x), int(coord.y), int(coord.z)] = i + 1 return image_output def labelize_from_disks(self): """ Create an image with regions labelized depending on values from reference. Typically, user inputs a segmentation image, and labels with disks position, and this function produces a segmentation image with vertebral levels labelized. Labels are assumed to be non-zero and incremented from top to bottom, assuming a RPI orientation """ image_output = msct_image.zeros_like(self.image_input) coordinates_input = self.image_input.getNonZeroCoordinates() coordinates_ref = self.image_ref.getNonZeroCoordinates(sorting='value') # for all points in input, find the value that has to be set up, depending on the vertebral level for i, coord in enumerate(coordinates_input): for j in range(0, len(coordinates_ref) - 1): if coordinates_ref[j + 1].z < coord.z <= coordinates_ref[j].z: image_output.data[int(coord.x), int(coord.y), int(coord.z)] = coordinates_ref[j].value return image_output def label_vertebrae(self, levels_user=None): """ Find the center of mass of vertebral levels specified by the user. :return: image_output: Image with labels. """ # get center of mass of each vertebral level image_cubic2point = self.cubic_to_point() # get list of coordinates for each label list_coordinates = image_cubic2point.getNonZeroCoordinates(sorting='value') # if user did not specify levels, include all: if levels_user[0] == 0: levels_user = [int(i.value) for i in list_coordinates] # loop across labels and remove those that are not listed by the user for i_label in range(len(list_coordinates)): # check if this level is NOT in levels_user if not levels_user.count(int(list_coordinates[i_label].value)): # if not, set value to zero image_cubic2point.data[int(list_coordinates[i_label].x), int(list_coordinates[i_label].y), int(list_coordinates[i_label].z)] = 0 # list all labels return image_cubic2point def MSE(self, threshold_mse=0): """ Compute the Mean Square Distance Error between two sets of labels (input and ref). Moreover, a warning is generated for each label mismatch. If the MSE is above the threshold provided (by default = 0mm), a log is reported with the filenames considered here. """ coordinates_input = self.image_input.getNonZeroCoordinates() coordinates_ref = self.image_ref.getNonZeroCoordinates() # check if all the labels in both the images match if len(coordinates_input) != len(coordinates_ref): sct.printv('ERROR: labels mismatch', 1, 'warning') for coord in coordinates_input: if np.round(coord.value) not in [np.round(coord_ref.value) for coord_ref in coordinates_ref]: sct.printv('ERROR: labels mismatch', 1, 'warning') for coord_ref in coordinates_ref: if np.round(coord_ref.value) not in [np.round(coord.value) for coord in coordinates_input]: sct.printv('ERROR: labels mismatch', 1, 'warning') result = 0.0 for coord in coordinates_input: for coord_ref in coordinates_ref: if np.round(coord_ref.value) == np.round(coord.value): result += (coord_ref.z - coord.z) ** 2 break result = np.sqrt(result / len(coordinates_input)) sct.printv('MSE error in Z direction = ' + str(result) + ' mm') if result > threshold_mse: parent, stem, ext = sct.extract_fname(self.image_input.absolutepath) fname_report = os.path.join(parent, 'error_log_{}.txt'.format(stem)) with open(fname_report, 'w') as f: f.write('The labels error (MSE) between {} and {} is: {}\n'.format( os.path.relpath(self.image_input.absolutepath, os.path.dirname(fname_report)), os.path.relpath(self.image_ref.absolutepath, os.path.dirname(fname_report)), result)) return result @staticmethod def remove_label_coord(coord_input, coord_ref, symmetry=False): """ coord_input and coord_ref should be sets of CoordinateValue in order to improve speed of intersection :param coord_input: set of CoordinateValue :param coord_ref: set of CoordinateValue :param symmetry: boolean, :return: intersection of CoordinateValue: list """ from msct_types import CoordinateValue if isinstance(coord_input[0], CoordinateValue) and isinstance(coord_ref[0], CoordinateValue) and symmetry: coord_intersection = list(set(coord_input).intersection(set(coord_ref))) result_coord_input = [coord for coord in coord_input if coord in coord_intersection] result_coord_ref = [coord for coord in coord_ref if coord in coord_intersection] else: result_coord_ref = coord_ref result_coord_input = [coord for coord in coord_input if list(filter(lambda x: x.value == coord.value, coord_ref))] if symmetry: result_coord_ref = [coord for coord in coord_ref if list(filter(lambda x: x.value == coord.value, result_coord_input))] return result_coord_input, result_coord_ref def remove_label(self, symmetry=False): """ Compare two label images and remove any labels in input image that are not in reference image. The symmetry option enables to remove labels from reference image that are not in input image """ # image_output = Image(self.image_input.dim, orientation=self.image_input.orientation, hdr=self.image_input.hdr, verbose=self.verbose) image_output = msct_image.zeros_like(self.image_input) result_coord_input, result_coord_ref = self.remove_label_coord(self.image_input.getNonZeroCoordinates(coordValue=True), self.image_ref.getNonZeroCoordinates(coordValue=True), symmetry) for coord in result_coord_input: image_output.data[int(coord.x), int(coord.y), int(coord.z)] = int(np.round(coord.value)) if symmetry: # image_output_ref = Image(self.image_ref.dim, orientation=self.image_ref.orientation, hdr=self.image_ref.hdr, verbose=self.verbose) image_output_ref = Image(self.image_ref, verbose=self.verbose) for coord in result_coord_ref: image_output_ref.data[int(coord.x), int(coord.y), int(coord.z)] = int(np.round(coord.value)) image_output_ref.absolutepath = self.fname_output[1] image_output_ref.save('minimize_int') self.fname_output = self.fname_output[0] return image_output def display_voxel(self): """ Display all the labels that are contained in the input image. The image is suppose to be RPI to display voxels. But works also for other orientations """ coordinates_input = self.image_input.getNonZeroCoordinates(sorting='value') self.useful_notation = '' for coord in coordinates_input: sct.printv('Position=(' + str(coord.x) + ',' + str(coord.y) + ',' + str(coord.z) + ') -- Value= ' + str(coord.value), verbose=self.verbose) if self.useful_notation: self.useful_notation = self.useful_notation + ':' self.useful_notation += str(coord) sct.printv('All labels (useful syntax):', verbose=self.verbose) sct.printv(self.useful_notation, verbose=self.verbose) return coordinates_input def get_physical_coordinates(self): """ This function returns the coordinates of the labels in the physical referential system. :return: a list of CoordinateValue, in the physical (scanner) space """ coord = self.image_input.getNonZeroCoordinates(sorting='value') phys_coord = [] for c in coord: # convert pixelar coordinates to physical coordinates c_p = self.image_input.transfo_pix2phys([[c.x, c.y, c.z]])[0] phys_coord.append(CoordinateValue([c_p[0], c_p[1], c_p[2], c.value])) return phys_coord def get_coordinates_in_destination(self, im_dest, type='discrete'): """ This function calculate the position of labels in the pixelar space of a destination image :param im_dest: Object Image :param type: 'discrete' or 'continuous' :return: a list of CoordinateValue, in the pixelar (image) space of the destination image """ phys_coord = self.get_physical_coordinates() dest_coord = [] for c in phys_coord: if type is 'discrete': c_p = im_dest.transfo_phys2pix([[c.x, c.y, c.y]])[0] elif type is 'continuous': c_p = im_dest.transfo_phys2pix([[c.x, c.y, c.y]], real=False)[0] else: raise ValueError("The value of 'type' should either be 'discrete' or 'continuous'.") dest_coord.append(CoordinateValue([c_p[0], c_p[1], c_p[2], c.value])) return dest_coord def diff(self): """ Detect any label mismatch between input image and reference image """ coordinates_input = self.image_input.getNonZeroCoordinates() coordinates_ref = self.image_ref.getNonZeroCoordinates() sct.printv("Label in input image that are not in reference image:") for coord in coordinates_input: isIn = False for coord_ref in coordinates_ref: if coord.value == coord_ref.value: isIn = True break if not isIn: sct.printv(coord.value) sct.printv("Label in ref image that are not in input image:") for coord_ref in coordinates_ref: isIn = False for coord in coordinates_input: if coord.value == coord_ref.value: isIn = True break if not isIn: sct.printv(coord_ref.value) def distance_interlabels(self, max_dist): """ Calculate the distances between each label in the input image. If a distance is larger than max_dist, a warning message is displayed. """ coordinates_input = self.image_input.getNonZeroCoordinates() # for all points with non-zeros neighbors, force the neighbors to 0 for i in range(0, len(coordinates_input) - 1): dist = np.sqrt((coordinates_input[i].x - coordinates_input[i + 1].x)**2 + (coordinates_input[i].y - coordinates_input[i + 1].y)**2 + (coordinates_input[i].z - coordinates_input[i + 1].z)**2) if dist < max_dist: sct.printv('Warning: the distance between label ' + str(i) + '[' + str(coordinates_input[i].x) + ',' + str(coordinates_input[i].y) + ',' + str( coordinates_input[i].z) + ']=' + str(coordinates_input[i].value) + ' and label ' + str(i + 1) + '[' + str( coordinates_input[i + 1].x) + ',' + str(coordinates_input[i + 1].y) + ',' + str(coordinates_input[i + 1].z) + ']=' + str( coordinates_input[i + 1].value) + ' is larger than ' + str(max_dist) + '. Distance=' + str(dist)) def continuous_vertebral_levels(self): """ This function transforms the vertebral levels file from the template into a continuous file. Instead of having integer representing the vertebral level on each slice, a continuous value that represents the position of the slice in the vertebral level coordinate system. The image must be RPI :return: """ im_input = Image(self.image_input, self.verbose) im_output = msct_image.zeros_like(self.image_input) # 1. extract vertebral levels from input image # a. extract centerline # b. for each slice, extract corresponding level nx, ny, nz, nt, px, py, pz, pt = im_input.dim from spinalcordtoolbox.centerline.core import get_centerline _, arr_ctl, _ = get_centerline(self.image_input, algo_fitting='bspline') x_centerline_fit, y_centerline_fit, z_centerline = arr_ctl value_centerline = np.array( [im_input.data[int(x_centerline_fit[it]), int(y_centerline_fit[it]), int(z_centerline[it])] for it in range(len(z_centerline))]) # 2. compute distance for each vertebral level --> Di for i being the vertebral levels vertebral_levels = {} for slice_image, level in enumerate(value_centerline): if level not in vertebral_levels: vertebral_levels[level] = slice_image length_levels = {} for level in vertebral_levels: indexes_slice = np.where(value_centerline == level) length_levels[level] = np.sum([np.sqrt(((x_centerline_fit[indexes_slice[0][index_slice + 1]] - x_centerline_fit[indexes_slice[0][index_slice]]) * px)**2 + ((y_centerline_fit[indexes_slice[0][index_slice + 1]] - y_centerline_fit[indexes_slice[0][index_slice]]) * py)**2 + ((z_centerline[indexes_slice[0][index_slice + 1]] - z_centerline[indexes_slice[0][index_slice]]) * pz)**2) for index_slice in range(len(indexes_slice[0]) - 1)]) # 2. for each slice: # a. identify corresponding vertebral level --> i # b. calculate distance of slice from upper vertebral level --> d # c. compute relative distance in the vertebral level coordinate system --> d/Di continuous_values = {} for it, iz in enumerate(z_centerline): level = value_centerline[it] indexes_slice = np.where(value_centerline == level) indexes_slice = indexes_slice[0][indexes_slice[0] >= it] distance_from_level = np.sum([np.sqrt(((x_centerline_fit[indexes_slice[index_slice + 1]] - x_centerline_fit[indexes_slice[index_slice]]) * px * px) ** 2 + ((y_centerline_fit[indexes_slice[index_slice + 1]] - y_centerline_fit[indexes_slice[index_slice]]) * py * py) ** 2 + ((z_centerline[indexes_slice[index_slice + 1]] - z_centerline[indexes_slice[index_slice]]) * pz * pz) ** 2) for index_slice in range(len(indexes_slice) - 1)]) continuous_values[iz] = level + 2.0 * distance_from_level / float(length_levels[level]) # 3. saving data # for each slice, get all non-zero pixels and replace with continuous values coordinates_input = self.image_input.getNonZeroCoordinates() im_output.change_type(np.float32) # for all points in input, find the value that has to be set up, depending on the vertebral level for i, coord in enumerate(coordinates_input): im_output.data[int(coord.x), int(coord.y), int(coord.z)] = continuous_values[coord.z] return im_output def launch_sagittal_viewer(self, labels): from spinalcordtoolbox.gui import base from spinalcordtoolbox.gui.sagittal import launch_sagittal_dialog params = base.AnatomicalParams() params.vertebraes = labels params.input_file_name = self.image_input.absolutepath params.output_file_name = self.fname_output params.subtitle = self.msg output = msct_image.zeros_like(self.image_input) output.absolutepath = self.fname_output launch_sagittal_dialog(self.image_input, output, params) return output def remove_or_keep_labels(self, labels, action): """ Create or remove labels from self.image_input :param list(int): Labels to keep or remove :param str: 'remove': remove specified labels (i.e. set to zero), 'keep': keep specified labels and remove the others """ if action == 'keep': image_output = msct_image.zeros_like(self.image_input) elif action == 'remove': image_output = self.image_input.copy() coordinates_input = self.image_input.getNonZeroCoordinates() for labelNumber in labels: isInLabels = False for coord in coordinates_input: if labelNumber == coord.value: new_coord = coord isInLabels = True if isInLabels: if action == 'keep': image_output.data[int(new_coord.x), int(new_coord.y), int(new_coord.z)] = new_coord.value elif action == 'remove': image_output.data[int(new_coord.x), int(new_coord.y), int(new_coord.z)] = 0.0 else: sct.printv("WARNING: Label " + str(float(labelNumber)) + " not found in input image.", type='warning') return image_output
def dummy_centerline(size_arr=(9, 9, 9), pixdim=(1, 1, 1), subsampling=1, dilate_ctl=0, hasnan=False, zeroslice=[], outlier=[], orientation='RPI', debug=False): """ Create a dummy Image centerline of small size. Return the full and sub-sampled version along z. Voxel resolution on fully-sampled data is 1x1x1 mm (so, 2x undersampled data along z would have resolution of 1x1x2 mm). :param size_arr: tuple: (nx, ny, nz) :param pixdim: tuple: (px, py, pz) :param subsampling: int >=1. Subsampling factor along z. 1: no subsampling. 2: centerline defined every other z. :param dilate_ctl: Dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. if dilate_ctl=0, result will be a single pixel per slice. :param hasnan: Bool: Image has non-numerical values: nan, inf. In this case, do not subsample. :param zeroslice: list int: zero all slices listed in this param :param outlier: list int: replace the current point with an outlier at the corner of the image for the slices listed :param orientation: :param debug: Bool: Write temp files :return: """ nx, ny, nz = size_arr # create regularized curve, within X-Z plane, located at y=ny/4, passing through the following points: x = np.array([round(nx / 4.), round(nx / 2.), round(3 * nx / 4.)]) z = np.array([0, round(nz / 2.), nz - 1]) # we use bspline (instead of poly) in order to avoid bad extrapolation at edges # see: https://github.com/spinalcordtoolbox/spinalcordtoolbox/pull/2754 xfit, _ = bspline(z, x, range(nz), 10) # p = P.fit(z, x, 3) # p = np.poly1d(np.polyfit(z, x, deg=3)) data = np.zeros((nx, ny, nz)) arr_ctl = np.array( [xfit.astype(np.int), [round(ny / 4.)] * len(range(nz)), range(nz)], dtype=np.uint16) # Loop across dilation of centerline. E.g., if dilate_ctl=1, result will be a square of 3x3 per slice. for ixiy_ctl in itertools.product(range(-dilate_ctl, dilate_ctl + 1, 1), range(-dilate_ctl, dilate_ctl + 1, 1)): data[(arr_ctl[0] + ixiy_ctl[0]).tolist(), (arr_ctl[1] + ixiy_ctl[1]).tolist(), arr_ctl[2].tolist()] = 1 # Zero specified slices if zeroslice is not []: data[:, :, zeroslice] = 0 # Add outlier if outlier is not []: # First, zero all the slice data[:, :, outlier] = 0 # Then, add point in the corner data[0, 0, outlier] = 1 # Create image with default orientation LPI affine = np.eye(4) affine[0:3, 0:3] = affine[0:3, 0:3] * pixdim nii = nib.nifti1.Nifti1Image(data, affine) img = Image(data, hdr=nii.header, dim=nii.header.get_data_shape()) # subsample data img_sub = img.copy() img_sub.data = np.zeros((nx, ny, nz)) for iz in range(0, nz, subsampling): img_sub.data[..., iz] = data[..., iz] # Add non-numerical values at the top corner of the image if hasnan: img.data[0, 0, 0] = np.nan img.data[1, 0, 0] = np.inf # Update orientation img.change_orientation(orientation) img_sub.change_orientation(orientation) if debug: img_sub.save('tmp_dummy_seg_' + datetime.now().strftime("%Y%m%d%H%M%S%f") + '.nii.gz') return img, img_sub, arr_ctl
def deep_segmentation_MSlesion(im_image, contrast_type, ctr_algo='svm', ctr_file=None, brain_bool=True, remove_temp_files=1, verbose=1): """ Segment lesions from MRI data. :param im_image: Image() object containing the lesions to segment :param contrast_type: Constrast of the image. Need to use one supported by the CNN models. :param ctr_algo: Algo to find the centerline. See sct_get_centerline :param ctr_file: Centerline or segmentation (optional) :param brain_bool: If brain if present or not in the image. :param remove_temp_files: :return: """ # create temporary folder with intermediate results tmp_folder = sct.TempFolder(verbose=verbose) tmp_folder_path = tmp_folder.get_path() if ctr_algo == 'file': # if the ctr_file is provided tmp_folder.copy_from(ctr_file) file_ctr = os.path.basename(ctr_file) else: file_ctr = None tmp_folder.chdir() # orientation of the image, should be RPI logger.info("\nReorient the image to RPI, if necessary...") fname_in = im_image.absolutepath original_orientation = im_image.orientation fname_orient = 'image_in_RPI.nii' im_image.change_orientation('RPI').save(fname_orient) input_resolution = im_image.dim[4:7] # find the spinal cord centerline - execute OptiC binary logger.info("\nFinding the spinal cord centerline...") contrast_type_ctr = contrast_type.split('_')[0] fname_res, centerline_filename = find_centerline(algo=ctr_algo, image_fname=fname_orient, contrast_type=contrast_type_ctr, brain_bool=brain_bool, folder_output=tmp_folder_path, remove_temp_files=remove_temp_files, centerline_fname=file_ctr) im_nii, ctr_nii = Image(fname_res), Image(centerline_filename) # crop image around the spinal cord centerline logger.info("\nCropping the image around the spinal cord...") crop_size = 48 X_CROP_LST, Y_CROP_LST, Z_CROP_LST, im_crop_nii = crop_image_around_centerline(im_in=im_nii, ctr_in=ctr_nii, crop_size=crop_size) del ctr_nii # normalize the intensity of the images logger.info("Normalizing the intensity...") im_norm_in = apply_intensity_normalization(img=im_crop_nii, contrast=contrast_type) del im_crop_nii # resample to 0.5mm isotropic fname_norm = sct.add_suffix(fname_orient, '_norm') im_norm_in.save(fname_norm) fname_res3d = sct.add_suffix(fname_norm, '_resampled3d') resampling.resample_file(fname_norm, fname_res3d, '0.5x0.5x0.5', 'mm', 'linear', verbose=0) # segment data using 3D convolutions logger.info("\nSegmenting the MS lesions using deep learning on 3D patches...") segmentation_model_fname = os.path.join(sct.__sct_dir__, 'data', 'deepseg_lesion_models', '{}_lesion.h5'.format(contrast_type)) fname_seg_crop_res = sct.add_suffix(fname_res3d, '_lesionseg') im_res3d = Image(fname_res3d) seg_im = segment_3d(model_fname=segmentation_model_fname, contrast_type=contrast_type, im=im_res3d.copy()) seg_im.save(fname_seg_crop_res) del im_res3d, seg_im # resample to the initial pz resolution fname_seg_res2d = sct.add_suffix(fname_seg_crop_res, '_resampled2d') initial_2d_resolution = 'x'.join(['0.5', '0.5', str(input_resolution[2])]) resampling.resample_file(fname_seg_crop_res, fname_seg_res2d, initial_2d_resolution, 'mm', 'linear', verbose=0) seg_crop = Image(fname_seg_res2d) # reconstruct the segmentation from the crop data logger.info("\nReassembling the image...") seg_uncrop_nii = uncrop_image(ref_in=im_nii, data_crop=seg_crop.copy().data, x_crop_lst=X_CROP_LST, y_crop_lst=Y_CROP_LST, z_crop_lst=Z_CROP_LST) fname_seg_res_RPI = sct.add_suffix(fname_in, '_res_RPI_seg') seg_uncrop_nii.save(fname_seg_res_RPI) del seg_crop # resample to initial resolution logger.info("Resampling the segmentation to the original image resolution...") initial_resolution = 'x'.join([str(input_resolution[0]), str(input_resolution[1]), str(input_resolution[2])]) fname_seg_RPI = sct.add_suffix(fname_in, '_RPI_seg') resampling.resample_file(fname_seg_res_RPI, fname_seg_RPI, initial_resolution, 'mm', 'linear', verbose=0) seg_initres_nii = Image(fname_seg_RPI) if ctr_algo == 'viewer': # resample and reorient the viewer labels fname_res_labels = sct.add_suffix(fname_orient, '_labels-centerline') resampling.resample_file(fname_res_labels, fname_res_labels, initial_resolution, 'mm', 'linear', verbose=0) im_image_res_labels_downsamp = Image(fname_res_labels).change_orientation(original_orientation) else: im_image_res_labels_downsamp = None if verbose == 2: fname_res_ctr = sct.add_suffix(fname_orient, '_ctr') resampling.resample_file(fname_res_ctr, fname_res_ctr, initial_resolution, 'mm', 'linear', verbose=0) im_image_res_ctr_downsamp = Image(fname_res_ctr).change_orientation(original_orientation) else: im_image_res_ctr_downsamp = None # binarize the resampled image to remove interpolation effects logger.info("\nBinarizing the segmentation to avoid interpolation effects...") thr = 0.1 seg_initres_nii.data[np.where(seg_initres_nii.data >= thr)] = 1 seg_initres_nii.data[np.where(seg_initres_nii.data < thr)] = 0 # reorient to initial orientation logger.info("\nReorienting the segmentation to the original image orientation...") tmp_folder.chdir_undo() # remove temporary files if remove_temp_files: logger.info("\nRemove temporary files...") tmp_folder.cleanup() # reorient to initial orientation return seg_initres_nii.change_orientation(original_orientation), im_image_res_labels_downsamp, im_image_res_ctr_downsamp
def dummy_segmentation(size_arr=(256, 256, 256), pixdim=(1, 1, 1), dtype=np.float64, orientation='LPI', shape='rectangle', angle_RL=0, angle_AP=0, angle_IS=0, radius_RL=5.0, radius_AP=3.0, zeroslice=[], debug=False): """Create a dummy Image with a ellipse or ones running from top to bottom in the 3rd dimension, and rotate the image to make sure that compute_csa and compute_shape properly estimate the centerline angle. :param size_arr: tuple: (nx, ny, nz) :param pixdim: tuple: (px, py, pz) :param dtype: Numpy dtype. :param orientation: Orientation of the image. Default: LPI :param shape: {'rectangle', 'ellipse'} :param angle_RL: int: angle around RL axis (in deg) :param angle_AP: int: angle around AP axis (in deg) :param angle_IS: int: angle around IS axis (in deg) :param radius_RL: float: 1st radius. With a, b = 50.0, 30.0 (in mm), theoretical CSA of ellipse is 4712.4 :param radius_AP: float: 2nd radius :param zeroslice: list int: zero all slices listed in this param :param debug: Write temp files for debug :return: img: Image object """ # Initialization padding = 15 # Padding size (isotropic) to avoid edge effect during rotation # Create a 3d array, with dimensions corresponding to x: RL, y: AP, z: IS nx, ny, nz = [int(size_arr[i] * pixdim[i]) for i in range(3)] data = np.random.random((nx, ny, nz)) * 0. xx, yy = np.mgrid[:nx, :ny] # loop across slices and add object for iz in range(nz): if shape == 'rectangle': # theoretical CSA: (a*2+1)(b*2+1) data[:, :, iz] = ((abs(xx - nx / 2) <= radius_RL) & (abs(yy - ny / 2) <= radius_AP)) * 1 if shape == 'ellipse': data[:, :, iz] = (((xx - nx / 2) / radius_RL) ** 2 + ((yy - ny / 2) / radius_AP) ** 2 <= 1) * 1 # Pad to avoid edge effect during rotation data = np.pad(data, padding, 'reflect') # ROTATION ABOUT IS AXIS # rotate (in deg), and re-grid using linear interpolation data_rotIS = rotate(data, angle_IS, resize=False, center=None, order=1, mode='constant', cval=0, clip=False, preserve_range=False) # ROTATION ABOUT RL AXIS # Swap x-z axes (to make a rotation within y-z plane, because rotate will apply rotation on the first 2 dims) data_rotIS_swap = data_rotIS.swapaxes(0, 2) # rotate (in deg), and re-grid using linear interpolation data_rotIS_swap_rotRL = rotate(data_rotIS_swap, angle_RL, resize=False, center=None, order=1, mode='constant', cval=0, clip=False, preserve_range=False) # swap back data_rotIS_rotRL = data_rotIS_swap_rotRL.swapaxes(0, 2) # ROTATION ABOUT AP AXIS # Swap y-z axes (to make a rotation within x-z plane) data_rotIS_rotRL_swap = data_rotIS_rotRL.swapaxes(1, 2) # rotate (in deg), and re-grid using linear interpolation data_rotIS_rotRL_swap_rotAP = rotate(data_rotIS_rotRL_swap, angle_AP, resize=False, center=None, order=1, mode='constant', cval=0, clip=False, preserve_range=False) # swap back data_rot = data_rotIS_rotRL_swap_rotAP.swapaxes(1, 2) # Crop image (to remove padding) data_rot_crop = data_rot[padding:nx+padding, padding:ny+padding, padding:nz+padding] # Zero specified slices if zeroslice is not []: data_rot_crop[:, :, zeroslice] = 0 # Create nibabel object xform = np.eye(4) for i in range(3): xform[i][i] = 1 # in [mm] nii = nib.nifti1.Nifti1Image(data_rot_crop.astype('float32'), xform) # Create nipy object and resample to desired resolution nii_nipy = nifti2nipy(nii) nii_nipy_r = resample_nipy(nii_nipy, new_size='x'.join([str(i) for i in pixdim]), new_size_type='mm', interpolation='linear', dtype=dtype) nii_r = nipy2nifti(nii_nipy_r) # Create Image object. Default orientation is LPI. # For debugging add .save() at the end of the command below img = Image(nii_r.get_data(), hdr=nii_r.header, dim=nii_r.header.get_data_shape()) # Update orientation img.change_orientation(orientation) if debug: img.save('tmp_dummy_seg_'+datetime.now().strftime("%Y%m%d%H%M%S%f")+'.nii.gz') return img