copick-utils 0.6.1__py3-none-any.whl → 1.0.1__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.
- copick_utils/__init__.py +1 -1
- copick_utils/cli/__init__.py +33 -0
- copick_utils/cli/clipmesh.py +161 -0
- copick_utils/cli/clippicks.py +154 -0
- copick_utils/cli/clipseg.py +163 -0
- copick_utils/cli/conversion_commands.py +32 -0
- copick_utils/cli/enclosed.py +191 -0
- copick_utils/cli/filter_components.py +166 -0
- copick_utils/cli/fit_spline.py +191 -0
- copick_utils/cli/hull.py +138 -0
- copick_utils/cli/input_output_selection.py +76 -0
- copick_utils/cli/logical_commands.py +29 -0
- copick_utils/cli/mesh2picks.py +170 -0
- copick_utils/cli/mesh2seg.py +167 -0
- copick_utils/cli/meshop.py +262 -0
- copick_utils/cli/picks2ellipsoid.py +171 -0
- copick_utils/cli/picks2mesh.py +181 -0
- copick_utils/cli/picks2plane.py +156 -0
- copick_utils/cli/picks2seg.py +134 -0
- copick_utils/cli/picks2sphere.py +170 -0
- copick_utils/cli/picks2surface.py +164 -0
- copick_utils/cli/picksin.py +146 -0
- copick_utils/cli/picksout.py +148 -0
- copick_utils/cli/processing_commands.py +18 -0
- copick_utils/cli/seg2mesh.py +135 -0
- copick_utils/cli/seg2picks.py +128 -0
- copick_utils/cli/segop.py +248 -0
- copick_utils/cli/separate_components.py +155 -0
- copick_utils/cli/skeletonize.py +164 -0
- copick_utils/cli/util.py +580 -0
- copick_utils/cli/validbox.py +155 -0
- copick_utils/converters/__init__.py +35 -0
- copick_utils/converters/converter_common.py +543 -0
- copick_utils/converters/ellipsoid_from_picks.py +335 -0
- copick_utils/converters/lazy_converter.py +576 -0
- copick_utils/converters/mesh_from_picks.py +209 -0
- copick_utils/converters/mesh_from_segmentation.py +119 -0
- copick_utils/converters/picks_from_mesh.py +542 -0
- copick_utils/converters/picks_from_segmentation.py +168 -0
- copick_utils/converters/plane_from_picks.py +251 -0
- copick_utils/converters/segmentation_from_mesh.py +291 -0
- copick_utils/{segmentation → converters}/segmentation_from_picks.py +123 -13
- copick_utils/converters/sphere_from_picks.py +306 -0
- copick_utils/converters/surface_from_picks.py +337 -0
- copick_utils/logical/__init__.py +43 -0
- copick_utils/logical/distance_operations.py +604 -0
- copick_utils/logical/enclosed_operations.py +222 -0
- copick_utils/logical/mesh_operations.py +443 -0
- copick_utils/logical/point_operations.py +303 -0
- copick_utils/logical/segmentation_operations.py +399 -0
- copick_utils/process/__init__.py +47 -0
- copick_utils/process/connected_components.py +360 -0
- copick_utils/process/filter_components.py +306 -0
- copick_utils/process/hull.py +106 -0
- copick_utils/process/skeletonize.py +326 -0
- copick_utils/process/spline_fitting.py +648 -0
- copick_utils/process/validbox.py +333 -0
- copick_utils/util/__init__.py +6 -0
- copick_utils/util/config_models.py +614 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/METADATA +15 -2
- copick_utils-1.0.1.dist-info/RECORD +71 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/WHEEL +1 -1
- copick_utils-1.0.1.dist-info/entry_points.txt +29 -0
- copick_utils/segmentation/picks_from_segmentation.py +0 -81
- copick_utils-0.6.1.dist-info/RECORD +0 -14
- /copick_utils/{segmentation → io}/__init__.py +0 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import trimesh as tm
|
|
5
|
+
from copick.util.log import get_logger
|
|
6
|
+
from sklearn.decomposition import PCA
|
|
7
|
+
|
|
8
|
+
from copick_utils.converters.converter_common import (
|
|
9
|
+
cluster,
|
|
10
|
+
create_batch_converter,
|
|
11
|
+
create_batch_worker,
|
|
12
|
+
store_mesh_with_stats,
|
|
13
|
+
validate_points,
|
|
14
|
+
)
|
|
15
|
+
from copick_utils.converters.lazy_converter import create_lazy_batch_converter
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from copick.models import CopickMesh, CopickRun
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fit_ellipsoid_to_points(points: np.ndarray) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
24
|
+
"""Fit an ellipsoid to a set of 3D points using PCA and least squares.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
points: Nx3 array of points.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple of (center, semi_axes, rotation_matrix).
|
|
31
|
+
"""
|
|
32
|
+
if len(points) < 6:
|
|
33
|
+
raise ValueError("Need at least 6 points to fit an ellipsoid")
|
|
34
|
+
|
|
35
|
+
# Center the points
|
|
36
|
+
center = np.mean(points, axis=0)
|
|
37
|
+
centered_points = points - center
|
|
38
|
+
|
|
39
|
+
# Use PCA to find principal axes
|
|
40
|
+
pca = PCA(n_components=3)
|
|
41
|
+
pca.fit(centered_points)
|
|
42
|
+
|
|
43
|
+
# Transform to PCA coordinates
|
|
44
|
+
transformed_points = pca.transform(centered_points)
|
|
45
|
+
|
|
46
|
+
# Estimate semi-axes lengths from the spread in each direction
|
|
47
|
+
semi_axes = np.sqrt(np.var(transformed_points, axis=0)) * 2 # 2 standard deviations
|
|
48
|
+
|
|
49
|
+
# Ensure positive and reasonable semi-axes
|
|
50
|
+
semi_axes = np.maximum(semi_axes, 0.1)
|
|
51
|
+
|
|
52
|
+
# Sort semi-axes in descending order and reorder components
|
|
53
|
+
sorted_indices = np.argsort(semi_axes)[::-1]
|
|
54
|
+
semi_axes = semi_axes[sorted_indices]
|
|
55
|
+
rotation_matrix = pca.components_[sorted_indices]
|
|
56
|
+
|
|
57
|
+
return center, semi_axes, rotation_matrix
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def deduplicate_ellipsoids(
|
|
61
|
+
ellipsoids: List[Tuple[np.ndarray, np.ndarray, np.ndarray]],
|
|
62
|
+
min_distance: float = None,
|
|
63
|
+
) -> List[Tuple[np.ndarray, np.ndarray, np.ndarray]]:
|
|
64
|
+
"""Merge ellipsoids that are too close to each other.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
ellipsoids: List of (center, semi_axes, rotation_matrix) tuples.
|
|
68
|
+
min_distance: Minimum distance between ellipsoid centers. If None, uses average of largest semi-axes.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
List of deduplicated (center, semi_axes, rotation_matrix) tuples.
|
|
72
|
+
"""
|
|
73
|
+
if len(ellipsoids) <= 1:
|
|
74
|
+
return ellipsoids
|
|
75
|
+
|
|
76
|
+
if min_distance is None:
|
|
77
|
+
# Use average of largest semi-axes as minimum distance
|
|
78
|
+
avg_major_axis = np.mean([semi_axes[0] for _, semi_axes, _ in ellipsoids])
|
|
79
|
+
min_distance = avg_major_axis * 0.5
|
|
80
|
+
|
|
81
|
+
deduplicated = []
|
|
82
|
+
used = set()
|
|
83
|
+
|
|
84
|
+
for i, (center1, semi_axes1, rotation1) in enumerate(ellipsoids):
|
|
85
|
+
if i in used:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
# Find all ellipsoids close to this one
|
|
89
|
+
close_ellipsoids = [(center1, semi_axes1, rotation1)]
|
|
90
|
+
used.add(i)
|
|
91
|
+
|
|
92
|
+
for j, (center2, semi_axes2, rotation2) in enumerate(ellipsoids):
|
|
93
|
+
if j in used or i == j:
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
distance = np.linalg.norm(center1 - center2)
|
|
97
|
+
if distance <= min_distance:
|
|
98
|
+
close_ellipsoids.append((center2, semi_axes2, rotation2))
|
|
99
|
+
used.add(j)
|
|
100
|
+
|
|
101
|
+
if len(close_ellipsoids) == 1:
|
|
102
|
+
# Single ellipsoid, keep as is
|
|
103
|
+
deduplicated.append((center1, semi_axes1, rotation1))
|
|
104
|
+
else:
|
|
105
|
+
# Merge multiple close ellipsoids
|
|
106
|
+
centers = np.array([center for center, _, _ in close_ellipsoids])
|
|
107
|
+
all_semi_axes = np.array([semi_axes for _, semi_axes, _ in close_ellipsoids])
|
|
108
|
+
|
|
109
|
+
# Use volume-weighted average for center and semi-axes
|
|
110
|
+
volumes = (4 / 3) * np.pi * np.prod(all_semi_axes, axis=1)
|
|
111
|
+
weights = volumes / np.sum(volumes)
|
|
112
|
+
merged_center = np.average(centers, axis=0, weights=weights)
|
|
113
|
+
merged_semi_axes = np.average(all_semi_axes, axis=0, weights=weights)
|
|
114
|
+
|
|
115
|
+
# Use first rotation matrix (could be improved with proper rotation averaging)
|
|
116
|
+
merged_rotation = close_ellipsoids[0][2]
|
|
117
|
+
|
|
118
|
+
deduplicated.append((merged_center, merged_semi_axes, merged_rotation))
|
|
119
|
+
logger.info(f"Merged {len(close_ellipsoids)} overlapping ellipsoids into one")
|
|
120
|
+
|
|
121
|
+
return deduplicated
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_ellipsoid_mesh(
|
|
125
|
+
center: np.ndarray,
|
|
126
|
+
semi_axes: np.ndarray,
|
|
127
|
+
rotation_matrix: np.ndarray,
|
|
128
|
+
subdivisions: int = 2,
|
|
129
|
+
) -> tm.Trimesh:
|
|
130
|
+
"""Create an ellipsoid mesh with given center, semi-axes, and orientation.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
center: 3D center point.
|
|
134
|
+
semi_axes: Three semi-axis lengths [a, b, c].
|
|
135
|
+
rotation_matrix: 3x3 rotation matrix.
|
|
136
|
+
subdivisions: Number of subdivisions for ellipsoid resolution.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Trimesh ellipsoid object.
|
|
140
|
+
"""
|
|
141
|
+
# Create unit sphere
|
|
142
|
+
sphere = tm.creation.icosphere(subdivisions=subdivisions, radius=1.0)
|
|
143
|
+
|
|
144
|
+
# Scale by semi-axes to create ellipsoid
|
|
145
|
+
scale_matrix = np.diag([semi_axes[0], semi_axes[1], semi_axes[2]])
|
|
146
|
+
|
|
147
|
+
# Apply scaling
|
|
148
|
+
ellipsoid_vertices = sphere.vertices @ scale_matrix.T
|
|
149
|
+
|
|
150
|
+
# Apply rotation
|
|
151
|
+
ellipsoid_vertices = ellipsoid_vertices @ rotation_matrix
|
|
152
|
+
|
|
153
|
+
# Translate to center
|
|
154
|
+
ellipsoid_vertices += center
|
|
155
|
+
|
|
156
|
+
# Create new mesh
|
|
157
|
+
ellipsoid = tm.Trimesh(vertices=ellipsoid_vertices, faces=sphere.faces)
|
|
158
|
+
|
|
159
|
+
return ellipsoid
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def ellipsoid_from_picks(
|
|
163
|
+
points: np.ndarray,
|
|
164
|
+
run: "CopickRun",
|
|
165
|
+
object_name: str,
|
|
166
|
+
session_id: str,
|
|
167
|
+
user_id: str,
|
|
168
|
+
use_clustering: bool = False,
|
|
169
|
+
clustering_method: str = "dbscan",
|
|
170
|
+
clustering_params: Optional[Dict[str, Any]] = None,
|
|
171
|
+
subdivisions: int = 2,
|
|
172
|
+
all_clusters: bool = True,
|
|
173
|
+
deduplicate_ellipsoids_flag: bool = True,
|
|
174
|
+
min_ellipsoid_distance: Optional[float] = None,
|
|
175
|
+
individual_meshes: bool = False,
|
|
176
|
+
session_id_template: Optional[str] = None,
|
|
177
|
+
) -> Optional[Tuple["CopickMesh", Dict[str, int]]]:
|
|
178
|
+
"""Create ellipsoid mesh(es) from pick points.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
points: Nx3 array of pick positions.
|
|
182
|
+
run: Copick run object.
|
|
183
|
+
object_name: Name of the mesh object.
|
|
184
|
+
session_id: Session ID for the mesh.
|
|
185
|
+
user_id: User ID for the mesh.
|
|
186
|
+
use_clustering: Whether to cluster points first.
|
|
187
|
+
clustering_method: Clustering method ('dbscan', 'kmeans').
|
|
188
|
+
clustering_params: Parameters for clustering.
|
|
189
|
+
e.g.
|
|
190
|
+
- {'eps': 5.0, 'min_samples': 3} for DBSCAN
|
|
191
|
+
- {'n_clusters': 3} for KMeans
|
|
192
|
+
subdivisions: Number of subdivisions for ellipsoid resolution.
|
|
193
|
+
all_clusters: If True, use all clusters; if False, use only the largest cluster.
|
|
194
|
+
deduplicate_ellipsoids_flag: Whether to merge overlapping ellipsoids.
|
|
195
|
+
min_ellipsoid_distance: Minimum distance between ellipsoid centers for deduplication.
|
|
196
|
+
individual_meshes: If True, create separate mesh objects for each ellipsoid.
|
|
197
|
+
session_id_template: Template for individual mesh session IDs.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Tuple of (CopickMesh object, stats dict) or None if creation failed.
|
|
201
|
+
Stats dict contains 'vertices_created' and 'faces_created' totals.
|
|
202
|
+
"""
|
|
203
|
+
if not validate_points(points, 6, "ellipsoid"):
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
if clustering_params is None:
|
|
207
|
+
clustering_params = {}
|
|
208
|
+
|
|
209
|
+
# Define ellipsoid creation function with special handling
|
|
210
|
+
def create_ellipsoid_from_points(cluster_points):
|
|
211
|
+
center, semi_axes, rotation_matrix = fit_ellipsoid_to_points(cluster_points)
|
|
212
|
+
return create_ellipsoid_mesh(center, semi_axes, rotation_matrix, subdivisions)
|
|
213
|
+
|
|
214
|
+
# Handle clustering workflow with special ellipsoid logic
|
|
215
|
+
if use_clustering:
|
|
216
|
+
point_clusters = cluster(
|
|
217
|
+
points,
|
|
218
|
+
clustering_method,
|
|
219
|
+
min_points_per_cluster=6, # Ellipsoids need at least 6 points
|
|
220
|
+
**clustering_params,
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
if not point_clusters:
|
|
224
|
+
logger.warning("No valid clusters found")
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
logger.info(f"Found {len(point_clusters)} clusters")
|
|
228
|
+
|
|
229
|
+
if all_clusters and len(point_clusters) > 1:
|
|
230
|
+
# Create ellipsoid parameters from all clusters
|
|
231
|
+
ellipsoid_params = []
|
|
232
|
+
for i, cluster_points in enumerate(point_clusters):
|
|
233
|
+
try:
|
|
234
|
+
center, semi_axes, rotation_matrix = fit_ellipsoid_to_points(cluster_points)
|
|
235
|
+
ellipsoid_params.append((center, semi_axes, rotation_matrix))
|
|
236
|
+
logger.info(f"Cluster {i}: ellipsoid at {center} with semi-axes {semi_axes}")
|
|
237
|
+
except Exception as e:
|
|
238
|
+
logger.critical(f"Failed to fit ellipsoid to cluster {i}: {e}")
|
|
239
|
+
continue
|
|
240
|
+
|
|
241
|
+
if not ellipsoid_params:
|
|
242
|
+
logger.warning("No valid ellipsoids created from clusters")
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
# Deduplicate overlapping ellipsoids if requested
|
|
246
|
+
if deduplicate_ellipsoids_flag:
|
|
247
|
+
final_ellipsoids = deduplicate_ellipsoids(ellipsoid_params, min_ellipsoid_distance)
|
|
248
|
+
else:
|
|
249
|
+
final_ellipsoids = ellipsoid_params
|
|
250
|
+
|
|
251
|
+
if individual_meshes:
|
|
252
|
+
# Create separate mesh objects for each ellipsoid
|
|
253
|
+
created_meshes = []
|
|
254
|
+
total_vertices = 0
|
|
255
|
+
total_faces = 0
|
|
256
|
+
|
|
257
|
+
for i, (center, semi_axes, rotation_matrix) in enumerate(final_ellipsoids):
|
|
258
|
+
ellipsoid_mesh = create_ellipsoid_mesh(center, semi_axes, rotation_matrix, subdivisions)
|
|
259
|
+
|
|
260
|
+
# Generate session ID using template if provided
|
|
261
|
+
if session_id_template:
|
|
262
|
+
ellipsoid_session_id = session_id_template.format(
|
|
263
|
+
base_session_id=session_id,
|
|
264
|
+
instance_id=i,
|
|
265
|
+
)
|
|
266
|
+
else:
|
|
267
|
+
ellipsoid_session_id = f"{session_id}-{i:03d}"
|
|
268
|
+
|
|
269
|
+
try:
|
|
270
|
+
copick_mesh = run.new_mesh(object_name, ellipsoid_session_id, user_id, exist_ok=True)
|
|
271
|
+
copick_mesh.mesh = ellipsoid_mesh
|
|
272
|
+
copick_mesh.store()
|
|
273
|
+
created_meshes.append(copick_mesh)
|
|
274
|
+
total_vertices += len(ellipsoid_mesh.vertices)
|
|
275
|
+
total_faces += len(ellipsoid_mesh.faces)
|
|
276
|
+
logger.info(
|
|
277
|
+
f"Created individual ellipsoid mesh {i} with {len(ellipsoid_mesh.vertices)} vertices",
|
|
278
|
+
)
|
|
279
|
+
except Exception as e:
|
|
280
|
+
logger.error(f"Failed to create mesh {i}: {e}")
|
|
281
|
+
continue
|
|
282
|
+
|
|
283
|
+
# Return the first mesh and total stats
|
|
284
|
+
if created_meshes:
|
|
285
|
+
stats = {"vertices_created": total_vertices, "faces_created": total_faces}
|
|
286
|
+
return created_meshes[0], stats
|
|
287
|
+
else:
|
|
288
|
+
return None
|
|
289
|
+
else:
|
|
290
|
+
# Create meshes from final ellipsoids and combine them
|
|
291
|
+
all_meshes = []
|
|
292
|
+
for center, semi_axes, rotation_matrix in final_ellipsoids:
|
|
293
|
+
ellipsoid_mesh = create_ellipsoid_mesh(center, semi_axes, rotation_matrix, subdivisions)
|
|
294
|
+
all_meshes.append(ellipsoid_mesh)
|
|
295
|
+
|
|
296
|
+
# Combine all meshes
|
|
297
|
+
combined_mesh = tm.util.concatenate(all_meshes)
|
|
298
|
+
else:
|
|
299
|
+
# Use largest cluster
|
|
300
|
+
cluster_sizes = [len(cluster) for cluster in point_clusters]
|
|
301
|
+
largest_cluster_idx = np.argmax(cluster_sizes)
|
|
302
|
+
points_to_use = point_clusters[largest_cluster_idx]
|
|
303
|
+
logger.info(f"Using largest cluster with {len(points_to_use)} points")
|
|
304
|
+
|
|
305
|
+
combined_mesh = create_ellipsoid_from_points(points_to_use)
|
|
306
|
+
else:
|
|
307
|
+
# Use all points without clustering
|
|
308
|
+
combined_mesh = create_ellipsoid_from_points(points)
|
|
309
|
+
|
|
310
|
+
# Store mesh and return stats
|
|
311
|
+
try:
|
|
312
|
+
return store_mesh_with_stats(run, combined_mesh, object_name, session_id, user_id, "ellipsoid")
|
|
313
|
+
except Exception as e:
|
|
314
|
+
logger.critical(f"Error creating mesh: {e}")
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# Create worker function using common infrastructure
|
|
319
|
+
_ellipsoid_from_picks_worker = create_batch_worker(ellipsoid_from_picks, "ellipsoid", min_points=6)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
# Create batch converter using common infrastructure
|
|
323
|
+
ellipsoid_from_picks_batch = create_batch_converter(
|
|
324
|
+
ellipsoid_from_picks,
|
|
325
|
+
"Converting picks to ellipsoid meshes",
|
|
326
|
+
"ellipsoid",
|
|
327
|
+
min_points=6,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
# Lazy batch converter for new architecture
|
|
331
|
+
|
|
332
|
+
ellipsoid_from_picks_lazy_batch = create_lazy_batch_converter(
|
|
333
|
+
converter_func=ellipsoid_from_picks,
|
|
334
|
+
task_description="Converting picks to ellipsoid meshes",
|
|
335
|
+
)
|