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,33 @@
1
+ """morph_spines."""
2
+
3
+ from importlib.metadata import version
4
+
5
+ __version__ = version(__package__)
6
+
7
+ from morph_spines_visualizer.core.k3d_core import (
8
+ k3d_version,
9
+ add_mesh_to_plot,
10
+ add_mesh_point_cloud_to_plot,
11
+ add_morphology_to_plot
12
+ )
13
+
14
+ from morph_spines_visualizer.core.data_loading import (
15
+ load_spiny_morphology
16
+ )
17
+
18
+ from morph_spines_visualizer.core.spines import (
19
+ get_spine_ids_by_section_id,
20
+ get_section_ids_for_sections_with_spines,
21
+ get_section_ids_with_spine_counts_for_sections_with_spines,
22
+ get_spine_counts_for_sections_with_spines
23
+ )
24
+
25
+ from morph_spines_visualizer.core.k3d_visualization import (
26
+ visualize_morphology_with_point_cloud,
27
+ visualization_morphology_with_synapses
28
+ )
29
+
30
+ from morph_spines_visualizer.utils.mesh_loading import (
31
+ load_mesh_vertices_and_faces,
32
+ load_mesh_vertices
33
+ )
@@ -0,0 +1,8 @@
1
+ from .k3d_core import (
2
+ add_mesh_point_cloud_to_plot,
3
+ add_morphology_to_plot)
4
+
5
+ from .data_loading import (
6
+ load_spiny_morphology,
7
+ load_mesh_vertices_pyvista
8
+ )
@@ -0,0 +1,146 @@
1
+ import pyvista as pv
2
+ import numpy as np
3
+ import trimesh
4
+ import os
5
+ import morph_spines
6
+ from morph_spines_visualizer.utils import supress
7
+
8
+ def load_mesh_vertices_and_faces_trimesh(mesh_path: str, scale_factor: float = 1.0):
9
+ """
10
+ Load a triangular mesh using trimesh and return its vertices and faces.
11
+
12
+ This function reads a mesh file (e.g., .obj, .ply, .stl) using trimesh,
13
+ optionally scales it, and extracts the vertex coordinates and triangle indices
14
+ in NumPy array format.
15
+
16
+ Args:
17
+ mesh_path (str): Path to the mesh file to load.
18
+ scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
19
+ Defaults to 1.0.
20
+
21
+ Returns:
22
+ tuple[np.ndarray, np.ndarray]:
23
+ - vertices (np.ndarray of shape (N, 3)): Vertex coordinates (x, y, z)
24
+ - faces (np.ndarray of shape (M, 3)): Triangle vertex indices
25
+ """
26
+ # Load the mesh using trimesh
27
+ mesh = trimesh.load(mesh_path, process=False)
28
+
29
+ # Scale vertices if needed
30
+ vertices = mesh.vertices * scale_factor
31
+ faces = mesh.faces
32
+
33
+ return vertices.astype(np.float32), faces.astype(np.uint32)
34
+
35
+
36
+ def load_mesh_vertices_trimesh(mesh_path: str, scale_factor: float = 1.0):
37
+ """
38
+ Load only the vertices of a mesh using trimesh.
39
+
40
+ Args:
41
+ mesh_path (str): Path to the mesh file.
42
+ scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
43
+ Defaults to 1.0.
44
+
45
+ Returns:
46
+ np.ndarray: Vertex coordinates (Nx3) in float32.
47
+ """
48
+ mesh = trimesh.load(mesh_path, process=False)
49
+ vertices = mesh.vertices * scale_factor
50
+ return vertices.astype(np.float32)
51
+
52
+ def load_mesh_vertices_and_faces_pyvista(mesh_path: str, scale_factor: float = 1.0):
53
+ """
54
+ Load a triangular mesh using PyVista and return its vertices and faces.
55
+
56
+ This function reads a mesh file (e.g., .obj, .ply, .stl, .vtu, etc.) using PyVista,
57
+ optionally scales it, and extracts the vertex coordinates and triangle indices
58
+ in NumPy array format. It is optimized for performance and simplicity compared
59
+ to K3D’s data loading.
60
+
61
+ Args:
62
+ mesh_path (str): Path to the mesh file to load.
63
+ scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
64
+ Defaults to 1.0.
65
+
66
+ Returns:
67
+ tuple[np.ndarray, np.ndarray]:
68
+ - **vertices** (`np.ndarray` of shape `(N, 3)`): Array of vertex coordinates (x, y, z).
69
+ - **faces** (`np.ndarray` of shape `(M, 3)`): Array of triangle vertex indices.
70
+ """
71
+ # Read mesh using PyVista
72
+ mesh = pv.read(mesh_path)
73
+
74
+ # Scale vertices if needed
75
+ mesh.points *= scale_factor
76
+
77
+ # Extract vertices and faces
78
+ vertices = mesh.points.astype(np.float32)
79
+
80
+ # PyVista stores faces as [n, v0, v1, v2, n, v0, v1, v2, ...]
81
+ # So we reshape and drop the leading 'n' (number of vertices per face)
82
+ faces = mesh.faces.reshape(-1, 4)[:, 1:4].astype(np.uint32)
83
+
84
+ return vertices, faces
85
+
86
+ def load_mesh_vertices_pyvista(mesh_path: str, scale_factor: float = 1.0):
87
+ """
88
+ Load only the vertices of a mesh using PyVista.
89
+
90
+ This function reads a mesh file (e.g., .obj, .ply, .stl, .vtu, etc.) using PyVista,
91
+ optionally scales its coordinates, and returns the vertex positions as a NumPy array.
92
+ It is lightweight and optimized for when face data is not needed.
93
+
94
+ Args:
95
+ mesh_path (str): Path to the mesh file to load.
96
+ scale_factor (float, optional): Scale factor to apply to the mesh coordinates.
97
+ Defaults to 1.0.
98
+
99
+ Returns:
100
+ np.ndarray:
101
+ **vertices** (`np.ndarray` of shape `(N, 3)`): Array of vertex coordinates (x, y, z)
102
+ in single precision (`float32`).
103
+ """
104
+ # Read mesh using PyVista
105
+ mesh = pv.read(mesh_path)
106
+
107
+ # Scale vertices if needed
108
+ mesh.points *= scale_factor
109
+
110
+ # Extract and convert vertices
111
+ vertices = mesh.points.astype(np.float32)
112
+
113
+ return vertices
114
+
115
+ def load_spiny_morphology(morphology_path: str):
116
+ """
117
+ Load a spiny neuronal morphology from a file using the morph-spine libaray.
118
+
119
+ This function uses the morph_spines library to load a neuronal morphology
120
+ that includes spines using the morph-spine library.
121
+ The morphology file can be in formats supported by morph_spines (e.g., .swc, .asc).
122
+
123
+ Args:
124
+ morphology_path (str): Path to the morphology file.
125
+
126
+ Returns:
127
+ morphology: A morphology object containing the spines.
128
+ Raises:
129
+ FileNotFoundError: If the morphology file does not exist at `morphology_path`.
130
+ RuntimeError: If the morphology cannot be loaded by `morph_spines`.
131
+ """
132
+ if not os.path.exists(morphology_path):
133
+ raise FileNotFoundError(f"Morphology file not found: {morphology_path}")
134
+
135
+ # Load morphology using morph_spines
136
+ try:
137
+ import warnings
138
+ warnings.filterwarnings("ignore", category=DeprecationWarning)
139
+ warnings.filterwarnings("ignore", category=UserWarning)
140
+
141
+ # To suppress the stdout/stderr output from morph_spines loading
142
+ with supress.SuppressOutput():
143
+ morphology = morph_spines.load_morphology_with_spines(morphology_path)
144
+ except Exception as e:
145
+ raise RuntimeError(f"Failed to load morphology from {morphology_path}: {e}")
146
+ return morphology
@@ -0,0 +1,99 @@
1
+ import numpy as np
2
+ from typing import Tuple
3
+
4
+ def get_section_points(
5
+ morphology, section_id: int) -> np.ndarray:
6
+ """
7
+ Retrieve the 3D points of a specific section in the morphology.
8
+
9
+ Args:
10
+ morphology: A morphology object containing sections with 3D points.
11
+ Expected structure: morphology.morphology.sections, where
12
+ each section has a `.points` attribute as an array-like of (x, y, z[, r]).
13
+ section_id (int): The ID of the section to retrieve points from.
14
+
15
+ Returns:
16
+ np.ndarray: An array of shape (N, 3) containing the 3D points of the section.
17
+ """
18
+ for section in morphology.morphology.sections:
19
+ if section.id == section_id:
20
+ pts = np.asarray(section.points)[:, :3].astype(np.float32)
21
+ return pts
22
+ raise ValueError(f"Section ID {section_id} not found in morphology.")
23
+
24
+ def get_sections_points(morphology):
25
+ """
26
+ Extract 3D point coordinates for each section in a neuron morphology.
27
+
28
+ Parameters
29
+ ----------
30
+ morphology : object
31
+ A morphology object containing a `.morphology.sections` iterable.
32
+ Each section is expected to have:
33
+ - section.points : array-like of shape (N, ≥3)
34
+ - section.id : unique identifier
35
+
36
+ Returns
37
+ -------
38
+ dict
39
+ A dictionary mapping section IDs to Nx3 NumPy arrays of float32
40
+ coordinates. Sections with fewer than 2 points are skipped.
41
+
42
+ Examples
43
+ --------
44
+ >>> sections = get_sections_points(morph)
45
+ >>> sections[12].shape
46
+ (45, 3)
47
+ """
48
+ sections_points = {}
49
+
50
+ for section in morphology.morphology.sections:
51
+ pts = np.asarray(section.points, dtype=np.float32)[:, :3]
52
+
53
+ # Skip if section does not contain enough points to define a segment
54
+ if pts.shape[0] < 2:
55
+ continue
56
+
57
+ sections_points[section.id] = pts
58
+
59
+ return sections_points
60
+
61
+ def compute_morphology_bounds(
62
+ morphology) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, float]:
63
+ """
64
+ Compute the bounding box and related metrics for a neuronal morphology.
65
+
66
+ Args:
67
+ morphology: A morphology object containing sections with 3D points.
68
+ Expected structure: morphology.morphology.sections, where
69
+ each section has a `.points` attribute as an array-like of (x, y, z[, r]).
70
+
71
+ Returns:
72
+ Tuple containing:
73
+ - min_pt (np.ndarray): Minimum coordinates [x, y, z] of the bounding box.
74
+ - max_pt (np.ndarray): Maximum coordinates [x, y, z] of the bounding box.
75
+ - center (np.ndarray): Center of the bounding box.
76
+ - extent (np.ndarray): Size of the bounding box along each axis (max - min).
77
+ - radius (float): Approximate radius for visualization purposes (0.6 * diagonal length).
78
+ """
79
+ section_points = {}
80
+
81
+ # Collect 3D points from all sections with at least 2 points
82
+ for section in morphology.morphology.sections:
83
+ pts = np.asarray(section.points)[:, :3].astype(np.float32)
84
+ if pts.shape[0] < 2:
85
+ continue
86
+ section_points[section.id] = pts
87
+
88
+ if not section_points:
89
+ raise ValueError("Morphology contains no sections with valid points.")
90
+
91
+ # Concatenate all vertices to compute bounding box
92
+ all_vertices = np.concatenate(list(section_points.values()), axis=0)
93
+ min_pt = all_vertices.min(axis=0)
94
+ max_pt = all_vertices.max(axis=0)
95
+ center = (min_pt + max_pt) / 2
96
+ extent = max_pt - min_pt
97
+ radius = np.linalg.norm(extent) * 0.6
98
+
99
+ return min_pt, max_pt, center, extent, radius
@@ -0,0 +1,171 @@
1
+ import k3d
2
+ import numpy as np
3
+ from typing import Optional
4
+
5
+ def k3d_version():
6
+ """
7
+ Returns the installed version of the k3d library.
8
+
9
+ Returns:
10
+ str: The instaleld version of the k3d library.
11
+ """
12
+ return k3d.__version__
13
+
14
+ def add_mesh_to_plot(
15
+ mesh_vertices: np.ndarray,
16
+ mesh_faces: np.ndarray,
17
+ plot: k3d.plot,
18
+ color: int = 0x00ffcc
19
+ ) -> k3d.mesh:
20
+ """
21
+ Add a triangular mesh to a K3D plot.
22
+
23
+ Args:
24
+ mesh_vertices (np.ndarray): Nx3 array of vertex coordinates (x, y, z).
25
+ mesh_faces (np.ndarray): Mx3 array of triangle vertex indices.
26
+ plot_ (k3d.plot): The K3D plot object to which the mesh will be added.
27
+ color (int, optional): RGB color of the mesh in 0xRRGGBB format. Defaults to 0x00ffcc.
28
+
29
+ Returns:
30
+ k3d.mesh: The K3D mesh object added to the plot.
31
+ """
32
+ # Flatten faces if necessary (K3D expects a 1D array of indices)
33
+ faces_flat = mesh_faces.flatten()
34
+
35
+ mesh_plot = k3d.mesh(
36
+ mesh_vertices,
37
+ faces_flat,
38
+ color=color,
39
+ opacity=1.0,
40
+ wireframe=False,
41
+ flat_shading=True
42
+ )
43
+
44
+ plot += mesh_plot
45
+ return mesh_plot
46
+
47
+ def add_point_cloud_to_plot(
48
+ points: np.ndarray,
49
+ plot: k3d.plot,
50
+ point_size: float = 0.15,
51
+ opacity: float = 0.15,
52
+ color: int = 0x00ffcc,
53
+ shader: str = 'flat'
54
+ ) -> k3d.points:
55
+ """
56
+ Add a 3D point cloud to a K3D plot.
57
+
58
+ Args:
59
+ points (np.ndarray): Nx3 array of points coordinates (x, y, z).
60
+ plot (k3d.plot): The K3D plot object to which the point cloud will be added.
61
+ point_size (float, optional): Size of the points. Defaults to 0.15.
62
+ opacity (float, optional): Opacity of the points. Defaults to 0.15.
63
+ color (int, optional): RGB color of the points in 0xRRGGBB format. Defaults to 0x00ffcc.
64
+ shader (str, optional): Shader to use for rendering points ('flat', 'mesh', 'sphere', etc.).
65
+ Defaults to 'flat'.
66
+
67
+ Returns:
68
+ k3d.points: The K3D point cloud object added to the plot.
69
+ """
70
+ # Create point cloud object
71
+ point_cloud_plot = k3d.points(
72
+ points,
73
+ point_size=point_size,
74
+ color=color,
75
+ opacity=opacity
76
+ )
77
+
78
+ # Set shader
79
+ point_cloud_plot.shader = shader
80
+
81
+ # Add to the plot
82
+ plot += point_cloud_plot
83
+
84
+ return point_cloud_plot
85
+
86
+ def add_mesh_point_cloud_to_plot(
87
+ mesh_points: np.ndarray,
88
+ plot: k3d.plot,
89
+ point_size: float = 0.15,
90
+ opacity: float = 0.15,
91
+ color: int = 0x00ffcc,
92
+ shader: str = 'flat'
93
+ ) -> k3d.points:
94
+ """
95
+ Add a 3D point cloud representing mesh vertices to a K3D plot.
96
+
97
+ Args:
98
+ mesh_points (np.ndarray): Nx3 array of vertex coordinates (x, y, z).
99
+ plot (k3d.plot): The K3D plot object to which the point cloud will be added.
100
+ point_size (float, optional): Size of the points. Defaults to 0.15.
101
+ opacity (float, optional): Opacity of the points. Defaults to 0.15.
102
+ color (int, optional): RGB color of the points in 0xRRGGBB format. Defaults to 0x00ffcc.
103
+ shader (str, optional): Shader to use for rendering points ('flat', 'mesh', 'sphere', etc.).
104
+ Defaults to 'flat'.
105
+
106
+ Returns:
107
+ k3d.points: The K3D point cloud object added to the plot.
108
+ """
109
+ # Create point cloud object
110
+ point_cloud_plot = k3d.points(
111
+ mesh_points,
112
+ point_size=point_size,
113
+ color=color,
114
+ opacity=opacity
115
+ )
116
+
117
+ # Set shader
118
+ point_cloud_plot.shader = shader
119
+
120
+ # Add to the plot
121
+ plot += point_cloud_plot
122
+
123
+ return point_cloud_plot
124
+
125
+ def add_morphology_to_plot(
126
+ morphology,
127
+ plot: k3d.plot,
128
+ line_color: int = 0x0000FF
129
+ ) -> Optional[k3d.line]:
130
+ """
131
+ Add a neuronal morphology to a K3D plot as connected lines.
132
+
133
+ Each section in the morphology is drawn as a line, with NaN separators
134
+ to prevent connecting separate sections.
135
+
136
+ Args:
137
+ morphology: A morphology object containing sections with points.
138
+ Expected structure: morphology.morphology.sections,
139
+ where each section has a `.points` attribute as an array-like of (x, y, z[, r]).
140
+ plot (k3d.plot): The K3D plot object to which the morphology lines will be added.
141
+ line_color (int, optional): RGB color of the morphology lines in 0xRRGGBB format.
142
+ Defaults to 0x0000FF (blue).
143
+
144
+ Returns:
145
+ k3d.line or None: The K3D line object added to the plot, or None if no valid sections exist.
146
+ """
147
+ all_points = []
148
+
149
+ # Collect all section points, with NaN separators between sections
150
+ for section in morphology.morphology.sections:
151
+ pts = np.asarray(section.points)[:, :3].astype(np.float32)
152
+ if pts.shape[0] < 2: # skip sections with fewer than 2 points
153
+ continue
154
+
155
+ all_points.append(pts)
156
+
157
+ # NaN separator to break the line between sections
158
+ all_points.append(np.array([[np.nan, np.nan, np.nan]], dtype=np.float32))
159
+
160
+ # Remove last NaN separator and concatenate
161
+ if not all_points:
162
+ return None
163
+
164
+ all_points = all_points[:-1]
165
+ vertices = np.vstack(all_points)
166
+
167
+ # Add single line plot for all vertices
168
+ line_plot = k3d.line(vertices, width=1.0, color=line_color, shader='simple')
169
+ plot += line_plot
170
+
171
+ return line_plot