copick-utils 0.6.1__py3-none-any.whl → 1.0.0__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.0.dist-info}/METADATA +15 -2
  61. copick_utils-1.0.0.dist-info/RECORD +71 -0
  62. copick_utils-1.0.0.dist-info/entry_points.txt +29 -0
  63. copick_utils/segmentation/picks_from_segmentation.py +0 -81
  64. copick_utils-0.6.1.dist-info/RECORD +0 -14
  65. /copick_utils/{segmentation → io}/__init__.py +0 -0
  66. {copick_utils-0.6.1.dist-info → copick_utils-1.0.0.dist-info}/WHEEL +0 -0
  67. {copick_utils-0.6.1.dist-info → copick_utils-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,47 @@
1
+ """Segmentation processing utilities for copick."""
2
+
3
+ from .connected_components import (
4
+ extract_individual_components,
5
+ print_component_stats,
6
+ separate_components_batch,
7
+ separate_connected_components_3d,
8
+ separate_segmentation_components,
9
+ )
10
+ from .skeletonize import (
11
+ TubeSkeletonizer3D,
12
+ find_matching_segmentations,
13
+ skeletonize_batch,
14
+ skeletonize_segmentation,
15
+ )
16
+ from .spline_fitting import (
17
+ SkeletonSplineFitter,
18
+ find_matching_segmentations_for_spline,
19
+ fit_spline_batch,
20
+ fit_spline_to_segmentation,
21
+ fit_spline_to_skeleton,
22
+ )
23
+ from .validbox import (
24
+ create_validbox_mesh,
25
+ generate_validbox,
26
+ validbox_batch,
27
+ )
28
+
29
+ __all__ = [
30
+ "separate_connected_components_3d",
31
+ "extract_individual_components",
32
+ "print_component_stats",
33
+ "separate_segmentation_components",
34
+ "separate_components_batch",
35
+ "TubeSkeletonizer3D",
36
+ "skeletonize_segmentation",
37
+ "find_matching_segmentations",
38
+ "skeletonize_batch",
39
+ "SkeletonSplineFitter",
40
+ "fit_spline_to_skeleton",
41
+ "fit_spline_to_segmentation",
42
+ "find_matching_segmentations_for_spline",
43
+ "fit_spline_batch",
44
+ "create_validbox_mesh",
45
+ "generate_validbox",
46
+ "validbox_batch",
47
+ ]
@@ -0,0 +1,360 @@
1
+ """Connected components processing for segmentation volumes."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple, Union
4
+
5
+ import numpy as np
6
+ from scipy import ndimage
7
+ from skimage import measure
8
+
9
+ if TYPE_CHECKING:
10
+ from copick.models import CopickRoot, CopickRun, CopickSegmentation
11
+
12
+
13
+ def separate_connected_components_3d(
14
+ volume: np.ndarray,
15
+ voxel_spacing: float,
16
+ connectivity: Union[int, str] = "all",
17
+ min_size: Optional[float] = None,
18
+ ) -> Tuple[np.ndarray, int, Dict[int, Dict[str, Any]]]:
19
+ """
20
+ Separate connected components in a 3D binary or labeled volume.
21
+
22
+ Args:
23
+ volume: 3D binary or labeled segmentation volume
24
+ voxel_spacing: Voxel spacing in angstroms
25
+ connectivity: Connectivity for connected components (default: "all")
26
+ String format: "face" (6-connected), "face-edge" (18-connected), "all" (26-connected)
27
+ Legacy int format: 6, 18, or 26 (for backward compatibility)
28
+ min_size: Minimum component volume in cubic angstroms (ų) to keep (None = keep all)
29
+
30
+ Returns:
31
+ Tuple of (labeled_volume, num_components, component_info):
32
+ - labeled_volume: Volume with each connected component labeled with unique integer
33
+ - num_components: Number of connected components found
34
+ - component_info: Dictionary with information about each component
35
+ """
36
+ # Convert to binary if not already
37
+ binary_volume = volume > 0 if volume.dtype != bool else volume.copy()
38
+
39
+ # Map connectivity to integer (support both string and legacy int format)
40
+ if isinstance(connectivity, str):
41
+ connectivity_map = {
42
+ "face": 6,
43
+ "face-edge": 18,
44
+ "all": 26,
45
+ }
46
+ connectivity_int = connectivity_map.get(connectivity, 26)
47
+ else:
48
+ connectivity_int = connectivity
49
+
50
+ # Define connectivity structure
51
+ if connectivity_int == 6:
52
+ structure = ndimage.generate_binary_structure(3, 1) # faces only
53
+ elif connectivity_int == 18:
54
+ structure = ndimage.generate_binary_structure(3, 2) # faces + edges
55
+ elif connectivity_int == 26:
56
+ structure = ndimage.generate_binary_structure(3, 3) # all neighbors
57
+ else:
58
+ raise ValueError("Connectivity must be 6, 18, or 26 (or 'face', 'face-edge', 'all')")
59
+
60
+ # Label connected components
61
+ labeled_volume, num_components = ndimage.label(binary_volume, structure=structure)
62
+
63
+ print(f"Found {num_components} connected components")
64
+
65
+ # Get component properties
66
+ component_info = {}
67
+ props = measure.regionprops(labeled_volume)
68
+
69
+ print(f"Found {len(props)} connected components")
70
+
71
+ # Calculate voxel volume in cubic angstroms
72
+ voxel_volume = voxel_spacing**3
73
+
74
+ # Filter by size if specified
75
+ if min_size is not None and min_size > 0:
76
+ for prop in props:
77
+ component_volume = prop.area * voxel_volume
78
+ if component_volume < min_size:
79
+ labeled_volume[labeled_volume == prop.label] = 0
80
+
81
+ # Relabel after filtering
82
+ labeled_volume, num_components = ndimage.label(labeled_volume > 0, structure=structure)
83
+ props = measure.regionprops(labeled_volume)
84
+ print(f"After filtering by size (min={min_size} ų): {num_components} components")
85
+
86
+ # Store component information
87
+ for _i, prop in enumerate(props, 1):
88
+ component_info[prop.label] = {
89
+ "volume": prop.area, # number of voxels
90
+ "centroid": prop.centroid,
91
+ "bbox": prop.bbox, # (min_z, min_y, min_x, max_z, max_y, max_x)
92
+ "extent": prop.extent, # ratio of component area to bounding box area
93
+ }
94
+
95
+ return labeled_volume, num_components, component_info
96
+
97
+
98
+ def extract_individual_components(labeled_volume: np.ndarray) -> List[np.ndarray]:
99
+ """
100
+ Extract each connected component as a separate binary volume.
101
+
102
+ Args:
103
+ labeled_volume: Volume with labeled connected components
104
+
105
+ Returns:
106
+ List of binary volumes, each containing one component
107
+ """
108
+ unique_labels = np.unique(labeled_volume)
109
+ unique_labels = unique_labels[unique_labels > 0] # exclude background (0)
110
+
111
+ components = []
112
+ for label in unique_labels:
113
+ component = (labeled_volume == label).astype(np.uint8)
114
+ components.append(component)
115
+
116
+ return components
117
+
118
+
119
+ def print_component_stats(component_info: Dict[int, Dict[str, Any]]) -> None:
120
+ """Print statistics about connected components."""
121
+ print("\nComponent Statistics:")
122
+ print("-" * 60)
123
+ print(f"{'Label':<8} {'Volume':<10} {'Centroid (z,y,x)':<25} {'Extent':<10}")
124
+ print("-" * 60)
125
+
126
+ for label, info in component_info.items():
127
+ centroid_str = f"({info['centroid'][0]:.1f},{info['centroid'][1]:.1f},{info['centroid'][2]:.1f})"
128
+ print(f"{label:<8} {info['volume']:<10} {centroid_str:<25} {info['extent']:<10.3f}")
129
+
130
+
131
+ def separate_segmentation_components(
132
+ segmentation: "CopickSegmentation",
133
+ connectivity: Union[int, str] = "all",
134
+ min_size: Optional[float] = None,
135
+ session_id_template: str = "inst-{instance_id}",
136
+ output_user_id: str = "components",
137
+ multilabel: bool = True,
138
+ session_id_prefix: str = None, # Deprecated, kept for backward compatibility
139
+ ) -> List["CopickSegmentation"]:
140
+ """
141
+ Separate connected components in a segmentation into individual segmentations.
142
+
143
+ Args:
144
+ segmentation: Input segmentation to process
145
+ connectivity: Connectivity for connected components (default: "all")
146
+ String format: "face" (6-connected), "face-edge" (18-connected), "all" (26-connected)
147
+ Legacy int format: 6, 18, or 26 (for backward compatibility)
148
+ min_size: Minimum component volume in cubic angstroms (ų) to keep (None = keep all)
149
+ session_id_template: Template for output session IDs with {instance_id} placeholder
150
+ output_user_id: User ID for output segmentations
151
+ multilabel: Whether to treat input as multilabel segmentation
152
+ session_id_prefix: Deprecated. Use session_id_template instead.
153
+
154
+ Returns:
155
+ List of created segmentations, one per component
156
+ """
157
+ # Handle deprecated session_id_prefix parameter
158
+ if session_id_prefix is not None:
159
+ session_id_template = f"{session_id_prefix}{{instance_id}}"
160
+ # Get the segmentation volume
161
+ volume = segmentation.numpy()
162
+ if volume is None:
163
+ raise ValueError("Could not load segmentation data")
164
+
165
+ run = segmentation.run
166
+ voxel_size = segmentation.voxel_size
167
+ name = segmentation.name
168
+
169
+ output_segmentations = []
170
+ component_count = 0
171
+
172
+ if multilabel:
173
+ # Process each label separately
174
+ unique_labels = np.unique(volume)
175
+ unique_labels = unique_labels[unique_labels > 0] # skip background
176
+
177
+ print(f"Processing multilabel segmentation with {len(unique_labels)} labels")
178
+
179
+ for label_value in unique_labels:
180
+ print(f"Processing label {label_value}")
181
+
182
+ # Extract binary volume for this label
183
+ binary_vol = volume == label_value
184
+
185
+ # Separate connected components
186
+ labeled_vol, n_components, component_info = separate_connected_components_3d(
187
+ binary_vol,
188
+ voxel_spacing=voxel_size,
189
+ connectivity=connectivity,
190
+ min_size=min_size,
191
+ )
192
+
193
+ # Extract individual components
194
+ individual_components = extract_individual_components(labeled_vol)
195
+
196
+ # Create segmentations for each component
197
+ for component_vol in individual_components:
198
+ session_id = session_id_template.replace("{instance_id}", str(component_count))
199
+
200
+ # Create new segmentation
201
+ output_seg = run.new_segmentation(
202
+ voxel_size=voxel_size,
203
+ name=name,
204
+ session_id=session_id,
205
+ is_multilabel=False,
206
+ user_id=output_user_id,
207
+ exist_ok=True,
208
+ )
209
+
210
+ # Store the component volume
211
+ output_seg.from_numpy(component_vol)
212
+ output_segmentations.append(output_seg)
213
+ component_count += 1
214
+
215
+ else:
216
+ # Process as binary segmentation
217
+ print("Processing binary segmentation")
218
+
219
+ # Separate connected components
220
+ labeled_vol, n_components, component_info = separate_connected_components_3d(
221
+ volume,
222
+ voxel_spacing=voxel_size,
223
+ connectivity=connectivity,
224
+ min_size=min_size,
225
+ )
226
+
227
+ # Extract individual components
228
+ individual_components = extract_individual_components(labeled_vol)
229
+
230
+ # Create segmentations for each component
231
+ for component_vol in individual_components:
232
+ session_id = session_id_template.replace("{instance_id}", str(component_count))
233
+
234
+ # Create new segmentation
235
+ output_seg = run.new_segmentation(
236
+ voxel_size=voxel_size,
237
+ name=name,
238
+ session_id=session_id,
239
+ is_multilabel=False,
240
+ user_id=output_user_id,
241
+ exist_ok=True,
242
+ )
243
+
244
+ # Store the component volume
245
+ output_seg.from_numpy(component_vol)
246
+ output_segmentations.append(output_seg)
247
+ component_count += 1
248
+
249
+ print(f"Created {len(output_segmentations)} component segmentations")
250
+ return output_segmentations
251
+
252
+
253
+ def _separate_components_worker(
254
+ run: "CopickRun",
255
+ segmentation_name: str,
256
+ segmentation_user_id: str,
257
+ segmentation_session_id: str,
258
+ connectivity: Union[int, str],
259
+ min_size: Optional[float],
260
+ session_id_template: str,
261
+ output_user_id: str,
262
+ multilabel: bool,
263
+ root: "CopickRoot",
264
+ ) -> Dict[str, Any]:
265
+ """Worker function for batch connected components separation."""
266
+ try:
267
+ # Get segmentation
268
+ segmentations = run.get_segmentations(
269
+ name=segmentation_name,
270
+ user_id=segmentation_user_id,
271
+ session_id=segmentation_session_id,
272
+ )
273
+
274
+ if not segmentations:
275
+ return {"processed": 0, "errors": [f"No segmentation found for {run.name}"]}
276
+
277
+ segmentation = segmentations[0]
278
+
279
+ # Separate components
280
+ output_segmentations = separate_segmentation_components(
281
+ segmentation=segmentation,
282
+ connectivity=connectivity,
283
+ min_size=min_size,
284
+ session_id_template=session_id_template,
285
+ output_user_id=output_user_id,
286
+ multilabel=multilabel,
287
+ )
288
+
289
+ return {
290
+ "processed": 1,
291
+ "errors": [],
292
+ "components_created": len(output_segmentations),
293
+ "segmentations": output_segmentations,
294
+ }
295
+
296
+ except Exception as e:
297
+ return {"processed": 0, "errors": [f"Error processing {run.name}: {e}"]}
298
+
299
+
300
+ def separate_components_batch(
301
+ root: "CopickRoot",
302
+ segmentation_name: str,
303
+ segmentation_user_id: str,
304
+ segmentation_session_id: str,
305
+ connectivity: Union[int, str] = "all",
306
+ min_size: Optional[float] = None,
307
+ session_id_template: str = "inst-{instance_id}",
308
+ output_user_id: str = "components",
309
+ multilabel: bool = True,
310
+ run_names: Optional[List[str]] = None,
311
+ workers: int = 8,
312
+ session_id_prefix: str = None, # Deprecated, kept for backward compatibility
313
+ ) -> Dict[str, Any]:
314
+ """
315
+ Batch separate connected components across multiple runs.
316
+
317
+ Args:
318
+ root: The copick root containing runs to process
319
+ segmentation_name: Name of the segmentation to process
320
+ segmentation_user_id: User ID of the segmentation to process
321
+ segmentation_session_id: Session ID of the segmentation to process
322
+ connectivity: Connectivity for connected components (default: "all")
323
+ String format: "face" (6-connected), "face-edge" (18-connected), "all" (26-connected)
324
+ Legacy int format: 6, 18, or 26 (for backward compatibility)
325
+ min_size: Minimum component volume in cubic angstroms (ų) to keep (None = keep all)
326
+ session_id_template: Template for output session IDs with {instance_id} placeholder. Default is "inst-{instance_id}".
327
+ output_user_id: User ID for output segmentations. Default is "components".
328
+ multilabel: Whether to treat input as multilabel segmentation. Default is True.
329
+ run_names: List of run names to process. If None, processes all runs.
330
+ workers: Number of worker processes. Default is 8.
331
+ session_id_prefix: Deprecated. Use session_id_template instead.
332
+
333
+ Returns:
334
+ Dictionary with processing results and statistics
335
+ """
336
+ from copick.ops.run import map_runs
337
+
338
+ # Handle deprecated session_id_prefix parameter
339
+ if session_id_prefix is not None:
340
+ session_id_template = f"{session_id_prefix}{{instance_id}}"
341
+
342
+ runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
343
+
344
+ results = map_runs(
345
+ callback=_separate_components_worker,
346
+ root=root,
347
+ runs=runs_to_process,
348
+ workers=workers,
349
+ task_desc="Separating connected components",
350
+ segmentation_name=segmentation_name,
351
+ segmentation_user_id=segmentation_user_id,
352
+ segmentation_session_id=segmentation_session_id,
353
+ connectivity=connectivity,
354
+ min_size=min_size,
355
+ session_id_template=session_id_template,
356
+ output_user_id=output_user_id,
357
+ multilabel=multilabel,
358
+ )
359
+
360
+ return results