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.
Files changed (67) hide show
  1. copick_utils/__init__.py +1 -1
  2. copick_utils/cli/__init__.py +33 -0
  3. copick_utils/cli/clipmesh.py +161 -0
  4. copick_utils/cli/clippicks.py +154 -0
  5. copick_utils/cli/clipseg.py +163 -0
  6. copick_utils/cli/conversion_commands.py +32 -0
  7. copick_utils/cli/enclosed.py +191 -0
  8. copick_utils/cli/filter_components.py +166 -0
  9. copick_utils/cli/fit_spline.py +191 -0
  10. copick_utils/cli/hull.py +138 -0
  11. copick_utils/cli/input_output_selection.py +76 -0
  12. copick_utils/cli/logical_commands.py +29 -0
  13. copick_utils/cli/mesh2picks.py +170 -0
  14. copick_utils/cli/mesh2seg.py +167 -0
  15. copick_utils/cli/meshop.py +262 -0
  16. copick_utils/cli/picks2ellipsoid.py +171 -0
  17. copick_utils/cli/picks2mesh.py +181 -0
  18. copick_utils/cli/picks2plane.py +156 -0
  19. copick_utils/cli/picks2seg.py +134 -0
  20. copick_utils/cli/picks2sphere.py +170 -0
  21. copick_utils/cli/picks2surface.py +164 -0
  22. copick_utils/cli/picksin.py +146 -0
  23. copick_utils/cli/picksout.py +148 -0
  24. copick_utils/cli/processing_commands.py +18 -0
  25. copick_utils/cli/seg2mesh.py +135 -0
  26. copick_utils/cli/seg2picks.py +128 -0
  27. copick_utils/cli/segop.py +248 -0
  28. copick_utils/cli/separate_components.py +155 -0
  29. copick_utils/cli/skeletonize.py +164 -0
  30. copick_utils/cli/util.py +580 -0
  31. copick_utils/cli/validbox.py +155 -0
  32. copick_utils/converters/__init__.py +35 -0
  33. copick_utils/converters/converter_common.py +543 -0
  34. copick_utils/converters/ellipsoid_from_picks.py +335 -0
  35. copick_utils/converters/lazy_converter.py +576 -0
  36. copick_utils/converters/mesh_from_picks.py +209 -0
  37. copick_utils/converters/mesh_from_segmentation.py +119 -0
  38. copick_utils/converters/picks_from_mesh.py +542 -0
  39. copick_utils/converters/picks_from_segmentation.py +168 -0
  40. copick_utils/converters/plane_from_picks.py +251 -0
  41. copick_utils/converters/segmentation_from_mesh.py +291 -0
  42. copick_utils/{segmentation → converters}/segmentation_from_picks.py +123 -13
  43. copick_utils/converters/sphere_from_picks.py +306 -0
  44. copick_utils/converters/surface_from_picks.py +337 -0
  45. copick_utils/logical/__init__.py +43 -0
  46. copick_utils/logical/distance_operations.py +604 -0
  47. copick_utils/logical/enclosed_operations.py +222 -0
  48. copick_utils/logical/mesh_operations.py +443 -0
  49. copick_utils/logical/point_operations.py +303 -0
  50. copick_utils/logical/segmentation_operations.py +399 -0
  51. copick_utils/process/__init__.py +47 -0
  52. copick_utils/process/connected_components.py +360 -0
  53. copick_utils/process/filter_components.py +306 -0
  54. copick_utils/process/hull.py +106 -0
  55. copick_utils/process/skeletonize.py +326 -0
  56. copick_utils/process/spline_fitting.py +648 -0
  57. copick_utils/process/validbox.py +333 -0
  58. copick_utils/util/__init__.py +6 -0
  59. copick_utils/util/config_models.py +614 -0
  60. {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/METADATA +15 -2
  61. copick_utils-1.0.1.dist-info/RECORD +71 -0
  62. {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/WHEEL +1 -1
  63. copick_utils-1.0.1.dist-info/entry_points.txt +29 -0
  64. copick_utils/segmentation/picks_from_segmentation.py +0 -81
  65. copick_utils-0.6.1.dist-info/RECORD +0 -14
  66. /copick_utils/{segmentation → io}/__init__.py +0 -0
  67. {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,333 @@
1
+ """Generate valid area box meshes for tomographic reconstructions."""
2
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
3
+
4
+ import numpy as np
5
+ import trimesh as tm
6
+ import zarr
7
+
8
+ if TYPE_CHECKING:
9
+ from copick.models import CopickRoot, CopickRun
10
+
11
+
12
+ def shift_3d(shift: np.ndarray) -> np.ndarray:
13
+ """
14
+ Create a 3D translation transformation matrix.
15
+
16
+ Args:
17
+ shift: Translation vector [x, y, z]
18
+
19
+ Returns:
20
+ 4x4 homogeneous transformation matrix
21
+ """
22
+ return np.array(
23
+ [
24
+ [1, 0, 0, shift[0]],
25
+ [0, 1, 0, shift[1]],
26
+ [0, 0, 1, shift[2]],
27
+ [0, 0, 0, 1],
28
+ ],
29
+ )
30
+
31
+
32
+ def rotation_3d_x(angle: float) -> np.ndarray:
33
+ """
34
+ Create a 3D rotation transformation matrix around X-axis.
35
+
36
+ Args:
37
+ angle: Rotation angle in degrees
38
+
39
+ Returns:
40
+ 4x4 homogeneous transformation matrix
41
+ """
42
+ phi = np.radians(angle)
43
+ return np.array(
44
+ [
45
+ [1, 0, 0, 0],
46
+ [0, np.cos(phi), -np.sin(phi), 0],
47
+ [0, np.sin(phi), np.cos(phi), 0],
48
+ [0, 0, 0, 1],
49
+ ],
50
+ )
51
+
52
+
53
+ def rotation_3d_y(angle: float) -> np.ndarray:
54
+ """
55
+ Create a 3D rotation transformation matrix around Y-axis.
56
+
57
+ Args:
58
+ angle: Rotation angle in degrees
59
+
60
+ Returns:
61
+ 4x4 homogeneous transformation matrix
62
+ """
63
+ phi = np.radians(angle)
64
+ return np.array(
65
+ [
66
+ [np.cos(phi), 0, np.sin(phi), 0],
67
+ [0, 1, 0, 0],
68
+ [-np.sin(phi), 0, np.cos(phi), 0],
69
+ [0, 0, 0, 1],
70
+ ],
71
+ )
72
+
73
+
74
+ def rotation_3d_z(angle: float) -> np.ndarray:
75
+ """
76
+ Create a 3D rotation transformation matrix around Z-axis.
77
+
78
+ Args:
79
+ angle: Rotation angle in degrees
80
+
81
+ Returns:
82
+ 4x4 homogeneous transformation matrix
83
+ """
84
+ phi = np.radians(angle)
85
+ return np.array(
86
+ [
87
+ [np.cos(phi), -np.sin(phi), 0, 0],
88
+ [np.sin(phi), np.cos(phi), 0, 0],
89
+ [0, 0, 1, 0],
90
+ [0, 0, 0, 1],
91
+ ],
92
+ )
93
+
94
+
95
+ def rotation_center(rot_matrix: np.ndarray, center: np.ndarray) -> np.ndarray:
96
+ """
97
+ Create a rotation transformation around a center point.
98
+
99
+ Args:
100
+ rot_matrix: 4x4 rotation matrix
101
+ center: Center point [x, y, z]
102
+
103
+ Returns:
104
+ 4x4 transformation matrix for rotation around center
105
+ """
106
+ s1 = shift_3d(-center)
107
+ s2 = shift_3d(center)
108
+ return s2 @ rot_matrix @ s1
109
+
110
+
111
+ def create_validbox_mesh(
112
+ run: "CopickRun",
113
+ voxel_spacing: float,
114
+ tomo_type: str = "wbp",
115
+ angle: float = 0.0,
116
+ ) -> Optional[tm.Trimesh]:
117
+ """
118
+ Create a box mesh representing the valid area of a reconstruction.
119
+
120
+ Args:
121
+ run: Copick run object
122
+ voxel_spacing: Voxel spacing for the tomogram
123
+ tomo_type: Type of tomogram to use as reference
124
+ angle: Rotation angle around Z-axis in degrees
125
+
126
+ Returns:
127
+ Trimesh box object or None if tomogram not found
128
+ """
129
+ # Negate angle to match coordinate system conventions
130
+ angle = -angle
131
+
132
+ # Get tomogram dimensions
133
+ vs = run.get_voxel_spacing(voxel_spacing)
134
+ tomo = vs.get_tomograms(tomo_type)[0]
135
+
136
+ if tomo is None:
137
+ print(f"Warning: Could not find tomogram of type '{tomo_type}' for run {run.name}")
138
+ return None
139
+
140
+ # Get pixel dimensions and calculate physical dimensions
141
+ pixel_max_dim = zarr.open(tomo.zarr())["0"].shape[::-1]
142
+ pixel_center = np.floor(np.array(pixel_max_dim) / 2) + 1
143
+ max_dim = np.array([d * voxel_spacing for d in pixel_max_dim])
144
+ center = np.array([c * voxel_spacing for c in pixel_center])
145
+
146
+ # Create rotation transformation
147
+ r = rotation_3d_z(angle)
148
+ transform = rotation_center(r, center)
149
+
150
+ # Create rotated box mesh with original tomogram dimensions
151
+ box = tm.creation.box(
152
+ extents=max_dim,
153
+ transform=transform @ shift_3d(max_dim / 2),
154
+ )
155
+
156
+ # Define the tomogram bounding planes
157
+ # The tomogram extends from (0, 0, 0) to max_dim
158
+ bounding_planes = [
159
+ # X planes: normal points inward (positive X direction for min plane, negative for max)
160
+ (np.array([1.0, 0.0, 0.0]), np.array([0.0, 0.0, 0.0])), # x >= 0
161
+ (np.array([-1.0, 0.0, 0.0]), np.array([max_dim[0], 0.0, 0.0])), # x <= max_dim[0]
162
+ # Y planes
163
+ (np.array([0.0, 1.0, 0.0]), np.array([0.0, 0.0, 0.0])), # y >= 0
164
+ (np.array([0.0, -1.0, 0.0]), np.array([0.0, max_dim[1], 0.0])), # y <= max_dim[1]
165
+ # Z planes
166
+ (np.array([0.0, 0.0, 1.0]), np.array([0.0, 0.0, 0.0])), # z >= 0
167
+ (np.array([0.0, 0.0, -1.0]), np.array([0.0, 0.0, max_dim[2]])), # z <= max_dim[2]
168
+ ]
169
+
170
+ # Start with the rotated box mesh
171
+ current_mesh = box
172
+
173
+ # Slice the rotated box with each bounding plane to clip it to tomogram bounds
174
+ for plane_normal, plane_origin in bounding_planes:
175
+ # Use trimesh's slice_plane method which properly caps the mesh
176
+ current_mesh = current_mesh.slice_plane(
177
+ plane_origin=plane_origin,
178
+ plane_normal=plane_normal,
179
+ cap=True, # This caps the mesh where it was sliced
180
+ )
181
+
182
+ # Clean up the mesh to remove degenerate faces and unused vertices
183
+ current_mesh.remove_unreferenced_vertices()
184
+ current_mesh.fix_normals()
185
+
186
+ return current_mesh
187
+
188
+
189
+ def generate_validbox(
190
+ run: "CopickRun",
191
+ voxel_spacing: float,
192
+ mesh_object_name: str,
193
+ mesh_user_id: str,
194
+ mesh_session_id: str,
195
+ tomo_type: str = "wbp",
196
+ angle: float = 0.0,
197
+ ) -> Optional[Dict[str, Any]]:
198
+ """
199
+ Generate a valid area box mesh for a single run.
200
+
201
+ Args:
202
+ run: Copick run object
203
+ voxel_spacing: Voxel spacing for the tomogram
204
+ mesh_object_name: Name of the mesh object to create
205
+ mesh_user_id: User ID for the mesh
206
+ mesh_session_id: Session ID for the mesh
207
+ tomo_type: Type of tomogram to use as reference
208
+ angle: Rotation angle around Z-axis in degrees
209
+
210
+ Returns:
211
+ Dictionary with result information or None if failed
212
+ """
213
+ try:
214
+ # Create the box mesh
215
+ box = create_validbox_mesh(
216
+ run=run,
217
+ voxel_spacing=voxel_spacing,
218
+ tomo_type=tomo_type,
219
+ angle=angle,
220
+ )
221
+
222
+ if box is None:
223
+ return {
224
+ "processed": 0,
225
+ "errors": [f"Could not create validbox for run {run.name}"],
226
+ "vertices_created": 0,
227
+ "faces_created": 0,
228
+ }
229
+
230
+ # Get or create mesh object
231
+ existing_meshes = run.get_meshes(
232
+ object_name=mesh_object_name,
233
+ user_id=mesh_user_id,
234
+ session_id=mesh_session_id,
235
+ )
236
+
237
+ if len(existing_meshes) == 0:
238
+ mesh_obj = run.new_mesh(
239
+ object_name=mesh_object_name,
240
+ user_id=mesh_user_id,
241
+ session_id=mesh_session_id,
242
+ )
243
+ else:
244
+ mesh_obj = existing_meshes[0]
245
+
246
+ # Store the mesh
247
+ mesh_obj.mesh = box
248
+ mesh_obj.store()
249
+
250
+ return {
251
+ "processed": 1,
252
+ "errors": [],
253
+ "vertices_created": len(box.vertices),
254
+ "faces_created": len(box.faces),
255
+ }
256
+
257
+ except Exception as e:
258
+ return {
259
+ "processed": 0,
260
+ "errors": [f"Error processing {run.name}: {e}"],
261
+ "vertices_created": 0,
262
+ "faces_created": 0,
263
+ }
264
+
265
+
266
+ def _validbox_worker(
267
+ run: "CopickRun",
268
+ voxel_spacing: float,
269
+ mesh_object_name: str,
270
+ mesh_user_id: str,
271
+ mesh_session_id: str,
272
+ tomo_type: str,
273
+ angle: float,
274
+ ) -> Dict[str, Any]:
275
+ """Worker function for batch validbox generation."""
276
+ return generate_validbox(
277
+ run=run,
278
+ voxel_spacing=voxel_spacing,
279
+ mesh_object_name=mesh_object_name,
280
+ mesh_user_id=mesh_user_id,
281
+ mesh_session_id=mesh_session_id,
282
+ tomo_type=tomo_type,
283
+ angle=angle,
284
+ )
285
+
286
+
287
+ def validbox_batch(
288
+ root: "CopickRoot",
289
+ voxel_spacing: float,
290
+ mesh_object_name: str,
291
+ mesh_user_id: str,
292
+ mesh_session_id: str,
293
+ tomo_type: str = "wbp",
294
+ angle: float = 0.0,
295
+ run_names: Optional[List[str]] = None,
296
+ workers: int = 8,
297
+ ) -> Dict[str, Any]:
298
+ """
299
+ Generate valid area box meshes across multiple runs.
300
+
301
+ Args:
302
+ root: The copick root containing runs to process
303
+ voxel_spacing: Voxel spacing for the tomograms
304
+ mesh_object_name: Name of the mesh object to create
305
+ mesh_user_id: User ID for the meshes
306
+ mesh_session_id: Session ID for the meshes
307
+ tomo_type: Type of tomogram to use as reference. Default is 'wbp'.
308
+ angle: Rotation angle around Z-axis in degrees. Default is 0.0.
309
+ run_names: List of run names to process. If None, processes all runs.
310
+ workers: Number of worker processes. Default is 8.
311
+
312
+ Returns:
313
+ Dictionary with processing results and statistics
314
+ """
315
+ from copick.ops.run import map_runs
316
+
317
+ runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
318
+
319
+ results = map_runs(
320
+ callback=_validbox_worker,
321
+ root=root,
322
+ runs=runs_to_process,
323
+ workers=workers,
324
+ task_desc="Generating validbox meshes",
325
+ voxel_spacing=voxel_spacing,
326
+ mesh_object_name=mesh_object_name,
327
+ mesh_user_id=mesh_user_id,
328
+ mesh_session_id=mesh_session_id,
329
+ tomo_type=tomo_type,
330
+ angle=angle,
331
+ )
332
+
333
+ return results
@@ -0,0 +1,6 @@
1
+ """Utility functions for copick-utils."""
2
+
3
+ # Pattern matching functionality has been migrated to use copick.util.uri
4
+ # Use copick.util.uri.get_copick_objects_by_type() instead
5
+
6
+ __all__ = []