kubakub regions from masks
Turn any pile of masks into named, groupable regions
- masks
- base_regions
- matrix
- scope
- regions
- region_masks
- regions_json
- preview
- report
- scope
Every road in this pack leads here. SAM 3 masks, render passes, ID renders, painted PNGs, Illustrator layers, a batch of masks from your own graph - they all arrive as a MASK batch plus a list of names, and this node turns that into the region atlas the plan, the sampler and the director work on.
If you only learn one node in the 2d/regions group, learn this one. It's the contract.
The core behaviour
masks is a batch, one mask per region, values over 0.5 count as inside. names is one name per line in batch order, or one comma-separated line. name = group sets the group explicitly; otherwise the group is the name without its trailing number, so W_F1_02 lands in group W_F1. Leave names empty and you get mask_01, mask_02, and so on - fine for a quick look, useless for a plan.
Where new masks overlap, the smaller one wins. That one rule is doing a lot of work: it's what lets a window mask sit cleanly inside a wall mask without you ordering anything, and it's the same rule the pack uses everywhere else that masks meet.
min_region_area (64 px by default) catches the junk - antialiased specks, single-pixel slivers - and merge_small_regions decides whether they join the neighbour they share the longest border with or get dropped. Keep the merge on. A dropped region is a hole that nothing will ever diffuse into.
Patching an existing atlas
This is the part that makes the node more than a converter. Connect a KUBA_REGIONS to the optional base_regions and set patch:
on_top- new masks cut themselves out of the regions below. A window gets cut out of the wall. A base region that loses every pixel is removed, with a note.underneath- new masks only fill pixels that no base region already has. For adding the bits the first pass missed.
The base keeps its names, groups, tags and scope, and new masks are clipped to that scope. Best detail: each new region is tagged with the base region it was cut from, plus that region's tags. So if you patch a balcony into a floor-1 wall region, the balcony inherits the tag that region had, and [tag:M_FLOOR_1] in your plan reaches it without you adding a rule. That's how you build a facade in passes - mask folder first, then SAM for the fiddly bits - and still write rules against the whole thing.
You also get, in the advanced inputs, scope_masks (masks that aren't regions but bound where regions may be), split_masks (one mask holding fifteen separate windows becomes fifteen regions windows_01..15 in group windows), and tag_only_masks (masks that only tag what they cover). All three are matched by name or wildcard.
Outputs
regions is the thing - a label map plus a table, for the plan, the mask node, the vector node, the director. region_masks gives you the batch back, one mask per region in region_id order. regions_json is the whole table as text: names, groups, tags, boxes, notes. preview is one colour per group with region borders and ids drawn over a greyed-out matrix, and it's the fastest way to spot a mis-named mask. scope echoes the scope that was used. And report tells you how many masks became how many regions and groups, how many pixels are still unassigned, and every note about renamed, resized, merged or dropped masks.
Read the report the first time. Half of "the sampler didn't do anything" turns out to be "the mask was dropped at 63 pixels".
Install
cd ComfyUI/custom_nodes
git clone https://github.com/kubakubkub/ComfyUI_KubaNodes
python -m pip install -r ComfyUI_KubaNodes/requirements.txt
Manager → kubakub nodes, or comfy node install comfyui-kubanodes. ComfyUI 0.37.0+ and OpenCV; no models, no Blender, no ffmpeg. This node works on a bare install.
Model-free test: tests/test_regions_from_masks.py, and example_workflows/regions_from_masks_patch.json demonstrates the patch flow on the sample facade.
Practical notes
Sizes have to agree. With no base_regions, matrix sets the size and masks of another size get resized with a note. With a base, the base's scope rules and mismatches are your problem. Don't discover this at the end of a chain.
The batch order is the contract. If you shuffle masks upstream - a split_masks on a different node, a different SAM prompt order - the names stop matching the shapes. The preview is where you see it.
Groups are the unit of authoring, regions are the unit of processing. You will write far more rules against groups than against individual windows, which is why the trailing-number convention exists. Name your masks w_f1_c03_glass, not glass3.
Inputs (11)
| Name | Type | Default | Description |
|---|---|---|---|
| masks | MASK | One mask per region (batch). Values over 0.5 are inside. | |
| names | STRING | One name per line in batch order, or one comma separated line. 'name = group' sets the group; otherwise the group is the name without its trailing number (W_F1_02 -> W_F1). Empty: mask_01.. | |
| patch | COMBO | on_top | With base_regions. on_top: new masks cut themselves out of the regions below. underneath: new masks only fill pixels no base region has. |
| min_region_area | INT | 640–100000000 | Regions smaller than this (pixels) are merged or dropped. |
| merge_small_regions | BOOLEAN | true | On: merge small regions into the neighbour with the longest shared border. Off: drop them. |
| base_regionsopt | KUBA_REGIONS | Existing atlas to patch. Its scope, names, groups and tags are kept. | |
| matrixopt | IMAGE | Background for the preview; also sets the size when there are no base_regions (masks of another size are resized, nearest). | |
| scopeopt | MASK | Pixels outside this mask are never part of a region. | |
| scope_masksopt | STRING | Names (wildcards ok) of masks that are no regions but the area regions may use. | |
| split_masksopt | STRING | Names of masks holding several shapes: one region per shape, <name>_01, _02 ... in group <name>. | |
| tag_only_masksopt | STRING | Names of masks that only tag the regions they cover. |
Outputs (6)
| Name | Type | Description |
|---|---|---|
| regions | KUBA_REGIONS | Label map + region table for the kubakub region nodes. |
| region_masks | MASK | One mask per region; batch index = region_id. |
| regions_json | STRING | The region table as text (names, groups, tags, boxes, notes). |
| preview | IMAGE | One colour per group, region borders and ids over a grey copy of the matrix. |
| report | STRING | How many masks became how many regions and groups, pixels left without a region, and the notes (renamed, resized, merged or dropped masks). |
| scope | MASK | The scope that was used: 1 where regions may be (all 1 without a scope). |