Nodes/kubakub nodes/kubakub mosaic illusion
ComfyUI Node

kubakub mosaic illusion

A mosaic where the tiles stay real material

By kubakubkub·Created about a month ago·Updated 2 days ago· 10
kubakub mosaic illusion
  • texture
  • target
  • image
  • tile_values
  • grid
◄tile_px48►
◄strength0.55►
◄modeselect►
◄quantise_texturetrue►
◄target_fitcover►
◄target_autocroptrue►
◄target_autolevelstrue►
◄target_blur6►
◄target_invertfalse►
◄contrast1.20►
◄adaptivefalse►
◄min_tile_px8►
◄detail0.060►
◄max_depth3►
◄edge_noise0.00►
◄edge_scale12►
◄mortar0.00►
◄mortar_value0.00►
◄jitter0.00►
◄palette_size12►
◄dither0.90►
◄level_radius6►
◄seed0►
◄tile_width_m0.000►
◄facade_width_m0.00►

Most "mosaic" filters blur an image and stamp a grid on top. The result looks like what it is: a picture with a grid over it. kubakub mosaic illusion does the real thing - it cuts a photo of actual material into tiles and then picks or shades each tile so that a target picture emerges when you step back. Up close it's stone. From the street it's a face. That's the whole trick, and it's the reason this one lives in the pack's lab group rather than in post-processing: it's a physical illusion for projection work, not a filter.

The two inputs

texture is the material photo - stone, bricks, glazed tiles - and it sets the output size. A batch gives you one mosaic per material image, which is how you test three stone samples in one queue.

target is the picture hidden in the mosaic. Only its brightness matters, which is the key constraint: you're not transferring colour, you're steering each tile's value along the material's own range. If your target only reads in colour, desaturate it yourself and check it still works before you queue.

mode is the important choice

mode has four settings, and they are genuinely different tools, not strengths of one thing:

  • select is the mosaicist's method, and the default. It builds a palette from the texture itself and, for each cell, picks the tile whose value the target needs. Nothing is tinted, nothing is invented - every cell is a real piece of the material. palette_size (default 12) is how many colours are on the tray, dither breaks the banding at the edges of the hidden shape, and level_radius (default 6 tiles) shifts a whole neighbourhood's level so more of the original pattern survives. This is the mode you use when the material has to look untouched.
  • multiply darkens and brightens the tiles towards the target. Uniform and forgiving, and it works with any texture.
  • mix blends each tile toward the target's grey.
  • lift only brightens - the one to use with a dark material where you can't go down any further.

strength (default 0.55) is the readability of the hidden picture: 0 is material only, 1 is the target plainly readable. The tooltip is worth noting because it's a workflow, not just a parameter - animate it to move the resolving distance. Ramping strength over frames is how the picture appears as the audience walks closer, which is a much better effect than a static mosaic.

quantise_texture (on by default) flattens each tile to one colour. Turn it off when the texture is already a tiled photo - otherwise you flatten it twice and it goes muddy.

tile_px is the tile size in pixels; with adaptive on it becomes the largest tile. And that's the pair to take seriously: with adaptive, a quadtree splits cells where the motif has detail and leaves them big where it's flat, controlled by min_tile_px, detail (how much the motif must change before a cell splits) and max_depth. On a real facade the adaptive grid is what makes the motif read at a low tile count.

The craft knobs

The defaults here are geometry. mortar (0 = none) and mortar_value draw the joints on the real cell outlines, so they work with either grid type. edge_noise and edge_scale roughen the tile outlines - 0.10 to 0.20 with a small edge_scale looks hand-cut, chipped; a large edge_scale is wobbly, like old render. jitter randomly shifts each tile's target value so the picture looks less mechanical. seed ties all of it down: same seed, same mosaic.

On the target side, target_fit (cover / contain / stretch), target_autocrop (trims a dark border so a cutout fills the frame), target_autolevels (stretches levels to full black-to-white so a flat photo still reads), target_blur (default 6; soften so the image emerges instead of stamping a silhouette), target_invert and contrast are the prep. Autolevels and blur are doing real work - turn them off and your first attempts will look like a silhouette pasted onto bricks.

Outputs: image (the mosaic), tile_values (the grey value each tile was asked for - the target as the tiles see it, which is how you debug "why doesn't the face read"), and grid (white tiles, black borders). Look at tile_values when the result disappoints; it tells you instantly whether the target had enough contrast to survive quantisation.

Install

Part of the pack:

