Skip to content

Commit 5feff6a

Browse files
Update active zone analysis for SNAP/MUNC data
1 parent 2ccf340 commit 5feff6a

File tree

4 files changed

+476
-6
lines changed

4 files changed

+476
-6
lines changed

scripts/cooper/analysis/active_zone_analysis.py

Lines changed: 193 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,22 @@
33

44
import h5py
55
import numpy as np
6+
import napari
7+
import pandas as pd
68

79
from scipy.ndimage import binary_closing
810
from skimage.measure import label
911
from synaptic_reconstruction.ground_truth.shape_refinement import edge_filter
12+
from synaptic_reconstruction.morphology import skeletonize_object
13+
from synaptic_reconstruction.distance_measurements import measure_segmentation_to_object_distances
1014
from tqdm import tqdm
1115

12-
ROOT = "/mnt/lustre-emmy-hdd/projects/nim00007/data/synaptic-reconstruction/cooper/20241102_TOMO_DATA_Imig2014/final_Imig2014_seg_autoComp" # noqa
16+
from compute_skeleton_area import calculate_surface_area
1317

14-
OUTPUT_AZ = "./boundary_az"
18+
ROOT = "./imig_data" # noqa
19+
OUTPUT_AZ = "./az_segmentation"
20+
21+
RESOLUTION = (1.554,) * 3
1522

1623

1724
def filter_az(path):
@@ -20,6 +27,7 @@ def filter_az(path):
2027
ds = os.path.basename(ds)
2128
out_path = os.path.join(OUTPUT_AZ, ds, fname)
2229
os.makedirs(os.path.join(OUTPUT_AZ, ds), exist_ok=True)
30+
2331
if os.path.exists(out_path):
2432
return
2533

@@ -56,11 +64,192 @@ def filter_az(path):
5664
f.create_dataset("filtered_az", data=az_filtered, compression="gzip")
5765

5866

59-
def main():
67+
def filter_all_azs():
6068
files = sorted(glob(os.path.join(ROOT, "**/*.h5"), recursive=True))
61-
for ff in tqdm(files):
69+
for ff in tqdm(files, desc="Filter AZ segmentations."):
6270
filter_az(ff)
6371

6472

