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.
@@ -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