copick-utils 0.6.1__py3-none-any.whl → 1.0.1__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 -1
- 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 +123 -13
- copick_utils/converters/sphere_from_picks.py +306 -0
- copick_utils/converters/surface_from_picks.py +337 -0
- 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/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.1.dist-info → copick_utils-1.0.1.dist-info}/METADATA +15 -2
- copick_utils-1.0.1.dist-info/RECORD +71 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.dist-info}/WHEEL +1 -1
- copick_utils-1.0.1.dist-info/entry_points.txt +29 -0
- copick_utils/segmentation/picks_from_segmentation.py +0 -81
- copick_utils-0.6.1.dist-info/RECORD +0 -14
- /copick_utils/{segmentation → io}/__init__.py +0 -0
- {copick_utils-0.6.1.dist-info → copick_utils-1.0.1.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")
|