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
|
@@ -1,13 +1,29 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Dict, Optional, Tuple
|
|
2
|
+
|
|
1
3
|
import numpy as np
|
|
2
4
|
import zarr
|
|
5
|
+
from copick.util.log import get_logger
|
|
3
6
|
from scipy.ndimage import zoom
|
|
4
|
-
import copick
|
|
5
7
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
from copick_utils.converters.converter_common import (
|
|
9
|
+
create_batch_converter,
|
|
10
|
+
create_batch_worker,
|
|
11
|
+
)
|
|
12
|
+
from copick_utils.converters.lazy_converter import create_lazy_batch_converter
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from copick.models import CopickObject, CopickPicks, CopickRun, CopickSegmentation
|
|
16
|
+
|
|
17
|
+
logger = get_logger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def from_picks(
|
|
21
|
+
pick: "CopickPicks",
|
|
22
|
+
seg_volume: np.ndarray,
|
|
23
|
+
radius: float = 10.0,
|
|
24
|
+
label_value: int = 1,
|
|
25
|
+
voxel_spacing: float = 10,
|
|
26
|
+
) -> np.ndarray:
|
|
11
27
|
"""
|
|
12
28
|
Paints picks into a segmentation volume as spheres.
|
|
13
29
|
|
|
@@ -26,12 +42,13 @@ def from_picks(pick,
|
|
|
26
42
|
Returns:
|
|
27
43
|
--------
|
|
28
44
|
numpy.ndarray
|
|
29
|
-
The modified segmentation volume with spheres inserted at pick locations.
|
|
45
|
+
The modified segmentation volume with spheres inserted at pick locations.
|
|
30
46
|
"""
|
|
47
|
+
|
|
31
48
|
def create_sphere(shape, center, radius, val):
|
|
32
49
|
zc, yc, xc = center
|
|
33
50
|
z, y, x = np.indices(shape)
|
|
34
|
-
distance_sq = (x - xc)**2 + (y - yc)**2 + (z - zc)**2
|
|
51
|
+
distance_sq = (x - xc) ** 2 + (y - yc) ** 2 + (z - zc) ** 2
|
|
35
52
|
sphere = np.zeros(shape, dtype=np.float32)
|
|
36
53
|
sphere[distance_sq <= radius**2] = val
|
|
37
54
|
return sphere
|
|
@@ -48,7 +65,11 @@ def from_picks(pick,
|
|
|
48
65
|
# Paint each pick as a sphere
|
|
49
66
|
for point in pick.points:
|
|
50
67
|
# Convert the pick's location from angstroms to voxel units
|
|
51
|
-
cx, cy, cz =
|
|
68
|
+
cx, cy, cz = (
|
|
69
|
+
point.location.x / voxel_spacing,
|
|
70
|
+
point.location.y / voxel_spacing,
|
|
71
|
+
point.location.z / voxel_spacing,
|
|
72
|
+
)
|
|
52
73
|
|
|
53
74
|
# Calculate subarray bounds
|
|
54
75
|
xLow, xHigh = get_relative_target_coordinates(cx, delta, seg_volume.shape[2])
|
|
@@ -65,12 +86,15 @@ def from_picks(pick,
|
|
|
65
86
|
sphere = create_sphere(subarray_shape, local_center, radius_voxel, label_value)
|
|
66
87
|
|
|
67
88
|
# Assign Sphere to Segmentation Target Volume
|
|
68
|
-
seg_volume[zLow:zHigh, yLow:yHigh, xLow:xHigh] = np.maximum(
|
|
89
|
+
seg_volume[zLow:zHigh, yLow:yHigh, xLow:xHigh] = np.maximum(
|
|
90
|
+
seg_volume[zLow:zHigh, yLow:yHigh, xLow:xHigh],
|
|
91
|
+
sphere,
|
|
92
|
+
)
|
|
69
93
|
|
|
70
94
|
return seg_volume
|
|
71
95
|
|
|
72
96
|
|
|
73
|
-
def downsample_to_exact_shape(array, target_shape):
|
|
97
|
+
def downsample_to_exact_shape(array: np.ndarray, target_shape: tuple) -> np.ndarray:
|
|
74
98
|
"""
|
|
75
99
|
Downsamples a 3D array to match the target shape using nearest-neighbor interpolation.
|
|
76
100
|
Ensures that the resulting array has the exact target shape.
|
|
@@ -79,7 +103,17 @@ def downsample_to_exact_shape(array, target_shape):
|
|
|
79
103
|
return zoom(array, zoom_factors, order=0)
|
|
80
104
|
|
|
81
105
|
|
|
82
|
-
def
|
|
106
|
+
def _create_segmentation_from_picks_legacy(
|
|
107
|
+
radius: float,
|
|
108
|
+
painting_segmentation_name: str,
|
|
109
|
+
run: "CopickRun",
|
|
110
|
+
voxel_spacing: float,
|
|
111
|
+
tomo_type: str,
|
|
112
|
+
pickable_object: "CopickObject",
|
|
113
|
+
pick_set: "CopickPicks",
|
|
114
|
+
user_id: str = "paintedPicks",
|
|
115
|
+
session_id: str = "0",
|
|
116
|
+
) -> "CopickSegmentation":
|
|
83
117
|
"""
|
|
84
118
|
Paints picks from a run into a multiscale segmentation array, representing them as spheres in 3D space.
|
|
85
119
|
|
|
@@ -115,7 +149,13 @@ def segmentation_from_picks(radius, painting_segmentation_name, run, voxel_spaci
|
|
|
115
149
|
raise ValueError("Tomogram not found for the given parameters.")
|
|
116
150
|
|
|
117
151
|
# Use copick to create a new segmentation if one does not exist
|
|
118
|
-
segs = run.get_segmentations(
|
|
152
|
+
segs = run.get_segmentations(
|
|
153
|
+
user_id=user_id,
|
|
154
|
+
session_id=session_id,
|
|
155
|
+
is_multilabel=True,
|
|
156
|
+
name=painting_segmentation_name,
|
|
157
|
+
voxel_size=voxel_spacing,
|
|
158
|
+
)
|
|
119
159
|
if len(segs) == 0:
|
|
120
160
|
seg = run.new_segmentation(voxel_spacing, painting_segmentation_name, session_id, True, user_id=user_id)
|
|
121
161
|
else:
|
|
@@ -142,7 +182,7 @@ def segmentation_from_picks(radius, painting_segmentation_name, run, voxel_spaci
|
|
|
142
182
|
segmentation_group[highest_res_name][:] = highest_res_seg
|
|
143
183
|
|
|
144
184
|
# Downsample to create lower resolution scales
|
|
145
|
-
multiscale_metadata = tomogram_zarr.attrs.get(
|
|
185
|
+
multiscale_metadata = tomogram_zarr.attrs.get("multiscales", [{}])[0].get("datasets", [])
|
|
146
186
|
for level_index, level_metadata in enumerate(multiscale_metadata):
|
|
147
187
|
if level_index == 0:
|
|
148
188
|
continue
|
|
@@ -154,8 +194,104 @@ def segmentation_from_picks(radius, painting_segmentation_name, run, voxel_spaci
|
|
|
154
194
|
scaled_array = downsample_to_exact_shape(highest_res_seg, expected_shape)
|
|
155
195
|
|
|
156
196
|
# Create/overwrite the Zarr array for this level
|
|
157
|
-
segmentation_group.create_dataset(
|
|
197
|
+
segmentation_group.create_dataset(
|
|
198
|
+
level_name,
|
|
199
|
+
shape=expected_shape,
|
|
200
|
+
data=scaled_array,
|
|
201
|
+
dtype=np.uint16,
|
|
202
|
+
overwrite=True,
|
|
203
|
+
)
|
|
158
204
|
|
|
159
205
|
segmentation_group[level_name][:] = scaled_array
|
|
160
206
|
|
|
161
207
|
return seg
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def segmentation_from_picks(
|
|
211
|
+
picks: "CopickPicks",
|
|
212
|
+
run: "CopickRun",
|
|
213
|
+
object_name: str,
|
|
214
|
+
session_id: str,
|
|
215
|
+
user_id: str,
|
|
216
|
+
radius: float,
|
|
217
|
+
voxel_spacing: float,
|
|
218
|
+
tomo_type: str = "wbp",
|
|
219
|
+
) -> Optional[Tuple["CopickSegmentation", Dict[str, int]]]:
|
|
220
|
+
"""
|
|
221
|
+
Convert CopickPicks to a segmentation by painting spheres.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
picks: CopickPicks object to convert
|
|
225
|
+
run: CopickRun object
|
|
226
|
+
object_name: Name for the output segmentation
|
|
227
|
+
session_id: Session ID for the output segmentation
|
|
228
|
+
user_id: User ID for the output segmentation
|
|
229
|
+
radius: Radius of the spheres in physical units
|
|
230
|
+
voxel_spacing: Voxel spacing for the segmentation
|
|
231
|
+
tomo_type: Type of tomogram to use as reference
|
|
232
|
+
|
|
233
|
+
Returns:
|
|
234
|
+
Tuple of (CopickSegmentation object, stats dict) or None if creation failed.
|
|
235
|
+
Stats dict contains 'points_converted' and 'voxels_created'.
|
|
236
|
+
"""
|
|
237
|
+
try:
|
|
238
|
+
# Get the pickable object for label information
|
|
239
|
+
root = run.root
|
|
240
|
+
pickable_object = root.get_object(picks.pickable_object_name)
|
|
241
|
+
if not pickable_object:
|
|
242
|
+
logger.error(f"Object '{picks.pickable_object_name}' not found in config")
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if not picks.points:
|
|
246
|
+
logger.error("No points found in pick set")
|
|
247
|
+
return None
|
|
248
|
+
|
|
249
|
+
# Create segmentation using the legacy function
|
|
250
|
+
seg = _create_segmentation_from_picks_legacy(
|
|
251
|
+
radius=radius,
|
|
252
|
+
painting_segmentation_name=object_name,
|
|
253
|
+
run=run,
|
|
254
|
+
voxel_spacing=voxel_spacing,
|
|
255
|
+
tomo_type=tomo_type,
|
|
256
|
+
pickable_object=pickable_object,
|
|
257
|
+
pick_set=picks,
|
|
258
|
+
user_id=user_id,
|
|
259
|
+
session_id=session_id,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Calculate statistics
|
|
263
|
+
# For now, we don't have easy access to the actual voxel count, so we estimate
|
|
264
|
+
# based on number of spheres and their volume
|
|
265
|
+
sphere_volume_voxels = (4 / 3) * np.pi * (radius / voxel_spacing) ** 3
|
|
266
|
+
estimated_voxels = int(len(picks.points) * sphere_volume_voxels)
|
|
267
|
+
|
|
268
|
+
stats = {
|
|
269
|
+
"points_converted": len(picks.points),
|
|
270
|
+
"voxels_created": estimated_voxels,
|
|
271
|
+
}
|
|
272
|
+
logger.info(f"Created segmentation from {stats['points_converted']} picks")
|
|
273
|
+
return seg, stats
|
|
274
|
+
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.error(f"Error creating segmentation: {e}")
|
|
277
|
+
return None
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Create worker function using common infrastructure
|
|
281
|
+
_segmentation_from_picks_worker = create_batch_worker(segmentation_from_picks, "segmentation", "picks", min_points=1)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
# Create batch converter using common infrastructure
|
|
285
|
+
segmentation_from_picks_batch = create_batch_converter(
|
|
286
|
+
segmentation_from_picks,
|
|
287
|
+
"Converting picks to segmentations",
|
|
288
|
+
"segmentation",
|
|
289
|
+
"picks",
|
|
290
|
+
min_points=1,
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
# Lazy batch converter for new architecture
|
|
294
|
+
segmentation_from_picks_lazy_batch = create_lazy_batch_converter(
|
|
295
|
+
converter_func=segmentation_from_picks,
|
|
296
|
+
task_description="Converting picks to segmentations",
|
|
297
|
+
)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Tuple
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import trimesh as tm
|
|
5
|
+
from copick.util.log import get_logger
|
|
6
|
+
from scipy.optimize import minimize
|
|
7
|
+
|
|
8
|
+
from copick_utils.converters.converter_common import (
|
|
9
|
+
cluster,
|
|
10
|
+
create_batch_converter,
|
|
11
|
+
create_batch_worker,
|
|
12
|
+
store_mesh_with_stats,
|
|
13
|
+
validate_points,
|
|
14
|
+
)
|
|
15
|
+
from copick_utils.converters.lazy_converter import create_lazy_batch_converter
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from copick.models import CopickMesh, CopickRun
|
|
19
|
+
|
|
20
|
+
logger = get_logger(__name__)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def fit_sphere_to_points(points: np.ndarray) -> Tuple[np.ndarray, float]:
|
|
24
|
+
"""Fit a sphere to a set of 3D points using least squares.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
points: Nx3 array of points.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Tuple of (center, radius).
|
|
31
|
+
"""
|
|
32
|
+
if len(points) < 4:
|
|
33
|
+
raise ValueError("Need at least 4 points to fit a sphere")
|
|
34
|
+
|
|
35
|
+
def sphere_residuals(params, points):
|
|
36
|
+
"""Calculate residuals for sphere fitting."""
|
|
37
|
+
cx, cy, cz, r = params
|
|
38
|
+
center = np.array([cx, cy, cz])
|
|
39
|
+
distances = np.linalg.norm(points - center, axis=1)
|
|
40
|
+
return distances - r
|
|
41
|
+
|
|
42
|
+
# Initial guess: center at centroid, radius as average distance to centroid
|
|
43
|
+
centroid = np.mean(points, axis=0)
|
|
44
|
+
distances = np.linalg.norm(points - centroid, axis=1)
|
|
45
|
+
initial_radius = np.mean(distances)
|
|
46
|
+
|
|
47
|
+
initial_params = [centroid[0], centroid[1], centroid[2], initial_radius]
|
|
48
|
+
|
|
49
|
+
# Fit sphere using least squares
|
|
50
|
+
result = minimize(lambda params: np.sum(sphere_residuals(params, points) ** 2), initial_params, method="L-BFGS-B")
|
|
51
|
+
|
|
52
|
+
if result.success:
|
|
53
|
+
cx, cy, cz, r = result.x
|
|
54
|
+
center = np.array([cx, cy, cz])
|
|
55
|
+
radius = abs(r) # Ensure positive radius
|
|
56
|
+
return center, radius
|
|
57
|
+
else:
|
|
58
|
+
# Fallback to simple centroid and average distance
|
|
59
|
+
radius = np.mean(np.linalg.norm(points - centroid, axis=1))
|
|
60
|
+
return centroid, radius
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def deduplicate_spheres(
|
|
64
|
+
spheres: List[Tuple[np.ndarray, float]],
|
|
65
|
+
min_distance: float = None,
|
|
66
|
+
) -> List[Tuple[np.ndarray, float]]:
|
|
67
|
+
"""Merge spheres that are too close to each other.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
spheres: List of (center, radius) tuples.
|
|
71
|
+
min_distance: Minimum distance between sphere centers. If None, uses average radius.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
List of deduplicated (center, radius) tuples.
|
|
75
|
+
"""
|
|
76
|
+
if len(spheres) <= 1:
|
|
77
|
+
return spheres
|
|
78
|
+
|
|
79
|
+
if min_distance is None:
|
|
80
|
+
# Use average radius as minimum distance
|
|
81
|
+
avg_radius = np.mean([radius for _, radius in spheres])
|
|
82
|
+
min_distance = avg_radius * 0.5
|
|
83
|
+
|
|
84
|
+
deduplicated = []
|
|
85
|
+
used = set()
|
|
86
|
+
|
|
87
|
+
for i, (center1, radius1) in enumerate(spheres):
|
|
88
|
+
if i in used:
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
# Find all spheres close to this one
|
|
92
|
+
close_spheres = [(center1, radius1)]
|
|
93
|
+
used.add(i)
|
|
94
|
+
|
|
95
|
+
for j, (center2, radius2) in enumerate(spheres):
|
|
96
|
+
if j in used or i == j:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
distance = np.linalg.norm(center1 - center2)
|
|
100
|
+
if distance <= min_distance:
|
|
101
|
+
close_spheres.append((center2, radius2))
|
|
102
|
+
used.add(j)
|
|
103
|
+
|
|
104
|
+
if len(close_spheres) == 1:
|
|
105
|
+
# Single sphere, keep as is
|
|
106
|
+
deduplicated.append((center1, radius1))
|
|
107
|
+
else:
|
|
108
|
+
# Merge multiple close spheres
|
|
109
|
+
centers = np.array([center for center, _ in close_spheres])
|
|
110
|
+
radii = np.array([radius for _, radius in close_spheres])
|
|
111
|
+
|
|
112
|
+
# Use weighted average for center (weight by volume)
|
|
113
|
+
volumes = (4 / 3) * np.pi * radii**3
|
|
114
|
+
weights = volumes / np.sum(volumes)
|
|
115
|
+
merged_center = np.average(centers, axis=0, weights=weights)
|
|
116
|
+
|
|
117
|
+
# Use volume-weighted average for radius
|
|
118
|
+
merged_radius = np.average(radii, weights=weights)
|
|
119
|
+
|
|
120
|
+
deduplicated.append((merged_center, merged_radius))
|
|
121
|
+
logger.info(f"Merged {len(close_spheres)} overlapping spheres into one")
|
|
122
|
+
|
|
123
|
+
return deduplicated
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def create_sphere_mesh(center: np.ndarray, radius: float, subdivisions: int = 2) -> tm.Trimesh:
|
|
127
|
+
"""Create a sphere mesh with given center and radius.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
center: 3D center point.
|
|
131
|
+
radius: Sphere radius.
|
|
132
|
+
subdivisions: Number of subdivisions for sphere resolution.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Trimesh sphere object.
|
|
136
|
+
"""
|
|
137
|
+
# Create unit sphere and scale/translate
|
|
138
|
+
sphere = tm.creation.icosphere(subdivisions=subdivisions, radius=radius)
|
|
139
|
+
sphere.apply_translation(center)
|
|
140
|
+
return sphere
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def sphere_from_picks(
|
|
144
|
+
points: np.ndarray,
|
|
145
|
+
run: "CopickRun",
|
|
146
|
+
object_name: str,
|
|
147
|
+
session_id: str,
|
|
148
|
+
user_id: str,
|
|
149
|
+
use_clustering: bool = False,
|
|
150
|
+
clustering_method: str = "dbscan",
|
|
151
|
+
clustering_params: Optional[Dict[str, Any]] = None,
|
|
152
|
+
subdivisions: int = 2,
|
|
153
|
+
all_clusters: bool = False,
|
|
154
|
+
deduplicate_spheres_flag: bool = True,
|
|
155
|
+
min_sphere_distance: Optional[float] = None,
|
|
156
|
+
individual_meshes: bool = False,
|
|
157
|
+
session_id_template: Optional[str] = None,
|
|
158
|
+
) -> Optional[Tuple["CopickMesh", Dict[str, int]]]:
|
|
159
|
+
"""Create sphere mesh(es) from pick points.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
points: Nx3 array of pick positions.
|
|
163
|
+
run: Copick run object.
|
|
164
|
+
object_name: Name of the mesh object.
|
|
165
|
+
session_id: Session ID for the mesh.
|
|
166
|
+
user_id: User ID for the mesh.
|
|
167
|
+
use_clustering: Whether to cluster points first.
|
|
168
|
+
clustering_method: Clustering method ('dbscan', 'kmeans').
|
|
169
|
+
clustering_params: Parameters for clustering.
|
|
170
|
+
e.g.
|
|
171
|
+
- {'eps': 5.0, 'min_samples': 3} for DBSCAN
|
|
172
|
+
- {'n_clusters': 3} for KMeans
|
|
173
|
+
subdivisions: Number of subdivisions for sphere resolution.
|
|
174
|
+
all_clusters: If True and clustering is used, use all clusters. If False, use only largest cluster.
|
|
175
|
+
deduplicate_spheres_flag: Whether to merge overlapping spheres.
|
|
176
|
+
min_sphere_distance: Minimum distance between sphere centers for deduplication.
|
|
177
|
+
individual_meshes: If True, create separate mesh objects for each sphere.
|
|
178
|
+
session_id_template: Template for individual mesh session IDs.
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Tuple of (CopickMesh object, stats dict) or None if creation failed.
|
|
182
|
+
Stats dict contains 'vertices_created' and 'faces_created' totals.
|
|
183
|
+
"""
|
|
184
|
+
if not validate_points(points, 4, "sphere"):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
if clustering_params is None:
|
|
188
|
+
clustering_params = {}
|
|
189
|
+
|
|
190
|
+
# Handle clustering workflow with special sphere logic
|
|
191
|
+
if use_clustering:
|
|
192
|
+
point_clusters = cluster(points, clustering_method, 4, **clustering_params)
|
|
193
|
+
|
|
194
|
+
if not point_clusters:
|
|
195
|
+
logger.warning("No valid clusters found")
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
logger.info(f"Found {len(point_clusters)} clusters")
|
|
199
|
+
|
|
200
|
+
if all_clusters and len(point_clusters) > 1:
|
|
201
|
+
# Create sphere parameters from all clusters
|
|
202
|
+
sphere_params = []
|
|
203
|
+
for i, cluster_points in enumerate(point_clusters):
|
|
204
|
+
try:
|
|
205
|
+
center, radius = fit_sphere_to_points(cluster_points)
|
|
206
|
+
sphere_params.append((center, radius))
|
|
207
|
+
logger.info(f"Cluster {i}: sphere at {center} with radius {radius:.2f}")
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.critical(f"Failed to fit sphere to cluster {i}: {e}")
|
|
210
|
+
continue
|
|
211
|
+
|
|
212
|
+
if not sphere_params:
|
|
213
|
+
logger.warning("No valid spheres created from clusters")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
# Deduplicate overlapping spheres if requested
|
|
217
|
+
if deduplicate_spheres_flag:
|
|
218
|
+
final_spheres = deduplicate_spheres(sphere_params, min_sphere_distance)
|
|
219
|
+
else:
|
|
220
|
+
final_spheres = sphere_params
|
|
221
|
+
|
|
222
|
+
if individual_meshes:
|
|
223
|
+
# Create separate mesh objects for each sphere
|
|
224
|
+
created_meshes = []
|
|
225
|
+
total_vertices = 0
|
|
226
|
+
total_faces = 0
|
|
227
|
+
|
|
228
|
+
for i, (center, radius) in enumerate(final_spheres):
|
|
229
|
+
sphere_mesh = create_sphere_mesh(center, radius, subdivisions)
|
|
230
|
+
|
|
231
|
+
# Generate session ID using template if provided
|
|
232
|
+
if session_id_template:
|
|
233
|
+
sphere_session_id = session_id_template.format(
|
|
234
|
+
base_session_id=session_id,
|
|
235
|
+
instance_id=i,
|
|
236
|
+
)
|
|
237
|
+
else:
|
|
238
|
+
sphere_session_id = f"{session_id}-{i:03d}"
|
|
239
|
+
|
|
240
|
+
try:
|
|
241
|
+
copick_mesh = run.new_mesh(object_name, sphere_session_id, user_id, exist_ok=True)
|
|
242
|
+
copick_mesh.mesh = sphere_mesh
|
|
243
|
+
copick_mesh.store()
|
|
244
|
+
created_meshes.append(copick_mesh)
|
|
245
|
+
total_vertices += len(sphere_mesh.vertices)
|
|
246
|
+
total_faces += len(sphere_mesh.faces)
|
|
247
|
+
logger.info(f"Created individual sphere mesh {i} with {len(sphere_mesh.vertices)} vertices")
|
|
248
|
+
except Exception as e:
|
|
249
|
+
logger.error(f"Failed to create mesh {i}: {e}")
|
|
250
|
+
continue
|
|
251
|
+
|
|
252
|
+
# Return the first mesh and total stats
|
|
253
|
+
if created_meshes:
|
|
254
|
+
stats = {"vertices_created": total_vertices, "faces_created": total_faces}
|
|
255
|
+
return created_meshes[0], stats
|
|
256
|
+
else:
|
|
257
|
+
return None
|
|
258
|
+
else:
|
|
259
|
+
# Create meshes from final spheres and combine them
|
|
260
|
+
all_meshes = []
|
|
261
|
+
for center, radius in final_spheres:
|
|
262
|
+
sphere_mesh = create_sphere_mesh(center, radius, subdivisions)
|
|
263
|
+
all_meshes.append(sphere_mesh)
|
|
264
|
+
|
|
265
|
+
# Combine all meshes
|
|
266
|
+
combined_mesh = tm.util.concatenate(all_meshes)
|
|
267
|
+
else:
|
|
268
|
+
# Use largest cluster
|
|
269
|
+
cluster_sizes = [len(cluster) for cluster in point_clusters]
|
|
270
|
+
largest_cluster_idx = np.argmax(cluster_sizes)
|
|
271
|
+
points_to_use = point_clusters[largest_cluster_idx]
|
|
272
|
+
logger.info(f"Using largest cluster with {len(points_to_use)} points")
|
|
273
|
+
|
|
274
|
+
center, radius = fit_sphere_to_points(points_to_use)
|
|
275
|
+
combined_mesh = create_sphere_mesh(center, radius, subdivisions)
|
|
276
|
+
else:
|
|
277
|
+
# Fit single sphere to all points
|
|
278
|
+
center, radius = fit_sphere_to_points(points)
|
|
279
|
+
combined_mesh = create_sphere_mesh(center, radius, subdivisions)
|
|
280
|
+
logger.info(f"Fitted sphere at {center} with radius {radius:.2f}")
|
|
281
|
+
|
|
282
|
+
# Store mesh and return stats
|
|
283
|
+
try:
|
|
284
|
+
return store_mesh_with_stats(run, combined_mesh, object_name, session_id, user_id, "sphere")
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.critical(f"Error creating mesh: {e}")
|
|
287
|
+
return None
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
# Create worker function using common infrastructure
|
|
291
|
+
_sphere_from_picks_worker = create_batch_worker(sphere_from_picks, "sphere", min_points=4)
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
# Create batch converter using common infrastructure
|
|
295
|
+
sphere_from_picks_batch = create_batch_converter(
|
|
296
|
+
sphere_from_picks,
|
|
297
|
+
"Converting picks to sphere meshes",
|
|
298
|
+
"sphere",
|
|
299
|
+
min_points=4,
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Lazy batch converter for new architecture
|
|
303
|
+
sphere_from_picks_lazy_batch = create_lazy_batch_converter(
|
|
304
|
+
converter_func=sphere_from_picks,
|
|
305
|
+
task_description="Converting picks to sphere meshes",
|
|
306
|
+
)
|