Skip to content

Commit 1810805

Browse files
Merge pull request #79 from computational-cell-analytics/first-release
Prepare first release
2 parents 458ec90 + 03d42a8 commit 1810805

20 files changed

+682
-367
lines changed

examples/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
data/
2+
set_up_pool.py
3+
*.h5
4+
*.tif
5+
*.mrc

examples/analysis_pipeline.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
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()

examples/domain_adaptation.py

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,35 +4,41 @@
44
a different electron tomogram with different specimen and sample preparation.
55
You don't need any annotations in the new domain to run this script.
66
7-
You can download example data for this script from:
8-
- Adaptation to 2d TEM data: TODO zenodo link
9-
- Adaptation to different tomography data: TODO zenodo link
7+
We use data from the SynapseNet publication for this example:
8+
- Adaptation to 2d TEM data: https://doi.org/10.5281/zenodo.14236381
9+
- Adaptation to different tomography data (3d data): https://doi.org/10.5281/zenodo.14232606
10+
11+
It is of course possible to adapt it to your own data.
1012
"""
1113

1214
import os
1315
from glob import glob
1416

1517
from sklearn.model_selection import train_test_split
18+
from synapse_net.inference.inference import get_model_path
19+
from synapse_net.sample_data import download_data_from_zenodo
1620
from synapse_net.training import mean_teacher_adaptation
17-
from synapse_net.tools.util import get_model_path
1821

1922

2023
def main():
2124
# Choose whether to adapt the model to 2D or to 3D data.
22-
train_2d_model = True
23-
24-
# TODO adjust to zenodo downloads
25-
# These are the data folders for the example data downloaded from zenodo.
26-
# Update these paths to apply the script to your own data.
27-
# Check out the example data to see the data format for training.
28-
data_root_folder_2d = "./data/2d_tem/train_unlabeled"
29-
data_root_folder_3d = "./data/..."
25+
train_2d_model = False
3026

31-
# Choose the correct data folder depending on 2d/3d training.
32-
data_root_folder = data_root_folder_2d if train_2d_model else data_root_folder_3d
27+
# Download the training data from zenodo.
28+
# You have to replace this if you want to train on your own data.
29+
# The training data should be stored in an hdf5 file per tomogram,
30+
# with tomgoram data stored in the internal dataset 'raw'.
31+
if train_2d_model:
32+
data_root = "./data/2d_tem"
33+
download_data_from_zenodo(data_root, "2d_tem")
34+
train_root_folder = os.path.join(data_root, "train_unlabeled")
35+
else:
36+
data_root = "./data/inner_ear_ribbon_synapse"
37+
download_data_from_zenodo(data_root, "inner_ear_ribbon_synapse")
38+
train_root_folder = data_root
3339

3440
# Get all files with ending .h5 in the training folder.
35-
files = sorted(glob(os.path.join(data_root_folder, "**", "*.h5"), recursive=True))
41+
files = sorted(glob(os.path.join(train_root_folder, "**", "*.h5"), recursive=True))
3642

3743
# Crate a train / val split.
3844
train_ratio = 0.85

examples/network_training.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,30 +5,36 @@
55
to adapt an already trained network to your data without the need for
66
additional annotations then check out `domain_adaptation.py`.
77
8-
You can download example data for this script from:
9-
TODO zenodo link to Single-Ax / Chemical Fix data.
8+
We will use the data from our manuscript here:
9+
https://doi.org/10.5281/zenodo.14330011
10+
11+
You can also use your own data, if you prepare it in the same format.
1012
"""
1113
import os
1214
from glob import glob
1315

1416
from sklearn.model_selection import train_test_split
17+
from synapse_net.sample_data import download_data_from_zenodo
1518
from synapse_net.training import supervised_training
1619

1720

1821
def main():
19-
# This is the folder that contains your training data.
20-
# The example was designed so that it runs for the sample data downloaded to './data'.
21-
# If you want to train on your own data than change this filepath accordingly.
22-
# TODO update to match zenodo download
23-
data_root_folder = "./data/vesicles/train"
22+
# Download the training data from zenodo.
23+
# You have to replace this if you want to train on your own data.
24+
# The training data should be stored in an hdf5 file per tomogram,
25+
# with tomgoram data stored in the internal dataset 'raw'
26+
# and the vesicle annotations stored in the internal dataset 'labels/vesicles'.
27+
data_root = "./data/training_data"
28+
download_data_from_zenodo(data_root, "training_data")
29+
train_root_folder = os.path.join(data_root, "vesicles/train")
2430

2531
# The training data should be saved as .h5 files, with:
2632
# an internal dataset called 'raw' that contains the image data
2733
# and another dataset that contains the training annotations.
2834
label_key = "labels/vesicles"
2935

3036
# Get all files with the ending .h5 in the training folder.
31-
files = sorted(glob(os.path.join(data_root_folder, "**", "*.h5"), recursive=True))
37+
files = sorted(glob(os.path.join(train_root_folder, "**", "*.h5"), recursive=True))
3238

3339
# Crate a train / val split.
3440
train_ratio = 0.85

