PVL Edge Extend
March the alpha outward
- image
- alpha
- image
- alpha
- intermediate_alpha
- premult_after_levels
- unpremult_after_pass1
- unpremult_after_pass2
- pre_final_premult_unpremult
If you've ever cut a subject out of a photo - with rembg, BiRefNet, SAM, whatever - and composited it onto a new background only to see a pale fringe or a dark halo around the edges, you know exactly what this node is for. PVL Edge Extend is a Nuke-style edge extension: it grows the alpha matte outward and fills the new territory with believable color sampled from the edge, instead of leaving you with a hard, wrong-colored rim. It's the most sophisticated node in the ComfyUI Assistant Node pack, and one of the few genuinely pro-level tools in the whole grab-bag.
The key idea is that your cutout image is premultiplied - RGB already multiplied by alpha - and the halo problem is what happens when premultiplied color sits next to transparent pixels. Edge extension works on the premultiplied pair, marches the coverage outward, and reconstructs color in the band it added. It's the same idea as Nuke's "edge extend," and it saves you from the alternative of eroding the subject until the fringe is gone but so is your edge detail.
How it works
It runs one to three blur-and-merge "passes," each growing the working alpha into a ring around itself. For each pass it blurs the current premultiplied RGB and alpha, unpremultiplies, computes the outside band by dilating the alpha and subtracting the alpha itself, then under-merges that blurred fill into the gap. Under compositing, a restore_detail slider adds high-frequency detail back - only inside the original matte's inner edge band, so you don't smear the fill across the whole image.
Two things make a real difference in practice:
- Levels first.
black_pointandwhite_pointremap your raw mask (defaults 0 and 1 = no change). Cutouts from cheap background removers often have a semi-transparent skirt around the subject; raising the black point kills that skirt before you march.alpha_erode_after_levelsthen nibbles the matte in by a pixel count, another classic halo fix. work_in_linear(on by default). The marching happens in linear RGB and converts back at the end, which is how you avoid grayish edges that a straight sRGB blur produces. Leave it on unless you have a reason not to.
Each enabled pass gets its own blur radius: edge_blur for pass one, then second_pass/second_edge_blur and third_pass/third_edge_blur. One pass with a moderate radius handles most halos; the extra passes are for stubborn cases or very wide fringes. opacity scales the final alpha.
The outputs
Seven of them, which is intimidating but honest: the main image and alpha outputs are what you wire into compositing, and the other five are diagnostics to compare at each stage - premult_after_levels, unpremult_after_pass1, unpremult_after_pass2, pre_final_premult_unpremult, plus intermediate_alpha. If a pass is making things worse, the diagnostics show you exactly where. You don't need to route them anywhere for the main result to work.
Installing it
It's part of pvlprk/comfyui-pvl-api-nodes. Use ComfyUI Manager (search "ComfyUI Assistant Node") or:
cd ComfyUI/custom_nodes
git clone https://github.com/pvlprk/comfyui-pvl-api-nodes
Restart, and it'll appear under the image/alpha category. It's pure torch - no API key, no model download.
Where people get burned
The usual mistake is expecting it to fix a bad matte that never had clean color under the alpha in the first place. Edge extension reconstructs from what's at the edge; if your cutout's fringe is garbage, the fill inherits that garbage. Do the levels + erode step first, then march. And watch restore_detail - at 1.0 it can reintroduce edge noise that the blur just removed, so 0.3–0.6 is the sane range for most footage.
Inputs (15)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| alpha | MASK | — | |
| black_point | FLOAT | 0.000–1 | — |
| white_point | FLOAT | 1.000–1 | — |
| alpha_erode_after_levels | INT | 00–50 | — |
| edge_blur | INT | 50–50 | — |
| opacity | FLOAT | 1.000–1 | — |
| restore_detail | FLOAT | 0.500–1 | — |
| second_passopt | BOOLEAN | false | — |
| second_edge_bluropt | INT | 30–50 | — |
| third_passopt | BOOLEAN | false | — |
| third_edge_bluropt | INT | 30–50 | — |
| restore_detail_radiusopt | INT | 30–128 | — |
| restore_detial_blur_edgeopt | INT | 00–64 | — |
| work_in_linearopt | BOOLEAN | true | — |
Outputs (7)
| Name | Type | Description |
|---|---|---|
| image | IMAGE | — |
| alpha | MASK | — |
| intermediate_alpha | MASK | — |
| premult_after_levels | IMAGE | — |
| unpremult_after_pass1 | IMAGE | — |
| unpremult_after_pass2 | IMAGE | — |
| pre_final_premult_unpremult | IMAGE | — |