Skip to content

Commit 50c9550

Browse files
authored
Merge pull request #137 from lucile-hashimoto/sulcal_graph
Watershed and Sulcal graph modules
2 parents b6d1aff + e85215f commit 50c9550

File tree

6 files changed

+869
-0
lines changed

6 files changed

+869
-0
lines changed

examples/example_sulcal_graph.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
###############################################################################
2+
# importation of slam modules
3+
import slam.io as sio
4+
import slam.sulcal_graph as ssg
5+
import numpy as np
6+
7+
###############################################################################
8+
# loading an examplar mesh and corresponding texture
9+
path_to_mesh = "../examples/data/example_mesh.gii"
10+
path_to_texture = "../examples/data/example_texture.gii"
11+
path_to_mask = ""
12+
13+
mesh = sio.load_mesh(path_to_mesh)
14+
side = "left"
15+
texture = sio.load_texture(path_to_texture)
16+
dpf = np.array(texture.darray[0])
17+
18+
###############################################################################
19+
# extract the sulcal graph from a mesh
20+
g = ssg.extract_sulcal_graph(side, path_to_mesh, path_to_features=None, path_to_output=None, path_to_mask=None)
21+
22+
###############################################################################
23+
# add an attribute to nodes
24+
25+
g = ssg.add_node_attribute_to_graph(g, dpf, name='pit_depth', save=False)
26+
print("Node attributes:\n", g.nodes[0].keys())
27+
print("First node:\n", g.nodes[0])
28+
29+
###############################################################################
30+
# add an attribute to edges
31+
g = ssg.add_edge_attribute_to_graph(g, dpf, name='ridge_depth_bis', save=False)
32+
print("Edge attributes:\n", g.edges[list(g.edges)[0]].keys())
33+
print("First edge:\n", g.edges[list(g.edges)[0]])
34+
35+
###############################################################################
36+
# add geodesic distances attribute to edges
37+
g = ssg.add_geodesic_distances_to_graph(g, mesh, save=False)
38+
39+
###############################################################################
40+
# add mean value to nodes attributes
41+
g = ssg.add_mean_value_to_graph(g, dpf, name='basin_mean_depth', save=False)
42+
43+
###############################################################################
44+
# get textures from graph
45+
tex_labels, tex_pits, tex_ridges = ssg.get_textures_from_graph(g, mesh, save=False, outdir=None)

examples/example_watershed.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
###############################################################################
2+
# importation of slam modules
3+
import slam.io as sio
4+
import slam.watershed as swat
5+
import slam.sulcal_graph as ssg
6+
7+
###############################################################################
8+
# loading an examplar mesh and corresponding texture
9+
path_to_mesh = "../examples/data/example_mesh.gii"
10+
path_to_mask = ""
11+
path_to_output = ""
12+
13+
mesh = sio.load_mesh(path_to_mesh)
14+
side = "left"
15+
16+
###############################################################################
17+
# compute curvature, dpf and voronoi
18+
_, dpf, voronoi = swat.compute_mesh_features(mesh, save=False, outdir=path_to_output, check_if_exist=True)
19+
20+
###############################################################################
21+
# normalize watershed thresholds
22+
thresh_dist, thresh_ridge, thresh_area = swat.normalize_thresholds(mesh, voronoi,
23+
thresh_dist=20.0,
24+
thresh_ridge=1.5,
25+
thresh_area=50.0,
26+
side=side)
27+
28+
###############################################################################
29+
# define the exclusion mask (cingular pole)
30+
if path_to_mask:
31+
mask = sio.load_texture(path_to_mask).darray[0]
32+
else:
33+
mask = None
34+
35+
###############################################################################
36+
# extract sulcal pits and associated basins
37+
basins, ridges, adjacency = swat.watershed(mesh, voronoi, dpf, thresh_dist, thresh_ridge, thresh_area, mask)
38+
39+
###############################################################################
40+
# generate the textures from watershed outputs
41+
tex_labels, tex_pits, tex_ridges = swat.get_textures_from_dict(mesh, basins, ridges, save=True, outdir=path_to_output)
42+
43+
###############################################################################
44+
# generate the sulcal graph
45+
g = ssg.get_sulcal_graph(mesh, basins, ridges, save=True, outdir=path_to_output)
46+
47+
###############################################################################
48+
# generate the textures from graph
49+
tex_labels, tex_pits, tex_ridges = ssg.get_textures_from_graph(g, mesh, save=True, outdir=path_to_output)

