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,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}")
|
copick_utils/cli/hull.py
ADDED
|
@@ -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")
|