Files
orion4d-illusion_node/TessellationNode.py
T
2025-06-22 10:41:52 +02:00

142 lines
5.7 KiB
Python

from PIL import Image, ImageEnhance
import numpy as np
import torch
import random
class TessellationNode:
CATEGORY = "illusion"
FUNCTION = "tessellate"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"input_image": ("IMAGE",),
"tile_width": ("INT", {"default": 128, "min": 8, "max": 2048}),
"tile_height": ("INT", {"default": 128, "min": 8, "max": 2048}),
"tiles_x": ("INT", {"default": 4, "min": 1, "max": 32}),
"tiles_y": ("INT", {"default": 4, "min": 1, "max": 32}),
"mode": (["repeat", "mirror", "diamond"], {"default": "repeat"}),
"mirror_axis": (["none", "x", "y", "xy", "random"], {"default": "none"}),
"offset_x": ("INT", {"default": 0, "min": -2048, "max": 2048}),
"offset_y": ("INT", {"default": 0, "min": -2048, "max": 2048}),
"rotation_mode": (["none", "by_tile", "random"], {"default": "none"}),
"rotation_angle": ("FLOAT", {"default": 0, "min": 0, "max": 360}),
"scale_mode": (["none", "by_tile", "random"], {"default": "none"}),
"scale_factor": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 4.0}),
"opacity": ("FLOAT", {"default": 1.0, "min": 0.1, "max": 1.0}),
"random_seed": ("INT", {"default": 0, "min": 0, "max": 999999}),
}
}
def tensor_to_pil(self, img_tensor):
arr = img_tensor[0] if isinstance(img_tensor, list) or len(img_tensor.shape) == 4 else img_tensor
arr = arr.cpu().numpy() if hasattr(arr, 'cpu') else arr
arr = np.clip(arr, 0, 1)
arr = (arr * 255).astype(np.uint8)
if arr.shape[-1] == 1:
arr = np.repeat(arr, 3, axis=-1)
return Image.fromarray(arr)
def tessellate(
self,
input_image,
tile_width,
tile_height,
tiles_x,
tiles_y,
mode,
mirror_axis,
offset_x,
offset_y,
rotation_mode,
rotation_angle,
scale_mode,
scale_factor,
opacity,
random_seed
):
random.seed(random_seed)
base_tile = self.tensor_to_pil(input_image).convert("RGBA")
if base_tile.size != (tile_width, tile_height):
base_tile = base_tile.resize((tile_width, tile_height), resample=Image.LANCZOS)
# Canvas size for diamond mode
if mode == "diamond":
result_w = int(tile_width * (tiles_x + tiles_y/2))
result_h = int(tile_height * (tiles_y/2 + 0.5))
else:
result_w = tile_width * tiles_x
result_h = tile_height * tiles_y
result = Image.new("RGBA", (result_w, result_h), (0, 0, 0, 0))
for iy in range(tiles_y):
for ix in range(tiles_x):
tile = base_tile.copy()
# SCALE
if scale_mode == "by_tile":
fac = scale_factor * (1 + 0.05 * ((ix + iy) % 3))
tw, th = max(8, int(tile_width * fac)), max(8, int(tile_height * fac))
tile = tile.resize((tw, th), resample=Image.LANCZOS)
elif scale_mode == "random":
fac = scale_factor * random.uniform(0.85, 1.15)
tw, th = max(8, int(tile_width * fac)), max(8, int(tile_height * fac))
tile = tile.resize((tw, th), resample=Image.LANCZOS)
else:
tw, th = tile_width, tile_height
# ROTATION
angle = 0
if rotation_mode == "by_tile":
angle = rotation_angle * ((ix + iy) % 4)
elif rotation_mode == "random":
angle = random.uniform(0, rotation_angle)
if angle != 0:
tile = tile.rotate(angle, expand=True, fillcolor=(0,0,0,0))
# MIRROR
if mirror_axis == "x" and (ix % 2 == 1):
tile = tile.transpose(Image.FLIP_LEFT_RIGHT)
if mirror_axis == "y" and (iy % 2 == 1):
tile = tile.transpose(Image.FLIP_TOP_BOTTOM)
if mirror_axis == "xy" and ((ix + iy) % 2 == 1):
tile = tile.transpose(Image.ROTATE_180)
if mirror_axis == "random" and random.random() < 0.5:
tile = tile.transpose(random.choice([
Image.FLIP_LEFT_RIGHT,
Image.FLIP_TOP_BOTTOM,
Image.ROTATE_180
]))
# OPACITY
if opacity < 1.0:
if tile.mode != "RGBA":
tile = tile.convert("RGBA")
alpha = tile.split()[-1]
alpha = ImageEnhance.Brightness(alpha).enhance(opacity)
tile.putalpha(alpha)
# OFFSETS (classique ou diamant)
if mode == "diamond":
px = int(ix * tile_width + iy * tile_width / 2 + (offset_x if (iy % 2 == 1) else 0))
py = int(iy * tile_height / 2 + (offset_y if (ix % 2 == 1) else 0))
else:
px = ix * tile_width + (offset_x if (iy % 2 == 1) else 0)
py = iy * tile_height + (offset_y if (ix % 2 == 1) else 0)
result.alpha_composite(tile, (int(px), int(py)))
arr_out = np.array(result.convert("RGB")).astype(np.float32) / 255.0
tensor = torch.from_numpy(arr_out).unsqueeze(0)
return (tensor,)
NODE_CLASS_MAPPINGS = {
"TessellationNode": TessellationNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TessellationNode": "Tessellation Composer (Advanced)",
}