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,262 @@
1
+ """CLI commands for mesh logical operations (boolean operations)."""
2
+
3
+ import click
4
+ import copick
5
+ from click_option_group import optgroup
6
+ from copick.cli.util import add_config_option, add_debug_option
7
+ from copick.util.log import get_logger
8
+ from copick.util.uri import parse_copick_uri
9
+
10
+ from copick_utils.cli.util import (
11
+ add_boolean_operation_option,
12
+ add_multi_input_options,
13
+ add_output_option,
14
+ add_workers_option,
15
+ )
16
+ from copick_utils.util.config_models import (
17
+ create_dual_selector_config,
18
+ create_multi_selector_config,
19
+ )
20
+
21
+
22
+ @click.command(
23
+ context_settings={"show_default": True},
24
+ short_help="Perform boolean operations between meshes.",
25
+ no_args_is_help=True,
26
+ )
27
+ @add_config_option
28
+ @optgroup.group("\nInput Options", help="Options related to input meshes.")
29
+ @optgroup.option(
30
+ "--run-names",
31
+ "-r",
32
+ multiple=True,
33
+ help="Specific run names to process (default: all runs).",
34
+ )
35
+ @add_multi_input_options("mesh")
36
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
37
+ @add_boolean_operation_option
38
+ @add_workers_option
39
+ @optgroup.group("\nOutput Options", help="Options related to output meshes.")
40
+ @add_output_option("mesh", default_tool="meshop")
41
+ @optgroup.option(
42
+ "--individual-meshes/--no-individual-meshes",
43
+ "-im",
44
+ is_flag=True,
45
+ default=False,
46
+ help="Create individual meshes for each instance (enables {instance_id} placeholder).",
47
+ )
48
+ @add_debug_option
49
+ def meshop(
50
+ config,
51
+ run_names,
52
+ input_uris,
53
+ operation,
54
+ workers,
55
+ output_uri,
56
+ individual_meshes,
57
+ debug,
58
+ ):
59
+ """
60
+ Perform boolean operations between meshes.
61
+
62
+ \b
63
+ URI Format:
64
+ Meshes: object_name:user_id/session_id
65
+
66
+ \b
67
+ Pattern Support:
68
+ - Glob (default): Use * and ? wildcards (e.g., "membrane:user*/session-*")
69
+ - Regex: Prefix with 're:' (e.g., "re:membrane:user\\d+/session-\\d+")
70
+
71
+ \b
72
+ Operations:
73
+ - union: Combine meshes using boolean union - accepts N≥1 inputs
74
+ - difference: First minus second - requires exactly 2 inputs
75
+ - intersection: Common volume - requires exactly 2 inputs
76
+ - exclusion: Exclusive or (XOR) - requires exactly 2 inputs
77
+ - concatenate: Simple concatenation without boolean ops - accepts N≥1 inputs
78
+
79
+ \b
80
+ Single-Input Pattern Expansion (union & concatenate):
81
+ When providing a single -i flag with a pattern, union/concatenate operations
82
+ will expand the pattern within each run and merge all matching meshes.
83
+ This is useful for combining multiple versions/annotations within each run.
84
+
85
+ \b
86
+ Examples:
87
+ # Single-input union: merge all matching meshes within each run
88
+ copick logical meshop --operation union \\
89
+ -i "membrane:user*/manual-*" \\
90
+ -o "merged"
91
+
92
+ # Single-input concatenation: concatenate all matching meshes per run
93
+ copick logical meshop --operation concatenate \\
94
+ -i "part*:user1/session-*" \\
95
+ -o "combined"
96
+
97
+ # N-way union with multiple -i flags (merge across different objects)
98
+ copick logical meshop --operation union \\
99
+ -i "membrane:user1/manual-*" \\
100
+ -i "vesicle:user2/auto-*" \\
101
+ -i "ribosome:user3/pred-*" \\
102
+ -o "merged"
103
+
104
+ # N-way union with regex patterns
105
+ copick logical meshop --operation union \\
106
+ -i "re:membrane:user1/manual-\\d+" \\
107
+ -i "re:vesicle:user2/auto-\\d+" \\
108
+ -o "merged"
109
+
110
+ # 2-way difference (exactly 2 inputs required)
111
+ copick logical meshop --operation difference \\
112
+ -i "membrane:user1/manual-001" \\
113
+ -i "mask:user1/mask-001" \\
114
+ -o "membrane:meshop/masked"
115
+
116
+ # N-way concatenation with multiple -i flags
117
+ copick logical meshop --operation concatenate \\
118
+ -i "part1:user1/session" \\
119
+ -i "part2:user1/session" \\
120
+ -i "part3:user1/session" \\
121
+ -o "combined"
122
+ """
123
+ logger = get_logger(__name__, debug=debug)
124
+
125
+ # VALIDATION: Check input count vs operation
126
+ num_inputs = len(input_uris)
127
+
128
+ if operation in ["difference", "intersection", "exclusion"]:
129
+ if num_inputs != 2:
130
+ raise click.BadParameter(
131
+ f"'{operation}' operation requires exactly 2 inputs, got {num_inputs}. Provide exactly 2 -i flags.",
132
+ )
133
+ elif operation in ["union", "concatenate"] and num_inputs < 1:
134
+ raise click.BadParameter(
135
+ f"'{operation}' operation requires at least 1 input, got {num_inputs}. Provide 1 or more -i flags.",
136
+ )
137
+
138
+ root = copick.from_file(config)
139
+ run_names_list = list(run_names) if run_names else None
140
+
141
+ # Create appropriate config
142
+ try:
143
+ if num_inputs == 1:
144
+ # Single input with pattern expansion (for union and concatenate)
145
+ from copick_utils.util.config_models import create_single_selector_config
146
+
147
+ task_config = create_single_selector_config(
148
+ input_uri=input_uris[0],
149
+ input_type="mesh",
150
+ output_uri=output_uri,
151
+ output_type="mesh",
152
+ command_name="meshop",
153
+ operation=operation,
154
+ )
155
+ elif num_inputs == 2:
156
+ task_config = create_dual_selector_config(
157
+ input1_uri=input_uris[0],
158
+ input2_uri=input_uris[1],
159
+ input_type="mesh",
160
+ output_uri=output_uri,
161
+ output_type="mesh",
162
+ individual_outputs=individual_meshes,
163
+ command_name="meshop",
164
+ )
165
+ else:
166
+ task_config = create_multi_selector_config(
167
+ input_uris=input_uris,
168
+ input_type="mesh",
169
+ output_uri=output_uri,
170
+ output_type="mesh",
171
+ individual_outputs=individual_meshes,
172
+ command_name="meshop",
173
+ )
174
+ except ValueError as e:
175
+ raise click.BadParameter(str(e)) from e
176
+
177
+ # Logging
178
+ if num_inputs == 1:
179
+ logger.info(f"Performing {operation} operation with pattern-based input expansion")
180
+ params = parse_copick_uri(input_uris[0], "mesh")
181
+ logger.info(f" Pattern: {params['object_name']} ({params['user_id']}/{params['session_id']})")
182
+ logger.info(" Note: Pattern will be expanded to multiple meshes per run")
183
+ else:
184
+ logger.info(f"Performing {operation} operation on {num_inputs} meshes")
185
+ for i, uri in enumerate(input_uris, start=1):
186
+ params = parse_copick_uri(uri, "mesh")
187
+ logger.info(f" Input {i}: {params['object_name']} ({params['user_id']}/{params['session_id']})")
188
+
189
+ output_params = parse_copick_uri(output_uri, "mesh")
190
+ logger.info(f"Target: {output_params['object_name']} ({output_params['user_id']}/{output_params['session_id']})")
191
+
192
+ # Select appropriate lazy batch converter
193
+ if num_inputs == 1:
194
+ # Single input with pattern expansion (union and concatenate)
195
+ from copick_utils.logical.mesh_operations import (
196
+ mesh_multi_concatenate_lazy_batch,
197
+ mesh_multi_union_lazy_batch,
198
+ )
199
+
200
+ lazy_batch_functions = {
201
+ "union": mesh_multi_union_lazy_batch,
202
+ "concatenate": mesh_multi_concatenate_lazy_batch,
203
+ }
204
+ elif num_inputs == 2:
205
+ from copick_utils.logical.mesh_operations import (
206
+ mesh_concatenate_lazy_batch,
207
+ mesh_difference_lazy_batch,
208
+ mesh_exclusion_lazy_batch,
209
+ mesh_intersection_lazy_batch,
210
+ mesh_union_lazy_batch,
211
+ )
212
+
213
+ lazy_batch_functions = {
214
+ "union": mesh_union_lazy_batch,
215
+ "difference": mesh_difference_lazy_batch,
216
+ "intersection": mesh_intersection_lazy_batch,
217
+ "exclusion": mesh_exclusion_lazy_batch,
218
+ "concatenate": mesh_concatenate_lazy_batch,
219
+ }
220
+ else:
221
+ from copick_utils.logical.mesh_operations import (
222
+ mesh_multi_concatenate_lazy_batch,
223
+ mesh_multi_union_lazy_batch,
224
+ )
225
+
226
+ lazy_batch_functions = {
227
+ "union": mesh_multi_union_lazy_batch,
228
+ "concatenate": mesh_multi_concatenate_lazy_batch,
229
+ }
230
+
231
+ lazy_batch_function = lazy_batch_functions[operation]
232
+
233
+ # Execute
234
+ results = lazy_batch_function(
235
+ root=root,
236
+ config=task_config,
237
+ run_names=run_names_list,
238
+ workers=workers,
239
+ )
240
+
241
+ # Aggregate results
242
+ successful = sum(1 for r in results.values() if r and r.get("processed", 0) > 0)
243
+ total_vertices = sum(r.get("vertices_created", 0) for r in results.values() if r)
244
+ total_faces = sum(r.get("faces_created", 0) for r in results.values() if r)
245
+ total_processed = sum(r.get("processed", 0) for r in results.values() if r)
246
+
247
+ all_errors = []
248
+ for result in results.values():
249
+ if result and result.get("errors"):
250
+ all_errors.extend(result["errors"])
251
+
252
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
253
+ logger.info(f"Total {operation} operations completed: {total_processed}")
254
+ logger.info(f"Total vertices created: {total_vertices}")
255
+ logger.info(f"Total faces created: {total_faces}")
256
+
257
+ if all_errors:
258
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
259
+ for error in all_errors[:5]:
260
+ logger.warning(f" - {error}")
261
+ if len(all_errors) > 5:
262
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -0,0 +1,171 @@
1
+ import click
2
+ import copick
3
+ from click_option_group import optgroup
4
+ from copick.cli.util import add_config_option, add_debug_option
5
+ from copick.util.log import get_logger
6
+ from copick.util.uri import parse_copick_uri
7
+
8
+ from copick_utils.cli.util import (
9
+ add_clustering_options,
10
+ add_input_option,
11
+ add_output_option,
12
+ add_workers_option,
13
+ )
14
+ from copick_utils.util.config_models import create_simple_config
15
+
16
+
17
+ @click.command(
18
+ context_settings={"show_default": True},
19
+ short_help="Convert picks to ellipsoid meshes.",
20
+ no_args_is_help=True,
21
+ )
22
+ @add_config_option
23
+ @optgroup.group("\nInput Options", help="Options related to the input picks.")
24
+ @optgroup.option(
25
+ "--run-names",
26
+ "-r",
27
+ multiple=True,
28
+ help="Specific run names to process (default: all runs).",
29
+ )
30
+ @add_input_option("picks")
31
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
32
+ @optgroup.option(
33
+ "--subdivisions",
34
+ type=int,
35
+ default=2,
36
+ help="Number of ellipsoid subdivisions for mesh resolution.",
37
+ )
38
+ @optgroup.option(
39
+ "--deduplicate-ellipsoids/--no-deduplicate-ellipsoids",
40
+ is_flag=True,
41
+ default=True,
42
+ help="Merge overlapping ellipsoids to avoid duplicates.",
43
+ )
44
+ @optgroup.option(
45
+ "--min-ellipsoid-distance",
46
+ type=float,
47
+ help="Minimum distance between ellipsoid centers for deduplication (default: 0.5 * average major axis).",
48
+ )
49
+ @add_clustering_options
50
+ @add_workers_option
51
+ @optgroup.group("\nOutput Options", help="Options related to output meshes.")
52
+ @add_output_option("mesh", default_tool="picks2ellipsoid")
53
+ @optgroup.option(
54
+ "--individual-meshes/--no-individual-meshes",
55
+ "-im",
56
+ is_flag=True,
57
+ default=False,
58
+ help="Create individual meshes for each instance (enables {instance_id} placeholder).",
59
+ )
60
+ @add_debug_option
61
+ def picks2ellipsoid(
62
+ config,
63
+ run_names,
64
+ input_uri,
65
+ use_clustering,
66
+ clustering_method,
67
+ clustering_eps,
68
+ clustering_min_samples,
69
+ clustering_n_clusters,
70
+ subdivisions,
71
+ deduplicate_ellipsoids,
72
+ min_ellipsoid_distance,
73
+ all_clusters,
74
+ workers,
75
+ output_uri,
76
+ individual_meshes,
77
+ debug,
78
+ ):
79
+ """
80
+ Convert picks to ellipsoid meshes.
81
+
82
+ \b
83
+ URI Format:
84
+ Picks: object_name:user_id/session_id
85
+ Meshes: object_name:user_id/session_id
86
+
87
+ \b
88
+ Examples:
89
+ # Convert single pick set to single ellipsoid mesh
90
+ copick convert picks2ellipsoid -i "ribosome:user1/manual-001" -o "ribosome:picks2ellipsoid/ellipsoid-001"
91
+
92
+ # Create individual ellipsoid meshes
93
+ copick convert picks2ellipsoid -i "ribosome:user1/manual-001" -o "ribosome:picks2ellipsoid/ellipsoid-{instance_id}" --individual-meshes
94
+
95
+ # Convert all manual picks using pattern matching
96
+ copick convert picks2ellipsoid -i "ribosome:user1/manual-.*" -o "ribosome:picks2ellipsoid/ellipsoid-{input_session_id}"
97
+ """
98
+ from copick_utils.converters.ellipsoid_from_picks import ellipsoid_from_picks_lazy_batch
99
+
100
+ logger = get_logger(__name__, debug=debug)
101
+
102
+ root = copick.from_file(config)
103
+ run_names_list = list(run_names) if run_names else None
104
+
105
+ # Create config directly from URIs with smart defaults
106
+ try:
107
+ task_config = create_simple_config(
108
+ input_uri=input_uri,
109
+ input_type="picks",
110
+ output_uri=output_uri,
111
+ output_type="mesh",
112
+ individual_outputs=individual_meshes,
113
+ command_name="picks2ellipsoid",
114
+ )
115
+ except ValueError as e:
116
+ raise click.BadParameter(str(e)) from e
117
+
118
+ # Extract parameters for logging
119
+ input_params = parse_copick_uri(input_uri, "picks")
120
+ output_params = parse_copick_uri(output_uri, "mesh")
121
+
122
+ # Prepare clustering parameters
123
+ clustering_params = {}
124
+ if clustering_method == "dbscan":
125
+ clustering_params = {"eps": clustering_eps, "min_samples": clustering_min_samples}
126
+ elif clustering_method == "kmeans":
127
+ clustering_params = {"n_clusters": clustering_n_clusters}
128
+
129
+ logger.info(f"Converting picks to ellipsoid mesh for object '{input_params['object_name']}'")
130
+ logger.info(f"Source picks pattern: {input_params['user_id']}/{input_params['session_id']}")
131
+ logger.info(
132
+ f"Target mesh template: {output_params['object_name']} ({output_params['user_id']}/{output_params['session_id']})",
133
+ )
134
+
135
+ # Parallel discovery and processing - no sequential bottleneck!
136
+ results = ellipsoid_from_picks_lazy_batch(
137
+ root=root,
138
+ config=task_config,
139
+ run_names=run_names_list,
140
+ workers=workers,
141
+ use_clustering=use_clustering,
142
+ clustering_method=clustering_method,
143
+ clustering_params=clustering_params,
144
+ subdivisions=subdivisions,
145
+ all_clusters=all_clusters,
146
+ deduplicate_ellipsoids_flag=deduplicate_ellipsoids,
147
+ min_ellipsoid_distance=min_ellipsoid_distance,
148
+ )
149
+
150
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
151
+ total_vertices = sum(result.get("vertices_created", 0) for result in results.values() if result)
152
+ total_faces = sum(result.get("faces_created", 0) for result in results.values() if result)
153
+ total_processed = sum(result.get("processed", 0) for result in results.values() if result)
154
+
155
+ # Collect all errors
156
+ all_errors = []
157
+ for result in results.values():
158
+ if result and result.get("errors"):
159
+ all_errors.extend(result["errors"])
160
+
161
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
162
+ logger.info(f"Total conversion tasks completed: {total_processed}")
163
+ logger.info(f"Total vertices created: {total_vertices}")
164
+ logger.info(f"Total faces created: {total_faces}")
165
+
166
+ if all_errors:
167
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
168
+ for error in all_errors[:5]: # Show first 5 errors
169
+ logger.warning(f" - {error}")
170
+ if len(all_errors) > 5:
171
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -0,0 +1,181 @@
1
+ import click
2
+ import copick
3
+ from click_option_group import optgroup
4
+ from copick.cli.util import add_config_option, add_debug_option
5
+ from copick.util.log import get_logger
6
+ from copick.util.uri import parse_copick_uri
7
+
8
+ from copick_utils.cli.util import (
9
+ add_clustering_options,
10
+ add_input_option,
11
+ add_output_option,
12
+ add_workers_option,
13
+ )
14
+ from copick_utils.util.config_models import create_simple_config
15
+
16
+
17
+ @click.command(
18
+ context_settings={"show_default": True},
19
+ short_help="Convert picks to mesh using convex hull or alpha shapes.",
20
+ no_args_is_help=True,
21
+ )
22
+ @add_config_option
23
+ @optgroup.group("\nInput Options", help="Options related to the input picks.")
24
+ @optgroup.option(
25
+ "--run-names",
26
+ "-r",
27
+ multiple=True,
28
+ help="Specific run names to process (default: all runs).",
29
+ )
30
+ @add_input_option("picks")
31
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
32
+ @optgroup.option(
33
+ "--mesh-type",
34
+ "-t",
35
+ type=click.Choice(["convex_hull", "alpha_shape"]),
36
+ default="convex_hull",
37
+ help="Type of mesh to create.",
38
+ )
39
+ @optgroup.option(
40
+ "--alpha",
41
+ "-a",
42
+ type=float,
43
+ help="Alpha parameter for alpha shapes (required if mesh-type=alpha_shape).",
44
+ )
45
+ @add_clustering_options
46
+ @add_workers_option
47
+ @optgroup.group("\nOutput Options", help="Options related to output meshes.")
48
+ @add_output_option("mesh", default_tool="picks2mesh")
49
+ @optgroup.option(
50
+ "--individual-meshes/--no-individual-meshes",
51
+ "-im",
52
+ is_flag=True,
53
+ default=False,
54
+ help="Create individual meshes for each instance (enables {instance_id} placeholder).",
55
+ )
56
+ @add_debug_option
57
+ def picks2mesh(
58
+ config,
59
+ run_names,
60
+ input_uri,
61
+ mesh_type,
62
+ alpha,
63
+ use_clustering,
64
+ clustering_method,
65
+ clustering_eps,
66
+ clustering_min_samples,
67
+ clustering_n_clusters,
68
+ workers,
69
+ output_uri,
70
+ individual_meshes,
71
+ all_clusters,
72
+ debug,
73
+ ):
74
+ """
75
+ Convert picks to meshes using convex hull or alpha shapes.
76
+
77
+ \b
78
+ URI Format:
79
+ Picks: object_name:user_id/session_id
80
+ Meshes: object_name:user_id/session_id
81
+
82
+ \b
83
+ Pattern Matching:
84
+ - Exact: "ribosome:user1/session1"
85
+ - Glob: "ribosome:user1/session*" or "ribosome:*/manual-*"
86
+ - Regex: "re:ribosome:user\\d+/session\\d+"
87
+ - Wildcard: "ribosome" (expands to "ribosome:*/*")
88
+
89
+ \b
90
+ Supports flexible input/output selection modes:
91
+ - One-to-one: exact session ID → exact session ID
92
+ - One-to-many: exact session ID → template with {instance_id}
93
+ - Many-to-many: pattern → template with {input_session_id} and {instance_id}
94
+
95
+ \b
96
+ Examples:
97
+ # Convert single pick set to single mesh
98
+ copick convert picks2mesh -i "ribosome:user1/manual-001" -o "ribosome:picks2mesh/mesh-001"
99
+
100
+ # Create individual meshes from clusters
101
+ copick convert picks2mesh -i "ribosome:user1/manual-001" -o "ribosome:picks2mesh/mesh-{instance_id}" --individual-meshes
102
+
103
+ # Convert all manual picks using pattern matching
104
+ copick convert picks2mesh -i "ribosome:user1/manual-.*" -o "ribosome:picks2mesh/mesh-{input_session_id}"
105
+ """
106
+ from copick_utils.converters.mesh_from_picks import mesh_from_picks_lazy_batch
107
+
108
+ logger = get_logger(__name__, debug=debug)
109
+
110
+ root = copick.from_file(config)
111
+ run_names_list = list(run_names) if run_names else None
112
+
113
+ if mesh_type == "alpha_shape" and alpha is None:
114
+ raise click.BadParameter("Alpha parameter is required for alpha shapes")
115
+
116
+ # Create config directly from URIs with smart defaults
117
+ try:
118
+ config = create_simple_config(
119
+ input_uri=input_uri,
120
+ input_type="picks",
121
+ output_uri=output_uri,
122
+ output_type="mesh",
123
+ individual_outputs=individual_meshes,
124
+ command_name="picks2mesh",
125
+ )
126
+ except ValueError as e:
127
+ raise click.BadParameter(str(e)) from e
128
+
129
+ # Extract parameters for logging
130
+ input_params = parse_copick_uri(input_uri, "picks")
131
+ output_params = parse_copick_uri(output_uri, "mesh")
132
+
133
+ # Prepare clustering parameters
134
+ clustering_params = {}
135
+ if clustering_method == "dbscan":
136
+ clustering_params = {"eps": clustering_eps, "min_samples": clustering_min_samples}
137
+ elif clustering_method == "kmeans":
138
+ clustering_params = {"n_clusters": clustering_n_clusters}
139
+
140
+ logger.info(f"Converting picks to {mesh_type} mesh for object '{input_params['object_name']}'")
141
+ logger.info(f"Source picks pattern: {input_params['user_id']}/{input_params['session_id']}")
142
+ logger.info(
143
+ f"Target mesh template: {output_params['object_name']} ({output_params['user_id']}/{output_params['session_id']})",
144
+ )
145
+
146
+ # Parallel discovery and processing - no sequential bottleneck!
147
+ results = mesh_from_picks_lazy_batch(
148
+ root=root,
149
+ config=config,
150
+ run_names=run_names_list,
151
+ workers=workers,
152
+ mesh_type=mesh_type,
153
+ alpha=alpha,
154
+ use_clustering=use_clustering,
155
+ clustering_method=clustering_method,
156
+ clustering_params=clustering_params,
157
+ all_clusters=all_clusters,
158
+ )
159
+
160
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
161
+ total_vertices = sum(result.get("vertices_created", 0) for result in results.values() if result)
162
+ total_faces = sum(result.get("faces_created", 0) for result in results.values() if result)
163
+ total_processed = sum(result.get("processed", 0) for result in results.values() if result)
164
+
165
+ # Collect all errors
166
+ all_errors = []
167
+ for result in results.values():
168
+ if result and result.get("errors"):
169
+ all_errors.extend(result["errors"])
170
+
171
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
172
+ logger.info(f"Total conversion tasks completed: {total_processed}")
173
+ logger.info(f"Total vertices created: {total_vertices}")
174
+ logger.info(f"Total faces created: {total_faces}")
175
+
176
+ if all_errors:
177
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
178
+ for error in all_errors[:5]: # Show first 5 errors
179
+ logger.warning(f" - {error}")
180
+ if len(all_errors) > 5:
181
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")