cd ComfyUI/custom_nodes && git clone https://github.com/kubakubkub/ComfyUI_KubaNodes
python -m pip install -r ComfyUI_KubaNodes/requirements.txt

No model, no Blender, no extra dependency - this one is torch tensors all the way down, so it runs on CPU as happily as on GPU.

Two things to know before you spend an evening on it

Get the tile size from the building, not from the screen. kubakub mosaic grid from camera exists exactly for this: give it the real facade width, the real width of one stone and your render width, and it hands back the tile_px that makes the grid land on actual material. Eyeballing it gives you a grid that's 40% too fine and a mosaic that reads as texture, not as mosaic.

Palette size sets the ceiling on the illusion. In select mode you're quantising to the material's own values, so 8 colours on a light stone gives you 8 levels for the whole picture. That's plenty for a strong graphic and nowhere near enough for a photograph. Use tile_values to find out which one you've got - and reach for a darker material or a higher contrast before you reach for more tiles.

Categorykubakub/lab/mosaic

Inputs (27)

NameTypeDefaultDescription
textureIMAGEThe material photo (stone, tiles, bricks) the mosaic is cut from. Sets the output size; a batch gives one mosaic per image.
targetIMAGEThe picture hidden in the mosaic. Only its brightness is used (first image of a batch).
tile_pxINT482–2048Tile size in pixels. With adaptive on this is the largest tile. Not used when tile_width_m and facade_width_m give the size in metres.
strengthFLOAT0.550–10 = material only, 1 = target fully readable. Animate to move the resolving distance.
modeCOMBOselectselect picks the tile from the material's own palette whose value the picture needs, the way a mosaicist picks from a tray. Nothing is tinted. multiply darkens / brightens the tiles, mix blends them toward the target grey, lift only brightens where the target is light.
quantise_textureBOOLEANtrueFlatten each tile to one colour. Turn off when the texture is already tiled.
target_fitoptCOMBOcoverHow the target fits the texture size. cover fills and crops, contain fits it whole on mid grey, stretch distorts it to fit.
target_autocropoptBOOLEANtrueTrim a dark border so a cutout fills the frame.
target_autolevelsoptBOOLEANtrueStretch the target's levels to full black to white, so a flat photo still reads.
target_bluroptINT60–200Soften the target so it emerges instead of stamping a silhouette.
target_invertoptBOOLEANfalseSwap light and dark in the target.
contrastoptFLOAT1.200–4Contrast of the target around mid grey. 1 = unchanged, higher makes the hidden picture punchier.
adaptiveoptBOOLEANfalseSplit tiles where the motif has detail, keep them large where it is flat.
min_tile_pxoptINT82–512adaptive only. Smallest tile in pixels; a tile is not split below this.
detailoptFLOAT0.0600.005–0.5adaptive only. How much the motif must change inside a tile before it is split. Lower splits more.
max_depthoptINT30–6adaptive only. How many times a tile may be halved (3 = down to 1/8 of tile_px).
edge_noiseoptFLOAT0.000–0.6Roughness of the tile outlines, as a fraction of tile size. 0.10 to 0.20 looks hand cut.
edge_scaleoptFLOAT122–200Noise wavelength in pixels. Small is jagged and chipped, large is wobbly.
mortaroptFLOAT0.000–0.45Width of the joints between tiles, as a fraction of tile size. 0 = no joints.
mortar_valueoptFLOAT0.000–1Grey level of the joints: 0 = black, 1 = white.
jitteroptFLOAT0.000–0.5Random change of each tile's target value, so the hidden picture looks less mechanical. 0 = off.
palette_sizeoptINT122–64select mode. How many tile colours are in the tray. Taken from the texture itself.
ditheroptFLOAT0.900–3select mode. Breaks banding at the edges of the hidden shape.
level_radiusoptFLOAT61–80select mode. Neighbourhood, in tiles, whose level gets shifted. Larger keeps more of the pattern.
seedoptINT00–4294967295Random seed for jitter, edge noise and dither. Same seed = same mosaic.
tile_width_moptFLOAT0.0000–50The tile size from real sizes instead of tile_px: the width of one tile on the building, in metres (a brick, a stone). Needs facade_width_m. 0 = use tile_px.
facade_width_moptFLOAT0.000–500Real width of the facade the texture covers, in metres. With tile_width_m it sets the tile size. 0 = use tile_px.

Outputs (3)

NameTypeDescription
imageIMAGEThe mosaic.
tile_valuesIMAGEThe grey value each tile was asked for (the target as the tiles see it).
gridIMAGEThe tile outlines: white tiles, black borders.