def stats_per_label(values, labels, include_labels=[], exclude_labels=[], weights=[], precision=1): """ Compute various statistical measures across vertices per label, optionally using weights (such as surface area per vertex). Example (area-weighted mean): average value = sum(a_i * v_i) / total_surface_area, where *a_i* and *v_i* are the area and value for each vertex *i*. Reference: Weighted skewness and kurtosis unbiased by sample size Lorenzo Rimoldini, arXiv:1304.6564 (2013) http://arxiv.org/abs/1304.6564 Note :: This function is different than means_per_label() in two ways: 1. It computes more than simply the (weighted) mean and sdev. 2. It only accepts 1-D arrays of values. Parameters ---------- values : numpy array of individual or lists of integers or floats values for all vertices labels : list or array of integers label for each value include_labels : list of integers labels to include exclude_labels : list of integers labels to be excluded weights : numpy array of floats weights to compute weighted statistical measures precision : integer number of decimal places to consider weights Returns ------- medians : list of floats median for each label mads : list of floats median absolute deviation for each label means : list of floats mean for each label sdevs : list of floats standard deviation for each label skews : list of floats skew for each label kurts : list of floats kurtosis value for each label lower_quarts : list of floats lower quartile for each label upper_quarts : list of floats upper quartile for each label label_list : list of integers list of unique labels Examples -------- >>> import numpy as np >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.guts.compute import stats_per_label >>> from mindboggle.mio.fetch_data import prep_tests >>> urls, fetch_data = prep_tests() >>> values_file = fetch_data(urls['left_mean_curvature']) >>> labels_file = fetch_data(urls['left_freesurfer_labels']) >>> area_file = fetch_data(urls['left_area']) >>> values, name = read_scalars(values_file, True, True) >>> areas, name = read_scalars(area_file, True, True) >>> labels, name = read_scalars(labels_file) >>> include_labels = [] >>> exclude_labels = [-1] >>> weights = areas >>> precision = 1 >>> medians, mads, means, sdevs, skews, kurts, lower_quarts, upper_quarts, label_list = stats_per_label(values, ... labels, include_labels, exclude_labels, weights, precision) >>> print(np.array_str(np.array(medians[0:5]), ... precision=5, suppress_small=True)) [-1.13602 -1.22961 -2.49665 -3.80782 -3.37309] >>> print(np.array_str(np.array(mads[0:5]), ... precision=5, suppress_small=True)) [ 1.17026 1.5045 1.28234 2.11515 1.69333] >>> print(np.array_str(np.array(means[0:5]), ... precision=5, suppress_small=True)) [-1.1793 -1.21405 -2.49318 -3.58116 -3.34987] >>> print(np.array_str(np.array(kurts[0:5]), ... precision=5, suppress_small=True)) [ 2.34118 -0.3969 -0.55787 -0.73993 0.3807 ] """ import numpy as np from scipy.stats import skew, kurtosis, scoreatpercentile from mindboggle.guts.compute import weighted_to_repeated_values, median_abs_dev # Make sure arguments are numpy arrays: if not isinstance(values, np.ndarray): values = np.asarray(values) if not isinstance(weights, np.ndarray): weights = np.asarray(weights) # Initialize all statistical lists: if include_labels: label_list = include_labels else: label_list = np.unique(labels) label_list = [int(x) for x in label_list if int(x) not in exclude_labels] medians = [] mads = [] means = [] sdevs = [] skews = [] kurts = [] lower_quarts = [] upper_quarts = [] # Extract all vertex indices for each label: for label in label_list: I = [i for i,x in enumerate(labels) if x == label] if I: # Get the vertex values: X = values[I] if len([x for x in X if x != 0]): # If there are as many weights as values, apply the weights to the values: if np.size(weights) == np.size(values): W = weights[I] sumW = np.sum(W) # If the sum of the weights and the standard deviation is non-zero, # compute all statistics of the weighted values: if sumW > 0: Xdiff = X - np.mean(X) Xstd = np.sqrt(np.sum(W * Xdiff**2) / sumW) means.append(np.sum(W * X) / sumW) sdevs.append(Xstd) if Xstd > 0: skews.append((np.sum(W * Xdiff**3) / sumW) / Xstd**3) kurts.append((np.sum(W * Xdiff**4) / sumW) / Xstd**4 - 3) else: skews.append(skew(X)) kurts.append(kurtosis(X)) X = weighted_to_repeated_values(X, W, precision) # If the sum of the weights equals zero, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # If there are no (or not enough) weights, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # Compute median, median absolute deviation, and lower and upper quartiles: if np.size(X): medians.append(np.median(X)) mads.append(median_abs_dev(X)) lower_quarts.append(scoreatpercentile(X, 25)) upper_quarts.append(scoreatpercentile(X, 75)) # If the weights are all smaller than the precision, then X will disappear, # so set the above statistics (in the 'if' block) to zero: else: medians.append(0) mads.append(0) lower_quarts.append(0) upper_quarts.append(0) # If all values are equal to zero, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) # If there are no vertices for the label, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) return medians, mads, means, sdevs, skews, kurts, \ lower_quarts, upper_quarts, label_list
def stats_per_label(values, labels, include_labels=[], exclude_labels=[], weights=[], precision=1): """ Compute various statistical measures across vertices per label, optionally using weights (such as surface area per vertex). Example (area-weighted mean): average value = sum(a_i * v_i) / total_surface_area, where *a_i* and *v_i* are the area and value for each vertex *i*. Note :: This function is different than means_per_label() in two ways: 1. It computes more than simply the (weighted) mean and sdev. 2. It only accepts 1-D arrays of values. Reference --------- Weighted skewness and kurtosis unbiased by sample size Lorenzo Rimoldini, arXiv:1304.6564 (2013) http://arxiv.org/abs/1304.6564 Parameters ---------- values : numpy array of individual or lists of integers or floats values for all vertices labels : list or array of integers label for each value include_labels : list of integers labels to include exclude_labels : list of integers labels to be excluded weights : numpy array of floats weights to compute weighted statistical measures precision : integer number of decimal places to consider weights Returns ------- medians : list of floats median for each label mads : list of floats median absolute deviation for each label means : list of floats mean for each label sdevs : list of floats standard deviation for each label skews : list of floats skew for each label kurts : list of floats kurtosis value for each label lower_quarts : list of floats lower quartile for each label upper_quarts : list of floats upper quartile for each label label_list : list of integers list of unique labels Examples -------- >>> import os >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.guts.compute import stats_per_label >>> data_path = os.environ['MINDBOGGLE_DATA'] >>> values_file = os.path.join(data_path, 'arno', 'shapes', 'lh.pial.mean_curvature.vtk') >>> area_file = os.path.join(data_path, 'arno', 'shapes', 'lh.pial.area.vtk') >>> labels_file = os.path.join(data_path, 'arno', 'labels', 'lh.labels.DKT25.manual.vtk') >>> values, name = read_scalars(values_file, True, True) >>> areas, name = read_scalars(area_file, True, True) >>> labels, name = read_scalars(labels_file) >>> include_labels = [] >>> exclude_labels = [-1] >>> weights = areas >>> precision = 1 >>> stats_per_label(values, labels, include_labels, exclude_labels, weights, precision) """ import numpy as np from scipy.stats import skew, kurtosis, scoreatpercentile from mindboggle.guts.compute import weighted_to_repeated_values, median_abs_dev # Make sure arguments are numpy arrays: if not isinstance(values, np.ndarray): values = np.asarray(values) if not isinstance(weights, np.ndarray): weights = np.asarray(weights) # Initialize all statistical lists: if include_labels: label_list = include_labels else: label_list = np.unique(labels) label_list = [int(x) for x in label_list if int(x) not in exclude_labels] medians = [] mads = [] means = [] sdevs = [] skews = [] kurts = [] lower_quarts = [] upper_quarts = [] # Extract all vertex indices for each label: for label in label_list: I = [i for i, x in enumerate(labels) if x == label] if I: # Get the vertex values: X = values[I] if len([x for x in X if x != 0]): # If there are as many weights as values, apply the weights to the values: if np.size(weights) == np.size(values): W = weights[I] sumW = np.sum(W) # If the sum of the weights and the standard deviation is non-zero, # compute all statistics of the weighted values: if sumW > 0: Xdiff = X - np.mean(X) Xstd = np.sqrt(np.sum(W * Xdiff**2) / sumW) means.append(np.sum(W * X) / sumW) sdevs.append(Xstd) if Xstd > 0: skews.append( (np.sum(W * Xdiff**3) / sumW) / Xstd**3) kurts.append((np.sum(W * Xdiff**4) / sumW) / Xstd**4 - 3) else: skews.append(skew(X)) kurts.append(kurtosis(X)) X = weighted_to_repeated_values(X, W, precision) # If the sum of the weights equals zero, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # If there are no (or not enough) weights, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # Compute median, median absolute deviation, and lower and upper quartiles: if np.size(X): medians.append(np.median(X)) mads.append(median_abs_dev(X)) lower_quarts.append(scoreatpercentile(X, 25)) upper_quarts.append(scoreatpercentile(X, 75)) # If the weights are all smaller than the precision, then X will disappear, # so set the above statistics (in the 'if' block) to zero: else: medians.append(0) mads.append(0) lower_quarts.append(0) upper_quarts.append(0) # If all values are equal to zero, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) # If there are no vertices for the label, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) return medians, mads, means, sdevs, skews, kurts, \ lower_quarts, upper_quarts, label_list
def extract_fundi(folds, curv_file, depth_file, min_separation=10, erode_ratio=0.1, erode_min_size=1, save_file=False, output_file='', background_value=-1, verbose=False): """ Extract fundi from folds. A fundus is a branching curve that runs along the deepest and most highly curved portions of a fold. This function extracts one fundus from each fold by finding the deepest vertices inside the fold, finding endpoints along the edge of the fold, and connecting the former to the latter with tracks that run along deep and curved paths (through vertices with high values of travel depth multiplied by curvature), and a final filtration step. The deepest vertices are those with values at least two median absolute deviations above the median (non-zero) value, with the higher value chosen if two of the vertices are within (a default of) 10 edges from each other (to reduce the number of possible fundus paths as well as computation time). To find the endpoints, the find_outer_endpoints function propagates multiple tracks from seed vertices at median depth in the fold through concentric rings toward the fold’s edge, selecting maximal values within each ring, and terminating at candidate endpoints. The final endpoints are those candidates at the end of tracks that have a high median value, with the higher value chosen if two candidate endpoints are within (a default of) 10 edges from each other (otherwise, the resulting fundi can have spurious branching at the fold’s edge). The connect_points_erosion function connects the deepest fold vertices to the endpoints with a skeleton of 1-vertex-thick curves by erosion. It erodes by iteratively removing simple topological points and endpoints in order of lowest to highest values, where a simple topological point is a vertex that when added to or removed from an object on a surface mesh (such as a fundus curve) does not alter the object's topology. Steps :: 1. Find fundus endpoints (outer anchors) with find_outer_endpoints(). 2. Include inner anchor points. 3. Connect anchor points using connect_points_erosion(); inner anchors are removed if they result in endpoints. Note :: Follow this with segment_by_region() to segment fundi by sulci. Parameters ---------- folds : numpy array or list of integers fold number for each vertex curv_file : string surface mesh file in VTK format with mean curvature values depth_file : string surface mesh file in VTK format with rescaled depth values likelihoods : list of integers fundus likelihood value for each vertex min_separation : integer minimum number of edges between inner/outer anchor points erode_ratio : float fraction of indices to test for removal at each iteration in connect_points_erosion() save_file : bool save output VTK file? output_file : string output VTK file background_value : integer or float background value verbose : bool print statements? Returns ------- fundus_per_fold : list of integers fundus numbers for all vertices, labeled by fold (-1 for non-fundus vertices) n_fundi_in_folds : integer number of fundi fundus_per_fold_file : string (if save_file) output VTK file with fundus numbers (-1 for non-fundus vertices) Examples -------- >>> # Extract fundus from one or more folds: >>> import numpy as np >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.features.fundi import extract_fundi >>> from mindboggle.mio.fetch_data import prep_tests >>> urls, fetch_data = prep_tests() >>> curv_file = fetch_data(urls['left_mean_curvature'], '', '.vtk') >>> depth_file = fetch_data(urls['left_travel_depth'], '', '.vtk') >>> folds_file = fetch_data(urls['left_folds'], '', '.vtk') >>> folds, name = read_scalars(folds_file, True, True) >>> # Limit number of folds to speed up the test: >>> limit_folds = True >>> if limit_folds: ... fold_numbers = [4] #[4, 6] ... i0 = [i for i,x in enumerate(folds) if x not in fold_numbers] ... folds[i0] = -1 >>> min_separation = 10 >>> erode_ratio = 0.10 >>> erode_min_size = 10 >>> save_file = True >>> output_file = 'extract_fundi_fold4.vtk' >>> background_value = -1 >>> verbose = False >>> o1, o2, fundus_per_fold_file = extract_fundi(folds, curv_file, ... depth_file, min_separation, erode_ratio, erode_min_size, ... save_file, output_file, background_value, verbose) >>> lens = [len([x for x in o1 if x == y]) ... for y in np.unique(o1) if y != background_value] >>> lens[0:10] # [66, 2914, 100, 363, 73, 331, 59, 30, 1, 14] # (if not limit_folds) [73] View result without background (skip test): >>> from mindboggle.mio.plots import plot_surfaces # doctest: +SKIP >>> from mindboggle.mio.vtks import rewrite_scalars # doctest: +SKIP >>> rewrite_scalars(fundus_per_fold_file, ... 'extract_fundi_fold4_no_background.vtk', o1, ... 'fundus_per_fold', folds) # doctest: +SKIP >>> plot_surfaces('extract_fundi_fold4_no_background.vtk') # doctest: +SKIP """ # Extract a skeleton to connect endpoints in a fold: import os import numpy as np from time import time from mindboggle.mio.vtks import read_scalars, read_vtk, rewrite_scalars from mindboggle.guts.compute import median_abs_dev from mindboggle.guts.paths import find_max_values from mindboggle.guts.mesh import find_neighbors_from_file from mindboggle.guts.mesh import find_complete_faces from mindboggle.guts.paths import find_outer_endpoints from mindboggle.guts.paths import connect_points_erosion if isinstance(folds, list): folds = np.array(folds) # Load values, inner anchor threshold, and neighbors: if os.path.isfile(curv_file): points, indices, lines, faces, curvs, scalar_names, npoints, \ input_vtk = read_vtk(curv_file, True, True) else: raise IOError("{0} doesn't exist!".format(curv_file)) if os.path.isfile(curv_file): depths, name = read_scalars(depth_file, True, True) else: raise IOError("{0} doesn't exist!".format(depth_file)) values = curvs * depths values0 = [x for x in values if x > 0] thr = np.median(values0) + 2 * median_abs_dev(values0) neighbor_lists = find_neighbors_from_file(curv_file) # ------------------------------------------------------------------------ # Loop through folds: # ------------------------------------------------------------------------ t1 = time() skeletons = [] unique_fold_IDs = [x for x in np.unique(folds) if x != background_value] if verbose: if len(unique_fold_IDs) == 1: print("Extract a fundus from 1 fold...") else: print("Extract a fundus from each of {0} folds...". format(len(unique_fold_IDs))) for fold_ID in unique_fold_IDs: indices_fold = [i for i,x in enumerate(folds) if x == fold_ID] if indices_fold: if verbose: print(' Fold {0}:'.format(int(fold_ID))) # ---------------------------------------------------------------- # Find outer anchor points on the boundary of the surface region, # to serve as fundus endpoints: # ---------------------------------------------------------------- outer_anchors, tracks = find_outer_endpoints(indices_fold, neighbor_lists, values, depths, min_separation, background_value, verbose) # ---------------------------------------------------------------- # Find inner anchor points: # ---------------------------------------------------------------- inner_anchors = find_max_values(points, values, min_separation, thr) # ---------------------------------------------------------------- # Connect anchor points to create skeleton: # ---------------------------------------------------------------- B = background_value * np.ones(npoints) B[indices_fold] = 1 skeleton = connect_points_erosion(B, neighbor_lists, outer_anchors, inner_anchors, values, erode_ratio, erode_min_size, [], '', background_value, verbose) if skeleton: skeletons.extend(skeleton) ## --------------------------------------------------------------- ## Remove fundus vertices if they make complete triangle faces: ## --------------------------------------------------------------- #Iremove = find_complete_faces(skeletons, faces) #if Iremove: # skeletons = list(frozenset(skeletons).difference(Iremove)) indices_skel = [x for x in skeletons if folds[x] != background_value] fundus_per_fold = background_value * np.ones(npoints) fundus_per_fold[indices_skel] = folds[indices_skel] n_fundi_in_folds = len([x for x in np.unique(fundus_per_fold) if x != background_value]) if n_fundi_in_folds == 1: sdum = 'fold fundus' else: sdum = 'fold fundi' if verbose: print(' ...Extracted {0} {1}; {2} total ({3:.2f} seconds)'. format(n_fundi_in_folds, sdum, n_fundi_in_folds, time() - t1)) # ------------------------------------------------------------------------ # Return fundi, number of fundi, and file name: # ------------------------------------------------------------------------ fundus_per_fold_file = None if n_fundi_in_folds > 0: fundus_per_fold = [int(x) for x in fundus_per_fold] if save_file: if output_file: fundus_per_fold_file = output_file else: fundus_per_fold_file = os.path.join(os.getcwd(), 'fundus_per_fold.vtk') rewrite_scalars(curv_file, fundus_per_fold_file, fundus_per_fold, 'fundi', [], background_value) if not os.path.exists(fundus_per_fold_file): raise IOError(fundus_per_fold_file + " not found") return fundus_per_fold, n_fundi_in_folds, fundus_per_fold_file
def extract_fundi(folds, curv_file, depth_file, min_separation=10, erode_ratio=0.1, erode_min_size=1, save_file=False, verbose=False): """ Extract fundi from folds. A fundus is a branching curve that runs along the deepest and most highly curved portions of a fold. Steps :: 1. Find fundus endpoints (outer anchors) with find_outer_anchors(). 2. Include inner anchor points. 3. Connect anchor points using connect_points_erosion(); inner anchors are removed if they result in endpoints. Parameters ---------- folds : numpy array or list of integers fold number for each vertex curv_file : string surface mesh file in VTK format with mean curvature values depth_file : string surface mesh file in VTK format with rescaled depth values likelihoods : list of integers fundus likelihood value for each vertex min_separation : integer minimum number of edges between inner/outer anchor points erode_ratio : float fraction of indices to test for removal at each iteration in connect_points_erosion() save_file : bool save output VTK file? verbose : bool print statements? Returns ------- fundus_per_fold : list of integers fundus numbers for all vertices, labeled by fold (-1 for non-fundus vertices) n_fundi_in_folds : integer number of fundi fundus_per_fold_file : string (if save_file) output VTK file with fundus numbers (-1 for non-fundus vertices) Examples -------- >>> # Extract fundus from one or more folds: >>> single_fold = True >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.features.fundi import extract_fundi >>> from mindboggle.mio.fetch_data import prep_tests >>> urls, fetch_data = prep_tests() >>> curv_file = fetch_data(urls['left_mean_curvature']) >>> depth_file = fetch_data(urls['left_travel_depth']) >>> folds_file = fetch_data(urls['left_folds']) >>> folds, name = read_scalars(folds_file, True, True) >>> if single_fold: ... fold_number = 2 #11 ... folds[folds != fold_number] = -1 >>> min_separation = 10 >>> erode_ratio = 0.10 >>> erode_min_size = 10 >>> save_file = True >>> verbose = False >>> o1, o2, fundus_per_fold_file = extract_fundi(folds, curv_file, ... depth_file, min_separation, erode_ratio, erode_min_size, ... save_file, verbose) >>> if single_fold: ... lens = [len([x for x in o1 if x == 2])] ... else: ... lens = [len([x for x in o1 if x == y]) for y in range(o2)] >>> lens[0:10] [115] View result (skip test): >>> from mindboggle.mio.plots import plot_surfaces >>> plot_surfaces(fundus_per_fold_file) # doctest: +SKIP """ # Extract a skeleton to connect endpoints in a fold: import os import numpy as np from time import time from mindboggle.mio.vtks import read_scalars, read_vtk, rewrite_scalars from mindboggle.guts.compute import median_abs_dev from mindboggle.guts.paths import find_max_values from mindboggle.guts.mesh import find_neighbors_from_file from mindboggle.guts.mesh import find_complete_faces from mindboggle.guts.paths import find_outer_anchors from mindboggle.guts.paths import connect_points_erosion if isinstance(folds, list): folds = np.array(folds) # Load values, inner anchor threshold, and neighbors: points, indices, lines, faces, curvs, scalar_names, npoints, \ input_vtk = read_vtk(curv_file, True, True) depths, name = read_scalars(depth_file, True, True) values = curvs * depths values0 = [x for x in values if x > 0] thr = np.median(values0) + 2 * median_abs_dev(values0) neighbor_lists = find_neighbors_from_file(curv_file) #------------------------------------------------------------------------- # Loop through folds: #------------------------------------------------------------------------- t1 = time() skeletons = [] unique_fold_IDs = [x for x in np.unique(folds) if x != -1] if verbose: if len(unique_fold_IDs) == 1: print("Extract a fundus from 1 fold...") else: print("Extract a fundus from each of {0} folds...".format( len(unique_fold_IDs))) for fold_ID in unique_fold_IDs: indices_fold = [i for i, x in enumerate(folds) if x == fold_ID] if indices_fold: if verbose: print(' Fold {0}:'.format(int(fold_ID))) #----------------------------------------------------------------- # Find outer anchor points on the boundary of the surface region, # to serve as fundus endpoints: #----------------------------------------------------------------- verbose = False outer_anchors, tracks = find_outer_anchors(indices_fold, neighbor_lists, values, depths, min_separation, verbose) #----------------------------------------------------------------- # Find inner anchor points: #----------------------------------------------------------------- inner_anchors = find_max_values(points, values, min_separation, thr) #----------------------------------------------------------------- # Connect anchor points to create skeleton: #----------------------------------------------------------------- B = -1 * np.ones(npoints) B[indices_fold] = 1 skeleton = connect_points_erosion(B, neighbor_lists, outer_anchors, inner_anchors, values, erode_ratio, erode_min_size, save_steps=[], save_vtk='', verbose=False) if skeleton: skeletons.extend(skeleton) #----------------------------------------------------------------- # Remove fundus vertices if they complete triangle faces: #----------------------------------------------------------------- Iremove = find_complete_faces(skeletons, faces) if Iremove: skeletons = list(frozenset(skeletons).difference(Iremove)) indices_skel = [x for x in skeletons if folds[x] != -1] fundus_per_fold = -1 * np.ones(npoints) fundus_per_fold[indices_skel] = folds[indices_skel] n_fundi_in_folds = len([x for x in np.unique(fundus_per_fold) if x != -1]) if n_fundi_in_folds == 1: sdum = 'fold fundus' else: sdum = 'fold fundi' if verbose: print(' ...Extracted {0} {1}; {2} total ({3:.2f} seconds)'.format( n_fundi_in_folds, sdum, n_fundi_in_folds, time() - t1)) #------------------------------------------------------------------------- # Return fundi, number of fundi, and file name: #------------------------------------------------------------------------- if n_fundi_in_folds > 0: fundus_per_fold = [int(x) for x in fundus_per_fold] if save_file: fundus_per_fold_file = os.path.join(os.getcwd(), 'fundus_per_fold.vtk') rewrite_scalars(curv_file, fundus_per_fold_file, fundus_per_fold, 'fundi', folds) if not os.path.exists(fundus_per_fold_file): raise IOError(fundus_per_fold_file + " not found") else: fundus_per_fold_file = None return fundus_per_fold, n_fundi_in_folds, fundus_per_fold_file
def stats_per_label(values, labels, include_labels=[], exclude_labels=[], weights=[], precision=1): """ Compute various statistical measures across vertices per label, optionally using weights (such as surface area per vertex). Example (area-weighted mean): average value = sum(a_i * v_i) / total_surface_area, where *a_i* and *v_i* are the area and value for each vertex *i*. Note :: This function is different than means_per_label() in two ways: 1. It computes more than simply the (weighted) mean and sdev. 2. It only accepts 1-D arrays of values. Reference --------- Weighted skewness and kurtosis unbiased by sample size Lorenzo Rimoldini, arXiv:1304.6564 (2013) http://arxiv.org/abs/1304.6564 Parameters ---------- values : numpy array of individual or lists of integers or floats values for all vertices labels : list or array of integers label for each value include_labels : list of integers labels to include exclude_labels : list of integers labels to be excluded weights : numpy array of floats weights to compute weighted statistical measures precision : integer number of decimal places to consider weights Returns ------- medians : list of floats median for each label mads : list of floats median absolute deviation for each label means : list of floats mean for each label sdevs : list of floats standard deviation for each label skews : list of floats skew for each label kurts : list of floats kurtosis value for each label lower_quarts : list of floats lower quartile for each label upper_quarts : list of floats upper quartile for each label label_list : list of integers list of unique labels Examples -------- >>> import os >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.guts.compute import stats_per_label >>> data_path = os.environ['MINDBOGGLE_DATA'] >>> values_file = os.path.join(data_path, 'arno', 'shapes', 'lh.pial.mean_curvature.vtk') >>> area_file = os.path.join(data_path, 'arno', 'shapes', 'lh.pial.area.vtk') >>> labels_file = os.path.join(data_path, 'arno', 'labels', 'lh.labels.DKT25.manual.vtk') >>> values, name = read_scalars(values_file, True, True) >>> areas, name = read_scalars(area_file, True, True) >>> labels, name = read_scalars(labels_file) >>> include_labels = [] >>> exclude_labels = [-1] >>> weights = areas >>> precision = 1 >>> stats_per_label(values, labels, include_labels, exclude_labels, weights, precision) """ import numpy as np from scipy.stats import skew, kurtosis, scoreatpercentile from mindboggle.guts.compute import weighted_to_repeated_values, median_abs_dev # Make sure arguments are numpy arrays: if not isinstance(values, np.ndarray): values = np.asarray(values) if not isinstance(weights, np.ndarray): weights = np.asarray(weights) # Initialize all statistical lists: if include_labels: label_list = include_labels else: label_list = np.unique(labels) label_list = [int(x) for x in label_list if int(x) not in exclude_labels] medians = [] mads = [] means = [] sdevs = [] skews = [] kurts = [] lower_quarts = [] upper_quarts = [] # Extract all vertex indices for each label: for label in label_list: I = [i for i, x in enumerate(labels) if x == label] if I: # Get the vertex values: X = values[I] if len([x for x in X if x != 0]): # If there are as many weights as values, apply the weights to the values: if np.size(weights) == np.size(values): W = weights[I] sumW = np.sum(W) # If the sum of the weights and the standard deviation is non-zero, # compute all statistics of the weighted values: if sumW > 0: Xdiff = X - np.mean(X) Xstd = np.sqrt(np.sum(W * Xdiff ** 2) / sumW) means.append(np.sum(W * X) / sumW) sdevs.append(Xstd) if Xstd > 0: skews.append((np.sum(W * Xdiff ** 3) / sumW) / Xstd ** 3) kurts.append((np.sum(W * Xdiff ** 4) / sumW) / Xstd ** 4 - 3) else: skews.append(skew(X)) kurts.append(kurtosis(X)) X = weighted_to_repeated_values(X, W, precision) # If the sum of the weights equals zero, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # If there are no (or not enough) weights, simply compute the statistics: else: means.append(np.mean(X)) sdevs.append(np.std(X)) skews.append(skew(X)) kurts.append(kurtosis(X)) # Compute median, median absolute deviation, and lower and upper quartiles: if np.size(X): medians.append(np.median(X)) mads.append(median_abs_dev(X)) lower_quarts.append(scoreatpercentile(X, 25)) upper_quarts.append(scoreatpercentile(X, 75)) # If the weights are all smaller than the precision, then X will disappear, # so set the above statistics (in the 'if' block) to zero: else: medians.append(0) mads.append(0) lower_quarts.append(0) upper_quarts.append(0) # If all values are equal to zero, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) # If there are no vertices for the label, set all statistics to zero: else: medians.append(0) mads.append(0) means.append(0) sdevs.append(0) skews.append(0) kurts.append(0) lower_quarts.append(0) upper_quarts.append(0) return medians, mads, means, sdevs, skews, kurts, lower_quarts, upper_quarts, label_list
def extract_fundi(folds, curv_file, depth_file, min_separation=10, erode_ratio=0.1, erode_min_size=1, save_file=False, output_file='', background_value=-1, verbose=False): """ Extract fundi from folds. A fundus is a branching curve that runs along the deepest and most highly curved portions of a fold. This function extracts one fundus from each fold by finding the deepest vertices inside the fold, finding endpoints along the edge of the fold, and connecting the former to the latter with tracks that run along deep and curved paths (through vertices with high values of travel depth multiplied by curvature), and a final filtration step. The deepest vertices are those with values at least two median absolute deviations above the median (non-zero) value, with the higher value chosen if two of the vertices are within (a default of) 10 edges from each other (to reduce the number of possible fundus paths as well as computation time). To find the endpoints, the find_outer_endpoints function propagates multiple tracks from seed vertices at median depth in the fold through concentric rings toward the fold’s edge, selecting maximal values within each ring, and terminating at candidate endpoints. The final endpoints are those candidates at the end of tracks that have a high median value, with the higher value chosen if two candidate endpoints are within (a default of) 10 edges from each other (otherwise, the resulting fundi can have spurious branching at the fold’s edge). The connect_points_erosion function connects the deepest fold vertices to the endpoints with a skeleton of 1-vertex-thick curves by erosion. It erodes by iteratively removing simple topological points and endpoints in order of lowest to highest values, where a simple topological point is a vertex that when added to or removed from an object on a surface mesh (such as a fundus curve) does not alter the object's topology. Steps :: 1. Find fundus endpoints (outer anchors) with find_outer_endpoints(). 2. Include inner anchor points. 3. Connect anchor points using connect_points_erosion(); inner anchors are removed if they result in endpoints. Note :: Follow this with segment_by_region() to segment fundi by sulci. Parameters ---------- folds : numpy array or list of integers fold number for each vertex curv_file : string surface mesh file in VTK format with mean curvature values depth_file : string surface mesh file in VTK format with rescaled depth values likelihoods : list of integers fundus likelihood value for each vertex min_separation : integer minimum number of edges between inner/outer anchor points erode_ratio : float fraction of indices to test for removal at each iteration in connect_points_erosion() save_file : bool save output VTK file? output_file : string output VTK file background_value : integer or float background value verbose : bool print statements? Returns ------- fundus_per_fold : list of integers fundus numbers for all vertices, labeled by fold (-1 for non-fundus vertices) n_fundi_in_folds : integer number of fundi fundus_per_fold_file : string (if save_file) output VTK file with fundus numbers (-1 for non-fundus vertices) Examples -------- >>> # Extract fundus from one or more folds: >>> import numpy as np >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.features.fundi import extract_fundi >>> from mindboggle.mio.fetch_data import prep_tests >>> urls, fetch_data = prep_tests() >>> curv_file = fetch_data(urls['left_mean_curvature'], '', '.vtk') >>> depth_file = fetch_data(urls['left_travel_depth'], '', '.vtk') >>> folds_file = fetch_data(urls['left_folds'], '', '.vtk') >>> folds, name = read_scalars(folds_file, True, True) >>> # Limit number of folds to speed up the test: >>> limit_folds = True >>> if limit_folds: ... fold_numbers = [4] #[4, 6] ... i0 = [i for i,x in enumerate(folds) if x not in fold_numbers] ... folds[i0] = -1 >>> min_separation = 10 >>> erode_ratio = 0.10 >>> erode_min_size = 10 >>> save_file = True >>> output_file = 'extract_fundi_fold4.vtk' >>> background_value = -1 >>> verbose = False >>> o1, o2, fundus_per_fold_file = extract_fundi(folds, curv_file, ... depth_file, min_separation, erode_ratio, erode_min_size, ... save_file, output_file, background_value, verbose) >>> lens = [len([x for x in o1 if x == y]) ... for y in np.unique(o1) if y != background_value] >>> lens[0:10] # [66, 2914, 100, 363, 73, 331, 59, 30, 1, 14] # (if not limit_folds) [73] View result without background (skip test): >>> from mindboggle.mio.plots import plot_surfaces # doctest: +SKIP >>> from mindboggle.mio.vtks import rewrite_scalars # doctest: +SKIP >>> rewrite_scalars(fundus_per_fold_file, ... 'extract_fundi_fold4_no_background.vtk', o1, ... 'fundus_per_fold', folds) # doctest: +SKIP >>> plot_surfaces('extract_fundi_fold4_no_background.vtk') # doctest: +SKIP """ # Extract a skeleton to connect endpoints in a fold: import os import numpy as np from time import time from mindboggle.mio.vtks import read_scalars, read_vtk, rewrite_scalars from mindboggle.guts.compute import median_abs_dev from mindboggle.guts.paths import find_max_values from mindboggle.guts.mesh import find_neighbors_from_file #from mindboggle.guts.mesh import find_complete_faces from mindboggle.guts.paths import find_outer_endpoints from mindboggle.guts.paths import connect_points_erosion if isinstance(folds, list): folds = np.array(folds) # Load values, inner anchor threshold, and neighbors: if os.path.isfile(curv_file): points, indices, lines, faces, curvs, scalar_names, npoints, \ input_vtk = read_vtk(curv_file, True, True) else: raise IOError("{0} doesn't exist!".format(curv_file)) if os.path.isfile(curv_file): depths, name = read_scalars(depth_file, True, True) else: raise IOError("{0} doesn't exist!".format(depth_file)) values = curvs * depths values0 = [x for x in values if x > 0] thr = np.median(values0) + 2 * median_abs_dev(values0) neighbor_lists = find_neighbors_from_file(curv_file) # ------------------------------------------------------------------------ # Loop through folds: # ------------------------------------------------------------------------ t1 = time() skeletons = [] unique_fold_IDs = [x for x in np.unique(folds) if x != background_value] if verbose: if len(unique_fold_IDs) == 1: print("Extract a fundus from 1 fold...") else: print("Extract a fundus from each of {0} folds...".format( len(unique_fold_IDs))) for fold_ID in unique_fold_IDs: indices_fold = [i for i, x in enumerate(folds) if x == fold_ID] if indices_fold: if verbose: print(' Fold {0}:'.format(int(fold_ID))) # ---------------------------------------------------------------- # Find outer anchor points on the boundary of the surface region, # to serve as fundus endpoints: # ---------------------------------------------------------------- outer_anchors, tracks = find_outer_endpoints( indices_fold, neighbor_lists, values, depths, min_separation, background_value, verbose) # ---------------------------------------------------------------- # Find inner anchor points: # ---------------------------------------------------------------- inner_anchors = find_max_values(points, values, min_separation, thr) # ---------------------------------------------------------------- # Connect anchor points to create skeleton: # ---------------------------------------------------------------- B = background_value * np.ones(npoints) B[indices_fold] = 1 skeleton = connect_points_erosion(B, neighbor_lists, outer_anchors, inner_anchors, values, erode_ratio, erode_min_size, [], '', background_value, verbose) if skeleton: skeletons.extend(skeleton) ## --------------------------------------------------------------- ## Remove fundus vertices if they make complete triangle faces: ## --------------------------------------------------------------- #Iremove = find_complete_faces(skeletons, faces) #if Iremove: # skeletons = list(frozenset(skeletons).difference(Iremove)) indices_skel = [x for x in skeletons if folds[x] != background_value] fundus_per_fold = background_value * np.ones(npoints) fundus_per_fold[indices_skel] = folds[indices_skel] n_fundi_in_folds = len( [x for x in np.unique(fundus_per_fold) if x != background_value]) if n_fundi_in_folds == 1: sdum = 'fold fundus' else: sdum = 'fold fundi' if verbose: print(' ...Extracted {0} {1}; {2} total ({3:.2f} seconds)'.format( n_fundi_in_folds, sdum, n_fundi_in_folds, time() - t1)) # ------------------------------------------------------------------------ # Return fundi, number of fundi, and file name: # ------------------------------------------------------------------------ fundus_per_fold_file = None if n_fundi_in_folds > 0: fundus_per_fold = [int(x) for x in fundus_per_fold] if save_file: if output_file: fundus_per_fold_file = output_file else: fundus_per_fold_file = os.path.join(os.getcwd(), 'fundus_per_fold.vtk') rewrite_scalars(curv_file, fundus_per_fold_file, fundus_per_fold, 'fundi', [], background_value) if not os.path.exists(fundus_per_fold_file): raise IOError(fundus_per_fold_file + " not found") return fundus_per_fold, n_fundi_in_folds, fundus_per_fold_file
def extract_fundi(folds, curv_file, depth_file, min_separation=10, erode_ratio=0.1, erode_min_size=1, save_file=False): """ Extract fundi from folds. A fundus is a branching curve that runs along the deepest and most highly curved portions of a fold. Steps :: 1. Find fundus endpoints (outer anchors) with find_outer_anchors(). 2. Include inner anchor points. 3. Connect anchor points using connect_points_erosion(); inner anchors are removed if they result in endpoints. Parameters ---------- folds : numpy array or list of integers fold number for each vertex curv_file : string surface mesh file in VTK format with mean curvature values depth_file : string surface mesh file in VTK format with rescaled depth values likelihoods : list of integers fundus likelihood value for each vertex min_separation : integer minimum number of edges between inner/outer anchor points erode_ratio : float fraction of indices to test for removal at each iteration in connect_points_erosion() save_file : Boolean save output VTK file? Returns ------- fundus_per_fold : list of integers fundus numbers for all vertices, labeled by fold (-1 for non-fundus vertices) n_fundi_in_folds : integer number of fundi fundus_per_fold_file : string (if save_file) output VTK file with fundus numbers (-1 for non-fundus vertices) Examples -------- >>> # Extract fundus from one or more folds: >>> single_fold = True >>> import os >>> from mindboggle.mio.vtks import read_scalars >>> from mindboggle.features.fundi import extract_fundi >>> from mindboggle.mio.plots import plot_surfaces >>> path = os.environ['MINDBOGGLE_DATA'] >>> curv_file = os.path.join(path, 'arno', 'shapes', 'lh.pial.mean_curvature.vtk') >>> depth_file = os.path.join(path, 'arno', 'shapes', 'travel_depth_rescaled.vtk') >>> folds_file = os.path.join(path, 'arno', 'features', 'folds.vtk') >>> folds, name = read_scalars(folds_file, True, True) >>> if single_fold: >>> fold_number = 2 #11 >>> folds[folds != fold_number] = -1 >>> min_separation = 10 >>> erode_ratio = 0.10 >>> erode_min_size = 10 >>> save_file = True >>> o1, o2, fundus_per_fold_file = extract_fundi(folds, curv_file, ... depth_file, min_separation, erode_ratio, erode_min_size, save_file) >>> # >>> # View: >>> plot_surfaces(fundi_file) """ # Extract a skeleton to connect endpoints in a fold: import os import numpy as np from time import time from mindboggle.mio.vtks import read_scalars, read_vtk, rewrite_scalars from mindboggle.guts.compute import median_abs_dev from mindboggle.guts.paths import find_max_values from mindboggle.guts.mesh import find_neighbors_from_file, find_complete_faces from mindboggle.guts.paths import find_outer_anchors, connect_points_erosion if isinstance(folds, list): folds = np.array(folds) # Load values, inner anchor threshold, and neighbors: points, indices, lines, faces, curvs, scalar_names, npoints, \ input_vtk = read_vtk(curv_file, True, True) depths, name = read_scalars(depth_file, True, True) values = curvs * depths values0 = [x for x in values if x > 0] thr = np.median(values0) + 2 * median_abs_dev(values0) neighbor_lists = find_neighbors_from_file(curv_file) #------------------------------------------------------------------------- # Loop through folds: #------------------------------------------------------------------------- t1 = time() skeletons = [] unique_fold_IDs = [x for x in np.unique(folds) if x != -1] if len(unique_fold_IDs) == 1: print("Extract a fundus from 1 fold...") else: print("Extract a fundus from each of {0} folds...". format(len(unique_fold_IDs))) for fold_ID in unique_fold_IDs: indices_fold = [i for i,x in enumerate(folds) if x == fold_ID] if indices_fold: print(' Fold {0}:'.format(int(fold_ID))) #----------------------------------------------------------------- # Find outer anchor points on the boundary of the surface region, # to serve as fundus endpoints: #----------------------------------------------------------------- outer_anchors, tracks = find_outer_anchors(indices_fold, neighbor_lists, values, depths, min_separation) #----------------------------------------------------------------- # Find inner anchor points: #----------------------------------------------------------------- inner_anchors = find_max_values(points, values, min_separation, thr) #----------------------------------------------------------------- # Connect anchor points to create skeleton: #----------------------------------------------------------------- B = -1 * np.ones(npoints) B[indices_fold] = 1 skeleton = connect_points_erosion(B, neighbor_lists, outer_anchors, inner_anchors, values, erode_ratio, erode_min_size, save_steps=[], save_vtk='') if skeleton: skeletons.extend(skeleton) #----------------------------------------------------------------- # Remove fundus vertices if they complete triangle faces: #----------------------------------------------------------------- Iremove = find_complete_faces(skeletons, faces) if Iremove: skeletons = list(frozenset(skeletons).difference(Iremove)) indices_skel = [x for x in skeletons if folds[x] != -1] fundus_per_fold = -1 * np.ones(npoints) fundus_per_fold[indices_skel] = folds[indices_skel] n_fundi_in_folds = len([x for x in np.unique(fundus_per_fold) if x != -1]) if n_fundi_in_folds == 1: sdum = 'fold fundus' else: sdum = 'fold fundi' print(' ...Extracted {0} {1}; {2} total ({3:.2f} seconds)'. format(n_fundi_in_folds, sdum, n_fundi_in_folds, time() - t1)) #------------------------------------------------------------------------- # Return fundi, number of fundi, and file name: #------------------------------------------------------------------------- if n_fundi_in_folds > 0: fundus_per_fold = [int(x) for x in fundus_per_fold] if save_file: fundus_per_fold_file = os.path.join(os.getcwd(), 'fundus_per_fold.vtk') rewrite_scalars(curv_file, fundus_per_fold_file, fundus_per_fold, 'fundi', folds) if not os.path.exists(fundus_per_fold_file): raise(IOError(fundus_per_fold_file + " not found")) else: fundus_per_fold_file = None return fundus_per_fold, n_fundi_in_folds, fundus_per_fold_file