slam/sulcal_graph.py

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import os
2+
import pickle
3+
import numpy as np
4+
from slam import io, geodesics, texture
5+
import networkx as nx
6+
import slam.watershed as swat
7+
8+
9+
def extract_sulcal_graph(side, path_to_mesh, path_to_features, path_to_output, path_to_mask=None):
10+
"""
11+
Main Function that extracts the sulcal graph from a mesh and saves it in the given directory.
12+
"""
13+
mesh = io.load_mesh(path_to_mesh)
14+
15+
_, dpf, voronoi = swat.compute_mesh_features(mesh, save=True, outdir=path_to_features, check_if_exist=True)
16+
thresh_dist, thresh_ridge, thresh_area = swat.normalize_thresholds(mesh, voronoi,
17+
thresh_dist=20.0,
18+
thresh_ridge=1.5,
19+
thresh_area=50.0,
20+
side=side)
21+
if path_to_mask:
22+
mask = io.load_texture(path_to_mask).darray[0]
23+
else:
24+
mask = None
25+
26+
basins, ridges, adjacency = swat.watershed(mesh, voronoi, dpf, thresh_dist, thresh_ridge, thresh_area, mask)
27+
g = get_sulcal_graph(mesh, basins, ridges, save=True, outdir=path_to_output)
28+
get_textures_from_graph(g, mesh, save=True, outdir=path_to_output)
29+
return g
30+
31+
32+
def get_sulcal_graph(adjacency, basins, ridges, save=True, outdir=None):
33+
"""
34+
Function that creates a graph from the outputs of the watershed.
35+
36+
Node attributes are:
37+
- pit_index: index of the pit
38+
- pit_depth: depth of the pit
39+
- basin_vertices: list of vertices in the basin
40+
- basin_area: area of the basin
41+
- basin_label: label of the basin
42+
43+
Edge attributes are:
44+
- ridge_index: index of the ridge
45+
- ridge_depth: depth of the ridge point
46+
- ridge_length: number of vertices in the ridge
47+
48+
"""
49+
################################################################################################
50+
# Initialize the graph using adjacency matrix
51+
################################################################################################
52+
53+
# As the adjacency matrix concerns all created basins during watershed, it still contains merged basins that
54+
# should not appear in the graph. Thus, we first remove rows and columns corresponding to unconnected basins (only
55+
# zeros inside)
56+
labels = list(basins.keys())
57+
adjacency = adjacency[labels, :][:, labels]
58+
# np.fill_diagonal(graph_adjacency, 1.) # apparently not necessary (visu identical)
59+
graph = nx.from_numpy_array(adjacency) # , nodelist=basins)
60+
# nodelist not adapted to attribution of labels in plotly_visu.py
61+
62+
################################################################################################
63+
# Set graph attributes
64+
################################################################################################
65+
66+
# Add node attributes
67+
node_attributes = {}
68+
for i, (label, values) in enumerate(basins.items()):
69+
node_attributes[i] = basins[label] # add all dictionary values
70+
node_attributes[i]['basin_label'] = label # add label value
71+
nx.set_node_attributes(graph, node_attributes)
72+
73+
# Add edge attributes
74+
edge_attributes = {}
75+
for pair, values in ridges.items():
76+
# Get new indices
77+
i = labels.index(pair[0])
78+
j = labels.index(pair[1])
79+
edge_attributes[i, j] = values # add all dictionary values
80+
nx.set_edge_attributes(graph, edge_attributes)
81+
82+
if save:
83+
if not outdir:
84+
outdir = ''
85+
save_graph(graph, outdir)
86+
87+
return graph
88+
89+
90+
def save_graph(graph, outdir):
91+
"""
92+
Save sulcal pits graph in the given directory under the name "graph.gpickle"
93+
"""
94+
file_path = os.path.join(outdir, "graph.gpickle")
95+
with open(file_path, 'wb') as f:
96+
pickle.dump(graph, f, pickle.HIGHEST_PROTOCOL)
97+
print("Graph saved in", file_path)
98+
return 0
99+
100+
101+
def add_node_attribute_to_graph(graph, texture, name, save=True, outdir=None):
102+
"""
103+
Add a node attribute to the graph using the value of the texture at pit positions
104+
"""
105+
if save and not outdir:
106+
outdir = ''
107+
108+
node_values = {}
109+
for basin in graph.nodes:
110+
# Get pits indices
111+
pit = graph.nodes[basin]['pit_index']
112+
# Get the texture values for each pit
113+
node_values[basin] = texture[pit]
114+
115+
# Add the attribute to the graph
116+
nx.set_node_attributes(graph, values=node_values, name=name)
117+
118+
if save:
119+
save_graph(graph, outdir)
120+
121+
return graph
122+
123+
124+
def add_edge_attribute_to_graph(graph, texture, name, save=True, outdir=None):
125+
"""
126+
Add an edge attribute to the graph using the value of the texture at ridge positions
127+
"""
128+
if save and not outdir:
129+
outdir = ''
130+
131+
# Get the adjacency matrix with ridge positions
132+
adjacency = nx.to_numpy_array(graph, weight='ridge_index', dtype=np.int8)
133+
# Create and fill a new edge dictionary with the texture values at ridge positions
134+
ridge_dict = {}
135+
for i, j in graph.edges:
136+
ridge_dict[(i, j)] = float(texture[adjacency[i][j]])
137+
# Add the attribute to the graph
138+
nx.set_edge_attributes(graph, ridge_dict, name=name)
139+
140+
if save:
141+
save_graph(graph, outdir)
142+
143+
return graph
144+
145+
146+
def add_geodesic_distances_to_graph(graph, mesh, save=True, outdir=None):
147+
"""
148+
Add the geodesic distances between ridge and pits to the corresponding ridge attributes in the graph:
149+
150+
- geodesic_distance_btw_ridge_pit_i: geodesic distance between the ridge and the first pit
151+
- geodesic_distance_btw_ridge_pit_j: geodesic distance between the ridge and the second pit
152+
- geodesic_distance_btw_pits: geodesic distance between the two pits connected by the ridge (sum of previous values)
153+
"""
154+
if save and not outdir:
155+
outdir = ''
156+
157+
# Create and fill a new edge dictionary with the geodesic distances
158+
geodistances = {}
159+
for i, j in graph.edges:
160+
ridge = graph.edges[(i, j)]['ridge_index']
161+
pit_i = graph.nodes[i]['pit_index']
162+
pit_j = graph.nodes[j]['pit_index']
163+
# Compute the geodesic distances between ridge and both pits
164+
gd_from_ridge = geodesics.compute_gdist(mesh, ridge)
165+
geodistances[(i, j)] = {}
166+
geodistances[(i, j)]['geodesic_distance_btw_ridge_pit_i'] = float(gd_from_ridge[pit_i])
167+
geodistances[(i, j)]['geodesic_distance_btw_ridge_pit_j'] = float(gd_from_ridge[pit_j])
168+
geodistances[(i, j)]['geodesic_distance_btw_pits'] = float(gd_from_ridge[pit_i]) + float(gd_from_ridge[pit_j])
169+
170+
# Add the attribute to the graph
171+
nx.set_edge_attributes(graph, geodistances)
172+
173+
if save:
174+
save_graph(graph, outdir)
175+
176+
return graph
177+
178+
179+
def add_mean_value_to_graph(graph, texture, name, save=True, outdir=None):
180+
"""
181+
Add the mean value of the texture over the vertices of each basin to the graph node attributes
182+
"""
183+
if save and not outdir:
184+
outdir = ''
185+
186+
average_values = {}
187+
for basin in graph.nodes:
188+
# Get the list of vertices
189+
vertices = graph.nodes[basin]['basin_vertices']
190+
# Compute the average value of texture over the vertices
191+
mean_value = np.mean(texture[vertices])
192+
average_values[basin] = mean_value
193+
194+
# Add the attribute to the graph
195+
nx.set_node_attributes(graph, average_values, name=name)
196+
197+
if save:
198+
save_graph(graph, outdir)
199+
200+
return graph
201+
202+
203+
def get_textures_from_graph(graph, mesh, save=True, outdir=None):
204+
"""
205+
Function that returns the textures from a graph of sulcal pits
206+
"""
207+
if save and not outdir:
208+
outdir = ''
209+
210+
vert = np.array(mesh.vertices)
211+
212+
# texture of labels
213+
labels = np.full(len(vert), -1, dtype=np.int64)
214+
for b in graph.nodes:
215+
labels[graph.nodes[b]['basin_vertices']] = graph.nodes[b]['basin_label']
216+
tex_labels = texture.TextureND(darray=labels.flatten())
217+
if save:
218+
io.write_texture(tex_labels, os.path.join(outdir, "labels.gii"))
219+
220+
# texture of pits
221+
atex_pits = np.zeros((len(vert), 1))
222+
pits_indices = list(nx.get_node_attributes(graph, 'pit_index').values())
223+
atex_pits[pits_indices] = 1
224+
tex_pits = texture.TextureND(darray=atex_pits.flatten())
225+
if save:
226+
io.write_texture(tex_pits, os.path.join(outdir, "pits_tex_from_graph.gii"))
227+
228+
# texture of ridges
229+
atex_ridges = np.zeros((len(vert), 1))
230+
ridges_indices = list(nx.get_edge_attributes(graph, 'ridge_index').values())
231+
atex_ridges[ridges_indices] = 1
232+
tex_ridges = texture.TextureND(darray=atex_ridges.flatten())
233+
if save:
234+
io.write_texture(tex_ridges, os.path.join(outdir, "rigdes_tex_from_graph.gii"))
235+
236+
return tex_labels, tex_pits, tex_ridges

0 commit comments

Comments
 (0)