73+
def process_az(path, view=True):
74+
key = "thin_az"
75+
76+
with h5py.File(path, "r") as f:
77+
if key in f and not view:
78+
return
79+
az_seg = f["filtered_az"][:]
80+
81+
az_thin = skeletonize_object(az_seg)
82+
83+
if view:
84+
ds, fname = os.path.split(path)
85+
ds = os.path.basename(ds)
86+
raw_path = os.path.join(ROOT, ds, fname)
87+
with h5py.File(raw_path, "r") as f:
88+
raw = f["raw"][:]
89+
v = napari.Viewer()
90+
v.add_image(raw)
91+
v.add_labels(az_seg)
92+
v.add_labels(az_thin)
93+
napari.run()
94+
else:
95+
with h5py.File(path, "a") as f:
96+
f.create_dataset(key, data=az_thin, compression="gzip")
97+
98+
99+
# Apply thinning to all active zones to obtain 1d surface.
100+
def process_all_azs():
101+
files = sorted(glob(os.path.join(OUTPUT_AZ, "**/*.h5"), recursive=True))
102+
for ff in tqdm(files, desc="Thin AZ segmentations."):
103+
process_az(ff, view=False)
104+
105+
106+
def measure_az_area(path):
107+
from skimage import measure
108+
109+
with h5py.File(path, "r") as f:
110+
seg = f["thin_az"][:]
111+
112+
# Try via surface mesh.
113+
verts, faces, normals, values = measure.marching_cubes(seg, spacing=RESOLUTION)
114+
surface_area1 = measure.mesh_surface_area(verts, faces)
115+
116+
# Try via custom function.
117+
surface_area2 = calculate_surface_area(seg, voxel_size=RESOLUTION)
118+
119+
ds, fname = os.path.split(path)
120+
ds = os.path.basename(ds)
121+
122+
return pd.DataFrame({
123+
"Dataset": [ds],
124+
"Tomogram": [fname],
125+
"surface_mesh [nm^2]": [surface_area1],
126+
"surface_custom [nm^2]": [surface_area2],
127+
})
128+
129+
130+
# Measure the AZ surface areas.
131+
def measure_all_areas():
132+
save_path = "./results/area_measurements.xlsx"
133+
if os.path.exists(save_path):
134+
return
135+
136+
files = sorted(glob(os.path.join(OUTPUT_AZ, "**/*.h5"), recursive=True))
137+
area_table = []
138+
for ff in tqdm(files, desc="Measure AZ areas."):
139+
area = measure_az_area(ff)
140+
area_table.append(area)
141+
area_table = pd.concat(area_table)
142+
area_table.to_excel(save_path, index=False)
143+
144+
manual_results = "/home/pape/Work/my_projects/synaptic-reconstruction/scripts/cooper/debug/surface/manualAZ_surface_area.xlsx" # noqa
145+
manual_results = pd.read_excel(manual_results)[["Dataset", "Tomogram", "manual"]]
146+
comparison_table = pd.merge(area_table, manual_results, on=["Dataset", "Tomogram"], how="inner")
147+
comparison_table.to_excel("./results/area_comparison.xlsx", index=False)
148+
149+
150+
def analyze_areas():
151+
import seaborn as sns
152+
import matplotlib.pyplot as plt
153+
154+
table = pd.read_excel("./results/area_comparison.xlsx")
155+
156+
fig, axes = plt.subplots(2)
157+
sns.scatterplot(data=table, x="manual", y="surface_mesh [nm^2]", ax=axes[0])
158+
sns.scatterplot(data=table, x="manual", y="surface_custom [nm^2]", ax=axes[1])
159+
plt.show()
160+
161+
162+
def measure_distances(ves_path, az_path):
163+
with h5py.File(az_path, "r") as f:
164+
az = f["thin_az"][:]
165+
166+
with h5py.File(ves_path, "r") as f:
167+
vesicles = f["vesicles/segment_from_combined_vesicles"][:]
168+
169+
distances, _, _, _ = measure_segmentation_to_object_distances(vesicles, az, resolution=RESOLUTION)
170+
171+
ds, fname = os.path.split(az_path)
172+
ds = os.path.basename(ds)
173+
174+
return pd.DataFrame({
175+
"Dataset": [ds] * len(distances),
176+
"Tomogram": [fname] * len(distances),
177+
"Distance": distances,
178+
})
179+
180+
181+
# Measure the AZ vesicle distances for all vesicles.
182+
def measure_all_distances():
183+
save_path = "./results/vesicle_az_distances.xlsx"
184+
if os.path.exists(save_path):
185+
return
186+
187+
ves_files = sorted(glob(os.path.join(ROOT, "**/*.h5"), recursive=True))
188+
az_files = sorted(glob(os.path.join(OUTPUT_AZ, "**/*.h5"), recursive=True))
189+
assert len(ves_files) == len(az_files)
190+
191+
dist_table = []
192+
for ves_file, az_file in tqdm(zip(ves_files, az_files), total=len(az_files), desc="Measure distances."):
193+
dist = measure_distances(ves_file, az_file)
194+
dist_table.append(dist)
195+
dist_table = pd.concat(dist_table)
196+
197+
dist_table.to_excel(save_path, index=False)
198+
199+
200+
def reformat_distances():
201+
tab = pd.read_excel("./results/vesicle_az_distances.xlsx")
202+
203+
munc_ko = {}
204+
munc_ctrl = {}
205+
206+
snap_ko = {}
207+
snap_ctrl = {}
208+
209+
for _, row in tab.iterrows():
210+
ds = row.Dataset
211+
tomo = row.Tomogram
212+
213+
if ds == "Munc13DKO":
214+
if "CTRL" in tomo:
215+
group = munc_ctrl
216+
else:
217+
group = munc_ko
218+
else:
219+
assert ds == "SNAP25"
220+
if "CTRL" in tomo:
221+
group = snap_ctrl
222+
else:
223+
group = snap_ko
224+
225+
name = os.path.splitext(tomo)[0]
226+
val = row["Distance [nm]"]
227+
if name in group:
228+
group[name].append(val)
229+
else:
230+
group[name] = [val]
231+
232+
def save_tab(group, path):
233+
n_ves_max = max(len(v) for v in group.values())
234+
group = {k: v + [np.nan] * (n_ves_max - len(v)) for k, v in group.items()}
235+
group_tab = pd.DataFrame(group)
236+
group_tab.to_excel(path, index=False)
237+
238+
os.makedirs("./results/distances_formatted", exist_ok=True)
239+
save_tab(munc_ko, "./results/distances_formatted/munc_ko.xlsx")
240+
save_tab(munc_ctrl, "./results/distances_formatted/munc_ctrl.xlsx")
241+
save_tab(snap_ko, "./results/distances_formatted/snap_ko.xlsx")
242+
save_tab(snap_ctrl, "./results/distances_formatted/snap_ctrl.xlsx")
243+
244+
245+
def main():
246+
# filter_all_azs()
247+
# process_all_azs()
248+
# measure_all_areas()
249+
# analyze_areas()
250+
# measure_all_distances()
251+
reformat_distances()
252+
253+
65254
if __name__ == "__main__":
66255
main()
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import numpy as np
2+
3+
4+
def calculate_surface_area(skeleton, voxel_size=(1.0, 1.0, 1.0)):
5+
"""
6+
Calculate the surface area of a 3D skeletonized object.
7+
8+
Parameters:
9+
skeleton (3D array): Binary 3D skeletonized array.
10+
voxel_size (tuple): Physical size of voxels (z, y, x).
11+
12+
Returns:
13+
float: Approximate surface area of the skeleton.
14+
"""
15+
# Define the voxel dimensions
16+
voxel_area = (
17+
voxel_size[1] * voxel_size[2], # yz-face area
18+
voxel_size[0] * voxel_size[2], # xz-face area
19+
voxel_size[0] * voxel_size[1], # xy-face area
20+
)
21+
22+
# Compute the number of exposed faces for each voxel
23+
exposed_faces = 0
24+
directions = [
25+
(1, 0, 0), (-1, 0, 0), # x-axis neighbors
26+
(0, 1, 0), (0, -1, 0), # y-axis neighbors
27+
(0, 0, 1), (0, 0, -1), # z-axis neighbors
28+
]
29+
30+
# Iterate over all voxels in the skeleton
31+
for z, y, x in np.argwhere(skeleton):
32+
for i, (dz, dy, dx) in enumerate(directions):
33+
neighbor = (z + dz, y + dy, x + dx)
34+
# Check if the neighbor is outside the volume or not part of the skeleton
35+
if (
36+
0 <= neighbor[0] < skeleton.shape[0] and
37+
0 <= neighbor[1] < skeleton.shape[1] and
38+
0 <= neighbor[2] < skeleton.shape[2] and
39+
skeleton[neighbor] == 1
40+
):
41+
continue
42+
exposed_faces += voxel_area[i // 2]
43+
44+
return exposed_faces

0 commit comments

Comments
 (0)