Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/copick_utils/cli/combine_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
@add_run_names_option
@optgroup.group("\nInput Options", help="Options related to the input segmentations.")
@add_input_option("segmentation")
@optgroup.option(
"--instances",
"instances_uri",
type=str,
default=None,
help="Instance segmentations to add to a panoptic output (-o ...?panoptic=true), e.g. "
'"microtubule:trace/1@10.0" (patterns allowed; ?instance=true is implied). Each one adds its object\'s '
"label and its instance IDs.",
)
@add_workers_option
@optgroup.group("\nOutput Options", help="Options related to the output segmentation.")
@add_output_option("segmentation", default_tool="combine")
Expand All @@ -28,6 +37,7 @@ def combine(
config,
run_names,
input_uri,
instances_uri,
workers,
output_uri,
debug,
Expand All @@ -43,11 +53,17 @@ def combine(
When multiple inputs overlap, the lowest label value wins. This resolution is
deterministic and reproducible, and overlapping voxels are logged as warnings.

With an output URI ending in `?panoptic=true`, the result is a panoptic segmentation
instead: the inputs fill its label channel (regions without instances, such as
membranes), and the instance segmentations given with `--instances` add their objects'
labels and their instance IDs. An instance ID is kept where its object's label won.

URI Format:

\b
Segmentations: name:user_id/session_id@voxel_spacing
Use glob/regex patterns to match multiple segmentations per run.
Panoptic output: append ?panoptic=true to the output URI

Examples:

Expand All @@ -63,6 +79,11 @@ def combine(
# Combine for specific runs
copick process combine -r run1 -r run2 -i "*:user1/session@10.0" -o "labels:combine/0"

\b
# Membranes as regions plus microtubule instances, as one panoptic segmentation
copick process combine -i "membrane:data-portal/*@10.0" --instances "microtubule:trace/1@10.0" \\
-o "cell:combine/0@10.0?panoptic=true"

See Also:

\b
Expand Down Expand Up @@ -100,6 +121,7 @@ def combine(
config=task_config,
run_names=run_names_list,
workers=workers,
instances_uri=instances_uri,
)

successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
Expand Down
37 changes: 37 additions & 0 deletions src/copick_utils/cli/filter_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@
help="Keep only the N largest connected components by voxel count (e.g. 1 = keep only the "
"single largest). Applied in addition to any --min-size/--max-size filter.",
)
@optgroup.option(
"--min-skeleton-length",
type=float,
default=None,
help="Minimum skeleton length of a component (or instance) to keep, in the unit set by --length-unit "
"(optional). Drops specks and short blobs of filamentous structures; see seg-stats --skeleton.",
)
@optgroup.option(
"--length-unit",
type=click.Choice(["angstrom", "voxel"]),
default="angstrom",
help="Unit for --min-skeleton-length: 'angstrom' for Å, 'voxel' for voxels.",
)
@add_workers_option
@optgroup.group("\nOutput Options", help="Options related to output segmentations.")
@add_output_option("segmentation", default_tool="filter-components")
Expand All @@ -66,6 +79,8 @@ def filter_components(
max_size,
size_unit,
keep_largest,
min_skeleton_length,
length_unit,
workers,
output_uri,
debug,
Expand All @@ -82,10 +97,17 @@ def filter_components(
top of any --min-size/--max-size limits. Run `copick process seg-stats` first to inspect
component sizes and choose sensible thresholds.

A multilabel segmentation is filtered label by label and keeps its labels. An instance
segmentation (`?instance=true`) is filtered instance by instance: whole instances are
dropped, and the IDs of the kept ones do not change, so picks and filaments with the same
IDs still match. `--min-skeleton-length` also drops components (or instances) whose
skeleton is shorter than the given length.

URI Format:

\b
Segmentations: name:user_id/session_id@voxel_spacing
Instance segmentations: append ?instance=true

Examples:

Expand All @@ -107,6 +129,11 @@ def filter_components(
# Keep only the single largest connected component
copick process filter-components -i "membrane:user1/auto-001@10.0" -o "membrane_main" --keep-largest 1

\b
# Drop microtubule instances shorter than 100 nm, keeping the IDs of the others
copick process filter-components -i "microtubule:trace/1@10.0?instance=true" -o "microtubule:trace/1-long" \\
--min-skeleton-length 1000

See Also:

\b
Expand Down Expand Up @@ -134,6 +161,13 @@ def filter_components(
if max_size is not None:
max_size = max_size * voxel_volume

if length_unit == "voxel" and min_skeleton_length is not None:
input_params = parse_copick_uri(input_uri, "segmentation")
vs_raw = input_params.get("voxel_spacing")
if vs_raw is None or vs_raw == "*":
raise click.BadParameter("--length-unit voxel requires voxel spacing in the input URI (e.g., @10.0)")
min_skeleton_length = min_skeleton_length * float(vs_raw)

# Create config from URIs with smart defaults
try:
task_config = create_simple_config(
Expand All @@ -159,6 +193,8 @@ def filter_components(
if keep_largest < 1:
raise click.BadParameter("--keep-largest must be >= 1")
logger.info(f"Keeping only the {keep_largest} largest component(s)")
if min_skeleton_length is not None:
logger.info(f"Minimum skeleton length: {min_skeleton_length} Å")

# Parallel discovery and processing
results = filter_components_lazy_batch(
Expand All @@ -170,6 +206,7 @@ def filter_components(
min_size=min_size,
max_size=max_size,
keep_largest=keep_largest,
min_skeleton_length=min_skeleton_length,
)

successful = sum(1 for result in results.values() if result and result.get("processed", 0) > 0)
Expand Down
19 changes: 19 additions & 0 deletions src/copick_utils/cli/seg_stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@
help="Include background (label=0) connected component analysis in CSV output. "
"Background components are never included in plots.",
)
@optgroup.option(
"--skeleton/--no-skeleton",
default=False,
help="Also skeletonize each component (or instance) and report its skeleton length, label radius "
"(median distance from the skeleton to the background) and branch, junction and endpoint counts, in angstroms.",
)
@add_workers_option
@optgroup.group("\nOutput Options", help="Options related to the output.")
@optgroup.option(
Expand All @@ -55,6 +61,7 @@ def seg_stats(
input_uri,
connectivity,
include_background,
skeleton,
workers,
output_format,
output_path,
Expand All @@ -68,11 +75,18 @@ def seg_stats(
Output can be a CSV file or a histogram plot, and you can choose the voxel
connectivity (face, face-edge, or all) used to define components.

An instance segmentation (`?instance=true`) gets one row per instance ID, and a panoptic
segmentation (`?panoptic=true`) one row per (label, instance) segment, plus the connected
components of regions without instances. Without a type flag, the URI matches binary and
multilabel segmentations only. With `--skeleton`, each row also reports the component's
skeleton length and label radius, for example to choose length thresholds for filaments.

URI Format:

\b
Segmentations: name:user_id/session_id@voxel_spacing
Voxel spacing is optional — omit to match all voxel spacings.
Instance or panoptic segmentations: append ?instance=true or ?panoptic=true

Examples:

Expand All @@ -96,6 +110,10 @@ def seg_stats(
# Analyze specific runs and export as PNG
copick process seg-stats -i "membrane:user1/auto-001@10.0" -f plot -op ./stats.png -r run1 -r run2

\b
# Per-instance volumes and skeleton lengths of a microtubule instance segmentation
copick process seg-stats -i "microtubule:trace/1@10.0?instance=true" --skeleton -f csv -op ./mt.csv

See Also:

\b
Expand All @@ -121,6 +139,7 @@ def seg_stats(
include_background=include_background,
run_names=run_names_list,
workers=workers,
skeleton=skeleton,
)

# Summarize
Expand Down
24 changes: 22 additions & 2 deletions src/copick_utils/cli/separate_components.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from copick_utils.cli.util import add_input_option, add_output_option, add_workers_option
from copick_utils.util.config_models import create_simple_config
from copick_utils.util.segmentations import type_from_uri


@click.command(
Expand Down Expand Up @@ -62,10 +63,17 @@ def separate_components(
For multilabel segmentations the analysis is performed on each label separately. Output
segmentations use the `{instance_id}` placeholder for auto-numbering (e.g. `inst-0`, `inst-1`).

Alternatively, write all components into one segmentation: an output URI with
`?instance=true` writes an instance segmentation of a binary input (each component is an
instance, numbered 1, 2, ... by size, largest first), and `?panoptic=true` writes a
panoptic segmentation that keeps every label of a multilabel input and numbers the
components of each label. These outputs need no `{instance_id}` placeholder.

URI Format:

\b
Segmentations: name:user_id/session_id@voxel_spacing
Instance or panoptic output: append ?instance=true or ?panoptic=true

Examples:

Expand All @@ -83,6 +91,16 @@ def separate_components(
copick process separate-components -i "membrane:user1/manual-001@10.0" \\
-o "membrane:components/comp-{instance_id}@10.0"

\b
# All components as one instance segmentation (IDs 1..K by size)
copick process separate-components -i "microtubule:easymode/job006@10.0" --binary \\
-o "microtubule:components/job006@10.0?instance=true"

\b
# Components of every label of a multilabel segmentation as one panoptic segmentation
copick process separate-components -i "labels:user1/auto@10.0?multilabel=true" \\
-o "cell:components/auto@10.0?panoptic=true"

See Also:

\b
Expand All @@ -97,14 +115,16 @@ def separate_components(
root = copick.from_file(config)
run_names_list = list(run_names) if run_names else None

# Create config from URIs with smart defaults (individual_outputs for {instance_id})
# One segmentation per component (with {instance_id}), unless the output URI names an instance or panoptic
# segmentation, which holds all components in one store.
try:
output_type = type_from_uri(output_uri)
task_config = create_simple_config(
input_uri=input_uri,
input_type="segmentation",
output_uri=output_uri,
output_type="segmentation",
individual_outputs=True,
individual_outputs=output_type not in ("instance", "panoptic"),
command_name="components",
)
except ValueError as e:
Expand Down
10 changes: 10 additions & 0 deletions src/copick_utils/cli/split_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,15 @@ def split(
values do not match the config object labels; only the listed values are then split. The
input URI must name an exact segmentation (no wildcards) and include a voxel spacing.

A panoptic segmentation (input URI ending in `?panoptic=true`) is split per object: the
voxels with an instance ID become an instance segmentation of that object (keeping the
IDs), and its voxels without one become a binary segmentation.

URI Format:

\b
Segmentations: name:user_id/session_id@voxel_spacing
Panoptic input: append ?panoptic=true

Label-to-Object Mapping:

Expand Down Expand Up @@ -92,6 +97,10 @@ def split(
# Split only specific label values with an explicit name:value map
copick process split -i "predictions:model/run-001@10.0" --labels "sample:1,vacuum:2"

\b
# Split a panoptic segmentation into instance and binary segmentations per object
copick process split -i "cell:combine/0@10.0?panoptic=true"

See Also:

\b
Expand Down Expand Up @@ -164,6 +173,7 @@ def split(
run_names=run_names_list,
workers=workers,
labels=labels_map,
panoptic=bool(input_params.get("panoptic")),
)

# Aggregate results
Expand Down
Loading
Loading