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
8 changes: 7 additions & 1 deletion labelmerge/config/snakebids.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ parse_args:
help: Space separated integer labels from the base labelmap to keep over overlay labels at the same voxels.
nargs: '*'
--overlay_exceptions:
help: Space separated integer labels from the overlay image to discard.
help: Space separated integer labels from the overlay image to be overwritten by base labels at the same voxels.
nargs: '*'
--base_drops:
help: Space separated integer labels from the base image to drop from the output.
nargs: '*'
--overlay_drops:
help: Space separated integer labels from the overlay image to drop from the output.
nargs: '*'

# Workflow specific config
Expand Down
9 changes: 8 additions & 1 deletion labelmerge/workflow/rules/label_harmonization.smk
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,17 @@ rule merge_labels:
overlay_exceptions=f"--overlay_exceptions {' '.join(config['overlay_exceptions'])}"
if config.get("overlay_exceptions")
else "",
base_drops=f"--base_drops {' '.join(config['base_drops'])}"
if config.get("base_drops")
else "",
overlay_drops=f"--overlay_drops {' '.join(config['overlay_drops'])}"
if config.get("overlay_drops")
else "",
resources:
script=str(Path(workflow.basedir) / "scripts" / "labelmerge.py"),
shell:
"python3 {resources.script} {input.base_map} {input.base_metadata} "
"{input.overlay_map} {input.overlay_metadata} "
"{output.merged_map} {output.merged_metadata} "
"{params.base_exceptions} {params.overlay_exceptions}"
"{params.base_exceptions} {params.overlay_exceptions} "
"{params.base_drops} {params.overlay_drops}"
39 changes: 35 additions & 4 deletions labelmerge/workflow/scripts/labelmerge.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#!/usr/bin/env python
"""Script to merge two labelmaps with their BIDS metadata."""
from __future__ import annotations

from argparse import ArgumentParser
Expand Down Expand Up @@ -49,15 +50,20 @@ def split_labels(
atlas: np.ndarray,
metadata: pd.DataFrame,
prefix: str = "",
exceptions: list[str] = [],
exceptions: list[int] | None = None,
drops: list[int] | None = None,
) -> list[xr.Dataset]:
if exceptions is None:
exceptions = []
if drops is None:
drops = []
unique_vals = np.unique(atlas[atlas > 0])
normal_ds = xr.Dataset(
dict(
[
assemble_mask(atlas, metadata, label, prefix)
for label in unique_vals
if label not in exceptions
if label not in exceptions + drops
]
)
)
Expand Down Expand Up @@ -133,7 +139,25 @@ def gen_parser() -> ArgumentParser:
nargs="*",
help=(
"Space separated list of integer labels from the overlay image to "
"discard."
"be overwritten by labels from the base image."
),
type=int,
)
parser.add_argument(
"--base_drops",
nargs="*",
help=(
"Space separated list of integer labels from the base image to "
"drop from the output map."
),
type=int,
)
parser.add_argument(
"--overlay_drops",
nargs="*",
help=(
"Space separated list of integer labels from the overlay image to "
"drop from the output map."
),
type=int,
)
Expand All @@ -151,21 +175,28 @@ def main():
overlay_exceptions = (
args.overlay_exceptions if args.overlay_exceptions else []
)
base_drops = args.base_drops if args.base_drops else []
overlay_drops = args.overlay_drops if args.overlay_drops else []
base_datasets = split_labels(
base_data,
base_metadata,
prefix="base ",
exceptions=base_exceptions,
drops=base_drops,
)
overlay_datasets = split_labels(
overlay_data,
overlay_metadata,
prefix="overlay ",
exceptions=overlay_exceptions,
drops=overlay_drops,
)
# Note that overlay exceptions are ignored
merged_map, merged_metadata = merge_labels(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you just walk me through this? From what I gather, we are adding (in order):

overlay exceptions -> base inclusions -> overlay inclusions -> base exceptions?

I am guessing this results in some overwriting of the existing labels?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure:
split_labels considers each unique label found in a label map. If that label is in the list of drops, it is ignored. If that label is in the list of exclusions, it's split into a dataset that contains all the exclusion masks. Otherwise, it's split into a dataset that contains all the "regular" masks. (If there are no exceptions, only the dataset of "regular" masks is returned)

So, after applying split_labels to the base and overlay input label maps, we have a base dataset, an overlay dataset, and maybe a base exclusions dataset and/or overlay exclusion dataset.

We then pass a list of those datasets in the priority order you identified (overlay exceptions (if applicable) -> base regular -> overlay regular -> base exceptions (if applicable)) to merge_labels, which reindexes all the label names that show up in the datasets, and writes the masks to an output 3D array with the new indices in order, so that later datasets overwrite labels written by earlier datasets.

Copy link
Collaborator

@kaitj kaitj Jan 11, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good - I think there is still a potential fundamental concern about mixed labels for voxels within a region that don't overlap, but that was present prior to this. In any case, this PR looks good to merge to me.

base_datasets[0:1] + overlay_datasets[0:1] + base_datasets[1:]
overlay_datasets[1:]
+ base_datasets[0:1]
+ overlay_datasets[0:1]
+ base_datasets[1:]
)
merged_img = nib.Nifti1Image(
dataobj=merged_map, affine=base_affine, header=base_header
Expand Down