Source code for napari_toska._measurements

import pandas as pd


[docs] def analyze_skeletons( labeled_skeletons: "napari.types.LabelsData", parsed_skeletons: "napari.types.LabelsData", neighborhood: str = "n8", viewer: "napari.Viewer" = None ) -> pd.DataFrame: """ Analyze a skeleton image and return a pandas dataframe. This function runs the `analyze_single_skeleton` function for every skeleton in the image and returns a pandas dataframe containing the measurements. Parameters ---------- labeled_skeletons : "napari.types.LabelsData" A labeled image of skeletons. parsed_skeletons : "napari.types.LabelsData" A parsed labeled image of skeletons. neighborhood : str, optional The neighborhood used for the skeletonization, by default "n8". For 2D images, use "n4" or "n8". For 3D images, use "n6", "n18" or "n26". Returns ------- df_all : pd.DataFrame A pandas dataframe containing the measurements. """ import numpy as np for label in np.unique(labeled_skeletons)[1:]: parsed_skeletons_single = parsed_skeletons * (labeled_skeletons == label) df = analyze_single_skeleton( parsed_skeletons_single, neighborhood=neighborhood) df["skeleton_id"] = label if label == 1: df_all = df else: df_all = pd.concat([df_all, df], axis=0) # move skeleton id to first column by its name col = df_all.pop('skeleton_id') # Remove column 'B' and store it in col df_all.insert(0, 'skeleton_id', col) # add label column df_all["label"] = df_all["skeleton_id"].values if viewer is not None: from napari_workflows._workflow import _get_layer_from_data from napari_skimage_regionprops import add_table skeleton_layer = _get_layer_from_data(viewer, labeled_skeletons) skeleton_layer.features = df_all add_table(skeleton_layer, viewer) return df_all
[docs] def analyze_single_skeleton( parsed_skeleton: "napari.types.LabelsData", neighborhood: str = "n8" ) -> pd.DataFrame: """ Analyze a single skeleton and return a pandas dataframe. This function calculates the following measurements for a single skeleton: - number of end points - number of branch points - number of nodes - number of branches - spine length (in network), number of edges in spine - spine length (in image), number of pixels in spine - number of cycle basis - number of possible undirected cycles Parameters ---------- parsed_skeleton : "napari.types.LabelsData" A parsed labeled image of a skeleton. neighborhood : str, optional The neighborhood used for the skeletonization, by default "n8". For 2D images, use "n4" or "n8". For 3D images, use "n6", "n18" or "n26". Returns ------- df : pd.DataFrame A pandas dataframe containing the measurements. """ import networkx as nx import numpy as np from ._network_analysis import ( create_adjacency_matrix, convert_adjacency_matrix_to_graph, create_spine_image) from ._labeled_skeletonization import ( label_branches ) from ._backend_toska_functions import skeleton_spine_search # create an adjacency matrix for the skeleton adjacency_matrix = create_adjacency_matrix(parsed_skeleton, neighborhood=neighborhood) graph = convert_adjacency_matrix_to_graph(adjacency_matrix) # number of end points n_endpoints = sum(adjacency_matrix.sum(axis=1) == 1) # number of branch points n_branch_points = sum(adjacency_matrix.sum(axis=1) > 1) # number of nodes n_nodes = n_endpoints + n_branch_points # number of branches n_branches = adjacency_matrix.shape[1] # # neighboring end points need to be handled separately # if n_branches == 0: # spine_length = 0 # image_spine_length = 0 # else: # # spine length (in network), number of edges in spine # _, spine_paths_length = skeleton_spine_search( # adjacency_matrix, graph) # spine_length = np.nansum(spine_paths_length) # # spine length (in image), number of pixels in spine # branch_labels = label_branches(parsed_skeleton, parsed_skeleton > 0, # neighborhood=neighborhood) # spine_image = create_spine_image(adjacency_matrix, branch_labels) # image_spine_length = calculate_spine_length(spine_image) # cycle basis directed_graph = graph.to_directed() possible_directed_cycles = list(nx.simple_cycles(directed_graph)) n_cycle_basis = len(nx.cycle_basis(graph)) n_possible_undirected_cycles = len( [x for x in possible_directed_cycles if len(x) > 2])//2 df = pd.DataFrame( { "n_endpoints": [n_endpoints], "n_branch_points": [n_branch_points], "n_nodes": [n_nodes], "n_branches": [n_branches], # "spine_length_network": [spine_length], # "spine_length_image": [image_spine_length], "n_cycle_basis": [n_cycle_basis], "n_possible_undirected_cycles": [n_possible_undirected_cycles] } ) return df
[docs] def analyze_single_skeleton_network( parsed_skeleton_single: "napari.types.LabelsData", neighborhood: str = "n8" ) -> pd.DataFrame: """ Analyze a single skeleton and return a pandas dataframe. This function categorizes ever element of the network representation of a skeleton as either a node or an edge and potentially its weight. Parameters ---------- parsed_skeleton_single : "napari.types.LabelsData" A parsed labeled image of a skeleton. neighborhood : str, optional The neighborhood used for the skeletonization, by default "n8". For 2D images, use "n4" or "n8". For 3D images, use "n6", "n18" or "n26". Returns ------- features : pd.DataFrame A pandas dataframe containing the measurements. """ import networkx as nx import numpy as np from skimage import measure import pandas as pd from ._network_analysis import ( create_adjacency_matrix, convert_adjacency_matrix_to_graph) from ._backend_toska_functions import skeleton_spine_search # create an adjacency matrix for the skeleton adjacency_matrix = create_adjacency_matrix(parsed_skeleton_single, neighborhood=neighborhood) graph = convert_adjacency_matrix_to_graph(adjacency_matrix) # get edge and node labels node_labels = np.arange(1, adjacency_matrix.shape[0]+1) edge_labels = np.arange(1, adjacency_matrix.shape[1]+1) # component type component_type = ['node'] * adjacency_matrix.shape[0] +\ ['edge'] * adjacency_matrix.shape[1] # Assemble the table features = pd.DataFrame( { "label": np.arange(1, np.amax(node_labels) + np.amax(edge_labels) +1), "component_type": component_type } ) # Measurement: Node degree node_degrees = adjacency_matrix.sum(axis=1) features.loc[features["component_type"] == "node", "degree"] = node_degrees # add all edge weights to dataframe edge_weights = nx.get_edge_attributes(graph, "weight") features.loc[features["component_type"] == "edge", "weight"] = list(edge_weights.values()) features.loc[features["component_type"] == "edge", "node_1"] = np.asarray(graph.edges)[:, 0] features.loc[features["component_type"] == "edge", "node_2"] = np.asarray(graph.edges)[:, 1] features.loc[features["component_type"] == "node", "node_labels"] = list(graph.nodes) return features
[docs] def calculate_branch_lengths( branch_label_image: "napari.types.LabelsData", viewer: "napari.Viewer" = None ) -> pd.DataFrame: """ Calculate the branch length for each branch in a branch image. This function calculates the branch length for each branch in a branch image. The branch length is calculated as the number of pixels in the branch and takes into account the adjacency relationship between subsequent pixels/voxels in a branch. Parameters ---------- branch_label_image : "napari.types.LabelsData" A labeled image of an individual skeleton's branches. Returns ------- df : pd.DataFrame A pandas dataframe containing the branch length for each branch. """ import numpy as np from skimage import measure from copy import deepcopy unique_branches = np.unique(branch_label_image)[1:] df = pd.DataFrame( { "label": unique_branches, } ) if len(branch_label_image.shape) == 2: # loop over all branches for branch in unique_branches: length = 0 masked_branch = branch_label_image * (branch_label_image == branch) labeled_branch = measure.label(masked_branch, connectivity=1) unique_branch_segments = np.unique(labeled_branch)[1:] # loop over all segments of the branch for label in unique_branch_segments: segment_size = np.sum(unique_branch_segments == label) if segment_size == 1: length += np.sqrt(2) else: length += segment_size df.loc[df["label"] == branch, "branch_length"] = length elif len(branch_label_image.shape) == 3: for branch in unique_branches: length = 0 masked_branch = branch_label_image * (branch_label_image == branch) n6_labeled_branch = measure.label(masked_branch, connectivity=1) n6_labeled_branch_copy = deepcopy(n6_labeled_branch) n6_unique_branch_segments = np.unique(n6_labeled_branch)[1:] # loop over all segments of the branch for segment in n6_unique_branch_segments: segment_size = np.sum(n6_labeled_branch == segment) if segment_size == 1: n6_labeled_branch_copy[n6_labeled_branch == segment] = 0 face_sharing_contributions = np.sum(n6_labeled_branch_copy > 0) length += face_sharing_contributions n18_labeled_branch = measure.label(masked_branch, connectivity=2) n18_unique_branch_segments = np.unique(n18_labeled_branch)[1:] # loop over all segments of the branch for segment in n18_unique_branch_segments: segment_size = np.sum(n18_labeled_branch == segment) if segment_size == 1: length += np.sqrt(3) n18_labeled_branch[n18_labeled_branch == segment] = 0 n18_remainder_objects = n18_labeled_branch - n6_labeled_branch_copy edge_sharing_contributions = np.sum(n18_remainder_objects > 0) length += edge_sharing_contributions * np.sqrt(2) df.loc[df["label"] == branch, "branch_length"] = length if viewer is not None: from napari_workflows._workflow import _get_layer_from_data from napari_skimage_regionprops import add_table branch_layer = _get_layer_from_data(viewer, branch_label_image) branch_layer.features = df add_table(branch_layer, viewer) return df
def calculate_spine_length( spine_image: "napari.types.LabelsData", ) -> float: """ Iterate over every present label in the spine image and calculate the spine length for each label. Parameters ---------- spine_image : "napari.types.LabelsData" A labeled image of a skeleton's spine. Returns ------- total_length : float The total length of the spine. """ segment_lengths = calculate_branch_lengths(spine_image) total_length = segment_lengths["branch_length"].sum() return total_length