kubakub regions from id renders
Houdini ID renders in, named regions out
- matrix
- scope
- regions
- preview
- reference
- regions_json
- report
ID renders are the older, more universal cousin of cryptomatte: a flat-colour image where every object is painted a unique RGB value, plus a text file that says which colour is which object. Practically every 3D tool can write one, which is why this node exists alongside the cryptomatte one.
kubakub regions from id renders reads a folder of them and turns the elements of a building into regions - and unlike render passes, one pass gives you one region per element, not one mask for everything.
The folder contract
ids_elements.png <- the ID map
ids_elements.txt <- its legend: "<name> rgb r g b"
ids_zone.png <- a second pass, used for tagging
ids_zone.txt
mask_window_left_03.exr <- optional full-res mask, alpha is the mask
png/mask_window_left_03.png <- or the PNG variant, in a png/ subfolder
facade_clay.png <- optional reference render
Four inputs steer it:
regions_pass- which pass gives the regions (elementsby default). Its legend becomes the region names.tag_passes- which passes tag the regions they cover by half or more. Default*means all of them, so azonepass and alevelpass and abaypass all become tags, and your plan can say[group:windows tag:level_1].split_parts- names (wildcards, default*) whose separate parts become separate regions, numberedwindows_01,windows_02… in reading order, all grouped underwindows. Set it to""for one region per name.overlap- how to resolve the fact that object masks ignore occlusion. Aface_frontmask covers the whole facade behind everything.
That last one deserves a moment, because it's the interesting engineering in this node. In an ID-map workflow you often have full-resolution per-object masks, and they overlap - so which one's in front? id_map (default) learns the depth order from the ID map itself: where two masks overlap, the one the ID map shows wins, and masks get painted back to front at full resolution. The upside is correct occlusion and full-res edges; the downside is that elements the ID map never shows at all - a recessed clock, a window behind a screen - vanish, and the report lists them so you know. smaller_wins is the other answer: every smaller mask goes in front, which brings hidden details back at the cost of being wrong when a genuinely small object sits behind something large.
Outputs
regions for the atlas pipeline. preview (one colour per group, region borders and ids over the matrix or the reference render), reference (the clay or beauty render of the folder, tone mapped, black if there isn't one), regions_json, and report - regions per group, the passes found, the reference render used, and notes. matrix sets the output size and the preview background; without it you get the mask files' size. scope bounds the regions as usual.
Scale reference from the pack's own testing, which is the most honest expectation-setting you'll get: a 3200×2160 station facade with four passes (elements 17, level 5, zone 4, bay 7) becomes 365 regions in about 12 seconds - 280 of which are glass panes between the window bars. The full-resolution result agrees with the ID map on 98% of interior pixels, and four elements were hidden in the ID map (a clock, some entrance windows, a glass front, a window back).
That "one region per glass pane" number is the thing to plan around. Splitting * by default is what gives you a per-pane region, which is either exactly what you want or 280 regions you'll never write rules for. Set split_parts deliberately.
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. ComfyUI 0.37.0+, OpenCV. For EXR masks you also want imageio - imageio is optional and only this feature needs it; PNG ID maps work without it, and the report tells you which route it took.
The folder is empty by default and runs on a built-in sample, so you can pipe sample regions into a plan and see the whole chain before exporting a single Houdini pass. Test: tests/test_idmaps.py, which writes a small Houdini-like folder and reads it back.
Things that will bite
Linear legends in 8-bit PNGs. Renderers write legend colours linear; 8-bit PNGs are sRGB-encoded. That's the same RGB value meaning different pixels, and it's why legend_colors exists. auto picks whichever reading matches more pixels - leave it there unless you have a reason.
Antialiased ID maps. Only exact colours are read, and blended edge pixels take the label of the nearest exact pixel. Occasionally a blend of two colours lands closer to a third one, which shows up as a sliver of the wrong region on an edge. min_region_area and merging deal with the fallout.
Half-size ID maps. Full-res mask files give you the edges; a pass with no mask files gets cut from its ID map, upscaled. Working at half res and leaning on the mask files is normal; working at half res with no mask files gives you soft edges everywhere.
It is fast, but it wasn't always. The pack's changelog is refreshingly specific about the optimisation: the ID map is decoded per distinct colour instead of per pixel and per legend entry, and a pass with an ID map but no mask files now uses the decoded labels directly, with no per-name masks and no depth-order pass. A 450-part test facade went from 127 seconds to 5.5 seconds with identical regions and tags. If you find an old thread complaining that this node is unusably slow, that's what changed.
Inputs (10)
| Name | Type | Default | Description |
|---|---|---|---|
| folder | STRING | Folder with the ID maps, legends and masks (png/ subfolder too). | |
| regions_pass | STRING | elements | The pass whose names become regions (ids_<pass>.txt). |
| tag_passes | STRING | * | Passes whose names tag the regions they cover by half or more (names or wildcards, e.g. 'zone level bay'); empty = none. |
| overlap | COMBO | id_map | id_map: what the camera sees wins (depth order learned from the ID map; hidden elements vanish). smaller_wins: a smaller mask is always in front (brings hidden details like a recessed clock back). |
| split_parts | STRING | * | Names (wildcards) whose separate parts become separate regions (windows_01..). Empty = one region per name. |
| min_region_area | INT | 4000–100000000 | Parts smaller than this (pixels) are merged into a neighbour or dropped. |
| merge_small_regions | BOOLEAN | true | On: merge small parts into the neighbour with the longest shared border. Off: drop them. |
| legend_colors | COMBO | auto | How the ID map's pixels relate to the legend (renderers write linear legends, 8 bit PNGs are sRGB). auto picks the better match. |
| matrixopt | IMAGE | Sets the output size (masks are resized to it) and is the preview background. Without: the size of the mask files. | |
| scopeopt | MASK | Pixels outside this mask are never part of a region. |
Outputs (5)
| Name | Type | Description |
|---|---|---|
| regions | KUBA_REGIONS | Label map + region table for the kubakub region nodes. |
| preview | IMAGE | One colour per group, region borders and ids over the matrix or the reference render. |
| reference | IMAGE | The clay / beauty render of the folder (tone mapped), black if none. |
| regions_json | STRING | The region table as text (names, groups, tags, passes, notes). |
| report | STRING | Regions per group, the passes found, the reference render used, and notes. |