Comfy-Org / Comfy-Org/ComfyUI

MaskComposite and/or/xor silently binarize feathered masks

Open
#15,660 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Python
Stars
133k
Forks
15.7k
Avg merge
1d 7h
Merged PRs (30d)
158

Description

If you combine two feathered (soft-edged) masks with `MaskComposite`'s `and`, `or` or `xor`, the soft edges do not survive: these operations first round every pixel at 0.5. Everything below 0.5 opacity is discarded to 0 and everything at or above it becomes 1, so the mask collapses to a hard edge along its own 0.5 contour — the outer half of every feather ramp is gone. Nothing warns you; it usually surfaces much later, as a hard seam where an inpaint or composite was supposed to blend.

[PR #15335](https://github.com/Comfy-Org/ComfyUI/pull/15335) adds `max` and `min` operations — the same union and intersection, with the feathering kept — and turns the hidden 0.5 cut point into an explicit `threshold` input for the times a binary result really is what you want.

## What happens now

A `MASK` is a float tensor in `[0, 1]`, and much of the ecosystem exists to produce the in-between shades — blur, feather, grow-with-blur, segmentation confidence. But `MaskComposite`'s three boolean operations snap each input to 0 or 1 before combining, via `.round().bool()` in [`comfy_extras/nodes_mask.py#L290-L294`](https://github.com/comfyanonymous/ComfyUI/blob/master/comfy_extras/nodes_mask.py#L290-L294):

```python
elif operation == "or":
output[...] = torch.bitwise_or(destination_portion.round().bool(), source_portion.round().bool()).float()
```

The result only ever contains 0.0 and 1.0. Two feathered masks combined with `or`:

![combining two feathered masks with the current or operation, showing that everything below 0.5 opacity is discarded](https://raw.githubusercontent.com/NubeBuster/ComfyUI/d7c80037c5ce7e80f46e124416d1b6438ba7eb5b/issue15660_v5_before_or_discarded.png)

## What is wrong with this

- **The feathering is lost everywhere**, not just where the masks overlap — a soft edge nowhere near the other mask still comes out hard.
- **Nothing fails.** The output is a valid mask, the graph runs, and the preview still looks roughly right. The damage shows up later, somewhere else.
- **No operation on this node unions or intersects soft masks.** `multiply` and `add` are product-style and saturating variants; the three operations actually named after set operations all round first — and nothing in their names says so.
- **The cut point is silent, and it is always 0.5.** Even when a hard result *is* what you want, there is no widget suggesting a threshold is involved, so a mask that peaks below 0.5 — a wide feather, a low-confidence segmentation — comes out empty.

## What PR #15335 changes

Two new operations, `max` and `min`: take the larger (or smaller) value at each pixel. On black-and-white masks they behave exactly like `or` and `and`; on soft masks they keep the gradient.

```python
elif operation == "max":
output[...] = torch.max(destination_portion, source_portion)
elif operation == "min":
output[...] = torch.min(destination_portion, source_portion)
```

![the same two feathered masks combined with or and with max, side by side](https://raw.githubusercontent.com/NubeBuster/ComfyUI/e4c0d5fdbfa9c52c98310d4138ffed585f2ff70f/issue15660_v6_before_after.png)

For the cases where a binary result is genuinely wanted, `and`, `or` and `xor` gain an optional `threshold` input (default `0.5`) in place of the hardcoded rounding. A pixel counts as set when it is strictly above the threshold, so `threshold = 0.0` keeps every pixel that is present at all — the union of the two footprints rather than of their confident cores.

Existing operations are untouched, the new ones are appended to the end of the list, and the default threshold reproduces the old rounding exactly, so saved workflows are unaffected. Both widgets carry a tooltip stating which operations keep intermediate values and which binarize.

Contributor guide

Open the contributing guide

Research direction

Start in comfy_extras/nodes_mask.py at lines 290-294 and review PR #15335, which is named in the issue. Check the existing boolean operations and the described max/min and threshold behavior against soft-mask cases; done means feathering is preserved where required and the default behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
backend, machine-learning
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Stale
Clarity
Clearly specified
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.