copick-utils 1.0.2__py3-none-any.whl → 1.1.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.
copick_utils/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # SPDX-FileCopyrightText: 2024-present Kyle Harrington <czi@kyleharrington.com>
2
2
  #
3
3
  # SPDX-License-Identifier: MIT
4
- __version__ = "1.0.2"
4
+ __version__ = "1.1.0"
@@ -5,6 +5,7 @@ from copick_utils.cli.fit_spline import fit_spline
5
5
  from copick_utils.cli.hull import hull
6
6
  from copick_utils.cli.separate_components import separate_components
7
7
  from copick_utils.cli.skeletonize import skeletonize
8
+ from copick_utils.cli.split_labels import split
8
9
  from copick_utils.cli.validbox import validbox
9
10
 
10
11
  # All commands are now available for import by the main CLI
@@ -15,4 +16,5 @@ __all__ = [
15
16
  "separate_components",
16
17
  "filter_components",
17
18
  "fit_spline",
19
+ "split",
18
20
  ]
@@ -0,0 +1,148 @@
1
+ """CLI command for splitting multilabel segmentations into individual single-class segmentations."""
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_workers_option
11
+
12
+
13
+ @click.command(
14
+ context_settings={"show_default": True},
15
+ short_help="Split multilabel segmentations into single-class segmentations.",
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
+ @add_workers_option
29
+ @optgroup.group("\nOutput Options", help="Options related to output segmentations.")
30
+ @optgroup.option(
31
+ "--output-user-id",
32
+ type=str,
33
+ default="split",
34
+ help="User ID for output segmentations.",
35
+ )
36
+ @add_debug_option
37
+ def split(
38
+ config,
39
+ run_names,
40
+ input_uri,
41
+ workers,
42
+ output_user_id,
43
+ debug,
44
+ ):
45
+ """
46
+ Split multilabel segmentations into individual single-class binary segmentations.
47
+
48
+ This command takes a multilabel segmentation and creates separate binary segmentations
49
+ for each label value. Each output segmentation is named after the corresponding
50
+ PickableObject (as defined in the copick config) and uses the same session ID as
51
+ the input.
52
+
53
+ \b
54
+ URI Format:
55
+ Segmentations: name:user_id/session_id@voxel_spacing
56
+
57
+ \b
58
+ Label-to-Object Mapping:
59
+ The tool looks up each label value in the pickable_objects configuration
60
+ and uses the object name for the output segmentation:
61
+ - Label 1 (ribosome) → ribosome:split/session-001@10.0
62
+ - Label 2 (membrane) → membrane:split/session-001@10.0
63
+ - Label 3 (proteasome) → proteasome:split/session-001@10.0
64
+
65
+ \b
66
+ Examples:
67
+ # Split multilabel segmentation (outputs named by pickable objects)
68
+ copick process split -i "predictions:model/run-001@10.0"
69
+
70
+ # Split with custom output user ID
71
+ copick process split -i "classes:annotator/manual@10.0" --output-user-id "per-class"
72
+
73
+ # Process specific runs only
74
+ copick process split -i "labels:*/*@10.0" --run-names TS_001 --run-names TS_002
75
+ """
76
+
77
+ logger = get_logger(__name__, debug=debug)
78
+
79
+ root = copick.from_file(config)
80
+ run_names_list = list(run_names) if run_names else None
81
+
82
+ # Parse input URI
83
+ try:
84
+ input_params = parse_copick_uri(input_uri, "segmentation")
85
+ except ValueError as e:
86
+ raise click.BadParameter(f"Invalid input URI: {e}") from e
87
+
88
+ segmentation_name = input_params["name"]
89
+ segmentation_user_id = input_params["user_id"]
90
+ segmentation_session_id = input_params["session_id"]
91
+ voxel_spacing = input_params.get("voxel_spacing")
92
+
93
+ if voxel_spacing is None or voxel_spacing == "*":
94
+ raise click.BadParameter("Input URI must include a specific voxel spacing (e.g., @10.0)")
95
+
96
+ # Check for patterns in critical fields
97
+ if "*" in segmentation_name or "*" in segmentation_user_id or "*" in segmentation_session_id:
98
+ raise click.BadParameter(
99
+ "Input URI cannot contain wildcards for splitting. "
100
+ "Please specify exact segmentation name, user_id, and session_id.",
101
+ )
102
+
103
+ logger.info(f"Splitting multilabel segmentation '{segmentation_name}'")
104
+ logger.debug(f"Input: {segmentation_user_id}/{segmentation_session_id} @ {voxel_spacing}Å")
105
+ logger.debug(f"Output user ID: {output_user_id}")
106
+ logger.debug(f"Workers: {workers}")
107
+
108
+ # Import batch function
109
+ from copick_utils.process.split_labels import split_labels_batch
110
+
111
+ # Process runs
112
+ results = split_labels_batch(
113
+ root=root,
114
+ segmentation_name=segmentation_name,
115
+ segmentation_user_id=segmentation_user_id,
116
+ segmentation_session_id=segmentation_session_id,
117
+ voxel_spacing=float(voxel_spacing),
118
+ output_user_id=output_user_id,
119
+ run_names=run_names_list,
120
+ workers=workers,
121
+ )
122
+
123
+ # Aggregate results
124
+ successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
125
+ total_labels = sum(result.get("labels_split", 0) for result in results.values() if result)
126
+
127
+ # Collect all unique object names created
128
+ all_object_names = set()
129
+ for result in results.values():
130
+ if result and result.get("object_names"):
131
+ all_object_names.update(result["object_names"])
132
+
133
+ # Collect all errors
134
+ all_errors = []
135
+ for result in results.values():
136
+ if result and result.get("errors"):
137
+ all_errors.extend(result["errors"])
138
+
139
+ logger.info(f"Completed: {successful}/{len(results)} runs processed successfully")
140
+ logger.info(f"Total labels split: {total_labels}")
141
+ logger.info(f"Object names created: {', '.join(sorted(all_object_names))}")
142
+
143
+ if all_errors:
144
+ logger.warning(f"Encountered {len(all_errors)} errors during processing")
145
+ for error in all_errors[:5]: # Show first 5 errors
146
+ logger.warning(f" - {error}")
147
+ if len(all_errors) > 5:
148
+ logger.warning(f" ... and {len(all_errors) - 5} more errors")
@@ -9,13 +9,11 @@ from .connected_components import (
9
9
  )
10
10
  from .skeletonize import (
11
11
  TubeSkeletonizer3D,
12
- find_matching_segmentations,
13
12
  skeletonize_batch,
14
13
  skeletonize_segmentation,
15
14
  )
16
15
  from .spline_fitting import (
17
16
  SkeletonSplineFitter,
18
- find_matching_segmentations_for_spline,
19
17
  fit_spline_batch,
20
18
  fit_spline_to_segmentation,
21
19
  fit_spline_to_skeleton,
@@ -34,12 +32,10 @@ __all__ = [
34
32
  "separate_components_batch",
35
33
  "TubeSkeletonizer3D",
36
34
  "skeletonize_segmentation",
37
- "find_matching_segmentations",
38
35
  "skeletonize_batch",
39
36
  "SkeletonSplineFitter",
40
37
  "fit_spline_to_skeleton",
41
38
  "fit_spline_to_segmentation",
42
- "find_matching_segmentations_for_spline",
43
39
  "fit_spline_batch",
44
40
  "create_validbox_mesh",
45
41
  "generate_validbox",
@@ -0,0 +1,214 @@
1
+ """Split multilabel segmentations into individual single-class segmentations."""
2
+
3
+ from typing import TYPE_CHECKING, Any, Dict, List, Optional
4
+
5
+ import numpy as np
6
+ from copick.util.log import get_logger
7
+
8
+ if TYPE_CHECKING:
9
+ from copick.models import CopickRoot, CopickRun, CopickSegmentation
10
+
11
+ logger = get_logger(__name__)
12
+
13
+
14
+ def split_multilabel_segmentation(
15
+ segmentation: "CopickSegmentation",
16
+ run: "CopickRun",
17
+ output_user_id: str = "split",
18
+ ) -> List["CopickSegmentation"]:
19
+ """
20
+ Split a multilabel segmentation into individual single-class binary segmentations.
21
+
22
+ For each label value in the multilabel segmentation, this function looks up the
23
+ corresponding PickableObject and creates a binary segmentation named after that object.
24
+
25
+ Args:
26
+ segmentation: Input multilabel segmentation to split
27
+ run: CopickRun object containing the segmentation
28
+ output_user_id: User ID for output segmentations (default: "split")
29
+
30
+ Returns:
31
+ List of created CopickSegmentation objects, one per label found in the input
32
+ """
33
+ # Load segmentation volume
34
+ volume = segmentation.numpy()
35
+ if volume is None:
36
+ raise ValueError("Could not load segmentation data")
37
+
38
+ if volume.size == 0:
39
+ raise ValueError("Empty segmentation data")
40
+
41
+ # Get root to access pickable objects configuration
42
+ root = run.root
43
+ voxel_size = segmentation.voxel_size
44
+ input_session_id = segmentation.session_id
45
+
46
+ # Find all unique non-zero labels
47
+ unique_labels = np.unique(volume)
48
+ unique_labels = unique_labels[unique_labels > 0] # Skip background (0)
49
+
50
+ logger.debug(f"Found {len(unique_labels)} unique labels: {unique_labels.tolist()}")
51
+
52
+ output_segmentations = []
53
+
54
+ # Process each label
55
+ for label_value in unique_labels:
56
+ # Look up the PickableObject with this label
57
+ pickable_obj = next((obj for obj in root.config.pickable_objects if obj.label == label_value), None)
58
+
59
+ if pickable_obj is None:
60
+ logger.warning(f"No pickable object found for label {label_value}, using label value as name")
61
+ object_name = str(label_value)
62
+ else:
63
+ object_name = pickable_obj.name
64
+ logger.debug(f"Label {label_value} → object '{object_name}'")
65
+
66
+ # Create binary mask for this label
67
+ binary_mask = (volume == label_value).astype(np.uint8)
68
+ voxel_count = int(np.sum(binary_mask))
69
+
70
+ if voxel_count == 0:
71
+ logger.warning(f"Label {label_value} has no voxels, skipping")
72
+ continue
73
+
74
+ logger.debug(f"Creating segmentation for '{object_name}' with {voxel_count} voxels")
75
+
76
+ # Create output segmentation
77
+ try:
78
+ output_seg = run.new_segmentation(
79
+ name=object_name,
80
+ user_id=output_user_id,
81
+ session_id=input_session_id,
82
+ is_multilabel=False,
83
+ voxel_size=voxel_size,
84
+ exist_ok=True,
85
+ )
86
+
87
+ # Store the binary mask
88
+ output_seg.from_numpy(binary_mask)
89
+ output_segmentations.append(output_seg)
90
+
91
+ logger.debug(f"Successfully created segmentation '{object_name}:{output_user_id}/{input_session_id}'")
92
+
93
+ except Exception as e:
94
+ logger.exception(f"Failed to create segmentation for label {label_value} ('{object_name}'): {e}")
95
+ continue
96
+
97
+ # Log single-line summary
98
+ if output_segmentations:
99
+ object_names = [seg.name for seg in output_segmentations]
100
+ logger.info(f"Run '{run.name}': Split {len(output_segmentations)} labels → {', '.join(object_names)}")
101
+
102
+ return output_segmentations
103
+
104
+
105
+ def _split_labels_worker(
106
+ run: "CopickRun",
107
+ segmentation_name: str,
108
+ segmentation_user_id: str,
109
+ segmentation_session_id: str,
110
+ voxel_spacing: float,
111
+ output_user_id: str,
112
+ ) -> Dict[str, Any]:
113
+ """
114
+ Worker function for batch splitting of multilabel segmentations.
115
+
116
+ Args:
117
+ run: CopickRun to process
118
+ segmentation_name: Name of the input segmentation
119
+ segmentation_user_id: User ID of the input segmentation
120
+ segmentation_session_id: Session ID of the input segmentation
121
+ voxel_spacing: Voxel spacing of the segmentation
122
+ output_user_id: User ID for output segmentations
123
+
124
+ Returns:
125
+ Dictionary with processing results and statistics
126
+ """
127
+ try:
128
+ # Get the input segmentation
129
+ segmentations = run.get_segmentations(
130
+ name=segmentation_name,
131
+ user_id=segmentation_user_id,
132
+ session_id=segmentation_session_id,
133
+ voxel_size=voxel_spacing,
134
+ is_multilabel=True,
135
+ )
136
+
137
+ if not segmentations:
138
+ return {"processed": 0, "errors": [f"No multilabel segmentation found for run {run.name}"]}
139
+
140
+ segmentation = segmentations[0]
141
+
142
+ # Verify it's multilabel
143
+ if not segmentation.is_multilabel:
144
+ return {
145
+ "processed": 0,
146
+ "errors": [f"Segmentation in run {run.name} is not multilabel (is_multilabel=False)"],
147
+ }
148
+
149
+ # Split the segmentation
150
+ output_segmentations = split_multilabel_segmentation(
151
+ segmentation=segmentation,
152
+ run=run,
153
+ output_user_id=output_user_id,
154
+ )
155
+
156
+ # Collect object names created
157
+ object_names = [seg.name for seg in output_segmentations]
158
+
159
+ return {
160
+ "processed": 1,
161
+ "errors": [],
162
+ "labels_split": len(output_segmentations),
163
+ "object_names": object_names,
164
+ }
165
+
166
+ except Exception as e:
167
+ logger.exception(f"Error processing run {run.name}: {e}")
168
+ return {"processed": 0, "errors": [f"Error processing run {run.name}: {e}"]}
169
+
170
+
171
+ def split_labels_batch(
172
+ root: "CopickRoot",
173
+ segmentation_name: str,
174
+ segmentation_user_id: str,
175
+ segmentation_session_id: str,
176
+ voxel_spacing: float,
177
+ output_user_id: str = "split",
178
+ run_names: Optional[List[str]] = None,
179
+ workers: int = 8,
180
+ ) -> Dict[str, Any]:
181
+ """
182
+ Batch split multilabel segmentations across multiple runs.
183
+
184
+ Args:
185
+ root: The copick root containing runs to process
186
+ segmentation_name: Name of the input segmentation
187
+ segmentation_user_id: User ID of the input segmentation
188
+ segmentation_session_id: Session ID of the input segmentation
189
+ voxel_spacing: Voxel spacing in angstroms
190
+ output_user_id: User ID for output segmentations (default: "split")
191
+ run_names: List of run names to process. If None, processes all runs.
192
+ workers: Number of worker processes (default: 8)
193
+
194
+ Returns:
195
+ Dictionary with processing results and statistics per run
196
+ """
197
+ from copick.ops.run import map_runs
198
+
199
+ runs_to_process = [run.name for run in root.runs] if run_names is None else run_names
200
+
201
+ results = map_runs(
202
+ callback=_split_labels_worker,
203
+ root=root,
204
+ runs=runs_to_process,
205
+ workers=workers,
206
+ task_desc="Splitting multilabel segmentations",
207
+ segmentation_name=segmentation_name,
208
+ segmentation_user_id=segmentation_user_id,
209
+ segmentation_session_id=segmentation_session_id,
210
+ voxel_spacing=voxel_spacing,
211
+ output_user_id=output_user_id,
212
+ )
213
+
214
+ return results
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: copick-utils
3
- Version: 1.0.2
3
+ Version: 1.1.0
4
4
  Summary: Utilities for copick
5
5
  Project-URL: Repository, https://github.com/KyleHarrington/copick-utils.git
6
6
  Project-URL: Issues, https://github.com/KyleHarrington/copick-utils/issues
@@ -1,4 +1,4 @@
1
- copick_utils/__init__.py,sha256=KV4Of88sktfBOKjUJLLuaknxcX88-X8dHO3hfmY1TKQ,135
1
+ copick_utils/__init__.py,sha256=Ob4phsN9gDkTb7bS7E38AH24iqiXU9mfPP12_gWtw2g,135
2
2
  copick_utils/cli/__init__.py,sha256=51i1xC-N2laWLdZLdpsKMSB9LdO3BEdqDdWIbyxJ8f0,634
3
3
  copick_utils/cli/clipmesh.py,sha256=M1MP8lw16diGHajipFXbZDQ4ZrsTvFiqAaHji0Smmtc,6172
4
4
  copick_utils/cli/clippicks.py,sha256=7lmEZj8GDyVqc9l_rvGhAFTYWUhSEnwZcWu8w9LStBU,5857
@@ -21,12 +21,13 @@ copick_utils/cli/picks2sphere.py,sha256=P-xFFDJdEowxHF20aLMO2Xymv7VHAaP4paBbgPWe
21
21
  copick_utils/cli/picks2surface.py,sha256=vXprZa3tHndSE3Sp1yPOEqyv2kRkAbV7QlHjF8oE1zg,5712
22
22
  copick_utils/cli/picksin.py,sha256=SD32-BJoxB2dGQU54ksuq2rCmcGjnQLldE0hrnV8pLA,5485
23
23
  copick_utils/cli/picksout.py,sha256=odm2JeTyDzBrXkr2SCsbE3REGj589bCgPOq3ldj8soA,5568
24
- copick_utils/cli/processing_commands.py,sha256=s9Mn0mvKQ60XQN6o6eyO2x7qZX_Hpq0K3kPwMuLHlKY,576
24
+ copick_utils/cli/processing_commands.py,sha256=3-zZhyM279_mQB8Z4-XM0DDowF8YK3i5cFOhUaw7MOM,637
25
25
  copick_utils/cli/seg2mesh.py,sha256=9fv3zaVHxAS4VhhlvU6qJ7W7wWjAYDedzDNeiaMZxTk,4732
26
26
  copick_utils/cli/seg2picks.py,sha256=05RUsOJETnOc9zMmW_x3DVr1t2F01w2e2R8sAaJrHg8,4549
27
27
  copick_utils/cli/segop.py,sha256=K_ju0MB4n-k5-wkOI7gOvkHCUslgHZCZUOFn-k2ta9M,9372
28
28
  copick_utils/cli/separate_components.py,sha256=m0c5v4ZXSeoUURcVoY7jCE6I_jGbTdZXqEoLUiURIHs,5670
29
29
  copick_utils/cli/skeletonize.py,sha256=CV_J9nibqLpKmckNRG6-7zlq6ROqPBLk166yDzhxJ5w,5646
30
+ copick_utils/cli/split_labels.py,sha256=J09-qAPLtzcmid6yln37zu9HwVRYz8wLzJ6PvHWgEgU,5443
30
31
  copick_utils/cli/util.py,sha256=tne45hU9dL0AepQhV9KKqkKjAMAfweR9_GLGHJnisnc,18011
31
32
  copick_utils/cli/validbox.py,sha256=Z-HY73QnWwxL-cLHLiIWNnYz4t-4jf63ho8pfk38xXU,5364
32
33
  copick_utils/converters/__init__.py,sha256=f3_UJJ2jJqkOAaEy6y4i3x1baFZEem9aTrGGdBQgXFQ,1701
@@ -55,17 +56,18 @@ copick_utils/logical/point_operations.py,sha256=a2KrTqSuXeb-GNvFywN7iDuATkTDsSz0
55
56
  copick_utils/logical/segmentation_operations.py,sha256=AlPHq3t7Hw46svW6qBnr3i1PPulcNvDNikoYD7_RnrI,12090
56
57
  copick_utils/pickers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
58
  copick_utils/pickers/grid_picker.py,sha256=KKfdv3fDmeY7XwqiVADRQJibr1eyjYoG9ZpaihcrgHw,2345
58
- copick_utils/process/__init__.py,sha256=ERh5Ka4vU0_ub4_UZFDWOXbr-my79s3RekfS9dV41wE,1227
59
+ copick_utils/process/__init__.py,sha256=lbjTjGxGQZdQgzt_ommpmCwety6tIWtGRp7tKzzAuDk,1069
59
60
  copick_utils/process/connected_components.py,sha256=9h_Feu3wxYWW84evaUmW73_UY0fGmTszS55R8oljjEo,13601
60
61
  copick_utils/process/filter_components.py,sha256=dU0l0PJ6ZejY6ANqsrDRtQ1RevdKWn6Txl4nr0sBq14,10587
61
62
  copick_utils/process/hull.py,sha256=tLah4_fcwIuiy9Kaly5sVVDHltGBh6euyO3i9W99pms,3191
62
63
  copick_utils/process/skeletonize.py,sha256=gLQgQdYlyuqFHyC19vzx8-gna15UiAM_1i85hF-mkMg,11713
63
64
  copick_utils/process/spline_fitting.py,sha256=6pBr4w7cKZb5xaKaU3b1R5A6cIVDrCTRKTVVUL4vDSI,25025
65
+ copick_utils/process/split_labels.py,sha256=5BZw4gzh57_4DgmYErwSwjWzBJKkiYbf3qu4On2MvCU,7352
64
66
  copick_utils/process/validbox.py,sha256=5uEbVjmyNJxp__2XJal1jP7Ecpqv2uZsBkAy26zpxx8,9494
65
67
  copick_utils/util/__init__.py,sha256=V76KYVdhERpiXkCsaap4WQBbvfXG04x3vGvdSayzpmk,190
66
68
  copick_utils/util/config_models.py,sha256=vzih8OsmvKQk8rULYyY6f6yiRHUnk48KKrCES5Pvq4w,21964
67
- copick_utils-1.0.2.dist-info/METADATA,sha256=D0LJF7w4_FX3kByXK8ee_Qn10sDSNgSZpBYZ47xrcaw,4562
68
- copick_utils-1.0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
69
- copick_utils-1.0.2.dist-info/entry_points.txt,sha256=qDi4JAAncjmkB86tEivSpc9QVr6YbdFC3T0SwIabNaM,1504
70
- copick_utils-1.0.2.dist-info/licenses/LICENSE,sha256=3UHKsYd99Gh_qf1a9s8G5sdKqafgbGs5WIMoeX0OcdY,1105
71
- copick_utils-1.0.2.dist-info/RECORD,,
69
+ copick_utils-1.1.0.dist-info/METADATA,sha256=ZA06pl5dUEy2OZF5AcQdpNOBl2g-AjDRcackrAaVS9M,4562
70
+ copick_utils-1.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
71
+ copick_utils-1.1.0.dist-info/entry_points.txt,sha256=qw3xz_Bs_-KijogXZ-vUU9efvQG0AlSF3SgG4HpNr50,1555
72
+ copick_utils-1.1.0.dist-info/licenses/LICENSE,sha256=3UHKsYd99Gh_qf1a9s8G5sdKqafgbGs5WIMoeX0OcdY,1105
73
+ copick_utils-1.1.0.dist-info/RECORD,,
@@ -26,4 +26,5 @@ fit_spline = copick_utils.cli.processing_commands:fit_spline
26
26
  hull = copick_utils.cli.processing_commands:hull
27
27
  separate_components = copick_utils.cli.processing_commands:separate_components
28
28
  skeletonize = copick_utils.cli.processing_commands:skeletonize
29
+ split = copick_utils.cli.processing_commands:split
29
30
  validbox = copick_utils.cli.processing_commands:validbox