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,306 @@
1
+ """Filter connected components in segmentations by size."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
4
+
5
+ import numpy as np
6
+ from copick.util.log import get_logger
7
+ from scipy.ndimage import generate_binary_structure, label
8
+
9
+ if TYPE_CHECKING:
10
+ from copick.models import CopickRoot, CopickRun, CopickSegmentation
11
+
12
+ logger = get_logger(__name__)
13
+
14
+
15
+ def _filter_components_by_size(
16
+ seg: np.ndarray,
17
+ voxel_spacing: float,
18
+ connectivity: str = "all",
19
+ min_size: Optional[float] = None,
20
+ max_size: Optional[float] = None,
21
+ ) -> Tuple[np.ndarray, int, int, list]:
22
+ """
23
+ Filter connected components in a segmentation by size.
24
+
25
+ Args:
26
+ seg: Binary mask segmentation (numpy array)
27
+ voxel_spacing: Voxel spacing in angstroms
28
+ connectivity: Connectivity for connected components (default: "all")
29
+ "face" = face connectivity (6-connected in 3D)
30
+ "face-edge" = face+edge connectivity (18-connected in 3D)
31
+ "all" = face+edge+corner connectivity (26-connected in 3D)
32
+ min_size: Minimum component volume in cubic angstroms (ų) to keep (None = no minimum)
33
+ max_size: Maximum component volume in cubic angstroms (ų) to keep (None = no maximum)
34
+
35
+ Returns:
36
+ Tuple of (seg_filtered, num_kept, num_removed, component_info)
37
+ - seg_filtered: Filtered segmentation with only components passing size criteria
38
+ - num_kept: Number of components kept
39
+ - num_removed: Number of components removed
40
+ - component_info: List of dicts with info about each component
41
+ """
42
+ # Map connectivity string to numeric value
43
+ connectivity_map = {
44
+ "face": 1,
45
+ "face-edge": 2,
46
+ "all": 3,
47
+ }
48
+ connectivity_value = connectivity_map.get(connectivity, 3)
49
+
50
+ # Define connectivity structure
51
+ struct = generate_binary_structure(seg.ndim, connectivity_value)
52
+
53
+ # Label connected components
54
+ labeled_seg, num_components = label(seg, structure=struct)
55
+
56
+ # Calculate voxel volume in cubic angstroms
57
+ voxel_volume = voxel_spacing**3
58
+
59
+ # Initialize output
60
+ seg_filtered = np.zeros_like(seg, dtype=bool)
61
+
62
+ component_info = []
63
+ num_kept = 0
64
+ num_removed = 0
65
+
66
+ # Check each component
67
+ for component_id in range(1, num_components + 1):
68
+ # Extract this component
69
+ component_mask = labeled_seg == component_id
70
+ component_voxels = int(np.sum(component_mask))
71
+ component_volume = component_voxels * voxel_volume
72
+
73
+ # Apply size filtering
74
+ passes_filter = True
75
+ if min_size is not None and component_volume < min_size:
76
+ passes_filter = False
77
+ if max_size is not None and component_volume > max_size:
78
+ passes_filter = False
79
+
80
+ # Store information
81
+ info = {
82
+ "component_id": component_id,
83
+ "voxels": component_voxels,
84
+ "volume": component_volume,
85
+ "kept": passes_filter,
86
+ }
87
+ component_info.append(info)
88
+
89
+ # Keep or remove component
90
+ if passes_filter:
91
+ seg_filtered = np.logical_or(seg_filtered, component_mask)
92
+ num_kept += 1
93
+ else:
94
+ num_removed += 1
95
+
96
+ return seg_filtered.astype(np.uint8), num_kept, num_removed, component_info
97
+
98
+
99
+ def filter_segmentation_components(
100
+ segmentation: "CopickSegmentation",
101
+ run: "CopickRun",
102
+ object_name: str,
103
+ session_id: str,
104
+ user_id: str,
105
+ voxel_spacing: float,
106
+ is_multilabel: bool = False,
107
+ connectivity: str = "all",
108
+ min_size: Optional[float] = None,
109
+ max_size: Optional[float] = None,
110
+ **kwargs,
111
+ ) -> Optional[Tuple["CopickSegmentation", Dict[str, int]]]:
112
+ """
113
+ Filter connected components in a segmentation by size.
114
+
115
+ Args:
116
+ segmentation: Input CopickSegmentation object
117
+ run: CopickRun object
118
+ object_name: Name for the output segmentation
119
+ session_id: Session ID for the output segmentation
120
+ user_id: User ID for the output segmentation
121
+ voxel_spacing: Voxel spacing for the output segmentation in angstroms
122
+ is_multilabel: Whether the segmentation is multilabel
123
+ connectivity: Connectivity for connected components (default: "all")
124
+ "face" = 6-connected, "face-edge" = 18-connected, "all" = 26-connected
125
+ min_size: Minimum component volume in cubic angstroms (ų) to keep (None = no minimum)
126
+ max_size: Maximum component volume in cubic angstroms (ų) to keep (None = no maximum)
127
+ **kwargs: Additional keyword arguments
128
+
129
+ Returns:
130
+ Tuple of (CopickSegmentation object, stats dict) or None if operation failed.
131
+ Stats dict contains 'voxels_kept', 'components_kept', 'components_removed'.
132
+ """
133
+ try:
134
+ # Load segmentation array
135
+ seg_array = segmentation.numpy()
136
+
137
+ if seg_array is None:
138
+ logger.error("Could not load segmentation data")
139
+ return None
140
+
141
+ if seg_array.size == 0:
142
+ logger.error("Empty segmentation data")
143
+ return None
144
+
145
+ # Convert to boolean array
146
+ bool_seg = seg_array.astype(bool)
147
+
148
+ # Filter components
149
+ result_array, num_kept, num_removed, component_info = _filter_components_by_size(
150
+ bool_seg,
151
+ voxel_spacing=voxel_spacing,
152
+ connectivity=connectivity,
153
+ min_size=min_size,
154
+ max_size=max_size,
155
+ )
156
+
157
+ # Create output segmentation
158
+ output_seg = run.new_segmentation(
159
+ name=object_name,
160
+ user_id=user_id,
161
+ session_id=session_id,
162
+ is_multilabel=is_multilabel,
163
+ voxel_size=voxel_spacing,
164
+ exist_ok=True,
165
+ )
166
+
167
+ # Store the result
168
+ output_seg.from_numpy(result_array)
169
+
170
+ stats = {
171
+ "voxels_kept": int(np.sum(result_array)),
172
+ "components_kept": num_kept,
173
+ "components_removed": num_removed,
174
+ "components_total": num_kept + num_removed,
175
+ }
176
+ logger.info(
177
+ f"Filtered components: kept {stats['components_kept']}/{stats['components_total']}, "
178
+ f"removed {stats['components_removed']} ({stats['voxels_kept']} voxels remaining)",
179
+ )
180
+ return output_seg, stats
181
+
182
+ except Exception as e:
183
+ logger.error(f"Error filtering segmentation components: {e}")
184
+ return None
185
+
186
+
187
+ def _filter_components_worker(
188
+ run: "CopickRun",
189
+ segmentation_name: str,
190
+ segmentation_user_id: str,
191
+ segmentation_session_id: str,
192
+ voxel_spacing: float,
193
+ connectivity: str,
194
+ min_size: Optional[float],
195
+ max_size: Optional[float],
196
+ output_user_id: str,
197
+ output_session_id: str,
198
+ is_multilabel: bool,
199
+ root: "CopickRoot",
200
+ ) -> Dict[str, Any]:
201
+ """Worker function for batch component filtering."""
202
+ try:
203
+ # Get segmentation
204
+ segmentations = run.get_segmentations(
205
+ name=segmentation_name,
206
+ user_id=segmentation_user_id,
207
+ session_id=segmentation_session_id,
208
+ voxel_size=voxel_spacing,
209
+ )
210
+
211
+ if not segmentations:
212
+ return {"processed": 0, "errors": [f"No segmentation found for {run.name}"]}
213
+
214
+ segmentation = segmentations[0]
215
+
216
+ # Filter components
217
+ result = filter_segmentation_components(
218
+ segmentation=segmentation,
219
+ run=run,
220
+ object_name=segmentation_name,
221
+ session_id=output_session_id,
222
+ user_id=output_user_id,
223
+ voxel_spacing=voxel_spacing,
224
+ is_multilabel=is_multilabel,
225
+ connectivity=connectivity,
226
+ min_size=min_size,
227
+ max_size=max_size,
228
+ )
229
+
230
+ if result is None:
231
+ return {"processed": 0, "errors": [f"Failed to filter components for {run.name}"]}
232
+
233
+ output_seg, stats = result
234
+
235
+ return {
236
+ "processed": 1,
237
+ "errors": [],
238
+ "voxels_kept": stats["voxels_kept"],
239
+ "components_kept": stats["components_kept"],
240
+ "components_removed": stats["components_removed"],
241
+ "components_total": stats["components_total"],
242
+ }
243
+
244
+ except Exception as e:
245
+ return {"processed": 0, "errors": [f"Error processing {run.name}: {e}"]}
246
+
247
+
248
+ def filter_components_batch(
249
+ root: "CopickRoot",
250
+ segmentation_name: str,
251
+ segmentation_user_id: str,
252
+ segmentation_session_id: str,
253
+ voxel_spacing: float,
254
+ connectivity: str = "all",
255
+ min_size: Optional[float] = None,
256
+ max_size: Optional[float] = None,
257
+ output_user_id: str = "filter-components",
258
+ output_session_id: str = "filtered",
259
+ is_multilabel: bool = False,
260
+ run_names: Optional[List[str]] = None,
261
+ workers: int = 8,
262
+ ) -> Dict[str, Any]:
263
+ """
264
+ Batch filter connected components by size across multiple runs.
265
+
266
+ Args:
267
+ root: The copick root containing runs to process
268
+ segmentation_name: Name of the segmentation to process
269
+ segmentation_user_id: User ID of the segmentation to process
270
+ segmentation_session_id: Session ID of the segmentation to process
271
+ voxel_spacing: Voxel spacing in angstroms
272
+ connectivity: Connectivity for connected components (default: "all")
273
+ min_size: Minimum component volume in ų to keep (None = no minimum)
274
+ max_size: Maximum component volume in ų to keep (None = no maximum)
275
+ output_user_id: User ID for output segmentations
276
+ output_session_id: Session ID for output segmentations
277
+ is_multilabel: Whether the segmentation is multilabel
278
+ run_names: List of run names to process. If None, processes all runs.
279
+ workers: Number of worker processes
280
+
281
+ Returns:
282
+ Dictionary with processing results and statistics
283
+ """
284
+ from copick.ops.run import map_runs
285
+
286
+ runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
287
+
288
+ results = map_runs(
289
+ callback=_filter_components_worker,
290
+ root=root,
291
+ runs=runs_to_process,
292
+ workers=workers,
293
+ task_desc="Filtering components by size",
294
+ segmentation_name=segmentation_name,
295
+ segmentation_user_id=segmentation_user_id,
296
+ segmentation_session_id=segmentation_session_id,
297
+ voxel_spacing=voxel_spacing,
298
+ connectivity=connectivity,
299
+ min_size=min_size,
300
+ max_size=max_size,
301
+ output_user_id=output_user_id,
302
+ output_session_id=output_session_id,
303
+ is_multilabel=is_multilabel,
304
+ )
305
+
306
+ return results
@@ -0,0 +1,106 @@
1
+ """Compute various hull operations on meshes."""
2
+ from typing import TYPE_CHECKING, Dict, Optional, Tuple
3
+
4
+ import trimesh as tm
5
+ from copick.util.log import get_logger
6
+
7
+ from copick_utils.converters.converter_common import create_batch_converter, store_mesh_with_stats
8
+ from copick_utils.converters.lazy_converter import create_lazy_batch_converter
9
+
10
+ if TYPE_CHECKING:
11
+ from copick.models import CopickMesh, CopickRun
12
+
13
+ logger = get_logger(__name__)
14
+
15
+
16
+ def compute_hull(
17
+ mesh: "CopickMesh",
18
+ run: "CopickRun",
19
+ object_name: str,
20
+ session_id: str,
21
+ user_id: str,
22
+ hull_type: str = "convex",
23
+ **kwargs,
24
+ ) -> Optional[Tuple["CopickMesh", Dict[str, int]]]:
25
+ """
26
+ Compute hull of a CopickMesh object.
27
+
28
+ Args:
29
+ mesh: CopickMesh object to compute hull for
30
+ run: CopickRun object
31
+ object_name: Name for the output mesh
32
+ session_id: Session ID for the output mesh
33
+ user_id: User ID for the output mesh
34
+ hull_type: Type of hull to compute ('convex')
35
+ **kwargs: Additional keyword arguments
36
+
37
+ Returns:
38
+ Tuple of (CopickMesh object, stats dict) or None if operation failed.
39
+ Stats dict contains 'vertices_created' and 'faces_created'.
40
+ """
41
+ try:
42
+ # Get trimesh object
43
+ trimesh_obj = mesh.mesh
44
+
45
+ if trimesh_obj is None:
46
+ logger.error("Could not load mesh data")
47
+ return None
48
+
49
+ # Ensure we have proper Trimesh object
50
+ if isinstance(trimesh_obj, tm.Scene):
51
+ if len(trimesh_obj.geometry) == 0:
52
+ logger.error("Mesh is empty")
53
+ return None
54
+ trimesh_obj = trimesh_obj.to_mesh()
55
+
56
+ if not isinstance(trimesh_obj, tm.Trimesh):
57
+ logger.error(f"Expected Trimesh object, got {type(trimesh_obj)}")
58
+ return None
59
+
60
+ # Compute hull based on type
61
+ if hull_type == "convex":
62
+ hull_mesh = trimesh_obj.convex_hull
63
+ shape_name = "convex hull"
64
+ else:
65
+ raise ValueError(f"Unknown hull type: {hull_type}")
66
+
67
+ if hull_mesh is None:
68
+ logger.error(f"Failed to compute {hull_type} hull")
69
+ return None
70
+
71
+ if hull_mesh.vertices.shape[0] == 0:
72
+ logger.error(f"{hull_type.capitalize()} hull resulted in empty mesh")
73
+ return None
74
+
75
+ # Store the result
76
+ copick_mesh, stats = store_mesh_with_stats(
77
+ run=run,
78
+ mesh=hull_mesh,
79
+ object_name=object_name,
80
+ session_id=session_id,
81
+ user_id=user_id,
82
+ shape_name=shape_name,
83
+ )
84
+
85
+ logger.info(f"Created {hull_type} hull mesh with {stats['vertices_created']} vertices")
86
+ return copick_mesh, stats
87
+
88
+ except Exception as e:
89
+ logger.error(f"Error computing {hull_type} hull: {e}")
90
+ return None
91
+
92
+
93
+ # Create batch converter
94
+ hull_from_mesh_batch = create_batch_converter(
95
+ compute_hull,
96
+ "Computing hull from meshes",
97
+ "mesh",
98
+ "mesh",
99
+ min_points=0,
100
+ )
101
+
102
+ # Lazy batch converter for new architecture
103
+ hull_lazy_batch = create_lazy_batch_converter(
104
+ converter_func=compute_hull,
105
+ task_description="Computing convex hulls",
106
+ )