Inpaint Canvas
The Krita-style mask editor that stops inpainting from fighting the graph
- result
- result_local
- crop_image
- crop_mask
- image
- mask
- stitch_info
- crop_width
- crop_height
- prompt
- control_image
- denoise
- seed
- mode
- negative
- setting_1
- setting_2
- setting_3
- setting_4
- setting_5
- setting_6
- setting_7
- setting_8
The loudest inpainting complaint in 2026 isn't model quality - it's the masking interface. ComfyUI renders flawless patches, but painting where one goes means hand-rolling masks, feathering, cropping, compositing, hoping the seam doesn't show. This node wraps that whole chore in a layered image editor that lives inside ComfyUI - the closest thing the graph has to a Krita masking surface.
It also lands on the right side of the 2026 model debate: masked inpainting's whole remaining case is bit-identical unmasked pixels, which an instruction-edit model like Flux 2 Kontext won't give you. This is crop-and-stitch with a paintbrush bolted on, and it keeps paid API editing chains in the loop.
What it actually is
Inpaint Canvas is one node that holds the image, the layers, the selection and the prompt. Click Open editor, load a photo, paint a selection with the brush, rectangle, lasso or polygon, and type a prompt. It emits a crop_image and a matching crop_mask - the selected region plus context, scaled, filled, grown and feathered. Run that crop through any chain (a local sampler, or a Flux.2/Kontext API node) and wire the decoded result back into the same node as result_local or result. It lands on the canvas as a new layer: aligned, colour-matched, seam feathered. Select the next spot, generate again.
Beyond that it has editing muscles the plain crop-and-stitch pair never will: SAM2 hover selection, SAM3 or GroundingDINO text selection, blend modes, ControlNet control layers, RMBG cutouts, filter layers, outpainting, and reference layers that ride along in crop_image for Flux.2 / Kontext multi-reference editing.
How the round trip works
ComfyUI rejects cycles, so the result links never reach the backend at all: the frontend strips them and passes the source nodes along as result_source / result_source_local. When the canvas node runs it expands an ephemeral Inpaint Canvas Stitch node that reads whichever source matches the api/local switch - the other chain is never connected, so it never executes. The stitch's UI output is attributed back to the canvas node, so the layer arrives without a second node on your graph.
Because it's an output node the chain runs even without a Save Image node downstream - and it always re-executes (IS_CHANGED returns NaN), since the stitch only happens while it runs. That's by design and cheap; don't try to "fix" it by caching.
The outputs you'll actually wire
The node has a lot of outputs, but the ones that matter for a minimal chain are few:
crop_imageandcrop_mask- feed your inpaint chain; in auto mode the mask is grown and feathered, the crop scaled totarget_sizeand rounded tomultiple_of.crop_width/crop_heightreport its size for generators that need an explicit number.result_local(decoded local chain) orresult(API chain) - wired back in as inputs, not outputs. Only the chain of the selected mode runs; wire just one and it's used regardless of the switch.denoiseandseed- from the editor's Generate section, straight into your sampler.prompt(andnegativefor SDXL) - what you typed in the editor.
The rest are there when you grow up to them: image/mask at full size, stitch_info for a standalone stitch, control_image for ControlNet, and setting_1–setting_8, wildcard outputs that let the editor drive widget inputs on other nodes.
The four widgets
All four defer to the editor's auto settings when those are on. padding is context around the selection (manual context only); target_size is the crop's longest side - 0 keeps native resolution, right for API models capped at 2048px; feather is the stitch-time blur (manual only); multiple_of rounds the crop, 64 for Flux.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/DenRakEiw/ComfyUI-InpaintCanvas
Restart ComfyUI. The node has no extra Python dependencies beyond what ComfyUI ships (OpenCV is used only for one fill mode). The rest is per-feature: comfyui-rmbg for SAM3 text selection and cutouts, comfyui_segment_anything as the fallback, Kijai's ComfyUI-segment-anything-2 for hover object selection, a Qwen3-VL or Gemini node for prompt upsampling. Their models (sam3.pt, sam2_hiera_base_plus.safetensors, Qwen3-VL) download on first use; the editor only lists backends it finds, so start bare.
Where people get burned
- Nothing comes back. The editor warns when no result input is wired at all; if your chain is wired but ignored, check the api/local switch matches where you plugged the result in.
- VAE-encode chains plus extra batch images. Turn on reference layers or "Original" alongside a fill mode and
crop_imagebecomes a batch - a VAE Encode would then encode the references too. Not meant for VAE chains. - The node "runs every time". Yes. That's the stitch working.
- Keyboard shortcuts vanish. While the editor is open it owns Ctrl+Z, Ctrl+S, everything. Close it and the graph is yours again.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| padding | INT | 640–4096 | Context pixels added around the selection before cropping. Ignored while the editor's Context is set to auto. |
| target_size | INT | 10240–8192 | Longest side of the emitted crop. 0 keeps the native size. |
| feather | INT | 160–512 | Blur radius applied to the selection edge when the result is stitched back. Ignored while the editor's Feather is set to auto. |
| multiple_of | INT | 641–256 | crop_image width and height are made a multiple of this (Flux wants 64). |
| resultopt | IMAGE | Wire the inpaint result of your API chain here (editor mode API). It is stitched back into the canvas as a new layer. | |
| result_localopt | IMAGE | Wire the decoded result of your local chain here (editor mode Local). Only the chain of the selected mode runs. |
Outputs (21)
| Name | Type | Description |
|---|---|---|
| crop_image | IMAGE | Selected region plus padding, scaled to target_size. Inpaint this. It becomes a batch when there is more to send: the untouched crop after the filled one (Crop option "Original" with a fill mode), then every visible reference layer fitted to the crop size (multi-reference editing with Flux.2 / Kontext, which flatten the batch into their image inputs). |
| crop_mask | MASK | Selection mask matching crop_image (grown and feathered when the editor's Feather is auto). |
| image | IMAGE | The flattened canvas at full size. |
| mask | MASK | Selection mask at full size. |
| stitch_info | STRING | Stitch parameters for a standalone Inpaint Canvas Stitch node. |
| crop_width | INT | Width of crop_image. Wire it into generators that need an explicit size. |
| crop_height | INT | Height of crop_image. |
| prompt | STRING | The prompt typed into the editor. |
| control_image | IMAGE | Layers marked as control (scribble, lineart, depth, pose) on black, cropped and scaled exactly like crop_image. Feed it to ControlNet. |
| denoise | FLOAT | Denoise strength from the editor's Generate section (1.0 = full repaint). Wire it into your local sampler. |
| seed | INT | Seed from the editor (random per run or fixed). Wire it into your local sampler. |
| mode | STRING | "api" or "local": which result input the editor expects the result on. |
| negative | STRING | Negative prompt from the editor (shown in local mode; for SDXL-class models). |
| setting_1 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_2 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_3 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_4 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_5 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_6 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_7 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |
| setting_8 | * | Editor-driven setting: wire it into any widget input (lora_name, ckpt_name, steps, ...) and a matching control appears in the editor. The next free slot shows up once this one is connected. |