😺NKD Mask Ops
The whole mask pipeline in one GPU pass — levels, specks, holes, blockify, temporal
- mask
- vae
- model
- mask
- mask_inverted
- latent_mask
Mask cleanup is the boring, essential step nobody wants to spend render time on - and in a video workflow it quietly becomes the slowest part, because the usual approach grows or feathers a mask frame by frame on the CPU. 😺NKD Mask Ops puts the whole pipeline in one node, runs it on the GPU over the entire batch at once, and claims a full pipeline on 81 frames of 1080p in about a third of a second - with expand around 50× faster than the node you're probably using. The author credits the mask-to-latent idea to the MaskVidExperiments conversation and sets the bar as KJNodes; this node is the "everything, tuned" version.
The pipeline, in fixed order
Everything sits in one panel, and anything left at 0 is skipped. Steps run in a fixed order - clean, stabilize, shape, soften - so a feathered edge is never re-hardened by a later step. In order:
- Levels -
black_point/white_pointcut the faint halo a segmentation model leaves around the subject. Set both to the same value for a hard threshold with no soft edge left. - Speck removal -
despeckledrops blobs thinner than about twice the width you give it, while what survives keeps its exact shape - fingers and hair don't get shaved off like a normal cleanup pass. - Fill holes / close gaps - solid shapes and bridged cracks (
close_gapsbridges without growing the mask overall). - Temporal - for video:
temporal_expandmakes each frame also cover what the mask covered a few frames before and after (so a segmentation that lags motion still covers it), andtemporal_smoothaverages across frames to stop the edge flickering. - Expand / contract - one signed
expandvalue. - Blockify - snaps the mask to a grid of squares so it survives the trip into latent space without bleeding into neighboring blocks.
- Feather - runs last, so nothing hardens it again.
The two inputs that make it latent-aware
vae- connect the VAE you're sampling with and Blockify turns on with that VAE's own grid, including, on a video VAE, the frames it collapses into a single latent. The mask then lands on latent boundaries exactly.model- alongside the VAE. A few models (MiniMax H3) read the mask themselves and regenerate a whole token the moment any part of it is covered, so the block that must stay clean is bigger than one latent. The node asks the model rather than assuming - and changes nothing for models where the latent is the unit.
With a VAE connected you also get the latent_mask output: the same mask already reduced to latent resolution, on the VAE's real grouping. Feed that one to Set Latent Noise Mask for video. A mask still in pixels gets resampled on the way in, spread evenly across frames - and a video VAE doesn't group them evenly, which on a MiniMax H3 grid can make a one-frame latent's mask vanish outright. Reduced here, it arrives exactly as you built it.
Outputs: mask, mask_inverted, and latent_mask.
Install and gotchas
Install via ComfyUI Manager (search ComfyUI-NKD-Basic-Tools) or:
cd ComfyUI/custom_nodes
git clone https://github.com/Nekodificador/ComfyUI-NKD-Basic-Tools
Restart after. Pack dependencies are onnxruntime and huggingface_hub; neither is needed here. The edge_low / edge_high pair at the bottom is the subtle one: some video models only react to a narrow band of mask values, so a feather drawn across the full range behaves like a hard edge - set that band and the whole feather becomes the transition, while fully black and fully white are left alone. And if you only need fill holes + expand + feather, the pack's own 😺NKD Mask Ops Lean is the same engine with three widgets - don't haul this panel around for a stills composite.
Inputs (17)
| Name | Type | Default | Description |
|---|---|---|---|
| mask | MASK | Mask or mask batch (one per video frame). | |
| invert | BOOLEAN | false | Swap masked and unmasked before anything else runs. |
| black_point | FLOAT | 0.000–1 | Mask values at or below this become empty. Raise it to cut the faint halo a segmentation model leaves around the subject. |
| white_point | FLOAT | 1.000–1 | Mask values at or above this become fully masked. Set it to the same value as Black Point for a hard threshold with no soft edge left. |
| despeckle | INT | 00–256 | Remove blobs thinner than about twice this many pixels. What survives keeps its exact shape — thin details like fingers or hair are not shaved off. |
| fill_holes | BOOLEAN | false | Fill gaps fully enclosed by the mask, so each area becomes a solid shape. |
| close_gaps | INT | 00–256 | Bridge cracks and notches narrower than about twice this many pixels without growing the mask overall. |
| temporal_expand | INT | 00–64 | Video only. Each frame also covers what the mask covered this many frames before and after, so a mask that lags the motion still covers it. |
| temporal_smooth | INT | 00–64 | Video only. Average each frame's mask with this many frames either side to stop the edge from flickering. |
| expand | INT | 0-512–512 | Grow the mask outward by this many pixels, or shrink it with a negative value. |
| blockify | INT | 00–256 | Snap the mask to a grid of squares this many pixels wide, so it survives the trip into latent space with no bleeding into neighbouring blocks. Connect a VAE and the size comes from it; otherwise set it yourself (8 or 16 for most models). Any value above 0 turns it on. |
| blockify_threshold | FLOAT | 0.500–1 | How much of a block must be covered for it to turn on fully. 0 keeps each block's average as a gray value instead, for a pixelated look. |
| feather | INT | 00–256 | Soften the edge by this many pixels. Runs last, so nothing hardens it again. |
| edge_low | FLOAT | 0.000–1 | Where the soft part of a feathered edge starts. Leave 0 and 1 and nothing happens. Some video models only react to a narrow band of mask values, so a feather drawn across the full range behaves like a hard edge; set that band here and the whole feather becomes the transition. Fully black and fully white are left alone, so what the mask protects stays protected. |
| edge_high | FLOAT | 1.000–1 | The other end of that band. |
| vaeopt | VAE | Optional. Connect the VAE you are sampling with and Blockify turns on with that VAE's own grid — including, on a video VAE, the frames it collapses into a single latent. The mask then lands on latent boundaries exactly, with nothing bleeding into the neighbouring blocks. | |
| modelopt | MODEL | Optional, alongside the VAE. A few models (MiniMax H3) read the mask themselves and regenerate a whole token the moment any part of it is covered, so the block that has to stay clean is bigger than one latent. Connect the model and Blockify uses whichever of the two applies — it asks the model rather than assuming, and changes nothing for the models where the latent is the unit. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| mask | MASK | — |
| mask_inverted | MASK | — |
| latent_mask | MASK | With a VAE connected, the same mask already reduced to latent resolution — feed this one to Set Latent Noise Mask. It arrives exactly as built: a mask still in pixels gets resampled on the way in, evenly across the frames, which is not how a video VAE groups them. Same as the mask output when no VAE is connected. |