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.
Files changed (67) hide show
  1. copick_utils/__init__.py +1 -1
  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 +123 -13
  43. copick_utils/converters/sphere_from_picks.py +306 -0
  44. copick_utils/converters/surface_from_picks.py +337 -0
  45. copick_utils/logical/__init__.py +43 -0
  46. copick_utils/logical/distance_operations.py +604 -0
  47. copick_utils/logical/enclosed_operations.py +222 -0
  48. copick_utils/logical/mesh_operations.py +443 -0
  49. copick_utils/logical/point_operations.py +303 -0
  50. copick_utils/logical/segmentation_operations.py +399 -0
  51. copick_utils/process/__init__.py +47 -0
  52. copick_utils/process/connected_components.py +360 -0
  53. copick_utils/process/filter_components.py +306 -0
  54. copick_utils/process/hull.py +106 -0
  55. copick_utils/process/skeletonize.py +326 -0
  56. copick_utils/process/spline_fitting.py +648 -0
  57. copick_utils/process/validbox.py +333 -0
  58. copick_utils/util/__init__.py +6 -0
  59. copick_utils/util/config_models.py +614 -0
  60. {copick_utils-0.6.1.dist-info → copick_utils-1.0.0.dist-info}/METADATA +15 -2
  61. copick_utils-1.0.0.dist-info/RECORD +71 -0
  62. copick_utils-1.0.0.dist-info/entry_points.txt +29 -0
  63. copick_utils/segmentation/picks_from_segmentation.py +0 -81
  64. copick_utils-0.6.1.dist-info/RECORD +0 -14
  65. /copick_utils/{segmentation → io}/__init__.py +0 -0
  66. {copick_utils-0.6.1.dist-info → copick_utils-1.0.0.dist-info}/WHEEL +0 -0
  67. {copick_utils-0.6.1.dist-info → copick_utils-1.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,248 @@
1
+ """CLI commands for segmentation 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 segmentations.",
25
+ no_args_is_help=True,
26
+ )
27
+ @add_config_option
28
+ @optgroup.group("\nInput Options", help="Options related to input segmentations.")
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("segmentation")
36
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
37
+ @add_boolean_operation_option
38
+ @optgroup.option(
39
+ "--voxel-spacing",
40
+ "-vs",
41
+ type=float,
42
+ required=True,
43
+ help="Voxel spacing for input and output segmentations.",
44
+ )
45
+ @add_workers_option
46
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
47
+ @add_output_option("segmentation", default_tool="segop")
48
+ @add_debug_option
49
+ def segop(
50
+ config,
51
+ run_names,
52
+ input_uris,
53
+ operation,
54
+ voxel_spacing,
55
+ workers,
56
+ output_uri,
57
+ debug,
58
+ ):
59
+ """
60
+ Perform boolean operations between segmentations.
61
+
62
+ \b
63
+ URI Format:
64
+ Segmentations: name:user_id/session_id (voxel spacing via --voxel-spacing)
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 segmentations (logical OR) - accepts N≥1 inputs
74
+ - difference: First minus second - requires exactly 2 inputs
75
+ - intersection: Common voxels (logical AND) - requires exactly 2 inputs
76
+ - exclusion: Exclusive or (XOR) - requires exactly 2 inputs
77
+
78
+ \b
79
+ Note: All segmentations are converted to binary for boolean operations.
80
+ Voxel spacing applies globally to all inputs and output.
81
+
82
+ \b
83
+ Single-Input Pattern Expansion (union only):
84
+ When providing a single -i flag with a pattern, the union operation will
85
+ expand the pattern within each run and merge all matching segmentations.
86
+ This is useful for combining multiple versions/annotations within each run.
87
+
88
+ \b
89
+ Examples:
90
+ # Single-input union: merge all matching segmentations within each run
91
+ copick logical segop --operation union -vs 10.0 \\
92
+ -i "membrane:user*/manual-*" \\
93
+ -o "merged"
94
+
95
+ # N-way union with multiple -i flags (merge across different objects)
96
+ copick logical segop --operation union -vs 10.0 \\
97
+ -i "membrane:user1/manual-*" \\
98
+ -i "vesicle:user2/auto-*" \\
99
+ -i "ribosome:user3/pred-*" \\
100
+ -o "merged"
101
+
102
+ # N-way union with regex patterns
103
+ copick logical segop --operation union -vs 10.0 \\
104
+ -i "re:membrane:user1/manual-\\d+" \\
105
+ -i "re:vesicle:user2/auto-\\d+" \\
106
+ -o "merged"
107
+
108
+ # 2-way difference (exactly 2 inputs required)
109
+ copick logical segop --operation difference -vs 10.0 \\
110
+ -i "membrane:user1/manual-001" \\
111
+ -i "mask:user1/mask-001" \\
112
+ -o "membrane:segop/masked"
113
+ """
114
+ logger = get_logger(__name__, debug=debug)
115
+
116
+ # VALIDATION: Check input count vs operation
117
+ num_inputs = len(input_uris)
118
+
119
+ if operation in ["difference", "intersection", "exclusion"]:
120
+ if num_inputs != 2:
121
+ raise click.BadParameter(
122
+ f"'{operation}' operation requires exactly 2 inputs, got {num_inputs}. Provide exactly 2 -i flags.",
123
+ )
124
+ elif operation == "union" and num_inputs < 1:
125
+ raise click.BadParameter(
126
+ f"'{operation}' operation requires at least 1 input, got {num_inputs}. Provide 1 or more -i flags.",
127
+ )
128
+
129
+ root = copick.from_file(config)
130
+ run_names_list = list(run_names) if run_names else None
131
+
132
+ # Append voxel spacing to all URIs
133
+ input_uris_full = [f"{uri}@{voxel_spacing}" if "@" not in uri else uri for uri in input_uris]
134
+ output_uri_full = f"{output_uri}@{voxel_spacing}" if "@" not in output_uri else output_uri
135
+
136
+ # Create appropriate config based on input count
137
+ try:
138
+ if num_inputs == 1:
139
+ # Single input with pattern expansion (only for union)
140
+ from copick_utils.util.config_models import create_single_selector_config
141
+
142
+ task_config = create_single_selector_config(
143
+ input_uri=input_uris_full[0],
144
+ input_type="segmentation",
145
+ output_uri=output_uri_full,
146
+ output_type="segmentation",
147
+ command_name="segop",
148
+ operation=operation,
149
+ )
150
+ elif num_inputs == 2:
151
+ # Use existing dual selector for 2-input operations
152
+ task_config = create_dual_selector_config(
153
+ input1_uri=input_uris_full[0],
154
+ input2_uri=input_uris_full[1],
155
+ input_type="segmentation",
156
+ output_uri=output_uri_full,
157
+ output_type="segmentation",
158
+ command_name="segop",
159
+ )
160
+ else:
161
+ # Use new multi selector for N-way operations (N≥3)
162
+ task_config = create_multi_selector_config(
163
+ input_uris=input_uris_full,
164
+ input_type="segmentation",
165
+ output_uri=output_uri_full,
166
+ output_type="segmentation",
167
+ command_name="segop",
168
+ )
169
+ except ValueError as e:
170
+ raise click.BadParameter(str(e)) from e
171
+
172
+ # Logging
173
+ if num_inputs == 1:
174
+ logger.info(f"Performing {operation} operation with pattern-based input expansion")
175
+ params = parse_copick_uri(input_uris[0], "segmentation")
176
+ logger.info(f" Pattern: {params['name']} ({params['user_id']}/{params['session_id']})")
177
+ logger.info(" Note: Pattern will be expanded to multiple segmentations per run")
178
+ else:
179
+ logger.info(f"Performing {operation} operation on {num_inputs} segmentations")
180
+ for i, uri in enumerate(input_uris, start=1):
181
+ params = parse_copick_uri(uri, "segmentation")
182
+ logger.info(f" Input {i}: {params['name']} ({params['user_id']}/{params['session_id']})")
183
+
184
+ output_params = parse_copick_uri(output_uri_full, "segmentation")
185
+ logger.info(f"Target: {output_params['name']} ({output_params['user_id']}/{output_params['session_id']})")
186
+
187
+ # Select appropriate lazy batch converter
188
+ if num_inputs == 1:
189
+ # Single input with pattern expansion (only union supports this)
190
+ from copick_utils.logical.segmentation_operations import segmentation_multi_union_lazy_batch
191
+
192
+ lazy_batch_functions = {
193
+ "union": segmentation_multi_union_lazy_batch,
194
+ }
195
+ elif num_inputs == 2:
196
+ # Use existing dual converters
197
+ from copick_utils.logical.segmentation_operations import (
198
+ segmentation_difference_lazy_batch,
199
+ segmentation_exclusion_lazy_batch,
200
+ segmentation_intersection_lazy_batch,
201
+ segmentation_union_lazy_batch,
202
+ )
203
+
204
+ lazy_batch_functions = {
205
+ "union": segmentation_union_lazy_batch,
206
+ "difference": segmentation_difference_lazy_batch,
207
+ "intersection": segmentation_intersection_lazy_batch,
208
+ "exclusion": segmentation_exclusion_lazy_batch,
209
+ }
210
+ else:
211
+ # Use new N-way converters (only union supports N>2)
212
+ from copick_utils.logical.segmentation_operations import segmentation_multi_union_lazy_batch
213
+
214
+ lazy_batch_functions = {
215
+ "union": segmentation_multi_union_lazy_batch,
216
+ }
217
+
218
+ lazy_batch_function = lazy_batch_functions[operation]
219
+
220
+ # Execute parallel discovery and processing
221
+ results = lazy_batch_function(
222
+ root=root,
223
+ config=task_config,
224
+ run_names=run_names_list,
225
+ workers=workers,
226
+ voxel_spacing=voxel_spacing,
227
+ )
228
+
229
+ # Aggregate results
230
+ successful = sum(1 for r in results.values() if r and r.get("processed", 0) > 0)
231
+ total_voxels = sum(r.get("voxels_created", 0) for r in results.values() if r)
232
+ total_processed = sum(r.get("processed", 0) for r in results.values() if r)
233
+
234
+ all_errors = []
235
+ for result in results.values():
236
+ if result and result.get("errors"):
237
+ all_errors.extend(result["errors"])
238
+
239
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
240
+ logger.info(f"Total {operation} operations completed: {total_processed}")
241
+ logger.info(f"Total voxels created: {total_voxels}")
242
+
243
+ if all_errors:
244
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
245
+ for error in all_errors[:5]:
246
+ logger.warning(f" - {error}")
247
+ if len(all_errors) > 5:
248
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -0,0 +1,155 @@
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 expand_output_uri, parse_copick_uri
7
+
8
+ from copick_utils.cli.util import add_input_option, add_output_option
9
+
10
+
11
+ @click.command(
12
+ context_settings={"show_default": True},
13
+ short_help="Separate connected components in segmentations.",
14
+ no_args_is_help=True,
15
+ )
16
+ @add_config_option
17
+ @optgroup.group("\nInput Options", help="Options related to the input segmentation.")
18
+ @optgroup.option(
19
+ "--run-names",
20
+ multiple=True,
21
+ help="Specific run names to process (default: all runs).",
22
+ )
23
+ @add_input_option("segmentation")
24
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
25
+ @optgroup.option(
26
+ "--connectivity",
27
+ "-cn",
28
+ type=click.Choice(["face", "face-edge", "all"]),
29
+ default="all",
30
+ help="Connectivity for connected components (face=6-connected, face-edge=18-connected, all=26-connected).",
31
+ )
32
+ @optgroup.option(
33
+ "--min-size",
34
+ type=float,
35
+ default=None,
36
+ help="Minimum component volume in cubic angstroms (ų) to keep (optional).",
37
+ )
38
+ @optgroup.option(
39
+ "--multilabel/--binary",
40
+ is_flag=True,
41
+ default=True,
42
+ help="Process as multilabel segmentation (analyze each label separately).",
43
+ )
44
+ @optgroup.option(
45
+ "--workers",
46
+ type=int,
47
+ default=8,
48
+ help="Number of worker processes.",
49
+ )
50
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
51
+ @add_output_option("segmentation", default_tool="components")
52
+ @add_debug_option
53
+ def separate_components(
54
+ config,
55
+ run_names,
56
+ input_uri,
57
+ connectivity,
58
+ min_size,
59
+ multilabel,
60
+ workers,
61
+ output_uri,
62
+ debug,
63
+ ):
64
+ """Separate connected components in segmentations into individual segmentations.
65
+
66
+ \b
67
+ URI Format:
68
+ Segmentations: name:user_id/session_id@voxel_spacing
69
+
70
+ \b
71
+ For multilabel segmentations, connected components analysis is performed on each
72
+ label separately. Output segmentations use {instance_id} placeholder for auto-numbering
73
+ (e.g., "inst-0", "inst-1", etc.).
74
+
75
+ \b
76
+ Examples:
77
+ # Separate components with smart defaults (auto user_id and session template)
78
+ copick process separate_components -i "membrane:user1/manual-001@10.0" -o "{instance_id}"
79
+
80
+ # Custom session prefix
81
+ copick process separate_components -i "membrane:user1/manual-001@10.0" -o "membrane:components/inst-{instance_id}"
82
+
83
+ # Full URI specification
84
+ copick process separate_components -i "membrane:user1/manual-001@10.0" -o "membrane:components/comp-{instance_id}@10.0"
85
+ """
86
+ from copick_utils.process.connected_components import separate_components_batch
87
+
88
+ logger = get_logger(__name__, debug=debug)
89
+
90
+ root = copick.from_file(config)
91
+ run_names_list = list(run_names) if run_names else None
92
+
93
+ # Expand output URI with smart defaults (individual_outputs=True for {instance_id})
94
+ try:
95
+ output_uri = expand_output_uri(
96
+ output_uri=output_uri,
97
+ input_uri=input_uri,
98
+ input_type="segmentation",
99
+ output_type="segmentation",
100
+ command_name="components",
101
+ individual_outputs=True,
102
+ )
103
+ except ValueError as e:
104
+ raise click.BadParameter(f"Error expanding output URI: {e}") from e
105
+
106
+ # Parse input URI
107
+ try:
108
+ input_params = parse_copick_uri(input_uri, "segmentation")
109
+ except ValueError as e:
110
+ raise click.BadParameter(f"Invalid input URI: {e}") from e
111
+
112
+ segmentation_name = input_params["name"]
113
+ segmentation_user_id = input_params["user_id"]
114
+ segmentation_session_id = input_params["session_id"]
115
+
116
+ # Parse output URI (now fully expanded)
117
+ try:
118
+ output_params = parse_copick_uri(output_uri, "segmentation")
119
+ except ValueError as e:
120
+ raise click.BadParameter(f"Invalid output URI: {e}") from e
121
+
122
+ output_user_id = output_params["user_id"]
123
+ output_session_id_template = output_params["session_id"]
124
+
125
+ # Validate that output_session_id_template contains {instance_id}
126
+ if "{instance_id}" not in output_session_id_template:
127
+ raise click.BadParameter("Output URI must contain {instance_id} placeholder for separate_components command")
128
+
129
+ logger.info(f"Separating connected components for segmentation '{segmentation_name}'")
130
+ logger.info(f"Source segmentation: {segmentation_user_id}/{segmentation_session_id}")
131
+ logger.info(f"Output template: {output_params['name']} ({output_user_id}/{output_session_id_template})")
132
+ logger.info(f"Connectivity: {connectivity}")
133
+ if min_size is not None:
134
+ logger.info(f"Minimum size: {min_size} ų")
135
+ logger.info(f"Processing as {'multilabel' if multilabel else 'binary'} segmentation")
136
+
137
+ results = separate_components_batch(
138
+ root=root,
139
+ segmentation_name=segmentation_name,
140
+ segmentation_user_id=segmentation_user_id,
141
+ segmentation_session_id=segmentation_session_id,
142
+ connectivity=connectivity,
143
+ min_size=min_size,
144
+ session_id_template=output_session_id_template,
145
+ output_user_id=output_user_id,
146
+ multilabel=multilabel,
147
+ run_names=run_names_list,
148
+ workers=workers,
149
+ )
150
+
151
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
152
+ total_components = sum(result.get("components_created", 0) for result in results.values() if result)
153
+
154
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
155
+ logger.info(f"Total components created: {total_components}")
@@ -0,0 +1,164 @@
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 expand_output_uri, parse_copick_uri
7
+
8
+ from copick_utils.cli.util import add_input_option, add_output_option
9
+
10
+
11
+ @click.command(
12
+ context_settings={"show_default": True},
13
+ short_help="3D skeletonization of segmentations.",
14
+ no_args_is_help=True,
15
+ )
16
+ @add_config_option
17
+ @optgroup.group("\nInput Options", help="Options related to the input segmentation.")
18
+ @optgroup.option(
19
+ "--run-names",
20
+ multiple=True,
21
+ help="Specific run names to process (default: all runs).",
22
+ )
23
+ @add_input_option("segmentation")
24
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
25
+ @optgroup.option(
26
+ "--method",
27
+ type=click.Choice(["skimage", "distance_transform"]),
28
+ default="skimage",
29
+ help="Skeletonization method.",
30
+ )
31
+ @optgroup.option(
32
+ "--remove-noise/--keep-noise",
33
+ is_flag=True,
34
+ default=True,
35
+ help="Remove small objects before skeletonization.",
36
+ )
37
+ @optgroup.option(
38
+ "--min-object-size",
39
+ type=int,
40
+ default=50,
41
+ help="Minimum size of objects to keep during preprocessing.",
42
+ )
43
+ @optgroup.option(
44
+ "--remove-short-branches/--keep-short-branches",
45
+ is_flag=True,
46
+ default=True,
47
+ help="Remove short branches from skeleton.",
48
+ )
49
+ @optgroup.option(
50
+ "--min-branch-length",
51
+ type=int,
52
+ default=5,
53
+ help="Minimum length of branches to keep.",
54
+ )
55
+ @optgroup.option(
56
+ "--workers",
57
+ type=int,
58
+ default=8,
59
+ help="Number of worker processes.",
60
+ )
61
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
62
+ @add_output_option("segmentation", default_tool="skel")
63
+ @add_debug_option
64
+ def skeletonize(
65
+ config,
66
+ run_names,
67
+ input_uri,
68
+ method,
69
+ remove_noise,
70
+ min_object_size,
71
+ remove_short_branches,
72
+ min_branch_length,
73
+ workers,
74
+ output_uri,
75
+ debug,
76
+ ):
77
+ """3D skeletonization of segmentations using pattern matching.
78
+
79
+ \b
80
+ URI Format:
81
+ Segmentations: name:user_id/session_id@voxel_spacing
82
+
83
+ \b
84
+ This command can process multiple segmentations by matching session IDs against
85
+ a pattern. This is useful for processing the output of connected components
86
+ separation (e.g., pattern "inst-.*" to match "inst-0", "inst-1", etc.).
87
+
88
+ \b
89
+ Examples:
90
+ # Skeletonize exact match
91
+ copick process skeletonize -i "membrane:user1/inst-0@10.0" -o "membrane:skel/skel-0@10.0"
92
+
93
+ # Skeletonize all instances using pattern
94
+ copick process skeletonize -i "membrane:user1/inst-.*@10.0" -o "membrane:skel/skel-{input_session_id}@10.0"
95
+ """
96
+ from copick_utils.process.skeletonize import skeletonize_batch
97
+
98
+ logger = get_logger(__name__, debug=debug)
99
+
100
+ root = copick.from_file(config)
101
+ run_names_list = list(run_names) if run_names else None
102
+
103
+ # Expand output URI with smart defaults
104
+ try:
105
+ output_uri = expand_output_uri(
106
+ output_uri=output_uri,
107
+ input_uri=input_uri,
108
+ input_type="segmentation",
109
+ output_type="segmentation",
110
+ command_name="skeletonize",
111
+ individual_outputs=False,
112
+ )
113
+ except ValueError as e:
114
+ raise click.BadParameter(f"Error expanding output URI: {e}") from e
115
+
116
+ # Parse input URI
117
+ try:
118
+ input_params = parse_copick_uri(input_uri, "segmentation")
119
+ except ValueError as e:
120
+ raise click.BadParameter(f"Invalid input URI: {e}") from e
121
+
122
+ segmentation_name = input_params["name"]
123
+ segmentation_user_id = input_params["user_id"]
124
+ session_id_pattern = input_params["session_id"]
125
+
126
+ # Parse output URI (now fully expanded)
127
+ try:
128
+ output_params = parse_copick_uri(output_uri, "segmentation")
129
+ except ValueError as e:
130
+ raise click.BadParameter(f"Invalid output URI: {e}") from e
131
+
132
+ output_user_id = output_params["user_id"]
133
+ output_session_id_template = output_params["session_id"]
134
+
135
+ logger.info(f"Skeletonizing segmentations '{segmentation_name}'")
136
+ logger.info(f"Source segmentations: {segmentation_user_id} matching pattern '{session_id_pattern}'")
137
+ logger.info(f"Method: {method}, output user ID: {output_user_id}")
138
+ logger.info(f"Preprocessing: remove_noise={remove_noise} (min_size={min_object_size})")
139
+ logger.info(f"Post-processing: remove_short_branches={remove_short_branches} (min_length={min_branch_length})")
140
+ logger.info(f"Output session ID template: '{output_session_id_template}'")
141
+
142
+ results = skeletonize_batch(
143
+ root=root,
144
+ segmentation_name=segmentation_name,
145
+ segmentation_user_id=segmentation_user_id,
146
+ session_id_pattern=session_id_pattern,
147
+ method=method,
148
+ remove_noise=remove_noise,
149
+ min_object_size=min_object_size,
150
+ remove_short_branches=remove_short_branches,
151
+ min_branch_length=min_branch_length,
152
+ output_session_id_template=output_session_id_template,
153
+ output_user_id=output_user_id,
154
+ run_names=run_names_list,
155
+ workers=workers,
156
+ )
157
+
158
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
159
+ total_skeletons = sum(result.get("skeletons_created", 0) for result in results.values() if result)
160
+ total_processed = sum(result.get("segmentations_processed", 0) for result in results.values() if result)
161
+
162
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
163
+ logger.info(f"Total segmentations processed: {total_processed}")
164
+ logger.info(f"Total skeletons created: {total_skeletons}")