|
| 1 | +import napari |
| 2 | +import pandas as pd |
| 3 | +import numpy as np |
| 4 | + |
| 5 | +from scipy.ndimage import binary_closing |
| 6 | +from skimage.measure import regionprops |
| 7 | +from skimage.segmentation import find_boundaries |
| 8 | + |
| 9 | +from synapse_net.distance_measurements import measure_segmentation_to_object_distances |
| 10 | +from synapse_net.file_utils import read_mrc |
| 11 | +from synapse_net.imod.to_imod import convert_segmentation_to_spheres |
| 12 | +from synapse_net.inference import compute_scale_from_voxel_size, get_model, run_segmentation |
| 13 | +from synapse_net.sample_data import get_sample_data |
| 14 | + |
| 15 | + |
| 16 | +def segment_structures(tomogram, voxel_size): |
| 17 | + # Segment the synaptic vesicles. The data will automatically be resized |
| 18 | + # to match the average voxel size of the training data. |
| 19 | + model_name = "vesicles_3d" # This is the name for the vesicle model for EM tomography. |
| 20 | + model = get_model(model_name) # Load the corresponding model. |
| 21 | + # Compute the scale to match the tomogram voxel size to the training data. |
| 22 | + scale = compute_scale_from_voxel_size(voxel_size, model_name) |
| 23 | + vesicles = run_segmentation(tomogram, model, model_name, scale=scale) |
| 24 | + |
| 25 | + # Segment the active zone. |
| 26 | + model_name = "active_zone" |
| 27 | + model = get_model(model_name) |
| 28 | + scale = compute_scale_from_voxel_size(voxel_size, model_name) |
| 29 | + active_zone = run_segmentation(tomogram, model, model_name, scale=scale) |
| 30 | + |
| 31 | + # Segment the synaptic compartments. |
| 32 | + model_name = "compartments" |
| 33 | + model = get_model(model_name) |
| 34 | + scale = compute_scale_from_voxel_size(voxel_size, model_name) |
| 35 | + compartments = run_segmentation(tomogram, model, model_name, scale=scale) |
| 36 | + |
| 37 | + return {"vesicles": vesicles, "active_zone": active_zone, "compartments": compartments} |
| 38 | + |
| 39 | + |
| 40 | +def n_vesicles(mask, ves): |
| 41 | + return len(np.unique(ves[mask])) - 1 |
| 42 | + |
| 43 | + |
| 44 | +def postprocess_segmentation(segmentations): |
| 45 | + # We find the compartment corresponding to the presynaptic terminal |
| 46 | + # by selecting the compartment with most vesicles. We filter out all |
| 47 | + # vesicles that do not overlap with this compartment. |
| 48 | + |
| 49 | + vesicles, compartments = segmentations["vesicles"], segmentations["compartments"] |
| 50 | + |
| 51 | + # First, we find the compartment with most vesicles. |
| 52 | + props = regionprops(compartments, intensity_image=vesicles, extra_properties=[n_vesicles]) |
| 53 | + compartment_ids = [prop.label for prop in props] |
| 54 | + vesicle_counts = [prop.n_vesicles for prop in props] |
| 55 | + compartments = (compartments == compartment_ids[np.argmax(vesicle_counts)]).astype("uint8") |
| 56 | + |
| 57 | + # Filter all vesicles that are not in the compartment. |
| 58 | + props = regionprops(vesicles, compartments) |
| 59 | + filter_ids = [prop.label for prop in props if prop.max_intensity == 0] |
| 60 | + vesicles[np.isin(vesicles, filter_ids)] = 0 |
| 61 | + |
| 62 | + segmentations["vesicles"], segmentations["compartments"] = vesicles, compartments |
| 63 | + |
| 64 | + # We also apply closing to the active zone segmentation to avoid gaps and then |
| 65 | + # intersect it with the boundary of the presynaptic compartment. |
| 66 | + active_zone = segmentations["active_zone"] |
| 67 | + active_zone = binary_closing(active_zone, iterations=4) |
| 68 | + boundary = find_boundaries(compartments) |
| 69 | + active_zone = np.logical_and(active_zone, boundary).astype("uint8") |
| 70 | + segmentations["active_zone"] = active_zone |
| 71 | + |
| 72 | + return segmentations |
| 73 | + |
| 74 | + |
| 75 | +def measure_distances(segmentations, voxel_size): |
| 76 | + # Here, we measure the distances from each vesicle to the active zone. |
| 77 | + # We use the function 'measure_segmentation_to_object_distances' for this, |
| 78 | + # which uses an euclidean distance transform scaled with the voxel size |
| 79 | + # to determine distances. |
| 80 | + vesicles, active_zone = segmentations["vesicles"], segmentations["active_zone"] |
| 81 | + voxel_size = tuple(voxel_size[ax] for ax in "zyx") |
| 82 | + distances, _, _, vesicle_ids = measure_segmentation_to_object_distances( |
| 83 | + vesicles, active_zone, resolution=voxel_size |
| 84 | + ) |
| 85 | + # We convert the result to a pandas data frame. |
| 86 | + return pd.DataFrame({"vesicle_id": vesicle_ids, "distance": distances}) |
| 87 | + |
| 88 | + |
| 89 | +def assign_vesicle_pools(vesicle_attributes): |
| 90 | + # We assign the vesicles to their respective pool, 'docked' and 'non-attached', |
| 91 | + # based on the criterion of being within 2 nm from the active zone. |
| 92 | + # We add the pool assignment as a new column to the dataframe with vesicle attributes. |
| 93 | + docked_vesicle_distance = 2 # nm |
| 94 | + vesicle_attributes["pool"] = vesicle_attributes["distance"].apply( |
| 95 | + lambda x: "docked" if x < docked_vesicle_distance else "non-attached" |
| 96 | + ) |
| 97 | + return vesicle_attributes |
| 98 | + |
| 99 | + |
| 100 | +def visualize_results(tomogram, segmentations, vesicle_attributes): |
| 101 | + # Here, we visualize the segmentation and pool assignment result in napari. |
| 102 | + |
| 103 | + # Create a segmentation to visualize the vesicle pools. |
| 104 | + docked_ids = vesicle_attributes[vesicle_attributes.pool == "docked"].vesicle_id |
| 105 | + non_attached_ids = vesicle_attributes[vesicle_attributes.pool == "non-attached"].vesicle_id |
| 106 | + vesicles = segmentations["vesicles"] |
| 107 | + vesicle_pools = np.isin(vesicles, docked_ids).astype("uint8") |
| 108 | + vesicle_pools[np.isin(vesicles, non_attached_ids)] = 2 |
| 109 | + |
| 110 | + # Create a napari viewer, add the tomogram data and the segmentation results. |
| 111 | + viewer = napari.Viewer() |
| 112 | + viewer.add_image(tomogram) |
| 113 | + for name, segmentation in segmentations.items(): |
| 114 | + viewer.add_labels(segmentation, name=name) |
| 115 | + viewer.add_labels(vesicle_pools) |
| 116 | + napari.run() |
| 117 | + |
| 118 | + |
| 119 | +def save_analysis(segmentations, vesicle_attributes, save_path): |
| 120 | + # Here, we compute the radii and centroid positions of the vesicles, |
| 121 | + # add them to the vesicle attributes and then save all vesicle attributes to |
| 122 | + # an excel table. You can use this table for evaluation of the analysis. |
| 123 | + vesicles = segmentations["vesicles"] |
| 124 | + coordinates, radii = convert_segmentation_to_spheres(vesicles, radius_factor=0.7) |
| 125 | + vesicle_attributes["radius"] = radii |
| 126 | + for ax_id, ax_name in enumerate("zyx"): |
| 127 | + vesicle_attributes[f"center-{ax_name}"] = coordinates[:, ax_id] |
| 128 | + vesicle_attributes.to_excel(save_path, index=False) |
| 129 | + |
| 130 | + |
| 131 | +def main(): |
| 132 | + """This script implements an example analysis pipeline with SynapseNet and applies it to a tomogram. |
| 133 | + Here, we analyze docked and non-attached vesicles in a sample tomogram.""" |
| 134 | + |
| 135 | + # Load the tomogram for our sample data. |
| 136 | + mrc_path = get_sample_data("tem_tomo") |
| 137 | + tomogram, voxel_size = read_mrc(mrc_path) |
| 138 | + |
| 139 | + # Segment synaptic vesicles, the active zone, and the synaptic compartment. |
| 140 | + segmentations = segment_structures(tomogram, voxel_size) |
| 141 | + |
| 142 | + # Post-process the segmentations, to find the presynaptic terminal, |
| 143 | + # filter out vesicles not in the terminal, and to 'snape' the AZ to the presynaptic boundary. |
| 144 | + segmentations = postprocess_segmentation(segmentations) |
| 145 | + |
| 146 | + # Measure the distances between the AZ and vesicles. |
| 147 | + vesicle_attributes = measure_distances(segmentations, voxel_size) |
| 148 | + |
| 149 | + # Assign the vesicle pools, 'docked' and 'non-attached' vesicles, based on the distances. |
| 150 | + vesicle_attributes = assign_vesicle_pools(vesicle_attributes) |
| 151 | + |
| 152 | + # Visualize the results. |
| 153 | + visualize_results(tomogram, segmentations, vesicle_attributes) |
| 154 | + |
| 155 | + # Compute the vesicle radii and combine and save all measurements. |
| 156 | + save_path = "analysis_results.xlsx" |
| 157 | + save_analysis(segmentations, vesicle_attributes, save_path) |
| 158 | + |
| 159 | + |
| 160 | +if __name__ == "__main__": |
| 161 | + main() |
0 commit comments