scripts/cooper/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ $ micromamba activate sam
1919
The segmentation scripts (`run_..._segmentation.py`) all work similarly and can either run segmentation for a single mrc file or for all mrcs in a folder structure.
2020
For example, you can run vesicle segmentation like this:
2121
```
22-
$ python run_vesicle_segmentation.py -i /path/to/input_folder -o /path/to/output_folder -m /path/to/vesicle_model.pt
22+
$ python run_vesicle_segmentation.py -i /path/to/input_folder -o /path/to/output_folder
2323
```
24-
The filepath after `-i` specifices the location of the folder with the mrcs to be segmented, the segmentation results will be stored (as tifs) in the folder following `-o` and `-m` is used to specify the path to the segmentation model.
24+
The filepath after `-i` specifices the location of the folder with the mrcs to be segmented and the segmentation results will be stored (as tifs) in the folder following `-o`.
2525
To segment vesicles with an additional mask, you can use the `--mask_path` option.
2626

2727
The segmentation scripts accept additional parameters, e.g. `--force` to overwrite existing segmentations in the output folder (by default these are skipped to avoid unnecessary computation) and `--tile_shape <TILE_Z> <TILE_Y> <TILE_X>` to specify a different tile shape (which may be necessary to avoid running out of GPU memory).

scripts/cooper/run_compartment_segmentation.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@
22
from functools import partial
33

44
from synapse_net.inference.compartments import segment_compartments
5+
from synapse_net.inference.inference import get_model_path
56
from synapse_net.inference.util import inference_helper, parse_tiling
67

78

89
def run_compartment_segmentation(args):
910
tiling = parse_tiling(args.tile_shape, args.halo)
11+
12+
if args.model is None:
13+
model_path = get_model_path("compartments")
14+
else:
15+
model_path = args.model
16+
1017
segmentation_function = partial(
11-
segment_compartments, model_path=args.model_path, verbose=False, tiling=tiling, scale=[0.25, 0.25, 0.25]
18+
segment_compartments, model_path=model_path, verbose=False, tiling=tiling, scale=[0.25, 0.25, 0.25]
1219
)
1320
inference_helper(
1421
args.input_path, args.output_path, segmentation_function, force=args.force, data_ext=args.data_ext
@@ -26,7 +33,7 @@ def main():
2633
help="The filepath to directory where the segmentation will be saved."
2734
)
2835
parser.add_argument(
29-
"--model_path", "-m", required=True, help="The filepath to the compartment model."
36+
"--model", "-m", help="The filepath to the compartment model."
3037
)
3138
parser.add_argument(
3239
"--force", action="store_true",

scripts/cooper/run_mitochondria_segmentation.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
from functools import partial
33

44
from synapse_net.inference.mitochondria import segment_mitochondria
5+
from synapse_net.inference.inference import get_model_path
56
from synapse_net.inference.util import inference_helper, parse_tiling
67

78

89
def run_mitochondria_segmentation(args):
10+
if args.model is None:
11+
model_path = get_model_path("mitochondria")
12+
else:
13+
model_path = args.model
14+
915
tiling = parse_tiling(args.tile_shape, args.halo)
1016
segmentation_function = partial(
11-
segment_mitochondria, model_path=args.model_path, verbose=False, tiling=tiling, scale=[0.5, 0.5, 0.5]
17+
segment_mitochondria, model_path=model_path, verbose=False, tiling=tiling, scale=[0.5, 0.5, 0.5]
1218
)
1319
inference_helper(
1420
args.input_path, args.output_path, segmentation_function,
@@ -27,7 +33,7 @@ def main():
2733
help="The filepath to directory where the segmentation will be saved."
2834
)
2935
parser.add_argument(
30-
"--model_path", "-m", required=True, help="The filepath to the mitochondria model."
36+
"--model", "-m", help="The filepath to the mitochondria model."
3137
)
3238
parser.add_argument(
3339
"--force", action="store_true",

scripts/cooper/run_vesicle_segmentation.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,19 @@
22
from functools import partial
33

44
from synapse_net.inference.vesicles import segment_vesicles
5+
from synapse_net.inference.inference import get_model_path
56
from synapse_net.inference.util import inference_helper, parse_tiling
67

78

89
def run_vesicle_segmentation(args):
10+
if args.model is None:
11+
model_path = get_model_path("vesicles_3d")
12+
else:
13+
model_path = args.model
14+
915
tiling = parse_tiling(args.tile_shape, args.halo)
1016
segmentation_function = partial(
11-
segment_vesicles, model_path=args.model_path, verbose=False, tiling=tiling,
17+
segment_vesicles, model_path=model_path, verbose=False, tiling=tiling,
1218
exclude_boundary=not args.include_boundary
1319
)
1420
inference_helper(
@@ -28,7 +34,7 @@ def main():
2834
help="The filepath to directory where the segmentations will be saved."
2935
)
3036
parser.add_argument(
31-
"--model_path", "-m", required=True, help="The filepath to the vesicle model."
37+
"--model_path", "-m", help="The filepath to the vesicle model."
3238
)
3339
parser.add_argument(
3440
"--mask_path", help="The filepath to a tif file with a mask that will be used to restrict the segmentation."

scripts/prepare_zenodo_uploads.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ def _export_az(train_root, test_tomos, name):
5656

5757
for tomo in tqdm(tomograms):
5858
fname = os.path.basename(tomo)
59-
if tomo in test_tomos:
59+
if fname in test_tomos:
6060
out_path = os.path.join(test_out, fname)
6161
else:
6262
out_path = os.path.join(train_out, fname)

synapse_net/__version__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.0.1"
1+
__version__ = "0.1.0"

0 commit comments

Comments
 (0)