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.
- copick_utils/__init__.py +1 -0
- 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 +151 -15
- copick_utils/converters/sphere_from_picks.py +306 -0
- copick_utils/converters/surface_from_picks.py +337 -0
- copick_utils/features/skimage.py +33 -13
- copick_utils/io/readers.py +62 -59
- copick_utils/io/writers.py +9 -14
- 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/pickers/grid_picker.py +5 -4
- 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.0.dist-info → copick_utils-1.0.0.dist-info}/METADATA +38 -12
- copick_utils-1.0.0.dist-info/RECORD +71 -0
- {copick_utils-0.6.0.dist-info → copick_utils-1.0.0.dist-info}/WHEEL +1 -1
- copick_utils-1.0.0.dist-info/entry_points.txt +29 -0
- copick_utils/__about__.py +0 -4
- copick_utils/segmentation/picks_from_segmentation.py +0 -67
- copick_utils-0.6.0.dist-info/RECORD +0 -15
- /copick_utils/{segmentation → io}/__init__.py +0 -0
- /copick_utils-0.6.0.dist-info/LICENSE.txt → /copick_utils-1.0.0.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
|
+
)
|