Image Mask Editor (C2C)
Paint a mask that actually reaches the node
- image
- input_mask
- mask
- preview
- info
ComfyUI's built-in mask editor is a right-click ritual: you open it on a LoadImage, paint something, save, and then discover the mask lives on that widget and is a pain to reuse, resize, or adjust. This is a full-window editor where the mask belongs to the node and - the part that matters - is a real MASK output you can wire anywhere.
You reach for it when a mask needs a human. Detectors are great until the subject holds an object in front of their face, and segmentation models are great until the edge is hair. Sometimes you just want to brush it in, at native resolution, over the exact image the node received.
How it works
The node keeps only an editor_id in the workflow. The painted masks are PNGs on the server under input/c2c_masks/<editor_id>/, so they survive a restart, and the workflow file stays small. The content hash of those masks is part of the cache key, which is the bit that makes it feel native: edit a mask and the graph re-runs, leave it alone and the graph stays cached.
What the editor shows depends on what you wire into image. A directly connected LoadImage or a C2C/VHS video loader shows its frames with no run needed. Anything computed - a crop, an upscale, a VAE decode - shows the exact frames the node received on its last run. Before that first run you get the nearest upstream preview with a banner telling you so, which is the honest way to handle it.
Tools are the set you'd expect and a couple you wouldn't: brush and eraser with size/hardness/opacity, rectangle, ellipse, polygon, lasso, bucket fill with tolerance, plus colour-range select and SAM smart-select if a SAM 2.1 model is available. Alt+stroke subtracts. View modes cycle overlay → matte → rubylith (the compositing convention: red tint over what's not selected) → outline → bare image. Batches get a frame strip with one mask per frame, and "Copy previous frame" is the shortcut that makes rotoscoping tolerable.
The parameters, and why this is the good design
grow, feather, threshold and invert are node parameters, not baked into the painting. Change a feather and nothing needs repainting. Note the sequencing the tooltip spells out: any non-zero grow hardens the edge first (every partly covered pixel counts), then feather softens it back. So grow +1 and feather 2 is a different mask from feather 2 alone.
frame_mode picks per_frame (each batch index uses its stored frame) or shared (frame 0 applies to everything). combine decides how the painted mask merges with an optional input_mask: replace, add, subtract, intersect - and without an input_mask connected, only replace does anything, since there's nothing to combine with.
Outputs: mask (the final [B,H,W] result, painted frames combined, then grown/feathered/thresholded/inverted), preview (your image with the mask tinted over it), and info (frame count, stored masks, mode, and any notes such as resized frames).
Install
cd ComfyUI/custom_nodes
git clone https://github.com/Code2Collapse/ComfyUI-CustomNodePacks.git
Or "CustomNodePacks" in ComfyUI Manager, then restart ComfyUI. The editor is a JS frontend component shipped with the pack, so a hard browser refresh after a pack update is worth doing if the dialog looks stale. The node itself needs no models; SAM smart-select needs a SAM 2.1 checkpoint and the sam2 package, and edge refine needs ViTMatte weights.
Where people get burned
The masks live on the server, not in the workflow. Send someone your .json and they get an empty mask, because their input/c2c_masks/<editor_id>/ doesn't have your PNGs. Export PNG from the frame menu if you need the mask to travel.
Second: editor_id says "auto-set by the editor UI - don't edit by hand" and it means it. Duplicating a node carries the id; two nodes sharing an id fight over the same stored masks, which looks like the editor randomly reverting frames.
Third: nothing is painted until you open the editor and press Enter to save. The growth stage will happily run on an all-black mask and produce an all-black mask, and it will not be the node's fault.
Inputs (9)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Input batch (B,H,W,3). Drives B/H/W. | |
| editor_id | STRING | Auto-set by the editor UI. Don't edit by hand. | |
| frame_mode | COMBO | per_frame | per_frame: each batch index uses stored frame i; shared: stored frame 0 applies to all. |
| grow | INT | 0-256–256 | Grow (positive) or shrink (negative) the mask by this many pixels. Any non-zero value makes the edge hard first (every partly covered pixel counts); feather softens it again afterwards. |
| feather | FLOAT | 00–256 | Soften the mask edge, in whole pixels (0 = off). Gaussian with sigma = 0.33 x this value, the same feather the Smart Crop/Stitch nodes use. |
| threshold | FLOAT | 0.000–1 | Binarize after feather. 0 = keep soft mask. |
| invert | BOOLEAN | false | Invert mask after threshold. |
| input_maskopt | MASK | Optional mask to combine with the painted mask. | |
| combineopt | COMBO | replace | How painted mask combines with input_mask. Without input_mask only replace applies. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| mask | MASK | The final mask [B,H,W] in 0-1: painted frames, combined with input_mask, then grow, feather, threshold and invert. |
| preview | IMAGE | The input image with the mask tinted over it. |
| info | STRING | Frame count, stored masks, mode and notes (resized frames, frame recording skipped). |