# ComfyUI-MultiMaskOps A collection of ComfyUI nodes for working with **multiple masks at once** — sorting them by position, picking one out, removing duplicates, and expanding/feathering them without letting neighbours overlap. These nodes are aimed at multi-subject workflows (e.g. multiple characters segmented from one image) where you end up with a batch or list of masks and need to organise, clean, or grow them individually. Expansion and feathering are implemented to match ComfyUI's native behaviour: dilation uses the same method as the built-in **GrowMask** node (including `tapered_corners`), and feathering uses the same Gaussian blur convention as **GrowMaskWithBlur** (KJNodes). --- ## Installation 1. Clone or copy this folder into your `ComfyUI/custom_nodes/` directory: ``` ComfyUI/custom_nodes/ComfyUI-MultiMaskOps/ ``` 2. Search for **MultiMaskOps** in ComfyUI manager and install it (soon) All nodes appear under the **MultiMaskOps** category. ### Requirements The nodes rely on `torch`, `scipy`, and `torchvision`, all of which ship with a standard ComfyUI install. No extra installation should be needed. --- ## A note on batches vs lists ComfyUI masks come in two shapes: - **Batch** — a single tensor of shape `[N, H, W]` holding N masks. - **List** — a Python list of separate mask tensors, processed one item at a time. Most nodes here operate on **lists** (so each mask is handled independently). Use **Mask Batch To List** to convert a batch into a list before feeding these nodes, and **Mask List To Batch** to convert back if a downstream node expects a batch. --- ## Nodes ### Mask Batch To List Splits a mask batch `[N, H, W]` into a list of N individual masks, each `[1, H, W]`. Use this to feed a batch into the list-based nodes below. **Inputs** - `masks` — a mask batch. **Outputs** - `masks` — a list of individual masks. ### Mask List To Batch Combines a list of masks back into a single batch tensor `[N, H, W]`. Masks of differing sizes are resized to match the first. **Inputs** - `masks` — a list of masks. **Outputs** - `masks` — a single mask batch. **Example:** ![batch to list list to batch.png](examples/batch%20to%20list%20list%20to%20batch.png) --- ### Mask Sort By Position Sorts a list of masks by their spatial position, so you can reliably address them as "first from the left", "top-most", and so on. Also outputs a debug image showing the reference point used for each mask. **Inputs** - `masks` — a list of masks to sort. - `reference_point` — `centroid` (value-weighted centre of the mask) or `extreme_point` (the outermost pixel along the primary sort direction, e.g. the left-most pixel when sorting left-to-right). - `sort_left_to_right` — horizontal direction. - `sort_top_to_bottom` — vertical direction. - `primary_axis_horizontal` — which axis sorts first; the other axis breaks ties. Full ties fall back to original order (stable sort). - `flatten_before_sort` — binarize each mask at `threshold` before computing its sort point (sorting only; output masks are unchanged). - `drop_below_threshold` — ignore pixels below `threshold` when computing the sort point (sorting only; output masks are unchanged). - `threshold` — cutoff used by the two options above. **Outputs** - `masks` — the input masks reordered by position. - `debug_image` — a white image with a black dot at each mask's sort reference point, in sorted order. **Example:** ![mask sort.png](examples/mask%20sort.png) --- ### Mask Pick By Index Returns a single mask from a list by index. Supports Python-style negative indexing (`-1` = last), and clamps out-of-range values to the nearest end so it always returns a valid mask (given a non-empty list). **Inputs** - `masks` — a list of masks. - `index` — which mask to return. `0` = first, `-1` = last, etc. **Outputs** - `mask` — the selected mask. Pairs well with **Mask Sort By Position**: sort left-to-right, then pick `0` for the left-most subject or `-1` for the right-most, regardless of how many masks were detected. **Example:** ![mask index.png](examples/mask%20index.png) --- ### Mask Deduplicate Detects and removes duplicate masks using **transitive IoU grouping**: if mask A overlaps B and B overlaps C (each above the threshold), all three are treated as one group — even if A and C don't directly overlap enough. Each group is then collapsed to a single mask. **Inputs** - `masks` — a list of masks to deduplicate. - `iou_threshold` — minimum Intersection-over-Union for two masks to count as duplicates. Higher = stricter. - `keep` — how to collapse each group: - `largest` / `smallest` / `first` — keep one existing mask from the group. - `OR` — union of the group (soft max). - `AND` — intersection of the group (soft min). - `threshold` — binarization cutoff for computing IoU and areas. **Outputs** - `masks` — the deduplicated masks, one per group. - `removed_count` — how many masks were removed. - `group_images` — one debug image per duplicate group, each mask drawn in a distinct colour with overlaps blended (so overlap of two masks reads as a blended colour). Only groups with 2+ members are shown. **Example:** ![mask deduplicate.png](examples/mask%20deduplicate.png) --- ### Mask Expand Without Overlap Expands (and optionally feathers) each mask in a list while making sure no mask ever leaks into another mask's original region. Each mask is grown, then the other masks' regions are subtracted from it. Dilation matches the native **GrowMask** method (with `tapered_corners`); feathering matches **GrowMaskWithBlur**'s Gaussian blur. **Inputs** - `masks` — a list of masks to expand. - `expand_pixels` — how many pixels to grow each mask. - `tapered_corners` — rounds the corners of the expansion (matches native GrowMask). - `feather_amount` — Gaussian blur radius for soft edges (`0` = no feather). - `feather_mode` — how feathering interacts with neighbours: - `cut_feather` — feather, then subtract neighbours (hard seam at the neighbour edge). - `preserve_feather` — subtract, feather, then subtract again (soft seam that never leaks). - `overlap_policy` — how expansions interact in empty space: - `allow` — expansions may overlap freely outside original regions (only originals are protected). - `retreat` — contested empty pixels are cleared in both masks (clean empty seam). - `priority` — earlier masks in the list win contested pixels (gap-free; deterministic by order). - `subtract_hard` — flatten the protected region so a mask can never leave any value inside another's area (recommended on). - `threshold` — binarization cutoff used when building the protection regions. **Outputs** - `masks` — the processed masks, expanded/feathered without overlapping each other's originals. **Example:** ![mask expand.png](examples/mask%20expand.png) --- ## Acknowledgements Built with the help of Claude (Anthropic), which assisted with the node design, implementation, and testing. --- ## License MIT