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,191 @@
1
+ """CLI command for enclosed segmentation operations (finding and absorbing enclosed components)."""
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 add_dual_input_options, add_output_option, add_workers_option
11
+ from copick_utils.util.config_models import create_dual_selector_config
12
+
13
+
14
+ @click.command(
15
+ context_settings={"show_default": True},
16
+ short_help="Remove enclosed components from a segmentation.",
17
+ no_args_is_help=True,
18
+ )
19
+ @add_config_option
20
+ @optgroup.group("\nInput Options", help="Options related to the input segmentations.")
21
+ @optgroup.option(
22
+ "--run-names",
23
+ "-r",
24
+ multiple=True,
25
+ help="Specific run names to process (default: all runs).",
26
+ )
27
+ @add_dual_input_options("segmentation")
28
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
29
+ @optgroup.option(
30
+ "--voxel-spacing",
31
+ "-vs",
32
+ type=float,
33
+ required=True,
34
+ help="Voxel spacing for input and output segmentations.",
35
+ )
36
+ @optgroup.option(
37
+ "--margin",
38
+ "-m",
39
+ type=int,
40
+ default=1,
41
+ help="Number of voxels to dilate when checking if components are enclosed.",
42
+ )
43
+ @optgroup.option(
44
+ "--connectivity",
45
+ "-cn",
46
+ type=click.Choice(["face", "face-edge", "all"]),
47
+ default="all",
48
+ help="Connectivity for connected components (face=6-connected, face-edge=18-connected, all=26-connected).",
49
+ )
50
+ @optgroup.option(
51
+ "--min-size",
52
+ type=float,
53
+ default=None,
54
+ help="Minimum component volume in cubic angstroms (ų) to consider (optional).",
55
+ )
56
+ @optgroup.option(
57
+ "--max-size",
58
+ type=float,
59
+ default=None,
60
+ help="Maximum component volume in cubic angstroms (ų) to consider (optional).",
61
+ )
62
+ @add_workers_option
63
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
64
+ @add_output_option("segmentation", default_tool="enclosed")
65
+ @add_debug_option
66
+ def enclosed(
67
+ config,
68
+ run_names,
69
+ input1_uri,
70
+ input2_uri,
71
+ voxel_spacing,
72
+ margin,
73
+ connectivity,
74
+ min_size,
75
+ max_size,
76
+ workers,
77
+ output_uri,
78
+ debug,
79
+ ):
80
+ """
81
+ Remove enclosed components from a segmentation.
82
+
83
+ This command identifies connected components in the first segmentation (inner) that are
84
+ completely surrounded by the second segmentation (outer), and removes them from the inner
85
+ segmentation. Useful for cleaning up noise, artifacts, or unwanted fragments.
86
+
87
+ \b
88
+ URI Format:
89
+ Segmentations: name:user_id/session_id (voxel spacing specified via --voxel-spacing)
90
+
91
+ \b
92
+ Algorithm:
93
+ 1. Label connected components in the inner segmentation (input1)
94
+ 2. Dilate each component by the specified margin
95
+ 3. Check if the dilated component is fully contained within the outer segmentation (input2)
96
+ 4. If enclosed (and within size limits), remove the component from the inner segmentation
97
+ 5. Output cleaned version of the inner segmentation
98
+
99
+ \b
100
+ Examples:
101
+ # Remove small vesicle fragments that are enclosed by membrane
102
+ copick logical enclosed -vs 10.0 -i1 "vesicle:user1/auto-001" -i2 "membrane:user1/manual-001" -o "vesicle_clean"
103
+
104
+ # Remove noise fragments with size filtering (volumes in ų)
105
+ copick logical enclosed -vs 10.0 -i1 "fragments:user1/.*" -i2 "cell:user1/.*" -o "cleaned" --min-size 1000 --max-size 100000 --margin 2
106
+ """
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
+ # Append voxel spacing to URIs (only if not already present)
114
+ input1_uri_full = f"{input1_uri}@{voxel_spacing}" if "@" not in input1_uri else input1_uri
115
+ input2_uri_full = f"{input2_uri}@{voxel_spacing}" if "@" not in input2_uri else input2_uri
116
+ output_uri_full = f"{output_uri}@{voxel_spacing}" if "@" not in output_uri else output_uri
117
+
118
+ # Create config directly from URIs with smart defaults
119
+ try:
120
+ task_config = create_dual_selector_config(
121
+ input1_uri=input1_uri_full,
122
+ input2_uri=input2_uri_full,
123
+ input_type="segmentation",
124
+ output_uri=output_uri_full,
125
+ output_type="segmentation",
126
+ command_name="enclosed",
127
+ )
128
+ except ValueError as e:
129
+ raise click.BadParameter(str(e)) from e
130
+
131
+ # Extract parameters for logging
132
+ input1_params = parse_copick_uri(input1_uri, "segmentation")
133
+ input2_params = parse_copick_uri(input2_uri, "segmentation")
134
+ output_params = parse_copick_uri(output_uri_full, "segmentation")
135
+
136
+ logger.info(
137
+ f"Removing enclosed components from '{input1_params['name']}' using '{input2_params['name']}' as reference",
138
+ )
139
+ logger.info(f"Segmentation to clean: {input1_params['user_id']}/{input1_params['session_id']}")
140
+ logger.info(f"Reference segmentation: {input2_params['user_id']}/{input2_params['session_id']}")
141
+ logger.info(
142
+ f"Target segmentation template: {output_params['name']} ({output_params['user_id']}/{output_params['session_id']})",
143
+ )
144
+ logger.info(f"Parameters: margin={margin}, connectivity={connectivity}, min_size={min_size}, max_size={max_size}")
145
+
146
+ # Map connectivity string to numeric value
147
+ connectivity_map = {
148
+ "face": 1,
149
+ "face-edge": 2,
150
+ "all": 3,
151
+ }
152
+ connectivity_value = connectivity_map[connectivity]
153
+
154
+ # Import the lazy batch converter
155
+ from copick_utils.logical.enclosed_operations import segmentation_enclosed_lazy_batch
156
+
157
+ # Parallel discovery and processing
158
+ results = segmentation_enclosed_lazy_batch(
159
+ root=root,
160
+ config=task_config,
161
+ run_names=run_names_list,
162
+ workers=workers,
163
+ voxel_spacing=voxel_spacing,
164
+ margin=margin,
165
+ connectivity=connectivity_value,
166
+ min_size=min_size,
167
+ max_size=max_size,
168
+ )
169
+
170
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
171
+ total_voxels_kept = sum(result.get("voxels_kept", 0) for result in results.values() if result)
172
+ total_processed = sum(result.get("processed", 0) for result in results.values() if result)
173
+ total_components_removed = sum(result.get("components_removed", 0) for result in results.values() if result)
174
+
175
+ # Collect all errors
176
+ all_errors = []
177
+ for result in results.values():
178
+ if result and result.get("errors"):
179
+ all_errors.extend(result["errors"])
180
+
181
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
182
+ logger.info(f"Total enclosed operations completed: {total_processed}")
183
+ logger.info(f"Total components removed: {total_components_removed}")
184
+ logger.info(f"Total voxels remaining in cleaned segmentations: {total_voxels_kept}")
185
+
186
+ if all_errors:
187
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
188
+ for error in all_errors[:5]: # Show first 5 errors
189
+ logger.warning(f" - {error}")
190
+ if len(all_errors) > 5:
191
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -0,0 +1,166 @@
1
+ """CLI command for filtering connected components by size."""
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 add_input_option, add_output_option, add_workers_option
11
+
12
+
13
+ @click.command(
14
+ context_settings={"show_default": True},
15
+ short_help="Filter connected components in segmentations by size.",
16
+ no_args_is_help=True,
17
+ )
18
+ @add_config_option
19
+ @optgroup.group("\nInput Options", help="Options related to the input segmentation.")
20
+ @optgroup.option(
21
+ "--run-names",
22
+ "-r",
23
+ multiple=True,
24
+ help="Specific run names to process (default: all runs).",
25
+ )
26
+ @add_input_option("segmentation")
27
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
28
+ @optgroup.option(
29
+ "--connectivity",
30
+ "-cn",
31
+ type=click.Choice(["face", "face-edge", "all"]),
32
+ default="all",
33
+ help="Connectivity for connected components (face=6-connected, face-edge=18-connected, all=26-connected).",
34
+ )
35
+ @optgroup.option(
36
+ "--min-size",
37
+ type=float,
38
+ default=None,
39
+ help="Minimum component volume in cubic angstroms (ų) to keep (optional).",
40
+ )
41
+ @optgroup.option(
42
+ "--max-size",
43
+ type=float,
44
+ default=None,
45
+ help="Maximum component volume in cubic angstroms (ų) to keep (optional).",
46
+ )
47
+ @add_workers_option
48
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
49
+ @add_output_option("segmentation", default_tool="filter-components")
50
+ @add_debug_option
51
+ def filter_components(
52
+ config,
53
+ run_names,
54
+ input_uri,
55
+ connectivity,
56
+ min_size,
57
+ max_size,
58
+ workers,
59
+ output_uri,
60
+ debug,
61
+ ):
62
+ """
63
+ Filter connected components in segmentations by size.
64
+
65
+ This command identifies connected components in a segmentation and removes those
66
+ that fall outside the specified size range (in cubic angstroms). Useful for
67
+ removing noise, small artifacts, or overly large components.
68
+
69
+ \b
70
+ URI Format:
71
+ Segmentations: name:user_id/session_id@voxel_spacing
72
+
73
+ \b
74
+ Examples:
75
+ # Remove small noise components (keep only larger than 50000 ų)
76
+ copick process filter-components -i "membrane:user1/auto-001@10.0" -o "membrane_clean" --min-size 50000
77
+
78
+ # Keep only medium-sized components (between 10000 and 1000000 ų)
79
+ copick process filter-components -i "particles:user1/.*@10.0" -o "particles_filtered" --min-size 10000 --max-size 1000000
80
+
81
+ # Remove large components (keep only smaller than 500000 ų)
82
+ copick process filter-components -i "noise:user1/pred@10.0" -o "small_features" --max-size 500000
83
+ """
84
+
85
+ logger = get_logger(__name__, debug=debug)
86
+
87
+ root = copick.from_file(config)
88
+ run_names_list = list(run_names) if run_names else None
89
+
90
+ # Parse input URI
91
+ try:
92
+ input_params = parse_copick_uri(input_uri, "segmentation")
93
+ except ValueError as e:
94
+ raise click.BadParameter(f"Invalid input URI: {e}") from e
95
+
96
+ segmentation_name = input_params["name"]
97
+ segmentation_user_id = input_params["user_id"]
98
+ segmentation_session_id = input_params["session_id"]
99
+ voxel_spacing = input_params.get("voxel_spacing")
100
+
101
+ if voxel_spacing is None:
102
+ raise click.BadParameter("Input URI must include voxel spacing (e.g., @10.0)")
103
+
104
+ # Parse output URI - if no voxel spacing specified, inherit from input
105
+ if "@" not in output_uri:
106
+ output_uri = f"{output_uri}@{voxel_spacing}"
107
+
108
+ try:
109
+ output_params = parse_copick_uri(output_uri, "segmentation")
110
+ except ValueError as e:
111
+ raise click.BadParameter(f"Invalid output URI: {e}") from e
112
+
113
+ output_name = output_params["name"]
114
+ output_user_id = output_params["user_id"]
115
+ output_session_id = output_params["session_id"]
116
+
117
+ logger.info(f"Filtering components for segmentation '{segmentation_name}'")
118
+ logger.info(f"Input segmentation: {segmentation_user_id}/{segmentation_session_id} @ {voxel_spacing}Å")
119
+ logger.info(f"Output segmentation: {output_name} ({output_user_id}/{output_session_id})")
120
+ logger.info(f"Connectivity: {connectivity}")
121
+ if min_size is not None:
122
+ logger.info(f"Minimum size: {min_size} ų")
123
+ if max_size is not None:
124
+ logger.info(f"Maximum size: {max_size} ų")
125
+
126
+ # Import batch function
127
+ from copick_utils.process.filter_components import filter_components_batch
128
+
129
+ # Process runs
130
+ results = filter_components_batch(
131
+ root=root,
132
+ segmentation_name=segmentation_name,
133
+ segmentation_user_id=segmentation_user_id,
134
+ segmentation_session_id=segmentation_session_id,
135
+ voxel_spacing=voxel_spacing,
136
+ connectivity=connectivity,
137
+ min_size=min_size,
138
+ max_size=max_size,
139
+ output_user_id=output_user_id,
140
+ output_session_id=output_session_id,
141
+ run_names=run_names_list,
142
+ workers=workers,
143
+ )
144
+
145
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
146
+ total_kept = sum(result.get("components_kept", 0) for result in results.values() if result)
147
+ total_removed = sum(result.get("components_removed", 0) for result in results.values() if result)
148
+ total_voxels = sum(result.get("voxels_kept", 0) for result in results.values() if result)
149
+
150
+ # Collect all errors
151
+ all_errors = []
152
+ for result in results.values():
153
+ if result and result.get("errors"):
154
+ all_errors.extend(result["errors"])
155
+
156
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
157
+ logger.info(f"Total components kept: {total_kept}")
158
+ logger.info(f"Total components removed: {total_removed}")
159
+ logger.info(f"Total voxels in filtered segmentations: {total_voxels}")
160
+
161
+ if all_errors:
162
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
163
+ for error in all_errors[:5]: # Show first 5 errors
164
+ logger.warning(f" - {error}")
165
+ if len(all_errors) > 5:
166
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -0,0 +1,191 @@
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="Fit 3D splines to skeletons and generate picks with orientations.",
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.option(
25
+ "--voxel-spacing",
26
+ "-vs",
27
+ type=float,
28
+ required=True,
29
+ help="Voxel spacing for coordinate scaling.",
30
+ )
31
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
32
+ @optgroup.option(
33
+ "--spacing-distance",
34
+ type=float,
35
+ required=True,
36
+ help="Distance between consecutive sampled points along the spline.",
37
+ )
38
+ @optgroup.option(
39
+ "--smoothing-factor",
40
+ type=float,
41
+ help="Smoothing parameter for spline fitting (auto if not provided).",
42
+ )
43
+ @optgroup.option(
44
+ "--degree",
45
+ type=int,
46
+ default=3,
47
+ help="Degree of the spline (1-5).",
48
+ )
49
+ @optgroup.option(
50
+ "--connectivity-radius",
51
+ type=float,
52
+ default=2.0,
53
+ help="Maximum distance to consider skeleton points as connected.",
54
+ )
55
+ @optgroup.option(
56
+ "--compute-transforms/--no-compute-transforms",
57
+ is_flag=True,
58
+ default=True,
59
+ help="Whether to compute orientations for picks.",
60
+ )
61
+ @optgroup.option(
62
+ "--curvature-threshold",
63
+ type=float,
64
+ default=0.2,
65
+ help="Maximum allowed curvature before outlier removal.",
66
+ )
67
+ @optgroup.option(
68
+ "--max-iterations",
69
+ type=int,
70
+ default=5,
71
+ help="Maximum number of outlier removal iterations.",
72
+ )
73
+ @optgroup.option(
74
+ "--workers",
75
+ type=int,
76
+ default=8,
77
+ help="Number of worker processes.",
78
+ )
79
+ @optgroup.group("\nOutput Options", help="Options related to output picks.")
80
+ @add_output_option("picks", default_tool="spline")
81
+ @add_debug_option
82
+ def fit_spline(
83
+ config,
84
+ run_names,
85
+ input_uri,
86
+ voxel_spacing,
87
+ spacing_distance,
88
+ smoothing_factor,
89
+ degree,
90
+ connectivity_radius,
91
+ compute_transforms,
92
+ curvature_threshold,
93
+ max_iterations,
94
+ workers,
95
+ output_uri,
96
+ debug,
97
+ ):
98
+ """Fit 3D splines to skeletonized segmentations and generate picks with orientations.
99
+
100
+ \b
101
+ URI Format:
102
+ Segmentations: name:user_id/session_id@voxel_spacing
103
+ Picks: object_name:user_id/session_id
104
+
105
+ \b
106
+ This command fits regularized 3D parametric splines to skeleton volumes and samples
107
+ points along the spline at regular intervals. Orientations are computed based on
108
+ the spline direction.
109
+
110
+ \b
111
+ Examples:
112
+ # Fit splines to skeletonized components
113
+ copick process fit_spline -i "skeleton:skel/inst-.*@10.0" -o "skeleton:spline/spline-{input_session_id}" --spacing-distance 4.4 --voxel-spacing 10.0
114
+
115
+ # Process specific skeleton
116
+ copick process fit_spline -i "skeleton:skel/skel-0@10.0" -o "skeleton:spline/spline-0" --spacing-distance 2.0 --voxel-spacing 10.0
117
+ """
118
+ from copick_utils.process.spline_fitting import fit_spline_batch
119
+
120
+ logger = get_logger(__name__, debug=debug)
121
+
122
+ root = copick.from_file(config)
123
+ run_names_list = list(run_names) if run_names else None
124
+
125
+ # Expand output URI with smart defaults
126
+ try:
127
+ output_uri = expand_output_uri(
128
+ output_uri=output_uri,
129
+ input_uri=input_uri,
130
+ input_type="segmentation",
131
+ output_type="picks",
132
+ command_name="fit_spline",
133
+ individual_outputs=False,
134
+ )
135
+ except ValueError as e:
136
+ raise click.BadParameter(f"Error expanding output URI: {e}") from e
137
+
138
+ # Parse input URI
139
+ try:
140
+ input_params = parse_copick_uri(input_uri, "segmentation")
141
+ except ValueError as e:
142
+ raise click.BadParameter(f"Invalid input URI: {e}") from e
143
+
144
+ segmentation_name = input_params["name"]
145
+ segmentation_user_id = input_params["user_id"]
146
+ session_id_pattern = input_params["session_id"]
147
+
148
+ # Parse output URI (now fully expanded)
149
+ try:
150
+ output_params = parse_copick_uri(output_uri, "picks")
151
+ except ValueError as e:
152
+ raise click.BadParameter(f"Invalid output URI: {e}") from e
153
+
154
+ output_user_id = output_params["user_id"]
155
+ output_session_id_template = output_params["session_id"]
156
+
157
+ logger.info(f"Fitting splines to segmentations '{segmentation_name}'")
158
+ logger.info(f"Source segmentations: {segmentation_user_id} matching pattern '{session_id_pattern}'")
159
+ logger.info(f"Spacing distance: {spacing_distance}, degree: {degree}")
160
+ logger.info(f"Smoothing factor: {smoothing_factor}, connectivity radius: {connectivity_radius}")
161
+ logger.info(f"Compute transforms: {compute_transforms}, output user ID: {output_user_id}")
162
+ logger.info(f"Curvature threshold: {curvature_threshold}, max iterations: {max_iterations}")
163
+ logger.info(f"Voxel spacing: {voxel_spacing}")
164
+ logger.info(f"Output session ID template: '{output_session_id_template}'")
165
+
166
+ results = fit_spline_batch(
167
+ root=root,
168
+ segmentation_name=segmentation_name,
169
+ segmentation_user_id=segmentation_user_id,
170
+ session_id_pattern=session_id_pattern,
171
+ spacing_distance=spacing_distance,
172
+ smoothing_factor=smoothing_factor,
173
+ degree=degree,
174
+ connectivity_radius=connectivity_radius,
175
+ compute_transforms=compute_transforms,
176
+ curvature_threshold=curvature_threshold,
177
+ max_iterations=max_iterations,
178
+ output_session_id_template=output_session_id_template,
179
+ output_user_id=output_user_id,
180
+ voxel_spacing=voxel_spacing,
181
+ run_names=run_names_list,
182
+ workers=workers,
183
+ )
184
+
185
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
186
+ total_picks = sum(result.get("picks_created", 0) for result in results.values() if result)
187
+ total_processed = sum(result.get("segmentations_processed", 0) for result in results.values() if result)
188
+
189
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
190
+ logger.info(f"Total segmentations processed: {total_processed}")
191
+ logger.info(f"Total picks created: {total_picks}")
@@ -0,0 +1,138 @@
1
+ """CLI command for computing various hull operations on meshes."""
2
+ import click
3
+ import copick
4
+ from click_option_group import optgroup
5
+ from copick.cli.util import add_config_option, add_debug_option
6
+ from copick.util.log import get_logger
7
+ from copick.util.uri import parse_copick_uri
8
+
9
+ from copick_utils.cli.util import (
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="Compute hull operations on meshes.",
20
+ no_args_is_help=True,
21
+ )
22
+ @add_config_option
23
+ @optgroup.group("\nInput Options", help="Options related to the input meshes.")
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("mesh")
31
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
32
+ @optgroup.option(
33
+ "--hull-type",
34
+ type=click.Choice(["convex"]),
35
+ default="convex",
36
+ help="Type of hull to compute.",
37
+ )
38
+ @add_workers_option
39
+ @optgroup.group("\nOutput Options", help="Options related to output meshes.")
40
+ @add_output_option("mesh", default_tool="hull")
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 hull(
50
+ config,
51
+ run_names,
52
+ input_uri,
53
+ hull_type,
54
+ workers,
55
+ output_uri,
56
+ individual_meshes,
57
+ debug,
58
+ ):
59
+ """
60
+ Compute hull operations on meshes.
61
+
62
+ \b
63
+ URI Format:
64
+ Meshes: object_name:user_id/session_id
65
+
66
+ \b
67
+ Currently supports convex hull computation, where the convex hull is the
68
+ smallest convex shape that contains all vertices of the original mesh.
69
+
70
+ \b
71
+ Examples:
72
+ # Compute convex hull for meshes
73
+ copick process hull -i "membrane:user1/session1" -o "membrane:hull/hull-session"
74
+
75
+ # Process specific runs
76
+ copick process hull -r run1 -r run2 -i "membrane:user1/session1" -o "membrane:hull/convex-001" --hull-type convex
77
+ """
78
+ from copick_utils.process.hull import hull_lazy_batch
79
+
80
+ logger = get_logger(__name__, debug=debug)
81
+
82
+ root = copick.from_file(config)
83
+ run_names_list = list(run_names) if run_names else None
84
+
85
+ # Create config directly from URIs with smart defaults
86
+ try:
87
+ task_config = create_simple_config(
88
+ input_uri=input_uri,
89
+ input_type="mesh",
90
+ output_uri=output_uri,
91
+ output_type="mesh",
92
+ individual_outputs=individual_meshes,
93
+ command_name="hull",
94
+ )
95
+ except ValueError as e:
96
+ raise click.BadParameter(str(e)) from e
97
+
98
+ # Extract parameters for logging
99
+ input_params = parse_copick_uri(input_uri, "mesh")
100
+ output_params = parse_copick_uri(output_uri, "mesh")
101
+
102
+ logger.info(f"Computing {hull_type} hull for meshes '{input_params['object_name']}'")
103
+ logger.info(f"Source mesh pattern: {input_params['user_id']}/{input_params['session_id']}")
104
+ logger.info(
105
+ f"Target mesh template: {output_params['object_name']} ({output_params['user_id']}/{output_params['session_id']})",
106
+ )
107
+
108
+ # Parallel discovery and processing - no sequential bottleneck!
109
+ results = hull_lazy_batch(
110
+ root=root,
111
+ config=task_config,
112
+ run_names=run_names_list,
113
+ workers=workers,
114
+ hull_type=hull_type,
115
+ )
116
+
117
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
118
+ total_vertices = sum(result.get("vertices_created", 0) for result in results.values() if result)
119
+ total_faces = sum(result.get("faces_created", 0) for result in results.values() if result)
120
+ total_processed = sum(result.get("processed", 0) for result in results.values() if result)
121
+
122
+ # Collect all errors
123
+ all_errors = []
124
+ for result in results.values():
125
+ if result and result.get("errors"):
126
+ all_errors.extend(result["errors"])
127
+
128
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
129
+ logger.info(f"Total {hull_type} hull operations completed: {total_processed}")
130
+ logger.info(f"Total vertices created: {total_vertices}")
131
+ logger.info(f"Total faces created: {total_faces}")
132
+
133
+ if all_errors:
134
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
135
+ for error in all_errors[:5]: # Show first 5 errors
136
+ logger.warning(f" - {error}")
137
+ if len(all_errors) > 5:
138
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")