copick-utils 0.6.0__py3-none-any.whl → 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. copick_utils/__init__.py +1 -0
  2. copick_utils/cli/__init__.py +33 -0
  3. copick_utils/cli/clipmesh.py +161 -0
  4. copick_utils/cli/clippicks.py +154 -0
  5. copick_utils/cli/clipseg.py +163 -0
  6. copick_utils/cli/conversion_commands.py +32 -0
  7. copick_utils/cli/enclosed.py +191 -0
  8. copick_utils/cli/filter_components.py +166 -0
  9. copick_utils/cli/fit_spline.py +191 -0
  10. copick_utils/cli/hull.py +138 -0
  11. copick_utils/cli/input_output_selection.py +76 -0
  12. copick_utils/cli/logical_commands.py +29 -0
  13. copick_utils/cli/mesh2picks.py +170 -0
  14. copick_utils/cli/mesh2seg.py +167 -0
  15. copick_utils/cli/meshop.py +262 -0
  16. copick_utils/cli/picks2ellipsoid.py +171 -0
  17. copick_utils/cli/picks2mesh.py +181 -0
  18. copick_utils/cli/picks2plane.py +156 -0
  19. copick_utils/cli/picks2seg.py +134 -0
  20. copick_utils/cli/picks2sphere.py +170 -0
  21. copick_utils/cli/picks2surface.py +164 -0
  22. copick_utils/cli/picksin.py +146 -0
  23. copick_utils/cli/picksout.py +148 -0
  24. copick_utils/cli/processing_commands.py +18 -0
  25. copick_utils/cli/seg2mesh.py +135 -0
  26. copick_utils/cli/seg2picks.py +128 -0
  27. copick_utils/cli/segop.py +248 -0
  28. copick_utils/cli/separate_components.py +155 -0
  29. copick_utils/cli/skeletonize.py +164 -0
  30. copick_utils/cli/util.py +580 -0
  31. copick_utils/cli/validbox.py +155 -0
  32. copick_utils/converters/__init__.py +35 -0
  33. copick_utils/converters/converter_common.py +543 -0
  34. copick_utils/converters/ellipsoid_from_picks.py +335 -0
  35. copick_utils/converters/lazy_converter.py +576 -0
  36. copick_utils/converters/mesh_from_picks.py +209 -0
  37. copick_utils/converters/mesh_from_segmentation.py +119 -0
  38. copick_utils/converters/picks_from_mesh.py +542 -0
  39. copick_utils/converters/picks_from_segmentation.py +168 -0
  40. copick_utils/converters/plane_from_picks.py +251 -0
  41. copick_utils/converters/segmentation_from_mesh.py +291 -0
  42. copick_utils/{segmentation → converters}/segmentation_from_picks.py +151 -15
  43. copick_utils/converters/sphere_from_picks.py +306 -0
  44. copick_utils/converters/surface_from_picks.py +337 -0
  45. copick_utils/features/skimage.py +33 -13
  46. copick_utils/io/readers.py +62 -59
  47. copick_utils/io/writers.py +9 -14
  48. copick_utils/logical/__init__.py +43 -0
  49. copick_utils/logical/distance_operations.py +604 -0
  50. copick_utils/logical/enclosed_operations.py +222 -0
  51. copick_utils/logical/mesh_operations.py +443 -0
  52. copick_utils/logical/point_operations.py +303 -0
  53. copick_utils/logical/segmentation_operations.py +399 -0
  54. copick_utils/pickers/grid_picker.py +5 -4
  55. copick_utils/process/__init__.py +47 -0
  56. copick_utils/process/connected_components.py +360 -0
  57. copick_utils/process/filter_components.py +306 -0
  58. copick_utils/process/hull.py +106 -0
  59. copick_utils/process/skeletonize.py +326 -0
  60. copick_utils/process/spline_fitting.py +648 -0
  61. copick_utils/process/validbox.py +333 -0
  62. copick_utils/util/__init__.py +6 -0
  63. copick_utils/util/config_models.py +614 -0
  64. {copick_utils-0.6.0.dist-info → copick_utils-1.0.0.dist-info}/METADATA +38 -12
  65. copick_utils-1.0.0.dist-info/RECORD +71 -0
  66. {copick_utils-0.6.0.dist-info → copick_utils-1.0.0.dist-info}/WHEEL +1 -1
  67. copick_utils-1.0.0.dist-info/entry_points.txt +29 -0
  68. copick_utils/__about__.py +0 -4
  69. copick_utils/segmentation/picks_from_segmentation.py +0 -67
  70. copick_utils-0.6.0.dist-info/RECORD +0 -15
  71. /copick_utils/{segmentation → io}/__init__.py +0 -0
  72. /copick_utils-0.6.0.dist-info/LICENSE.txt → /copick_utils-1.0.0.dist-info/licenses/LICENSE +0 -0
