morph-spines-visualizer 0.2.4__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- morph_spines_visualizer/__init__.py +33 -0
- morph_spines_visualizer/core/__init__.py +8 -0
- morph_spines_visualizer/core/data_loading.py +146 -0
- morph_spines_visualizer/core/geometry.py +99 -0
- morph_spines_visualizer/core/k3d_core.py +171 -0
- morph_spines_visualizer/core/k3d_visualization.py +392 -0
- morph_spines_visualizer/core/spines.py +172 -0
- morph_spines_visualizer/utils/mesh_loading.py +64 -0
- morph_spines_visualizer/utils/supress.py +20 -0
- morph_spines_visualizer-0.2.4.dist-info/METADATA +39 -0
- morph_spines_visualizer-0.2.4.dist-info/RECORD +14 -0
- morph_spines_visualizer-0.2.4.dist-info/WHEEL +5 -0
- morph_spines_visualizer-0.2.4.dist-info/licenses/LICENSE +201 -0
- morph_spines_visualizer-0.2.4.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import k3d
|
|
2
|
+
import numpy as np
|
|
3
|
+
from morph_spines_visualizer.core import k3d_core, data_loading, geometry
|
|
4
|
+
from morph_spines_visualizer.core import spines as spines_lib
|
|
5
|
+
import ipywidgets as widgets
|
|
6
|
+
from IPython.display import display
|
|
7
|
+
|
|
8
|
+
def create_spiny_sections_dropdown_options(section_ids_with_spine_counts):
|
|
9
|
+
"""
|
|
10
|
+
Create dropdown options for neuron sections that have associated spine counts.
|
|
11
|
+
|
|
12
|
+
Parameters
|
|
13
|
+
----------
|
|
14
|
+
section_ids_with_spine_counts : dict or list
|
|
15
|
+
A mapping or list-like object containing section IDs and their corresponding
|
|
16
|
+
spine counts. If it's a dictionary, keys should be section IDs and values
|
|
17
|
+
the spine counts. If it's a list of tuples, each tuple should be (section_id, spine_count).
|
|
18
|
+
|
|
19
|
+
Returns
|
|
20
|
+
-------
|
|
21
|
+
list of tuple
|
|
22
|
+
A list of (label, value) pairs suitable for use in a dropdown menu.
|
|
23
|
+
The first entry is a placeholder option: ("-- Select Section --", None).
|
|
24
|
+
Each subsequent entry has the format:
|
|
25
|
+
("Section <ID> (<count> spines)", <ID>)
|
|
26
|
+
|
|
27
|
+
Examples
|
|
28
|
+
--------
|
|
29
|
+
>>> data = [(1, 12), (2, 8), (3, 15)]
|
|
30
|
+
>>> create_spiny_sections_dropdown_options(data)
|
|
31
|
+
[
|
|
32
|
+
("-- Select Section --", None),
|
|
33
|
+
("Section 1 (12 spines)", 1),
|
|
34
|
+
("Section 2 (8 spines)", 2),
|
|
35
|
+
("Section 3 (15 spines)", 3)
|
|
36
|
+
]
|
|
37
|
+
"""
|
|
38
|
+
# Handle both dicts and list-of-tuples inputs
|
|
39
|
+
if isinstance(section_ids_with_spine_counts, dict):
|
|
40
|
+
items = section_ids_with_spine_counts.items()
|
|
41
|
+
else:
|
|
42
|
+
items = section_ids_with_spine_counts
|
|
43
|
+
|
|
44
|
+
dropdown_options = [
|
|
45
|
+
(f"Section {section_id} ({spine_count} spines)", section_id)
|
|
46
|
+
for section_id, spine_count in items
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
# Insert default placeholder option
|
|
50
|
+
dropdown_options.insert(0, ("-- Select Section --", None))
|
|
51
|
+
|
|
52
|
+
return dropdown_options
|
|
53
|
+
|
|
54
|
+
def create_spiny_sections_dropdown_menu(section_ids_with_spine_counts, width=300):
|
|
55
|
+
"""
|
|
56
|
+
Create an interactive dropdown menu widget for selecting neuron sections
|
|
57
|
+
with associated spine counts.
|
|
58
|
+
|
|
59
|
+
Parameters
|
|
60
|
+
----------
|
|
61
|
+
section_ids_with_spine_counts : dict or list
|
|
62
|
+
A mapping or list-like object containing section IDs and their corresponding
|
|
63
|
+
spine counts. This data is passed to
|
|
64
|
+
`create_spiny_sections_dropdown_options` to build the menu options.
|
|
65
|
+
|
|
66
|
+
width : int, optional
|
|
67
|
+
The width of the dropdown menu in pixels (default is 300).
|
|
68
|
+
|
|
69
|
+
Returns
|
|
70
|
+
-------
|
|
71
|
+
ipywidgets.Dropdown
|
|
72
|
+
A configured dropdown widget where each option label shows the section ID
|
|
73
|
+
and its number of spines (e.g., "Section 5 (12 spines)").
|
|
74
|
+
The first entry is a placeholder: "-- Select Section --".
|
|
75
|
+
|
|
76
|
+
Examples
|
|
77
|
+
--------
|
|
78
|
+
>>> data = [(1, 12), (2, 8), (3, 15)]
|
|
79
|
+
>>> dropdown = create_spiny_sections_dropdown_menu(data, width=250)
|
|
80
|
+
>>> display(dropdown)
|
|
81
|
+
"""
|
|
82
|
+
# Create dropdown options from the provided section and spine data
|
|
83
|
+
options = create_spiny_sections_dropdown_options(section_ids_with_spine_counts)
|
|
84
|
+
|
|
85
|
+
# Configure the dropdown widget
|
|
86
|
+
section_dropdown = widgets.Dropdown(
|
|
87
|
+
options=options,
|
|
88
|
+
value=None,
|
|
89
|
+
description="Select Section:",
|
|
90
|
+
style={"description_width": "initial"},
|
|
91
|
+
layout=widgets.Layout(width=f"{width}px")
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return section_dropdown
|
|
95
|
+
|
|
96
|
+
def visualize_morphology_with_point_cloud(
|
|
97
|
+
mesh_path: str,
|
|
98
|
+
morphology_path: str,
|
|
99
|
+
grid_visible: bool = False,
|
|
100
|
+
axes_helper: bool = False,
|
|
101
|
+
background_color: int = 0xffffff
|
|
102
|
+
):
|
|
103
|
+
"""
|
|
104
|
+
Visualize a neuron morphology together with its mesh as a point cloud using K3D.
|
|
105
|
+
|
|
106
|
+
This function loads a spiny neuron morphology and its corresponding mesh, creates
|
|
107
|
+
a K3D plot, adds the morphology and mesh points, and provides interactive controls:
|
|
108
|
+
- Section selection dropdown (highlights section and its spines)
|
|
109
|
+
- Camera reset and predefined orthogonal views (XY, XZ, YZ)
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
mesh_path (str): Path to the mesh file.
|
|
113
|
+
morphology_path (str): Path to the morphology file.
|
|
114
|
+
grid_visible (bool, optional): Show grid in K3D plot. Defaults to False.
|
|
115
|
+
axes_helper (bool, optional): Show axes helper. Defaults to False.
|
|
116
|
+
background_color (int, optional): Background color (hex). Defaults to 0xffffff.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
# Load data
|
|
120
|
+
morphology = data_loading.load_spiny_morphology(morphology_path)
|
|
121
|
+
mesh_vertices = data_loading.load_mesh_vertices_pyvista(mesh_path, scale_factor=1e-3)
|
|
122
|
+
|
|
123
|
+
# Create K3D plot
|
|
124
|
+
plot = k3d.plot(
|
|
125
|
+
grid_visible=grid_visible,
|
|
126
|
+
axes_helper=axes_helper,
|
|
127
|
+
background_color=background_color
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
# Add mesh point cloud
|
|
131
|
+
k3d_core.add_mesh_point_cloud_to_plot(
|
|
132
|
+
mesh_points=mesh_vertices,
|
|
133
|
+
plot=plot,
|
|
134
|
+
point_size=0.15,
|
|
135
|
+
color=0x00ffcc
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Add morphology lines
|
|
139
|
+
k3d_core.add_morphology_to_plot(
|
|
140
|
+
morphology=morphology,
|
|
141
|
+
plot=plot,
|
|
142
|
+
line_color=0xff6666
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# State variables for interactivity
|
|
146
|
+
highlighted_line = [None] # Current highlighted section line
|
|
147
|
+
highlighted_points = [None] # Optional points highlight
|
|
148
|
+
current_center = [None] # Center of currently selected section
|
|
149
|
+
current_radius = [None] # Radius of currently selected section
|
|
150
|
+
spine_meshes = [] # List of displayed spine meshes
|
|
151
|
+
|
|
152
|
+
# Camera utilities
|
|
153
|
+
def set_camera(position, target, up):
|
|
154
|
+
"""Set the camera position and orientation."""
|
|
155
|
+
plot.camera_auto_fit = False
|
|
156
|
+
plot.camera = list(position) + list(target) + list(up)
|
|
157
|
+
|
|
158
|
+
def reset_camera(_=None):
|
|
159
|
+
"""Reset the camera to default full neuron view."""
|
|
160
|
+
plot.camera_reset()
|
|
161
|
+
plot.camera_auto_fit = True
|
|
162
|
+
|
|
163
|
+
def view_xy(_=None):
|
|
164
|
+
"""Top-down view (+Z)."""
|
|
165
|
+
if current_center[0] is None:
|
|
166
|
+
return
|
|
167
|
+
pos = current_center[0] + np.array([0, 0, current_radius[0]])
|
|
168
|
+
up = np.array([0, 1, 0])
|
|
169
|
+
set_camera(pos, current_center[0], up)
|
|
170
|
+
|
|
171
|
+
def view_xz(_=None):
|
|
172
|
+
"""Front view (-Y)."""
|
|
173
|
+
if current_center[0] is None:
|
|
174
|
+
return
|
|
175
|
+
pos = current_center[0] + np.array([0, -current_radius[0], 0])
|
|
176
|
+
up = np.array([0, 0, 1])
|
|
177
|
+
set_camera(pos, current_center[0], up)
|
|
178
|
+
|
|
179
|
+
def view_yz(_=None):
|
|
180
|
+
"""Side view (+X)."""
|
|
181
|
+
if current_center[0] is None:
|
|
182
|
+
return
|
|
183
|
+
pos = current_center[0] + np.array([current_radius[0], 0, 0])
|
|
184
|
+
up = np.array([0, 0, 1])
|
|
185
|
+
set_camera(pos, current_center[0], up)
|
|
186
|
+
|
|
187
|
+
# Compute initial neuron bounds
|
|
188
|
+
_, _, center, extent, radius = geometry.compute_morphology_bounds(morphology)
|
|
189
|
+
current_center[0] = center
|
|
190
|
+
current_radius[0] = radius
|
|
191
|
+
|
|
192
|
+
# Initial camera setup
|
|
193
|
+
distance_factor = 1.0
|
|
194
|
+
camera_position = center + np.array([0, 0, distance_factor * radius], dtype=np.float32)
|
|
195
|
+
plot.camera_auto_fit = False
|
|
196
|
+
plot.camera = camera_position.tolist() + center.tolist() + [0, 1, 0]
|
|
197
|
+
|
|
198
|
+
# Section dropdown menu
|
|
199
|
+
spiny_sections_dropdown_menu = create_spiny_sections_dropdown_menu(
|
|
200
|
+
section_ids_with_spine_counts=spines_lib.get_section_ids_with_spine_counts_for_sections_with_spines(
|
|
201
|
+
morphology=morphology
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
# Map section ID to points
|
|
206
|
+
sections_points = geometry.get_sections_points(morphology)
|
|
207
|
+
|
|
208
|
+
# Section selection callback
|
|
209
|
+
def focus_on_selected_section(change):
|
|
210
|
+
"""
|
|
211
|
+
Highlight the selected section and its spines, and adjust the camera.
|
|
212
|
+
|
|
213
|
+
Handles:
|
|
214
|
+
- Clearing previous highlights
|
|
215
|
+
- Resetting camera for empty selection
|
|
216
|
+
- Highlighting section and drawing spines for selected section
|
|
217
|
+
"""
|
|
218
|
+
nonlocal plot, spine_meshes
|
|
219
|
+
|
|
220
|
+
if change.get("name") != "value":
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
sec_id = change.get("new")
|
|
224
|
+
|
|
225
|
+
# Clear previous highlights
|
|
226
|
+
for obj_list in [highlighted_line, highlighted_points]:
|
|
227
|
+
if obj_list[0] is not None:
|
|
228
|
+
try:
|
|
229
|
+
plot -= obj_list[0]
|
|
230
|
+
except Exception:
|
|
231
|
+
pass
|
|
232
|
+
obj_list[0] = None
|
|
233
|
+
|
|
234
|
+
# Clear previous spine meshes
|
|
235
|
+
for spine_obj in spine_meshes:
|
|
236
|
+
try:
|
|
237
|
+
plot -= spine_obj
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
spine_meshes.clear()
|
|
241
|
+
|
|
242
|
+
# Empty selection → reset camera
|
|
243
|
+
if sec_id not in sections_points:
|
|
244
|
+
current_center[0] = center
|
|
245
|
+
current_radius[0] = radius
|
|
246
|
+
|
|
247
|
+
camera_pos = center + np.array([0, 0, 1.0 * radius], dtype=np.float32)
|
|
248
|
+
plot.camera_auto_fit = False
|
|
249
|
+
plot.camera = camera_pos.tolist() + center.tolist() + [0, 1, 0]
|
|
250
|
+
return
|
|
251
|
+
|
|
252
|
+
# Section selected → highlight
|
|
253
|
+
pts = sections_points[sec_id]
|
|
254
|
+
|
|
255
|
+
# Highlight the section line
|
|
256
|
+
highlight = k3d.line(pts, width=0.15, color=0xFF0000, shader='flat')
|
|
257
|
+
plot += highlight
|
|
258
|
+
highlighted_line[0] = highlight
|
|
259
|
+
|
|
260
|
+
# Compute section bounds and update state
|
|
261
|
+
min_pt, max_pt = pts.min(axis=0), pts.max(axis=0)
|
|
262
|
+
sec_center = (min_pt + max_pt) / 2.0
|
|
263
|
+
sec_radius = np.linalg.norm(max_pt - min_pt) / 2.0
|
|
264
|
+
current_center[0], current_radius[0] = sec_center, sec_radius
|
|
265
|
+
|
|
266
|
+
# Adjust camera
|
|
267
|
+
camera_pos = sec_center + np.array([0, 0, 2.0 * sec_radius], dtype=np.float32)
|
|
268
|
+
plot.camera_auto_fit = False
|
|
269
|
+
plot.camera = camera_pos.tolist() + sec_center.tolist() + [0, 1, 0]
|
|
270
|
+
|
|
271
|
+
# Draw spine meshes
|
|
272
|
+
spine_colors = spines_lib.get_spine_colors()
|
|
273
|
+
spine_list = morphology.spines.spine_meshes_for_section(sec_id + 1) # MICrONS IDs start at 1
|
|
274
|
+
|
|
275
|
+
for i, spine_mesh in enumerate(spine_list):
|
|
276
|
+
if spine_mesh.is_empty or len(spine_mesh.vertices) == 0:
|
|
277
|
+
continue
|
|
278
|
+
|
|
279
|
+
color = spine_colors[i % len(spine_colors)]
|
|
280
|
+
vertices = np.asarray(spine_mesh.vertices, dtype=np.float32)
|
|
281
|
+
|
|
282
|
+
if len(spine_mesh.faces) == 0:
|
|
283
|
+
# Display as point cloud if faces are missing
|
|
284
|
+
points = k3d.points(vertices, point_size=0.05, color=color)
|
|
285
|
+
plot += points
|
|
286
|
+
spine_meshes.append(points)
|
|
287
|
+
else:
|
|
288
|
+
faces = np.asarray(spine_mesh.faces, dtype=np.uint32)
|
|
289
|
+
mesh = k3d.mesh(vertices, faces, color=color, flat_shading=True)
|
|
290
|
+
plot += mesh
|
|
291
|
+
spine_meshes.append(mesh)
|
|
292
|
+
|
|
293
|
+
# Connect dropdown menu to callback
|
|
294
|
+
spiny_sections_dropdown_menu.observe(focus_on_selected_section, names='value')
|
|
295
|
+
|
|
296
|
+
# Camera control buttons
|
|
297
|
+
reset_btn = widgets.Button(description="🔄 Reset", button_style='primary')
|
|
298
|
+
xy_btn = widgets.Button(description="XY View")
|
|
299
|
+
xz_btn = widgets.Button(description="XZ View")
|
|
300
|
+
yz_btn = widgets.Button(description="YZ View")
|
|
301
|
+
|
|
302
|
+
# Bind actions
|
|
303
|
+
reset_btn.on_click(reset_camera)
|
|
304
|
+
xy_btn.on_click(view_xy)
|
|
305
|
+
xz_btn.on_click(view_xz)
|
|
306
|
+
yz_btn.on_click(view_yz)
|
|
307
|
+
|
|
308
|
+
# Layout controls
|
|
309
|
+
controls = widgets.HBox([reset_btn, xy_btn, xz_btn, yz_btn, spiny_sections_dropdown_menu])
|
|
310
|
+
display(widgets.VBox([plot, controls]))
|
|
311
|
+
|
|
312
|
+
def visualization_morphology_with_synapses(
|
|
313
|
+
mesh_path: str,
|
|
314
|
+
morphology_path: str,
|
|
315
|
+
grid_visible: bool = False,
|
|
316
|
+
axes_helper: bool = False,
|
|
317
|
+
background_color: int = 0xffffff):
|
|
318
|
+
"""
|
|
319
|
+
Visualize a spiny neuron morphology together with its mesh and synaptic locations using K3D.
|
|
320
|
+
|
|
321
|
+
This function:
|
|
322
|
+
1. Loads a neuron morphology and its corresponding mesh.
|
|
323
|
+
2. Creates a K3D plot with optional grid and axes helper.
|
|
324
|
+
3. Adds the mesh as a point cloud.
|
|
325
|
+
4. Adds the morphology lines.
|
|
326
|
+
5. Adds synaptic locations as a colored point cloud.
|
|
327
|
+
|
|
328
|
+
Args:
|
|
329
|
+
mesh_path (str): Path to the mesh file (e.g., .obj, .vtu).
|
|
330
|
+
morphology_path (str): Path to the neuron morphology file.
|
|
331
|
+
grid_visible (bool, optional): If True, display a grid in the plot. Defaults to False.
|
|
332
|
+
axes_helper (bool, optional): If True, show axes helper in the plot. Defaults to False.
|
|
333
|
+
background_color (int, optional): Background color of the plot in hexadecimal. Defaults to 0xffffff.
|
|
334
|
+
|
|
335
|
+
Workflow:
|
|
336
|
+
- Loads morphology and mesh data using `data_loading`.
|
|
337
|
+
- Converts mesh vertices to point cloud format for K3D visualization.
|
|
338
|
+
- Adds the neuron morphology as line structures.
|
|
339
|
+
- Retrieves synaptic locations from the morphology and visualizes them as points.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
# Load data
|
|
343
|
+
morphology = data_loading.load_spiny_morphology(morphology_path)
|
|
344
|
+
mesh_vertices = data_loading.load_mesh_vertices_pyvista(mesh_path, scale_factor=1e-3)
|
|
345
|
+
|
|
346
|
+
# Create K3D plot
|
|
347
|
+
plot = k3d.plot(
|
|
348
|
+
grid_visible=grid_visible,
|
|
349
|
+
axes_helper=axes_helper,
|
|
350
|
+
background_color=background_color
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
# Add mesh as point cloud
|
|
354
|
+
k3d_core.add_mesh_point_cloud_to_plot(
|
|
355
|
+
mesh_points=mesh_vertices,
|
|
356
|
+
plot=plot,
|
|
357
|
+
point_size=0.15,
|
|
358
|
+
color=0x00ffcc
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Add morphology lines
|
|
362
|
+
k3d_core.add_morphology_to_plot(
|
|
363
|
+
morphology=morphology,
|
|
364
|
+
plot=plot,
|
|
365
|
+
line_color=0xff6666
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
# # Add synaptic locations
|
|
369
|
+
synaptic_locations = spines_lib.get_synaptic_locations(morphology=morphology)
|
|
370
|
+
k3d_core.add_point_cloud_to_plot(
|
|
371
|
+
points=np.array(synaptic_locations, dtype=np.float32),
|
|
372
|
+
plot=plot,
|
|
373
|
+
point_size=1.0,
|
|
374
|
+
opacity=1.0,
|
|
375
|
+
color=0xFF8000
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
# Example button
|
|
379
|
+
reset_btn = widgets.Button(description="Reset Camera")
|
|
380
|
+
|
|
381
|
+
def reset_camera(btn):
|
|
382
|
+
plot.camera_reset()
|
|
383
|
+
plot.camera_auto_fit = True
|
|
384
|
+
|
|
385
|
+
reset_btn.on_click(reset_camera)
|
|
386
|
+
|
|
387
|
+
# Combine plot and button in a VBox
|
|
388
|
+
container = widgets.VBox([plot, reset_btn])
|
|
389
|
+
|
|
390
|
+
# Display everything
|
|
391
|
+
display(container)
|
|
392
|
+
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
from typing import List, Tuple
|
|
2
|
+
|
|
3
|
+
def get_spine_ids_by_section_id(
|
|
4
|
+
morphology,
|
|
5
|
+
section_id: int) -> List[int]:
|
|
6
|
+
"""
|
|
7
|
+
Get the IDs of spines that belong to a specific section of a neuronal morphology.
|
|
8
|
+
|
|
9
|
+
This function queries the morphology object (assumed to follow NeuroM-style indexing)
|
|
10
|
+
and returns all spine IDs associated with the given section.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
morphology: A morphology object that contains spines. Expected to have a method
|
|
14
|
+
`spine_indices_for_section(section_index: int) -> List[int]`.
|
|
15
|
+
section_id (int): The ID of the section to query. Uses zero-based indexing.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
List[int]: List of spine IDs belonging to the specified section.
|
|
19
|
+
"""
|
|
20
|
+
# NeuroM indexing might be 1-based internally, so adjust if necessary
|
|
21
|
+
return morphology.spines.spine_indices_for_section(section_id + 1)
|
|
22
|
+
|
|
23
|
+
def get_section_ids_for_sections_with_spines(
|
|
24
|
+
morphology) -> List[int]:
|
|
25
|
+
"""
|
|
26
|
+
Get the IDs of sections in a morphology that have at least one spine.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
morphology: A morphology object containing sections and spines.
|
|
30
|
+
Each section should have an `.id` attribute, and the
|
|
31
|
+
morphology object should be compatible with
|
|
32
|
+
`get_spine_ids_by_section_id`.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
List[int]: List of section IDs that contain one or more spines.
|
|
36
|
+
"""
|
|
37
|
+
ids_of_sections_with_spines = []
|
|
38
|
+
|
|
39
|
+
for section in morphology.morphology.sections:
|
|
40
|
+
spine_ids = get_spine_ids_by_section_id(morphology, section.id)
|
|
41
|
+
if len(spine_ids) > 0:
|
|
42
|
+
ids_of_sections_with_spines.append(section.id)
|
|
43
|
+
|
|
44
|
+
return ids_of_sections_with_spines
|
|
45
|
+
|
|
46
|
+
def get_section_ids_with_spine_counts_for_sections_with_spines(
|
|
47
|
+
morphology) -> List[Tuple[int, int]]:
|
|
48
|
+
"""
|
|
49
|
+
Get the IDs of sections that have spines along with the number of spines in each section.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
morphology: A morphology object containing sections and spines.
|
|
53
|
+
Each section should have an `.id` attribute, and the
|
|
54
|
+
morphology object should be compatible with
|
|
55
|
+
`get_spine_ids_by_section_id`.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
List[Tuple[int, int]]: List of tuples `(section_id, spine_count)` for each section
|
|
59
|
+
that contains one or more spines.
|
|
60
|
+
"""
|
|
61
|
+
section_ids_and_spine_counts = []
|
|
62
|
+
|
|
63
|
+
for section in morphology.morphology.sections:
|
|
64
|
+
spine_ids = get_spine_ids_by_section_id(morphology, section.id)
|
|
65
|
+
if len(spine_ids) > 0:
|
|
66
|
+
section_ids_and_spine_counts.append((section.id, len(spine_ids)))
|
|
67
|
+
|
|
68
|
+
return section_ids_and_spine_counts
|
|
69
|
+
|
|
70
|
+
def get_spine_counts_for_sections_with_spines(
|
|
71
|
+
morphology) -> List[int]:
|
|
72
|
+
"""
|
|
73
|
+
Get the number of spines for each section that contains at least one spine.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
morphology: A morphology object containing sections and spines.
|
|
77
|
+
Each section should have an `.id` attribute, and the
|
|
78
|
+
morphology object should be compatible with
|
|
79
|
+
`get_spine_ids_by_section_id`.
|
|
80
|
+
Returns:
|
|
81
|
+
List[int]: List of spine counts for each section that contains one or more spines.
|
|
82
|
+
"""
|
|
83
|
+
spine_counts = []
|
|
84
|
+
for section in morphology.morphology.sections:
|
|
85
|
+
spine_ids = get_spine_ids_by_section_id(morphology, section.id)
|
|
86
|
+
if len(spine_ids) > 0:
|
|
87
|
+
spine_counts.append(len(spine_ids))
|
|
88
|
+
return spine_counts
|
|
89
|
+
|
|
90
|
+
def get_spine_meshes_for_section(
|
|
91
|
+
morphology,
|
|
92
|
+
section_id: int) -> List:
|
|
93
|
+
"""
|
|
94
|
+
Retrieve the spine mesh objects associated with a specific section in the morphology.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
morphology: A morphology object containing spines.
|
|
98
|
+
Expected to have a `.spines` attribute with a method
|
|
99
|
+
`spine_indices_for_section(section_index: int) -> List[int]`
|
|
100
|
+
and a `.spine_meshes` attribute that is indexable.
|
|
101
|
+
section_id (int): The ID of the section to retrieve spine meshes for.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
List: List of spine mesh objects associated with the specified section.
|
|
105
|
+
"""
|
|
106
|
+
spine_ids = get_spine_ids_by_section_id(morphology, section_id)
|
|
107
|
+
spine_meshes = [morphology.spines.spine_meshes[spine_id] for spine_id in spine_ids]
|
|
108
|
+
return spine_meshes
|
|
109
|
+
|
|
110
|
+
def get_spine_colors():
|
|
111
|
+
"""
|
|
112
|
+
Returns 25 colors for the spines that we can basically switch between.
|
|
113
|
+
"""
|
|
114
|
+
spine_colors = [
|
|
115
|
+
0xFF0000, # Red
|
|
116
|
+
0x00FF00, # Lime
|
|
117
|
+
0x0000FF, # Blue
|
|
118
|
+
0xFFFF00, # Yellow
|
|
119
|
+
0xFF00FF, # Magenta
|
|
120
|
+
0x00FFFF, # Cyan
|
|
121
|
+
0xFF8000, # Orange
|
|
122
|
+
0x8000FF, # Violet
|
|
123
|
+
0x00FF80, # Spring Green
|
|
124
|
+
0x0080FF, # Sky Blue
|
|
125
|
+
0xFF0080, # Hot Pink
|
|
126
|
+
0x80FF00, # Chartreuse
|
|
127
|
+
0x00FFFF, # Aqua
|
|
128
|
+
0xFFBF00, # Amber
|
|
129
|
+
0xBF00FF, # Purple
|
|
130
|
+
0x00FFBF, # Mint
|
|
131
|
+
0x00BFFF, # Deep Sky Blue
|
|
132
|
+
0xFF00BF, # Fuchsia
|
|
133
|
+
0xBFFF00, # Lime-Yellow
|
|
134
|
+
0xFF4000, # Vermilion
|
|
135
|
+
0x40FF00, # Bright Green
|
|
136
|
+
0x0040FF, # Royal Blue
|
|
137
|
+
0xFF0040, # Crimson
|
|
138
|
+
0x00FF40, # Neon Green
|
|
139
|
+
0x4000FF # Indigo
|
|
140
|
+
]
|
|
141
|
+
return spine_colors
|
|
142
|
+
|
|
143
|
+
def get_synaptic_locations(morphology):
|
|
144
|
+
"""
|
|
145
|
+
Extract the 3D locations of synaptic (afferent surface) points from a neuron morphology.
|
|
146
|
+
|
|
147
|
+
Parameters
|
|
148
|
+
----------
|
|
149
|
+
morphology : object
|
|
150
|
+
Morphology object containing a `.spines.spine_table` DataFrame with
|
|
151
|
+
columns 'afferent_surface_x', 'afferent_surface_y', 'afferent_surface_z'.
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
list of tuple
|
|
156
|
+
A list of (x, y, z) coordinates for each spine synapse.
|
|
157
|
+
|
|
158
|
+
Example
|
|
159
|
+
-------
|
|
160
|
+
>>> locations = get_synaptic_locations(morph)
|
|
161
|
+
>>> locations[:5]
|
|
162
|
+
[(12.3, 45.6, 7.8), (14.5, 48.2, 9.1), ...]
|
|
163
|
+
"""
|
|
164
|
+
# Extract columns from spine table
|
|
165
|
+
x = morphology.spines.spine_table['afferent_surface_x'].to_numpy()
|
|
166
|
+
y = morphology.spines.spine_table['afferent_surface_y'].to_numpy()
|
|
167
|
+
z = morphology.spines.spine_table['afferent_surface_z'].to_numpy()
|
|
168
|
+
|
|
169
|
+
# Combine into a list of (x, y, z) tuples
|
|
170
|
+
locations = list(zip(x, y, z))
|
|
171
|
+
|
|
172
|
+
return locations
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import morph_spines_visualizer.core.data_loading as data_loading
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
def load_mesh_vertices_and_faces(
|
|
5
|
+
mesh_path: str,
|
|
6
|
+
scale_factor: float = 1.0,
|
|
7
|
+
use_pyvista: bool = True):
|
|
8
|
+
"""
|
|
9
|
+
Load a triangular mesh and return its vertices and faces.
|
|
10
|
+
|
|
11
|
+
This function uses either PyVista or trimesh to load a mesh file
|
|
12
|
+
(e.g., .obj, .ply, .stl). It optionally scales the mesh and extracts
|
|
13
|
+
the vertex coordinates and triangle indices in NumPy array format.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
mesh_path (str): Path to the mesh file to load.
|
|
17
|
+
scale_factor (float, optional): txt = hello, and welcome to my world.
|
|
18
|
+
|
|
19
|
+
x = txt.capitalize()
|
|
20
|
+
|
|
21
|
+
print(x) factor to apply to the mesh coordinates.
|
|
22
|
+
Defaults to 1.0.
|
|
23
|
+
use_pyvista (bool, optional): If True, use PyVista for loading; otherwise use trimesh.
|
|
24
|
+
Defaults to True.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
tuple[np.ndarray, np.ndarray]:
|
|
28
|
+
- vertices (np.ndarray of shape (N, 3)): Vertex coordinates (x, y, z)
|
|
29
|
+
- faces (np.ndarray of shape (M, 3)): Triangle vertex indices
|
|
30
|
+
Raises:
|
|
31
|
+
FileNotFoundError: If the mesh file does not exist at `mesh_path`.
|
|
32
|
+
"""
|
|
33
|
+
if not os.path.exists(mesh_path):
|
|
34
|
+
raise FileNotFoundError(f"Mesh file not found: {mesh_path}")
|
|
35
|
+
if use_pyvista:
|
|
36
|
+
return data_loading.load_mesh_vertices_and_faces_pyvista(mesh_path, scale_factor)
|
|
37
|
+
else:
|
|
38
|
+
return data_loading.load_mesh_vertices_and_faces_trimesh(mesh_path, scale_factor)
|
|
39
|
+
|
|
40
|
+
def load_mesh_vertices(
|
|
41
|
+
mesh_path: str,
|
|
42
|
+
scale_factor: float = 1.0,
|
|
43
|
+
use_pyvista: bool = True):
|
|
44
|
+
"""
|
|
45
|
+
Load only the vertices of a mesh.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
mesh_path (str): Path to the mesh file.
|
|
49
|
+
scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
|
|
50
|
+
Defaults to 1.0.
|
|
51
|
+
use_pyvista (bool, optional): If True, use PyVista for loading; otherwise use trimesh.
|
|
52
|
+
Defaults to True.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
np.ndarray: Vertex coordinates (Nx3) in float32.
|
|
56
|
+
Raises:
|
|
57
|
+
FileNotFoundError: If the mesh file does not exist at `mesh_path`.
|
|
58
|
+
"""
|
|
59
|
+
if not os.path.exists(mesh_path):
|
|
60
|
+
raise FileNotFoundError(f"Mesh file not found: {mesh_path}")
|
|
61
|
+
if use_pyvista:
|
|
62
|
+
return data_loading.load_mesh_vertices_pyvista(mesh_path, scale_factor)
|
|
63
|
+
else:
|
|
64
|
+
return data_loading.load_mesh_vertices_trimesh(mesh_path, scale_factor)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
class SuppressOutput:
|
|
4
|
+
def __enter__(self):
|
|
5
|
+
self.null_fd = os.open(os.devnull, os.O_WRONLY)
|
|
6
|
+
|
|
7
|
+
self.old_stdout = os.dup(1)
|
|
8
|
+
self.old_stderr = os.dup(2)
|
|
9
|
+
|
|
10
|
+
os.dup2(self.null_fd, 1)
|
|
11
|
+
os.dup2(self.null_fd, 2)
|
|
12
|
+
return self
|
|
13
|
+
|
|
14
|
+
def __exit__(self, exc_type, exc, tb):
|
|
15
|
+
os.dup2(self.old_stdout, 1)
|
|
16
|
+
os.dup2(self.old_stderr, 2)
|
|
17
|
+
|
|
18
|
+
os.close(self.null_fd)
|
|
19
|
+
os.close(self.old_stdout)
|
|
20
|
+
os.close(self.old_stderr)
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morph_spines_visualizer
|
|
3
|
+
Version: 0.2.4
|
|
4
|
+
Summary: Package to load and visualize morphologies with spines
|
|
5
|
+
Author-email: Open Brain Institute <info@openbraininstitute.org>
|
|
6
|
+
Maintainer-email: Open Brain Institute <info@openbraininstitute.org>
|
|
7
|
+
License: my-license
|
|
8
|
+
Project-URL: documentation, https://morph-spines-visualizer.readthedocs.io/en/stable
|
|
9
|
+
Project-URL: repository, https://github.com/openbraininstitute/morph-spines-visualizer
|
|
10
|
+
Project-URL: changelog, https://github.com/openbraininstitute/morph-spines-visualizer/CHANGELOG.rst
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Requires-Dist: morph_spines
|
|
18
|
+
Requires-Dist: pyvista
|
|
19
|
+
Requires-Dist: k3d
|
|
20
|
+
Requires-Dist: ipython
|
|
21
|
+
Requires-Dist: ipywidgets
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
## Introduction
|
|
25
|
+
|
|
26
|
+
morph-spine-vislizaer is a mini-package to visualize morphologies with spines.
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
The package can be installed through `pip`.
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
## Examples
|
|
35
|
+
|
|
36
|
+
See `examples` folder for usage examples.
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Copyright (c) 2025 Open Brain Institute
|