copick-utils 0.6.0__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 (72) hide show
  1. copick_utils/__init__.py +1 -0
  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 +151 -15
  43. copick_utils/converters/sphere_from_picks.py +306 -0
  44. copick_utils/converters/surface_from_picks.py +337 -0
  45. copick_utils/features/skimage.py +33 -13
  46. copick_utils/io/readers.py +62 -59
  47. copick_utils/io/writers.py +9 -14
  48. copick_utils/logical/__init__.py +43 -0
  49. copick_utils/logical/distance_operations.py +604 -0
  50. copick_utils/logical/enclosed_operations.py +222 -0
  51. copick_utils/logical/mesh_operations.py +443 -0
  52. copick_utils/logical/point_operations.py +303 -0
  53. copick_utils/logical/segmentation_operations.py +399 -0
  54. copick_utils/pickers/grid_picker.py +5 -4
  55. copick_utils/process/__init__.py +47 -0
  56. copick_utils/process/connected_components.py +360 -0
  57. copick_utils/process/filter_components.py +306 -0
  58. copick_utils/process/hull.py +106 -0
  59. copick_utils/process/skeletonize.py +326 -0
  60. copick_utils/process/spline_fitting.py +648 -0
  61. copick_utils/process/validbox.py +333 -0
  62. copick_utils/util/__init__.py +6 -0
  63. copick_utils/util/config_models.py +614 -0
  64. {copick_utils-0.6.0.dist-info → copick_utils-1.0.0.dist-info}/METADATA +38 -12
  65. copick_utils-1.0.0.dist-info/RECORD +71 -0
  66. {copick_utils-0.6.0.dist-info → copick_utils-1.0.0.dist-info}/WHEEL +1 -1
  67. copick_utils-1.0.0.dist-info/entry_points.txt +29 -0
  68. copick_utils/__about__.py +0 -4
  69. copick_utils/segmentation/picks_from_segmentation.py +0 -67
  70. copick_utils-0.6.0.dist-info/RECORD +0 -15
  71. /copick_utils/{segmentation → io}/__init__.py +0 -0
  72. /copick_utils-0.6.0.dist-info/LICENSE.txt → /copick_utils-1.0.0.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,326 @@
