def vertebral_detection(fname, fname_seg, contrast, param, init_disc=[], verbose=1, path_template='', initc2='auto', path_output='../'): """ Find intervertebral discs in straightened image using template matching :param fname: :param fname_seg: :param contrast: :param param: advanced parameters :param init_disc: :param verbose: :param path_template: :param path_output: output path for verbose=2 pictures :return: """ printv('\nLook for template...', verbose) # if path_template == '': # # get path of SCT # from os import path # path_script = path.dirname(__file__) # path_sct = slash_at_the_end(path.dirname(path_script), 1) # folder_template = 'data/template/' # path_template = path_sct+folder_template printv('Path template: '+path_template, verbose) # adjust file names if MNI-Poly-AMU template is used fname_level = get_file_label(path_template+'template/', 'vertebral', output='filewithpath') fname_template = get_file_label(path_template+'template/', contrast.upper()+'-weighted', output='filewithpath') # if not len(glob(path_template+'MNI-Poly-AMU*.*')) == 0: # contrast = contrast.upper() # file_level = '*_level.nii.gz' # else: # file_level = '*_levels.nii.gz' # # # retrieve file_template based on contrast # try: # fname_template_list = glob(path_template + '*' + contrast + '.nii.gz') # fname_template = fname_template_list[0] # except IndexError: # printv('\nERROR: No template found. Please check the provided path.', 1, 'error') # retrieve disc level from template # try: # fname_level_list = glob(path_template+file_level) # fname_level = fname_level_list[0] # except IndexError: # printv('\nERROR: File *_levels.nii.gz not found.', 1, 'error') # Open template and vertebral levels printv('\nOpen template and vertebral levels...', verbose) data_template = Image(fname_template).data data_disc_template = Image(fname_level).data # open anatomical volume im_input = Image(fname) data = im_input.data # smooth data from scipy.ndimage.filters import gaussian_filter data = gaussian_filter(data, param.smooth_factor, output=None, mode="reflect") # get dimension of src nx, ny, nz = data.shape # define xc and yc (centered in the field of view) xc = int(round(nx/2)) # direction RL yc = int(round(ny/2)) # direction AP # get dimension of template nxt, nyt, nzt = data_template.shape # define xc and yc (centered in the field of view) xct = int(round(nxt/2)) # direction RL yct = int(round(nyt/2)) # direction AP # define mean distance (in voxel) between adjacent discs: [C1/C2 -> C2/C3], [C2/C3 -> C4/C5], ..., [L1/L2 -> L2/L3] centerline_level = data_disc_template[xct, yct, :] # attribute value to each disc. Starts from max level, then decrease. min_level = centerline_level[centerline_level.nonzero()].min() max_level = centerline_level[centerline_level.nonzero()].max() list_disc_value_template = range(min_level, max_level) # add disc above top one list_disc_value_template.insert(int(0), min_level - 1) printv('\nDisc values from template: ' + str(list_disc_value_template), verbose) # get diff to find transitions (i.e., discs) diff_centerline_level = np.diff(centerline_level) # get disc z-values list_disc_z_template = diff_centerline_level.nonzero()[0].tolist() list_disc_z_template.reverse() printv('Z-values for each disc: ' + str(list_disc_z_template), verbose) list_distance_template = ( np.diff(list_disc_z_template) * (-1)).tolist() # multiplies by -1 to get positive distances printv('Distances between discs (in voxel): ' + str(list_distance_template), verbose) # if automatic mode, find C2/C3 disc if init_disc == [] and initc2 == 'auto': printv('\nDetect C2/C3 disk...', verbose) zrange = range(0, nz) ind_c2 = list_disc_value_template.index(2) z_peak = compute_corr_3d(src=data, target=data_template, x=xc, xshift=0, xsize=param.size_RL_initc2, y=yc, yshift=param.shift_AP_initc2, ysize=param.size_AP_initc2, z=0, zshift=param.shift_IS_initc2, zsize=param.size_IS_initc2, xtarget=xct, ytarget=yct, ztarget=list_disc_z_template[ind_c2], zrange=zrange, verbose=verbose, save_suffix='_initC2', gaussian_weighting=True, path_output=path_output) init_disc = [z_peak, 2] # if manual mode, open viewer for user to click on C2/C3 disc if init_disc == [] and initc2 == 'manual': from sct_viewer import ClickViewer # reorient image to SAL to be compatible with viewer im_input_SAL = im_input.copy() im_input_SAL.change_orientation('SAL') viewer = ClickViewer(im_input_SAL, orientation_subplot=['sag', 'ax']) viewer.number_of_slices = 1 pz = 1 viewer.gap_inter_slice = int(10 / pz) viewer.calculate_list_slices() viewer.help_url = 'https://sourceforge.net/p/spinalcordtoolbox/wiki/sct_label_vertebrae/attachment/label_vertebrae_viewer.png' # start the viewer that ask the user to enter a few points along the spinal cord mask_points = viewer.start() if mask_points: # create the mask containing either the three-points or centerline mask for initialization mask_filename = sct.add_suffix(fname, "_mask_viewer") sct.run("sct_label_utils -i " + fname + " -create " + mask_points + " -o " + mask_filename, verbose=False) else: sct.printv('\nERROR: the viewer has been closed before entering all manual points. Please try again.', verbose, type='error') # assign new init_disc_z value, which corresponds to the first vector of mask_points. Note, we need to substract from nz due to SAL orientation: in the viewer, orientation is S-I while in this code, it is I-S. init_disc = [nz-int(mask_points.split(',')[0]), 2] # display init disc if verbose == 2: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt plt.matshow(np.mean(data[xc-param.size_RL:xc+param.size_RL, :, :], axis=0).transpose(), fignum=50, cmap=plt.cm.gray, clim=[0, 800], origin='lower') plt.title('Anatomical image') plt.autoscale(enable=False) # to prevent autoscale of axis when displaying plot plt.figure(50), plt.scatter(yc + param.shift_AP_visu, init_disc[0], c='yellow', s=50) plt.text(yc + param.shift_AP_visu + 4, init_disc[0], str(init_disc[1]) + '/' + str(init_disc[1] + 1), verticalalignment='center', horizontalalignment='left', color='pink', fontsize=15), plt.draw() # plt.ion() # enables interactive mode # FIND DISCS # =========================================================================== printv('\nDetect intervertebral discs...', verbose) # assign initial z and disc current_z = init_disc[0] current_disc = init_disc[1] # mean_distance = mean_distance * pz # mean_distance_real = np.zeros(len(mean_distance)) # create list for z and disc list_disc_z = [] list_disc_value = [] zrange = range(-10, 10) direction = 'superior' search_next_disc = True while search_next_disc: printv('Current disc: '+str(current_disc)+' (z='+str(current_z)+'). Direction: '+direction, verbose) try: # get z corresponding to current disc on template current_z_template = list_disc_z_template[current_disc] except: # in case reached the bottom (see issue #849) printv('WARNING: Reached the bottom of the template. Stop searching.', verbose, 'warning') break # find next disc # N.B. Do not search for C1/C2 disc (because poorly visible), use template distance instead if not current_disc in [1]: current_z = compute_corr_3d(src=data, target=data_template, x=xc, xshift=0, xsize=param.size_RL, y=yc, yshift=param.shift_AP, ysize=param.size_AP, z=current_z, zshift=0, zsize=param.size_IS, xtarget=xct, ytarget=yct, ztarget=current_z_template, zrange=zrange, verbose=verbose, save_suffix='_disc'+str(current_disc), gaussian_weighting=False, path_output=path_output) # display new disc if verbose == 2: plt.figure(50), plt.scatter(yc+param.shift_AP_visu, current_z, c='yellow', s=50) plt.text(yc + param.shift_AP_visu + 4, current_z, str(current_disc)+'/'+str(current_disc+1), verticalalignment='center', horizontalalignment='left', color='yellow', fontsize=15), plt.draw() # append to main list if direction == 'superior': # append at the beginning list_disc_z.insert(0, current_z) list_disc_value.insert(0, current_disc) elif direction == 'inferior': # append at the end list_disc_z.append(current_z) list_disc_value.append(current_disc) # adjust correcting factor based on already-identified discs if len(list_disc_z) > 1: # compute distance between already-identified discs list_distance_current = (np.diff(list_disc_z) * (-1)).tolist() # retrieve the template distance corresponding to the already-identified discs index_disc_identified = [i for i, j in enumerate(list_disc_value_template) if j in list_disc_value[:-1]] list_distance_template_identified = [list_distance_template[i] for i in index_disc_identified] # divide subject and template distances for the identified discs list_subject_to_template_distance = [float(list_distance_current[i]) / list_distance_template_identified[i] for i in range(len(list_distance_current))] # average across identified discs to obtain an average correcting factor correcting_factor = np.mean(list_subject_to_template_distance) printv('.. correcting factor: '+str(correcting_factor), verbose) else: correcting_factor = 1 # update list_distance specific for the subject list_distance = [int(round(list_distance_template[i] * correcting_factor)) for i in range(len(list_distance_template))] # updated average_disc_distance (in case it is needed) # average_disc_distance = int(round(np.mean(list_distance))) # assign new current_z and disc value if direction == 'superior': try: approx_distance_to_next_disc = list_distance[list_disc_value_template.index(current_disc-1)] except ValueError: printv('WARNING: Disc value not included in template. Using previously-calculated distance: '+str(approx_distance_to_next_disc)) # assign new current_z and disc value current_z = current_z + approx_distance_to_next_disc current_disc = current_disc - 1 elif direction == 'inferior': try: approx_distance_to_next_disc = list_distance[list_disc_value_template.index(current_disc)] except: printv('WARNING: Disc value not included in template. Using previously-calculated distance: '+str(approx_distance_to_next_disc)) # assign new current_z and disc value current_z = current_z - approx_distance_to_next_disc current_disc = current_disc + 1 # if current_z is larger than searching zone, switch direction (and start from initial z minus approximate distance from updated template distance) if current_z >= nz or current_disc == 0: printv('.. Switching to inferior direction.', verbose) direction = 'inferior' current_disc = init_disc[1] + 1 current_z = init_disc[0] - list_distance[list_disc_value_template.index(current_disc)] # if current_z is lower than searching zone, stop searching if current_z <= 0: search_next_disc = False # if verbose == 2: # # close figures # plt.figure(fig_corr), plt.close() # plt.figure(fig_pattern), plt.close() # if upper disc is not 1, add disc above top disc based on mean_distance_adjusted upper_disc = min(list_disc_value) # if not upper_disc == 1: printv('Adding top disc based on adjusted template distance: #'+str(upper_disc-1), verbose) approx_distance_to_next_disc = list_distance[list_disc_value_template.index(upper_disc-1)] next_z = max(list_disc_z) + approx_distance_to_next_disc printv('.. approximate distance: '+str(approx_distance_to_next_disc), verbose) # make sure next disc does not go beyond FOV in superior direction if next_z > nz: list_disc_z.insert(0, nz) else: list_disc_z.insert(0, next_z) # assign disc value list_disc_value.insert(0, upper_disc-1) # Label segmentation label_segmentation(fname_seg, list_disc_z, list_disc_value, verbose=verbose) # save figure if verbose == 2: plt.figure(50), plt.savefig(path_output + 'fig_anat_straight_with_labels.png')
path_fname, file_fname, ext_fname = sct.extract_fname(input_filename) # if centerline or mask is asked using viewer if use_viewer: # make sure image is in SAL orientation, as it is the orientation used by PropSeg image_input_orientation = orientation(image_input, get=True, verbose=False) reoriented_image_filename = 'tmp.' + sct.add_suffix(file_fname + ext_fname, "_SAL") path_tmp_viewer = sct.tmp_create(verbose=verbose) sct.run('sct_image -i ' + input_filename + ' -o ' + path_tmp_viewer + reoriented_image_filename + ' -setorient SAL -v 0', verbose=False) from sct_viewer import ClickViewer image_input_reoriented = Image(path_tmp_viewer + reoriented_image_filename) viewer = ClickViewer(image_input_reoriented) viewer.help_url = 'https://sourceforge.net/p/spinalcordtoolbox/wiki/correction_PropSeg/attachment/propseg_viewer.png' if use_viewer == "mask": viewer.number_of_slices = 3 viewer.gap_inter_slice = int(10 / pz) if viewer.gap_inter_slice == 0: viewer.gap_inter_slice = 1 viewer.calculate_list_slices() #else: # viewer.gap_inter_slice = 3 # start the viewer that ask the user to enter a few points along the spinal cord mask_points = viewer.start() if mask_points: # create the mask containing either the three-points or centerline mask for initialization mask_filename = sct.add_suffix(reoriented_image_filename, "_mask_viewer") sct.run("sct_label_utils -i " + path_tmp_viewer + reoriented_image_filename + " -create " + mask_points + " -o " + path_tmp_viewer + mask_filename, verbose=False) # reorient the initialization mask to correspond to input image orientation