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.
- 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.0.dist-info}/METADATA +15 -2
- copick_utils-1.0.0.dist-info/RECORD +71 -0
- copick_utils-1.0.0.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.0.dist-info}/WHEEL +0 -0
- {copick_utils-0.6.1.dist-info → 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
|