1
+ """3D skeletonization processing for segmentation volumes."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
4
+
5
+ import numpy as np
6
+ from copick.util.uri import get_copick_objects_by_type
7
+ from scipy import ndimage
8
+ from skimage import morphology
9
+ from skimage.morphology import remove_small_objects, skeletonize
10
+
11
+ if TYPE_CHECKING:
12
+ from copick.models import CopickRoot, CopickRun, CopickSegmentation
13
+
14
+
15
+ class TubeSkeletonizer3D:
16
+ """3D tube skeletonization class based on scikit-image."""
17
+
18
+ def __init__(self):
19
+ self.original_volume = None
20
+ self.skeleton = None
21
+ self.skeleton_coords = None
22
+
23
+ def load_volume(self, volume_array: np.ndarray):
24
+ """
25
+ Load 3D volume from array.
26
+
27
+ Args:
28
+ volume_array: 3D binary array where tube is 1, background is 0
29
+ """
30
+ self.original_volume = volume_array.astype(bool)
31
+
32
+ def preprocess_volume(self, remove_noise: bool = True, min_object_size: int = 100):
33
+ """
34
+ Preprocess the volume before skeletonization.
35
+
36
+ Args:
37
+ remove_noise: Whether to remove small objects (noise)
38
+ min_object_size: Minimum size of objects to keep
39
+ """
40
+ if remove_noise and np.any(self.original_volume):
41
+ self.original_volume = remove_small_objects(self.original_volume, min_size=min_object_size)
42
+
43
+ def skeletonize(self, method: str = "skimage"):
44
+ """
45
+ Perform 3D skeletonization.
46
+
47
+ Args:
48
+ method: Method to use ('skimage', 'distance_transform')
49
+ """
50
+ if not np.any(self.original_volume):
51
+ print("Warning: Volume is empty, creating empty skeleton")
52
+ self.skeleton = np.zeros_like(self.original_volume, dtype=bool)
53
+ self.skeleton_coords = np.array([]).reshape(0, 3)
54
+ return
55
+
56
+ if method == "skimage":
57
+ # Use scikit-image's 3D skeletonization
58
+ self.skeleton = skeletonize(self.original_volume)
59
+
60
+ elif method == "distance_transform":
61
+ # Alternative method using distance transform
62
+ # Compute distance transform
63
+ distance = ndimage.distance_transform_edt(self.original_volume)
64
+
65
+ # Find local maxima of distance transform
66
+ local_maxima = morphology.local_maxima(distance)
67
+
68
+ # Clean up the skeleton
69
+ self.skeleton = local_maxima & self.original_volume
70
+
71
+ else:
72
+ raise ValueError(f"Unknown skeletonization method: {method}")
73
+
74
+ # Get skeleton coordinates
75
+ self.skeleton_coords = np.array(np.where(self.skeleton)).T
76
+
77
+ def post_process_skeleton(self, remove_short_branches: bool = True, min_branch_length: int = 5):
78
+ """
79
+ Post-process the skeleton to remove artifacts.
80
+
81
+ Args:
82
+ remove_short_branches: Whether to remove short branches
83
+ min_branch_length: Minimum length of branches to keep
84
+ """
85
+ if remove_short_branches and len(self.skeleton_coords) > 0:
86
+ # Remove small objects from skeleton
87
+ cleaned_skeleton = remove_small_objects(self.skeleton, min_size=min_branch_length)
88
+ self.skeleton = cleaned_skeleton
89
+ self.skeleton_coords = np.array(np.where(self.skeleton)).T
90
+
91
+ def get_skeleton_properties(self) -> Dict[str, Any]:
92
+ """
93
+ Calculate properties of the skeleton.
94
+
95
+ Returns:
96
+ Dict of skeleton properties
97
+ """
98
+ if self.skeleton_coords is None or len(self.skeleton_coords) == 0:
99
+ return {"n_voxels": 0, "bounding_box": {"min": None, "max": None}}
100
+
101
+ properties = {
102
+ "n_voxels": len(self.skeleton_coords),
103
+ "bounding_box": {
104
+ "min": np.min(self.skeleton_coords, axis=0).tolist(),
105
+ "max": np.max(self.skeleton_coords, axis=0).tolist(),
106
+ },
107
+ }
108
+
109
+ return properties
110
+
111
+
112
+ def skeletonize_segmentation(
113
+ segmentation: "CopickSegmentation",
114
+ method: str = "skimage",
115
+ remove_noise: bool = True,
116
+ min_object_size: int = 50,
117
+ remove_short_branches: bool = True,
118
+ min_branch_length: int = 5,
119
+ output_session_id: Optional[str] = None,
120
+ output_user_id: str = "skel",
121
+ ) -> Optional["CopickSegmentation"]:
122
+ """
123
+ Skeletonize a segmentation volume.
124
+
125
+ Args:
126
+ segmentation: Input segmentation to skeletonize
127
+ method: Skeletonization method ('skimage', 'distance_transform')
128
+ remove_noise: Whether to remove small objects before skeletonization
129
+ min_object_size: Minimum size of objects to keep during preprocessing
130
+ remove_short_branches: Whether to remove short branches from skeleton
131
+ min_branch_length: Minimum length of branches to keep
132
+ output_session_id: Session ID for output segmentation (default: same as input)
133
+ output_user_id: User ID for output segmentation
134
+
135
+ Returns:
136
+ Created skeleton segmentation or None if failed
137
+ """
138
+ # Get the segmentation volume
139
+ volume = segmentation.numpy()
140
+ if volume is None:
141
+ print(f"Error: Could not load segmentation data for {segmentation.run.name}")
142
+ return None
143
+
144
+ run = segmentation.run
145
+ voxel_size = segmentation.voxel_size
146
+ name = segmentation.name
147
+
148
+ # Use input session_id if no output session_id specified
149
+ if output_session_id is None:
150
+ output_session_id = segmentation.session_id
151
+
152
+ print(f"Skeletonizing segmentation {segmentation.session_id} in run {run.name}")
153
+
154
+ # Initialize skeletonizer
155
+ skeletonizer = TubeSkeletonizer3D()
156
+
157
+ # Load volume
158
+ skeletonizer.load_volume(volume)
159
+
160
+ # Preprocess
161
+ skeletonizer.preprocess_volume(remove_noise=remove_noise, min_object_size=min_object_size)
162
+
163
+ # Skeletonize
164
+ skeletonizer.skeletonize(method=method)
165
+
166
+ # Post-process
167
+ skeletonizer.post_process_skeleton(remove_short_branches=remove_short_branches, min_branch_length=min_branch_length)
168
+
169
+ # Get properties
170
+ properties = skeletonizer.get_skeleton_properties()
171
+ print(f"Skeleton properties: {properties['n_voxels']} voxels")
172
+
173
+ # Create output segmentation
174
+ try:
175
+ output_seg = run.new_segmentation(
176
+ voxel_size=voxel_size,
177
+ name=name,
178
+ session_id=output_session_id,
179
+ is_multilabel=False,
180
+ user_id=output_user_id,
181
+ exist_ok=True,
182
+ )
183
+
184
+ # Store the skeleton volume
185
+ output_seg.from_numpy(skeletonizer.skeleton.astype(np.uint8))
186
+
187
+ print(f"Created skeleton segmentation with session_id: {output_session_id}")
188
+ return output_seg
189
+
190
+ except Exception as e:
191
+ print(f"Error creating skeleton segmentation: {e}")
192
+ return None
193
+
194
+
195
+ def _skeletonize_worker(
196
+ run: "CopickRun",
197
+ segmentation_name: str,
198
+ segmentation_user_id: str,
199
+ session_id_pattern: str,
200
+ method: str,
201
+ remove_noise: bool,
202
+ min_object_size: int,
203
+ remove_short_branches: bool,
204
+ min_branch_length: int,
205
+ output_session_id_template: Optional[str],
206
+ output_user_id: str,
207
+ ) -> Dict[str, Any]:
208
+ """Worker function for batch skeletonization."""
209
+ try:
210
+ # Find matching segmentations using copick's official URI resolution
211
+ matching_segmentations = get_copick_objects_by_type(
212
+ root=run.root,
213
+ object_type="segmentation",
214
+ run_name=run.name,
215
+ name=segmentation_name,
216
+ user_id=segmentation_user_id,
217
+ session_id=session_id_pattern,
218
+ pattern_type="glob",
219
+ )
220
+
221
+ if not matching_segmentations:
222
+ return {
223
+ "processed": 0,
224
+ "errors": [f"No segmentations found matching pattern '{session_id_pattern}' in {run.name}"],
225
+ "skeletons_created": 0,
226
+ }
227
+
228
+ skeletons_created = 0
229
+ errors = []
230
+
231
+ for segmentation in matching_segmentations:
232
+ # Determine output session ID
233
+ if output_session_id_template:
234
+ # Replace placeholders in template
235
+ output_session_id = output_session_id_template.replace("{input_session_id}", segmentation.session_id)
236
+ else:
237
+ output_session_id = segmentation.session_id
238
+
239
+ # Skeletonize
240
+ skeleton_seg = skeletonize_segmentation(
241
+ segmentation=segmentation,
242
+ method=method,
243
+ remove_noise=remove_noise,
244
+ min_object_size=min_object_size,
245
+ remove_short_branches=remove_short_branches,
246
+ min_branch_length=min_branch_length,
247
+ output_session_id=output_session_id,
248
+ output_user_id=output_user_id,
249
+ )
250
+
251
+ if skeleton_seg:
252
+ skeletons_created += 1
253
+ else:
254
+ errors.append(f"Failed to skeletonize {segmentation.session_id}")
255
+
256
+ return {
257
+ "processed": 1,
258
+ "errors": errors,
259
+ "skeletons_created": skeletons_created,
260
+ "segmentations_processed": len(matching_segmentations),
261
+ }
262
+
263
+ except Exception as e:
264
+ return {"processed": 0, "errors": [f"Error processing {run.name}: {e}"], "skeletons_created": 0}
265
+
266
+
267
+ def skeletonize_batch(
268
+ root: "CopickRoot",
269
+ segmentation_name: str,
270
+ segmentation_user_id: str,
271
+ session_id_pattern: str,
272
+ method: str = "skimage",
273
+ remove_noise: bool = True,
274
+ min_object_size: int = 50,
275
+ remove_short_branches: bool = True,
276
+ min_branch_length: int = 5,
277
+ output_session_id_template: Optional[str] = None,
278
+ output_user_id: str = "skel",
279
+ run_names: Optional[List[str]] = None,
280
+ workers: int = 8,
281
+ ) -> Dict[str, Any]:
282
+ """
283
+ Batch skeletonize segmentations across multiple runs.
284
+
285
+ Args:
286
+ root: The copick root containing runs to process
287
+ segmentation_name: Name of the segmentations to process
288
+ segmentation_user_id: User ID of the segmentations to process
289
+ session_id_pattern: Regex pattern or exact session ID to match segmentations
290
+ method: Skeletonization method ('skimage', 'distance_transform'). Default is 'skimage'.
291
+ remove_noise: Whether to remove small objects before skeletonization. Default is True.
292
+ min_object_size: Minimum size of objects to keep during preprocessing. Default is 50.
293
+ remove_short_branches: Whether to remove short branches from skeleton. Default is True.
294
+ min_branch_length: Minimum length of branches to keep. Default is 5.
295
+ output_session_id_template: Template for output session IDs. Use {input_session_id} as placeholder.
296
+ If None, uses the same session ID as input.
297
+ output_user_id: User ID for output segmentations. Default is "skel".
298
+ run_names: List of run names to process. If None, processes all runs.
299
+ workers: Number of worker processes. Default is 8.
300
+
301
+ Returns:
302
+ Dictionary with processing results and statistics
303
+ """
304
+ from copick.ops.run import map_runs
305
+
306
+ runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
307
+
308
+ results = map_runs(
309
+ callback=_skeletonize_worker,
310
+ root=root,
311
+ runs=runs_to_process,
312
+ workers=workers,
313
+ task_desc="Skeletonizing segmentations",
314
+ segmentation_name=segmentation_name,
315
+ segmentation_user_id=segmentation_user_id,
316
+ session_id_pattern=session_id_pattern,
317
+ method=method,
318
+ remove_noise=remove_noise,
319
+ min_object_size=min_object_size,
320
+ remove_short_branches=remove_short_branches,
321
+ min_branch_length=min_branch_length,
322
+ output_session_id_template=output_session_id_template,
323
+ output_user_id=output_user_id,
324
+ )
325
+
326
+ return results