@@ -0,0 +1,76 @@
1
+ """Input/output selection logic for conversion CLI commands."""
2
+
3
+ import re
4
+
5
+
6
+ def _is_regex_pattern(pattern: str) -> bool:
7
+ """Check if string is a regex pattern by trying to compile it and seeing if it has special chars."""
8
+ # Check for common regex special characters
9
+ regex_chars = r"[.*+?^${}()|[\]\\"
10
+ has_regex_chars = any(char in pattern for char in regex_chars)
11
+
12
+ if not has_regex_chars:
13
+ return False
14
+
15
+ # Try to compile as regex
16
+ try:
17
+ re.compile(pattern)
18
+ return True
19
+ except re.error:
20
+ return False
21
+
22
+
23
+ def validate_placeholders(
24
+ pick_session_id: str,
25
+ mesh_session_id: str,
26
+ individual_meshes: bool,
27
+ ) -> None:
28
+ """
29
+ Validate that session ID templates contain required placeholders.
30
+
31
+ Args:
32
+ pick_session_id: Input session ID or pattern
33
+ mesh_session_id: Output session ID template
34
+ individual_meshes: Whether individual meshes are being created
35
+
36
+ Raises:
37
+ ValueError: If template validation fails
38
+ """
39
+ if individual_meshes and "{instance_id}" not in mesh_session_id:
40
+ raise ValueError(
41
+ "Session ID template must contain {instance_id} placeholder when individual-meshes is enabled",
42
+ )
43
+
44
+ # Check if this is many-to-many mode (input has regex pattern)
45
+ if _is_regex_pattern(pick_session_id) and "{input_session_id}" not in mesh_session_id:
46
+ raise ValueError(
47
+ "Session ID template must contain {input_session_id} placeholder when using regex input pattern",
48
+ )
49
+
50
+
51
+ def validate_conversion_placeholders(
52
+ input_session_id: str,
53
+ output_session_id: str,
54
+ individual_outputs: bool,
55
+ ) -> None:
56
+ """
57
+ Validate that session ID templates contain required placeholders.
58
+
59
+ Args:
60
+ input_session_id: Input session ID or pattern
61
+ output_session_id: Output session ID template
62
+ individual_outputs: Whether individual outputs are being created
63
+
64
+ Raises:
65
+ ValueError: If template validation fails
66
+ """
67
+ if individual_outputs and "{instance_id}" not in output_session_id:
68
+ raise ValueError(
69
+ "Session ID template must contain {instance_id} placeholder when individual outputs are enabled",
70
+ )
71
+
72
+ # Check if this is many-to-many mode (input has regex pattern)
73
+ if _is_regex_pattern(input_session_id) and "{input_session_id}" not in output_session_id:
74
+ raise ValueError(
75
+ "Session ID template must contain {input_session_id} placeholder when using regex input pattern",
76
+ )
@@ -0,0 +1,29 @@
1
+ """CLI commands for logical operations (boolean operations, distance limiting, point filtering).
2
+
3
+ This module imports all logical operation commands from specialized files for better organization.
4
+ """
5
+
6
+
7
+ from copick_utils.cli.clipmesh import clipmesh
8
+ from copick_utils.cli.clippicks import clippicks
9
+ from copick_utils.cli.clipseg import clipseg
10
+ from copick_utils.cli.enclosed import enclosed
11
+ from copick_utils.cli.meshop import meshop
12
+ from copick_utils.cli.picksin import picksin
13
+ from copick_utils.cli.picksout import picksout
14
+ from copick_utils.cli.segop import segop
15
+
16
+ # All commands are now available for import by the main CLI
17
+ __all__ = [
18
+ # Boolean operation commands
19
+ "meshop",
20
+ "segop",
21
+ "enclosed",
22
+ # Distance limiting commands
23
+ "clipmesh",
24
+ "clipseg",
25
+ "clippicks",
26
+ # Point filtering commands
27
+ "picksin",
28
+ "picksout",
29
+ ]
@@ -0,0 +1,170 @@
1
+ """CLI commands for converting meshes to picks."""
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_tomogram_option, add_workers_option
11
+ from copick_utils.util.config_models import create_simple_config
12
+
13
+
14
+ @click.command(
15
+ context_settings={"show_default": True},
16
+ short_help="Convert mesh to picks.",
17
+ no_args_is_help=True,
18
+ )
19
+ @add_config_option
20
+ @optgroup.group("\nInput Options", help="Options related to the input meshes.")
21
+ @optgroup.option(
22
+ "--run-names",
23
+ "-r",
24
+ multiple=True,
25
+ help="Specific run names to process (default: all runs).",
26
+ )
27
+ @add_input_option("mesh")
28
+ @add_tomogram_option(required=True)
29
+ @optgroup.group("\nTool Options", help="Options related to this tool.")
30
+ @optgroup.option(
31
+ "--sampling-type",
32
+ type=click.Choice(["inside", "surface", "outside", "vertices"]),
33
+ required=True,
34
+ help="Type of sampling: inside (points inside mesh), surface (points on mesh surface), outside (points outside mesh), vertices (return mesh vertices).",
35
+ )
36
+ @optgroup.option(
37
+ "--n-points",
38
+ type=int,
39
+ default=1000,
40
+ help="Number of points to sample (ignored for 'vertices' type).",
41
+ )
42
+ @optgroup.option(
43
+ "--min-dist",
44
+ type=float,
45
+ help="Minimum distance between points (default: 2 * voxel_spacing).",
46
+ )
47
+ @optgroup.option(
48
+ "--edge-dist",
49
+ type=float,
50
+ default=32.0,
51
+ help="Distance from volume edges in voxels.",
52
+ )
53
+ @optgroup.option(
54
+ "--include-normals/--no-include-normals",
55
+ is_flag=True,
56
+ default=False,
57
+ help="Include surface normals as orientations (surface sampling only).",
58
+ )
59
+ @optgroup.option(
60
+ "--random-orientations/--no-random-orientations",
61
+ is_flag=True,
62
+ default=False,
63
+ help="Generate random orientations for points.",
64
+ )
65
+ @optgroup.option(
66
+ "--seed",
67
+ type=int,
68
+ help="Random seed for reproducible results.",
69
+ )
70
+ @add_workers_option
71
+ @optgroup.group("\nOutput Options", help="Options related to output picks.")
72
+ @add_output_option("picks", default_tool="mesh2picks")
73
+ @add_debug_option
74
+ def mesh2picks(
75
+ config,
76
+ run_names,
77
+ input_uri,
78
+ tomogram_uri,
79
+ sampling_type,
80
+ n_points,
81
+ min_dist,
82
+ edge_dist,
83
+ include_normals,
84
+ random_orientations,
85
+ seed,
86
+ workers,
87
+ output_uri,
88
+ debug,
89
+ ):
90
+ """
91
+ Convert meshes to picks using different sampling strategies.
92
+
93
+ \b
94
+ URI Format:
95
+ Meshes: object_name:user_id/session_id
96
+ Picks: object_name:user_id/session_id
97
+ Tomograms: tomo_type@voxel_spacing
98
+
99
+ \b
100
+ Examples:
101
+ # Convert single mesh to picks with surface sampling
102
+ copick convert mesh2picks -i "boundary:user1/boundary-001" --tomogram wbp@10.0 --sampling-type surface -o "boundary"
103
+
104
+ # Convert all boundary meshes using pattern matching
105
+ copick convert mesh2picks -i "boundary:user1/boundary-.*" -t wbp@10.0 --sampling-type inside -o "{input_session_id}"
106
+ """
107
+ from copick_utils.converters.picks_from_mesh import picks_from_mesh_lazy_batch
108
+
109
+ logger = get_logger(__name__, debug=debug)
110
+
111
+ root = copick.from_file(config)
112
+ run_names_list = list(run_names) if run_names else None
113
+
114
+ # Parse tomogram URI to extract tomo_type and voxel_spacing
115
+ try:
116
+ tomogram_params = parse_copick_uri(tomogram_uri, "tomogram")
117
+ except ValueError as e:
118
+ raise click.BadParameter(f"Invalid tomogram URI: {e}") from e
119
+
120
+ tomo_type = tomogram_params["tomo_type"]
121
+ voxel_spacing = tomogram_params["voxel_spacing"]
122
+ if isinstance(voxel_spacing, str):
123
+ voxel_spacing = float(voxel_spacing)
124
+
125
+ # Create config directly from URIs with smart defaults
126
+ try:
127
+ task_config = create_simple_config(
128
+ input_uri=input_uri,
129
+ input_type="mesh",
130
+ output_uri=output_uri,
131
+ output_type="picks",
132
+ command_name="mesh2picks",
133
+ )
134
+ except ValueError as e:
135
+ raise click.BadParameter(str(e)) from e
136
+
137
+ # Extract parameters for logging
138
+ input_params = parse_copick_uri(input_uri, "mesh")
139
+ output_params = parse_copick_uri(output_uri, "picks")
140
+
141
+ logger.info(f"Converting mesh to picks for object '{input_params['object_name']}'")
142
+ logger.info(f"Source mesh pattern: {input_params['user_id']}/{input_params['session_id']}")
143
+ logger.info(
144
+ f"Target picks template: {output_params['object_name']} ({output_params['user_id']}/{output_params['session_id']})",
145
+ )
146
+ logger.info(f"Tomogram: {tomo_type}@{voxel_spacing}")
147
+ logger.info(f"Sampling type: {sampling_type}, n_points: {n_points}")
148
+
149
+ # Parallel discovery and processing with consistent architecture!
150
+ results = picks_from_mesh_lazy_batch(
151
+ root=root,
152
+ config=task_config,
153
+ run_names=run_names_list,
154
+ workers=workers,
155
+ sampling_type=sampling_type,
156
+ n_points=n_points,
157
+ voxel_spacing=voxel_spacing,
158
+ tomo_type=tomo_type,
159
+ min_dist=min_dist,
160
+ edge_dist=edge_dist,
161
+ include_normals=include_normals,
162
+ random_orientations=random_orientations,
163
+ seed=seed,
164
+ )
165
+
166
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
167
+ total_points = sum(result.get("points_created", 0) for result in results.values() if result)
168
+
169
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
170
+ logger.info(f"Total points created: {total_points}")
@@ -0,0 +1,167 @@
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_input_option,
10
+ add_mesh_voxelization_options,
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 mesh to segmentation.",
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
+ @add_mesh_voxelization_options
33
+ @optgroup.option(
34
+ "--tomo-type",
35
+ "-tt",
36
+ default="wbp",
37
+ help="Type of tomogram to use as reference.",
38
+ )
39
+ @add_workers_option
40
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
41
+ @add_output_option("segmentation", default_tool="mesh2seg")
42
+ @add_debug_option
43
+ def mesh2seg(
44
+ config,
45
+ run_names,
46
+ input_uri,
47
+ mode,
48
+ boundary_sampling_density,
49
+ invert,
50
+ tomo_type,
51
+ workers,
52
+ output_uri,
53
+ debug,
54
+ ):
55
+ """
56
+ Convert meshes to segmentation volumes with multiple voxelization modes.
57
+
58
+ \b
59
+ URI Format:
60
+ Meshes: object_name:user_id/session_id
61
+ Segmentations: name:user_id/session_id@voxel_spacing?multilabel=true
62
+
63
+ \b
64
+ Pattern Matching:
65
+ - Exact: "membrane:user1/session1"
66
+ - Glob: "membrane:user1/session*" or "membrane:*/manual-*"
67
+ - Regex: "re:membrane:user\\d+/session\\d+"
68
+ - Wildcard: "membrane" (expands to "membrane:*/*")
69
+
70
+ \b
71
+ Voxelization modes:
72
+ - watertight: Fill entire interior volume using ray casting
73
+ - boundary: Voxelize only the surface with controllable sampling density
74
+
75
+ \b
76
+ Additional options:
77
+ - --invert: Fill outside instead of inside (watertight mode)
78
+ - --boundary-sampling-density: Surface sampling density (boundary mode)
79
+
80
+ \b
81
+ Examples:
82
+ # Convert mesh interior to segmentation (default)
83
+ copick convert mesh2seg -i "membrane:user1/manual-001" -o "membrane:mesh2seg/from-mesh-001@10.0"
84
+
85
+ # Convert mesh boundary only with high sampling density
86
+ copick convert mesh2seg --mode boundary --boundary-sampling-density 2.0 \\
87
+ -i "membrane:user1/manual-001" -o "membrane:mesh2seg/boundary-001@10.0"
88
+
89
+ # Invert watertight mesh (fill outside)
90
+ copick convert mesh2seg --invert -i "membrane:user1/manual-001" -o "membrane:mesh2seg/inverted-001@10.0"
91
+
92
+ # Convert all manual meshes using pattern matching with multilabel output
93
+ copick convert mesh2seg -i "membrane:user1/manual-.*" -o "membrane:mesh2seg/from-mesh-{input_session_id}@10.0?multilabel=true"
94
+ """
95
+ from copick_utils.converters.segmentation_from_mesh import segmentation_from_mesh_lazy_batch
96
+
97
+ logger = get_logger(__name__, debug=debug)
98
+
99
+ root = copick.from_file(config)
100
+ run_names_list = list(run_names) if run_names else None
101
+
102
+ # Create config directly from URIs with smart defaults
103
+ try:
104
+ task_config = create_simple_config(
105
+ input_uri=input_uri,
106
+ input_type="mesh",
107
+ output_uri=output_uri,
108
+ output_type="segmentation",
109
+ command_name="mesh2seg",
110
+ )
111
+ except ValueError as e:
112
+ raise click.BadParameter(str(e)) from e
113
+
114
+ # Extract parameters for logging and processing
115
+ input_params = parse_copick_uri(input_uri, "mesh")
116
+ output_params = parse_copick_uri(output_uri, "segmentation")
117
+
118
+ voxel_spacing_output = output_params["voxel_spacing"]
119
+ if isinstance(voxel_spacing_output, str):
120
+ voxel_spacing_output = float(voxel_spacing_output)
121
+ multilabel_output = output_params.get("multilabel") or False
122
+
123
+ logger.info(f"Converting mesh to segmentation for object '{input_params['object_name']}'")
124
+ logger.info(f"Source mesh pattern: {input_params['user_id']}/{input_params['session_id']}")
125
+ logger.info(
126
+ f"Target segmentation template: {output_params['name']} ({output_params['user_id']}/{output_params['session_id']})",
127
+ )
128
+ logger.info(f"Mode: {mode}, voxel spacing: {voxel_spacing_output}, multilabel: {multilabel_output}")
129
+ if mode == "boundary":
130
+ logger.info(f"Boundary sampling density: {boundary_sampling_density}")
131
+ if invert:
132
+ logger.info("Volume inversion: enabled")
133
+
134
+ # Parallel discovery and processing - no sequential bottleneck!
135
+ results = segmentation_from_mesh_lazy_batch(
136
+ root=root,
137
+ config=task_config,
138
+ run_names=run_names_list,
139
+ workers=workers,
140
+ voxel_spacing=voxel_spacing_output,
141
+ tomo_type=tomo_type,
142
+ is_multilabel=multilabel_output,
143
+ mode=mode,
144
+ boundary_sampling_density=boundary_sampling_density,
145
+ invert=invert,
146
+ )
147
+
148
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
149
+ total_voxels = sum(result.get("voxels_created", 0) for result in results.values() if result)
150
+ total_processed = sum(result.get("processed", 0) for result in results.values() if result)
151
+
152
+ # Collect all errors
153
+ all_errors = []
154
+ for result in results.values():
155
+ if result and result.get("errors"):
156
+ all_errors.extend(result["errors"])
157
+
158
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
159
+ logger.info(f"Total conversion tasks completed: {total_processed}")
160
+ logger.info(f"Total voxels created: {total_voxels}")
161
+
162
+ if all_errors:
163
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
164
+ for error in all_errors[:5]: # Show first 5 errors
165
+ logger.warning(f" - {error}")
166
+ if len(all_errors) > 5:
167
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")