This commit is contained in:
Bouletto
2025-06-22 10:41:52 +02:00
parent 525b184831
commit 319f2efb34
12 changed files with 1292 additions and 0 deletions
+126
View File
@@ -0,0 +1,126 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
*.manifest
*.spec
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
.hypothesis/
.pytest_cache/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# IDE
.vscode/
.idea/
*.swp
*.swo
*~
# OS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# ComfyUI specific
models/
input/
output/
temp/
+64
View File
@@ -0,0 +1,64 @@
from PIL import Image
import numpy as np
import torch
class CheckerboardNode:
CATEGORY = "illusion"
FUNCTION = "generate_checkerboard"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"img1": ("IMAGE",), # Première image ou couleur
"img2": ("IMAGE",), # Deuxième image ou couleur
"tiles_x": ("INT", {"default": 8, "min": 1, "max": 128}), # Cases sur X
"tiles_y": ("INT", {"default": 8, "min": 1, "max": 128}), # Cases sur Y
"tile_width": ("INT", {"default": 128, "min": 8, "max": 1024}), # Largeur carreau
"tile_height": ("INT", {"default": 128, "min": 8, "max": 1024}), # Hauteur carreau
"tile_mode": (["crop", "resize"], {"default": "resize"}),
}
}
def generate_checkerboard(self, img1, img2, tiles_x, tiles_y, tile_width, tile_height, tile_mode):
# Conversion de torch.Tensor vers numpy si besoin
if hasattr(img1, "cpu"):
img1 = img1.cpu().numpy()
if hasattr(img2, "cpu"):
img2 = img2.cpu().numpy()
if isinstance(img1, list) or img1.ndim == 4:
img1 = img1[0]
if isinstance(img2, list) or img2.ndim == 4:
img2 = img2[0]
im1 = Image.fromarray(np.clip((img1 * 255), 0, 255).astype(np.uint8))
im2 = Image.fromarray(np.clip((img2 * 255), 0, 255).astype(np.uint8))
# Nouvelle taille finale
final_width = tiles_x * tile_width
final_height = tiles_y * tile_height
# Préparer les dalles
if tile_mode == "resize":
tile1 = im1.resize((tile_width, tile_height))
tile2 = im2.resize((tile_width, tile_height))
else: # "crop"
tile1 = im1.crop((0, 0, tile_width, tile_height))
tile2 = im2.crop((0, 0, tile_width, tile_height))
result = Image.new("RGB", (final_width, final_height))
for y in range(tiles_y):
for x in range(tiles_x):
tile = tile1 if (x + y) % 2 == 0 else tile2
px, py = x * tile_width, y * tile_height
result.paste(tile, (px, py))
arr = np.array(result).astype(np.float32) / 255.0
tensor = torch.from_numpy(arr).unsqueeze(0)
return (tensor,)
NODE_CLASS_MAPPINGS = {
"CheckerboardNode": CheckerboardNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"CheckerboardNode": "Checkerboard Composer",
}
+104
View File
@@ -0,0 +1,104 @@
from PIL import Image
import numpy as np
import torch
def parse_color(color):
# Gère hex, noms, tuple/list
if isinstance(color, str):
color = color.lstrip("#")
if len(color) == 6:
return tuple(int(color[i:i+2], 16) for i in (0, 2, 4))
elif len(color) == 3:
return tuple(int(color[i]*2, 16) for i in range(3))
else:
try:
img = Image.new("RGB", (1, 1), color)
return img.getpixel((0, 0))
except:
return (0, 0, 0)
elif isinstance(color, (tuple, list)) and len(color) == 3:
return tuple(int(c) for c in color)
else:
return (0, 0, 0)
class ColorImageNode:
CATEGORY = "illusion"
FUNCTION = "generate_color"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 512, "min": 16, "max": 4096}),
"height": ("INT", {"default": 512, "min": 16, "max": 4096}),
"mode": (["solid", "linear", "radial", "angular", "mirror", "diamond"], {"default": "solid"}),
"color1": ("STRING", {"default": "#ffffff"}),
"color2": ("STRING", {"default": "#000000"}),
"angle": ("FLOAT", {"default": 0.0, "min": 0, "max": 360, "step": 0.1}),
}
}
def generate_color(self, width, height, mode, color1, color2, angle):
rgb1 = parse_color(color1)
rgb2 = parse_color(color2)
arr = np.zeros((height, width, 3), dtype=np.uint8)
cx, cy = width // 2, height // 2
Y, X = np.ogrid[:height, :width]
if mode == "solid":
arr[:, :] = rgb1
elif mode == "linear":
x = np.linspace(0, 1, width)
y = np.linspace(0, 1, height)
Xg, Yg = np.meshgrid(x, y)
theta = np.deg2rad(angle)
t = Xg * np.cos(theta) + Yg * np.sin(theta)
t = (t - t.min()) / (t.max() - t.min())
for i in range(3):
arr[..., i] = (rgb1[i] * (1 - t) + rgb2[i] * t).astype(np.uint8)
elif mode == "radial":
dist = np.sqrt((X - cx) ** 2 + (Y - cy) ** 2)
dist = dist / dist.max()
for i in range(3):
arr[..., i] = (rgb1[i] * (1 - dist) + rgb2[i] * dist).astype(np.uint8)
elif mode == "angular": # Sweep/angle Photoshop
Xg = X - cx
Yg = Y - cy
theta = np.arctan2(Yg, Xg) # -π à π
offset = np.deg2rad(angle)
t = ((theta + np.pi + offset) % (2 * np.pi)) / (2 * np.pi)
for i in range(3):
arr[..., i] = (rgb1[i] * (1 - t) + rgb2[i] * t).astype(np.uint8)
elif mode == "mirror": # Réfléchi
x = np.linspace(0, 1, width)
y = np.linspace(0, 1, height)
Xg, Yg = np.meshgrid(x, y)
theta = np.deg2rad(angle)
t = Xg * np.cos(theta) + Yg * np.sin(theta)
t = np.abs((t - 0.5) * 2) # miroir autour du centre
t = (t - t.min()) / (t.max() - t.min())
for i in range(3):
arr[..., i] = (rgb1[i] * (1 - t) + rgb2[i] * t).astype(np.uint8)
elif mode == "diamond":
dx = np.abs((X - cx) / (width / 2))
dy = np.abs((Y - cy) / (height / 2))
t = (dx + dy) / 2
t = np.clip(t, 0, 1)
for i in range(3):
arr[..., i] = (rgb1[i] * (1 - t) + rgb2[i] * t).astype(np.uint8)
img = torch.from_numpy(arr.astype(np.float32) / 255.0).unsqueeze(0)
return (img,)
NODE_CLASS_MAPPINGS = {
"ColorImageNode": ColorImageNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"ColorImageNode": "Color/Gradient Image",
}
+100
View File
@@ -0,0 +1,100 @@
from PIL import Image, ImageDraw
import numpy as np
import torch
import math
class OpticalGeometricNode:
CATEGORY = "illusion"
FUNCTION = "generate_geometric"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"pattern_type": (
["concentric_squares", "concentric_triangles", "wavy_grid", "starburst", "hexagons", "waves"],
{"default": "concentric_squares"}
),
"size": ("INT", {"default": 512, "min": 128, "max": 2048}),
"frequency": ("INT", {"default": 10, "min": 2, "max": 100}),
"line_width": ("INT", {"default": 3, "min": 1, "max": 50}),
"color1": ("STRING", {"default": "#FFFFFF"}),
"color2": ("STRING", {"default": "#000000"})
}
}
def generate_geometric(self, pattern_type, size, frequency, line_width, color1, color2):
img = Image.new('RGB', (size, size), color1)
draw = ImageDraw.Draw(img)
cx, cy = size // 2, size // 2
if pattern_type == "concentric_squares":
step = size // (2 * frequency)
for i in range(frequency):
offset = step * i
draw.rectangle(
[offset, offset, size - offset, size - offset],
outline=color2 if i % 2 == 0 else color1, width=line_width
)
elif pattern_type == "concentric_triangles":
for i in range(frequency):
r = (size // 2) * (i + 1) / frequency
points = [
(cx, cy - r),
(cx - r * math.sin(math.pi / 3), cy + r * 0.5),
(cx + r * math.sin(math.pi / 3), cy + r * 0.5)
]
draw.polygon(points, outline=color2 if i % 2 == 0 else color1, width=line_width)
elif pattern_type == "wavy_grid":
waves = frequency
amp = size / 30
for y in range(0, size, size // waves):
points = [
(x, int(y + amp * math.sin(2 * math.pi * x / size * waves)))
for x in range(size)
]
draw.line(points, fill=color2, width=line_width)
for x in range(0, size, size // waves):
points = [
(int(x + amp * math.sin(2 * math.pi * y / size * waves)), y)
for y in range(size)
]
draw.line(points, fill=color2, width=line_width)
elif pattern_type == "starburst":
rays = frequency * 2
for i in range(rays):
angle = 2 * math.pi * i / rays
x = cx + (size // 2) * math.cos(angle)
y = cy + (size // 2) * math.sin(angle)
draw.line([(cx, cy), (x, y)], fill=color2 if i % 2 == 0 else color1, width=line_width)
elif pattern_type == "hexagons":
# motif nid d’abeille
hex_r = size // (2 * frequency)
for y in range(-hex_r, size + hex_r, int(hex_r * 1.5)):
for x in range(-hex_r, size + hex_r, int(hex_r * math.sqrt(3))):
x_shift = x + (hex_r * math.sqrt(3)/2 if (y // (hex_r * 1.5)) % 2 else 0)
points = [
(x_shift + hex_r * math.cos(a), y + hex_r * math.sin(a))
for a in [math.radians(60 * k) for k in range(6)]
]
draw.polygon(points, outline=color2, width=line_width)
elif pattern_type == "waves":
# Superposition de vagues sinusoïdales (motif Op Art simple)
for i in range(frequency):
amp = size / (30 + i * 5)
y_offset = i * size // (frequency + 1)
points = [
(x, int(y_offset + amp * math.sin(2 * math.pi * x / size * (i+1))))
for x in range(size)
]
draw.line(points, fill=color2 if i % 2 == 0 else color1, width=line_width)
img_array = np.array(img).astype(np.float32) / 255.0
tensor = torch.from_numpy(img_array).unsqueeze(0)
return (tensor,)
+66
View File
@@ -0,0 +1,66 @@
from PIL import Image, ImageDraw
import numpy as np
import torch
import math
class OpticalIllusionNode:
CATEGORY = "illusion"
FUNCTION = "generate_illusion"
RETURN_TYPES = ("IMAGE",)
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"illusion_type": (["checkerboard", "circles", "lines", "spiral"], {"default": "checkerboard"}),
"size": ("INT", {"default": 512, "min": 128, "max": 2048}),
"frequency": ("INT", {"default": 10, "min": 2, "max": 100}),
"line_width": ("INT", {"default": 3, "min": 1, "max": 100}),
"color1": ("STRING", {"default": "#FFFFFF"}),
"color2": ("STRING", {"default": "#000000"})
}
}
def generate_illusion(self, illusion_type, size, frequency, line_width, color1, color2):
img = Image.new('RGB', (size, size), color1)
draw = ImageDraw.Draw(img)
if illusion_type == "checkerboard":
tile = size // frequency
for y in range(frequency):
for x in range(frequency):
if (x + y) % 2 == 0:
draw.rectangle([x*tile, y*tile, (x+1)*tile, (y+1)*tile], fill=color2)
elif illusion_type == "circles":
step = size / (frequency * 2)
for i in range(frequency):
radius = step * (i + 1)
bbox = [size//2 - radius, size//2 - radius, size//2 + radius, size//2 + radius]
draw.ellipse(bbox, outline=color2 if i % 2 == 0 else color1, width=line_width)
elif illusion_type == "lines":
spacing = size / frequency
for i in range(frequency):
offset = i * spacing
draw.line([(offset, 0), (offset, size)], fill=color2 if i % 2 == 0 else color1, width=line_width)
elif illusion_type == "spiral":
cx, cy = size // 2, size // 2
max_radius = size * 0.48
num_turns = frequency
step_theta = math.pi / 720 # très fin = très lisse
a = 0
b = max_radius / (2 * math.pi * num_turns)
theta = 0
while theta < 2 * math.pi * num_turns:
r = a + b * theta
bbox = [cx - r, cy - r, cx + r, cy + r]
start = math.degrees(theta)
end = math.degrees(theta + step_theta)
draw.arc(bbox, start, end, fill=color2, width=line_width)
theta += step_theta
img_array = np.array(img).astype(np.float32) / 255.0
tensor = torch.from_numpy(img_array).unsqueeze(0)
return (tensor,)
+149
View File
@@ -0,0 +1,149 @@
import numpy as np
import torch
from PIL import Image, ImageDraw, ImageColor
import random
class PatternGeneratorNode:
PATTERN_TYPES = ["Stripes", "Checkerboard", "Random Dots", "Solid Color", "Gradient", "Noise"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 128, "min": 16, "max": 4096, "step": 8}),
"height": ("INT", {"default": 128, "min": 16, "max": 4096, "step": 8}),
"pattern_type": (cls.PATTERN_TYPES, {"default": "Noise"}),
"color1_hex": ("STRING", {"default": "#000000", "multiline": False}),
"color2_hex": ("STRING", {"default": "#FFFFFF", "multiline": False}),
"parameter1": ("INT", {"default": 1, "min": 0, "max": 256, "step": 1, "tooltip":"Stripes:width; Dots:density%; Gradient:direction; Noise:0=Color/1=Grayscale"}),
"parameter2": ("INT", {"default": 1, "min": 1, "max": 64, "step": 1, "tooltip":"Dots:max_radius; Noise:block_scale"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFF}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "generate_pattern"
CATEGORY = "illusion"
def _hex_to_rgb(self, hex_color_string):
try:
return ImageColor.getrgb(hex_color_string)
except ValueError:
print(f"PatternGeneratorNode Warning: Invalid color string '{hex_color_string}'. Defaulting to black.")
return (0, 0, 0)
def generate_pattern(self, width, height, pattern_type, color1_hex, color2_hex, parameter1, parameter2, seed):
np.random.seed(seed)
random.seed(seed)
c1 = self._hex_to_rgb(color1_hex)
c2 = self._hex_to_rgb(color2_hex)
image_np = np.zeros((height, width, 3), dtype=np.uint8)
if pattern_type == "Stripes":
stripe_width = max(1, parameter1) # Stripe width
orientation = "Vertical" # Could be an input later
for y_coord in range(height):
for x_coord in range(width):
if orientation == "Vertical":
if (x_coord // stripe_width) % 2 == 0:
image_np[y_coord, x_coord] = c1
else:
image_np[y_coord, x_coord] = c2
else: # Horizontal
if (y_coord // stripe_width) % 2 == 0:
image_np[y_coord, x_coord] = c1
else:
image_np[y_coord, x_coord] = c2
elif pattern_type == "Checkerboard":
square_size = max(1, parameter1) # Square size
for y_coord in range(height):
for x_coord in range(width):
if ((x_coord // square_size) % 2 == (y_coord // square_size) % 2):
image_np[y_coord, x_coord] = c1
else:
image_np[y_coord, x_coord] = c2
elif pattern_type == "Random Dots":
density_percent = np.clip(parameter1, 1, 100) # Density percentage
dot_radius_min = 1
dot_radius_max = np.clip(parameter2, 1, min(width, height)//4) # Max dot radius
avg_radius = (dot_radius_min + dot_radius_max) / 2.0
if avg_radius < 1: avg_radius = 1
num_dots = int((width * height * (density_percent / 100.0)) / (np.pi * avg_radius**2))
num_dots = max(10, num_dots)
num_dots = min(num_dots, width * height // 2) # Prevent extreme overdraw
pil_image = Image.fromarray(image_np)
pil_image = pil_image.convert("RGB")
pil_image.paste(c1, (0,0,width,height))
draw = ImageDraw.Draw(pil_image)
for _ in range(num_dots):
dot_x = random.randint(0, width - 1)
dot_y = random.randint(0, height - 1)
dot_radius = random.randint(dot_radius_min, dot_radius_max)
color_choice = c2
bbox = (dot_x - dot_radius, dot_y - dot_radius,
dot_x + dot_radius, dot_y + dot_radius)
try: # Add try-except for rare issues with ellipse on tiny images/radii
draw.ellipse(bbox, fill=color_choice)
except ValueError:
pass # Skip dot if it causes an issue
image_np = np.array(pil_image)
elif pattern_type == "Solid Color":
image_np[:, :] = c1
elif pattern_type == "Gradient":
direction = parameter1 % 4 # Gradient direction
for y_coord in range(height):
for x_coord in range(width):
if direction == 0: # Left to Right
ratio = x_coord / (width -1) if width > 1 else 0
elif direction == 1: # Top to Bottom
ratio = y_coord / (height -1) if height > 1 else 0
elif direction == 2: # Right to Left
ratio = (width - 1 - x_coord) / (width - 1) if width > 1 else 0
else: # Bottom to Top (direction == 3)
ratio = (height - 1 - y_coord) / (height - 1) if height > 1 else 0
r_val = int(c1[0] * (1 - ratio) + c2[0] * ratio)
g_val = int(c1[1] * (1 - ratio) + c2[1] * ratio)
b_val = int(c1[2] * (1 - ratio) + c2[2] * ratio)
image_np[y_coord, x_coord] = (r_val, g_val, b_val)
elif pattern_type == "Noise":
is_grayscale_noise = parameter1 == 1 # 0 for color, 1 for grayscale
block_scale = max(1, parameter2) # Scale of noise blocks, 1 for pixel noise
for y_base in range(0, height, block_scale):
for x_base in range(0, width, block_scale):
if is_grayscale_noise:
val = random.randint(0, 255)
chosen_color = (val, val, val)
else:
chosen_color = (random.randint(0, 255),
random.randint(0, 255),
random.randint(0, 255))
for y_offset in range(block_scale):
for x_offset in range(block_scale):
y = y_base + y_offset
x = x_base + x_offset
if y < height and x < width:
image_np[y, x] = chosen_color
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 255.0).unsqueeze(0)
return (image_tensor,)
NODE_CLASS_MAPPINGS = {
"PatternGeneratorNode": PatternGeneratorNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"PatternGeneratorNode": "Pattern Generator"
}
+158
View File
@@ -0,0 +1,158 @@
# ComfyUI Illusion & Pattern Nodes
This repository contains a collection of custom nodes for ComfyUI, designed for generating various patterns, optical illusions, and performing related image manipulations. All nodes are categorized under "illusion" in the ComfyUI menu.
## Installation
1. Navigate to your ComfyUI `custom_nodes` directory:
```bash
cd ComfyUI/custom_nodes/
```
2. Clone this repository:
```bash
git clone <your_repository_url_here> ComfyUI-IllusionNodes
```
(Replace `<your_repository_url_here>` with the actual URL of your Git repository).
3. Restart ComfyUI.
The nodes should now be available in the "illusion" category when you right-click or use the "Add Node" menu.
## Nodes Overview
Below is a summary of each node provided in this pack:
---
### 1. Pattern Generator (`PatternGenerator_node.py`)
* **Display Name:** `Pattern Generator`
* **Function:** Generates various 2D procedural patterns.
* **Key Features:**
* **Pattern Types:**
* `Stripes`: Creates vertical or horizontal stripes using two colors.
* `parameter1`: Stripe width.
* `Checkerboard`: Creates a checkerboard pattern.
* `parameter1`: Square size.
* `Random Dots`: Scatters dots of `color2` over a `color1` background.
* `parameter1`: Density percentage (1-100).
* `parameter2`: Maximum dot radius.
* `Solid Color`: Fills the image with `color1`.
* `Gradient`: Creates a linear gradient between `color1` and `color2`.
* `parameter1`: Direction (0: L-R, 1: T-B, 2: R-L, 3: B-T).
* `Noise`: Generates blocky random noise.
* `parameter1`: 0 for Color Noise, 1 for Grayscale Noise.
* `parameter2`: Block scale (1 for pixel-level noise).
* Customizable `width`, `height`, `color1_hex`, `color2_hex`, and `seed`.
---
### 2. Tessellation Composer (Advanced) (`TessellationNode.py`)
* **Display Name:** `Tessellation Composer (Advanced)`
* **Function:** Creates complex tiled patterns from an input image, with various transformations per tile.
* **Key Features:**
* Uses an `input_image` as the base tile.
* `tile_width`, `tile_height`: Dimensions of the base tile (input image will be resized).
* `tiles_x`, `tiles_y`: Number of tiles in horizontal and vertical directions.
* `mode`: Tiling strategy (`repeat`, `mirror`, `diamond`).
* `mirror_axis`: Optional mirroring of tiles (`none`, `x`, `y`, `xy`, `random`).
* `offset_x`, `offset_y`: Offsets applied to alternating rows/columns.
* `rotation_mode`: How rotation is applied (`none`, `by_tile`, `random`).
* `rotation_angle`: Base rotation angle.
* `scale_mode`: How scaling is applied (`none`, `by_tile`, `random`).
* `scale_factor`: Base scale factor.
* `opacity`: Opacity of the composited tiles.
* `random_seed`: For random operations.
---
### 3. Optical Illusion Generator (`OpticalIllusionNode.py`)
* **Display Name:** `OpticalIllusionNode` (or similar, based on class name if not explicitly mapped)
* **Function:** Generates classic optical illusion patterns.
* **Key Features:**
* `illusion_type`:
* `checkerboard`: Standard checkerboard.
* `circles`: Concentric circles.
* `lines`: Parallel lines.
* `spiral`: Archimedean spiral.
* Customizable `size`, `frequency` (density/count of elements), `line_width`, `color1` (background), and `color2` (foreground/lines).
---
### 4. Optical Geometric Pattern Generator (`OpticalGeometricNode.py`)
* **Display Name:** `OpticalGeometricNode` (or similar, based on class name if not explicitly mapped)
* **Function:** Generates various geometric optical art patterns.
* **Key Features:**
* `pattern_type`:
* `concentric_squares`
* `concentric_triangles`
* `wavy_grid`: Grid lines distorted by sine waves.
* `starburst`: Lines radiating from the center.
* `hexagons`: Honeycomb pattern.
* `waves`: Superimposed sinusoidal waves.
* Customizable `size`, `frequency`, `line_width`, `color1`, and `color2`.
---
### 5. Color/Gradient Image (`ColorImageNode.py`)
* **Display Name:** `Color/Gradient Image`
* **Function:** Creates images with solid colors or various types of gradients.
* **Key Features:**
* `mode`:
* `solid`: Fills with `color1`.
* `linear`: Linear gradient between `color1` and `color2`, controlled by `angle`.
* `radial`: Radial gradient from center (`color1`) to edges (`color2`).
* `angular`: Angular (sweep/cone) gradient, rotation controlled by `angle`.
* `mirror`: Reflected linear gradient.
* `diamond`: Diamond-shaped gradient.
* Customizable `width`, `height`, `color1`, `color2`, and `angle`.
---
### 6. Autostereogram Creator (Advanced) (`autostereogram_node.py`)
* **Display Name:** `Autostereogram Creator (Advanced)`
* **Function:** Generates Single Image Random Dot Stereograms (SIRDS), also known as "Magic Eye" images.
* **Key Features:**
* Takes a `depth_map` (grayscale image where brightness indicates depth) and a `pattern` image.
* `eye_separation_pixels`: Simulates the distance between eyes projected onto the image plane, influencing the pattern period.
* `depth_scale_factor`: Controls the intensity of the 3D effect (how much objects "pop out" or recede).
---
### 7. Checkerboard Composer (`CheckerboardNode.py`)
* **Display Name:** `Checkerboard Composer`
* **Function:** Creates a checkerboard pattern using two input images as alternating tiles.
* **Key Features:**
* Takes `img1` and `img2` as inputs for the two alternating tiles.
* `tiles_x`, `tiles_y`: Number of tiles in the checkerboard.
* `tile_width`, `tile_height`: Desired dimensions for each tile.
* `tile_mode`:
* `resize`: Input images are resized to `tile_width` x `tile_height`.
* `crop`: Input images are cropped from the top-left to `tile_width` x `tile_height`.
---
### 8. Tile Image Repeater (Smart Resize) (`TileImageRepeaterNode.py`)
* **Display Name:** `Tile Image Repeater (Smart Resize)`
* **Function:** Repeats an input image to create a larger tiled image, with intelligent resizing options for the base tile.
* **Key Features:**
* Takes an `image` as the base tile.
* `horizontal_repeats`, `vertical_repeats`: Number of times to repeat the tile.
* `resize_mode`: How the base tile is resized before tiling:
* `None`: No resizing.
* `Width`: Tile is resized to `tile_target_size` width, height adjusted by aspect ratio.
* `Height`: Tile is resized to `tile_target_size` height, width adjusted by aspect ratio.
* `Shortest Side`: The shorter side of the tile is resized to `tile_target_size`.
* `Longest Side`: The longer side of the tile is resized to `tile_target_size`.
* `tile_target_size`: The target dimension for resizing (if `resize_mode` is not `None`).
* `resampling_filter`: Filter used for resizing (`lanczos`, `bicubic`, `bilinear`, `nearest`).
---
Enjoy creating illusions and patterns!
+141
View File
@@ -0,0 +1,141 @@
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)",
}
+131
View File
@@ -0,0 +1,131 @@
import numpy as np
import torch
from PIL import Image
class TileImageRepeaterNode:
RESIZE_MODES = ["None", "Width", "Height", "Shortest Side", "Longest Side"] # Ajout de None, et de Shortest/Longest Side
RESAMPLING_FILTERS = ["lanczos", "bicubic", "bilinear", "nearest"]
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"horizontal_repeats": ("INT", {"default": 3, "min": 1, "max": 32, "step": 1}),
"vertical_repeats": ("INT", {"default": 3, "min": 1, "max": 32, "step": 1}),
"resize_mode": (cls.RESIZE_MODES, {"default": "None"}),
"tile_target_size": ("INT", {"default": 256, "min": 0, "max": 8192, "step": 8, "tooltip": "Target size for the chosen dimension (Width, Height, Shortest/Longest Side). 0 or 'None' mode to disable resize."}),
"resampling_filter": (cls.RESAMPLING_FILTERS, {"default": "lanczos"}),
}
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "repeat_image_as_tiles"
CATEGORY = "illusion"
def repeat_image_as_tiles(self, image, horizontal_repeats, vertical_repeats, resize_mode, tile_target_size, resampling_filter):
if not isinstance(image, torch.Tensor):
if isinstance(image, list) and len(image) > 0 and isinstance(image[0], torch.Tensor):
image_tensor = image[0]
else:
raise TypeError(f"Input image must be a torch.Tensor or a list containing one, got {type(image)}")
else:
image_tensor = image
if image_tensor.ndim == 3:
image_bchw_float = image_tensor.unsqueeze(0)
elif image_tensor.ndim == 4:
image_bchw_float = image_tensor
else:
raise ValueError(f"Input image tensor must be 3D (H,W,C) or 4D (B,H,W,C), got {image_tensor.ndim}D shape: {image_tensor.shape}")
single_image_hwc_float = image_bchw_float[0].cpu().numpy()
pil_image_mode = 'RGB'
if single_image_hwc_float.ndim == 3 and single_image_hwc_float.shape[2] == 1:
pil_image_mode = 'L'
elif single_image_hwc_float.ndim == 2: # Si c'est déjà 2D (grayscale)
pil_image_mode = 'L'
pil_image = Image.fromarray((single_image_hwc_float * 255).squeeze().astype(np.uint8), mode=pil_image_mode)
original_width, original_height = pil_image.size
resized_image_hwc_float = single_image_hwc_float # Par défaut, pas de redimensionnement
if resize_mode != "None" and tile_target_size > 0:
target_w = original_width
target_h = original_height
aspect_ratio = original_width / original_height if original_height != 0 else 1
if resize_mode == "Width":
target_w = tile_target_size
target_h = int(target_w / aspect_ratio) if aspect_ratio != 0 else original_height
elif resize_mode == "Height":
target_h = tile_target_size
target_w = int(target_h * aspect_ratio)
elif resize_mode == "Shortest Side":
if original_width < original_height: # Width is shortest
target_w = tile_target_size
target_h = int(target_w / aspect_ratio) if aspect_ratio != 0 else original_height
else: # Height is shortest (or square)
target_h = tile_target_size
target_w = int(target_h * aspect_ratio)
elif resize_mode == "Longest Side":
if original_width > original_height: # Width is longest
target_w = tile_target_size
target_h = int(target_w / aspect_ratio) if aspect_ratio != 0 else original_height
else: # Height is longest (or square)
target_h = tile_target_size
target_w = int(target_h * aspect_ratio)
# S'assurer que les dimensions cibles ne sont pas nulles
target_w = max(1, target_w)
target_h = max(1, target_h)
if (target_w != original_width or target_h != original_height):
resampling_map = {
"lanczos": Image.Resampling.LANCZOS, "bicubic": Image.Resampling.BICUBIC,
"bilinear": Image.Resampling.BILINEAR, "nearest": Image.Resampling.NEAREST
}
resample_pil = resampling_map.get(resampling_filter, Image.Resampling.LANCZOS)
print(f"TileImageRepeaterNode: Resizing tile from {original_width}x{original_height} to {target_w}x{target_h} using {resampling_filter}")
pil_image_resized = pil_image.resize((target_w, target_h), resample=resample_pil)
# Reconvertir en NumPy array et s'assurer qu'il a 3 canaux si l'original en avait 3
resized_np = np.array(pil_image_resized).astype(np.float32) / 255.0
if resized_np.ndim == 2: # Si PIL retourne une image en niveaux de gris (L mode)
resized_image_hwc_float = np.stack((resized_np,) * 3, axis=-1) if single_image_hwc_float.shape[2] == 3 else resized_np[:,:,np.newaxis]
else: # Déjà RGB
resized_image_hwc_float = resized_np
else:
resized_image_hwc_float = single_image_hwc_float # Aucune redimension effective
else: # resize_mode == "None" or tile_target_size == 0
resized_image_hwc_float = single_image_hwc_float
# S'assurer que le nombre de canaux est correct après toutes les opérations
# Surtout si l'entrée était grayscale
if single_image_hwc_float.shape[2] == 1 and resized_image_hwc_float.ndim == 3 and resized_image_hwc_float.shape[2] == 3:
# Si l'entrée était grayscale mais que le redimensionnement a produit RGB (ex: mode 'L' vers 'RGB'), prendre la moyenne
resized_image_hwc_float = np.mean(resized_image_hwc_float, axis=2, keepdims=True)
elif single_image_hwc_float.shape[2] == 3 and resized_image_hwc_float.ndim == 2:
# Si l'entrée était RGB mais que le redimensionnement a produit Grayscale, répéter le canal
resized_image_hwc_float = np.stack((resized_image_hwc_float,) * 3, axis=-1)
elif single_image_hwc_float.shape[2] == 3 and resized_image_hwc_float.ndim == 3 and resized_image_hwc_float.shape[2] == 1:
resized_image_hwc_float = np.repeat(resized_image_hwc_float, 3, axis=2)
tiled_image_np_float = np.tile(resized_image_hwc_float,
(vertical_repeats, horizontal_repeats, 1))
output_tensor_bhwc = torch.from_numpy(tiled_image_np_float).unsqueeze(0)
return (output_tensor_bhwc,)
NODE_CLASS_MAPPINGS = {
"TileImageRepeaterNode": TileImageRepeaterNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TileImageRepeaterNode": "Tile Image Repeater (Smart Resize)"
}
+76
View File
@@ -0,0 +1,76 @@
NODE_CLASS_MAPPINGS = {}
NODE_DISPLAY_NAME_MAPPINGS = {}
# AdvancedAutostereogramNode
try:
from .autostereogram_node import NODE_CLASS_MAPPINGS as ADV_AS_CLASS_MAPPINGS
NODE_CLASS_MAPPINGS.update(ADV_AS_CLASS_MAPPINGS)
from .autostereogram_node import NODE_DISPLAY_NAME_MAPPINGS as ADV_AS_DISPLAY_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS.update(ADV_AS_DISPLAY_MAPPINGS)
except (ImportError, AttributeError) as e:
print(f"[illusion_node] Avertissement (AdvancedAutostereogramNode): {e}")
# PatternGeneratorNode
try:
from .PatternGenerator_node import NODE_CLASS_MAPPINGS as PG_CLASS_MAPPINGS
NODE_CLASS_MAPPINGS.update(PG_CLASS_MAPPINGS)
from .PatternGenerator_node import NODE_DISPLAY_NAME_MAPPINGS as PG_DISPLAY_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS.update(PG_DISPLAY_MAPPINGS)
except (ImportError, AttributeError) as e:
print(f"[illusion_node] Avertissement (PatternGeneratorNode): {e}")
# TileImageRepeaterNode
try:
from .TileImageRepeaterNode import NODE_CLASS_MAPPINGS as TIR_CLASS_MAPPINGS
NODE_CLASS_MAPPINGS.update(TIR_CLASS_MAPPINGS)
from .TileImageRepeaterNode import NODE_DISPLAY_NAME_MAPPINGS as TIR_DISPLAY_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS.update(TIR_DISPLAY_MAPPINGS)
except (ImportError, AttributeError) as e:
print(f"[illusion_node] Avertissement (TileImageRepeaterNode): {e}")
# OpticalIllusionNode
try:
from .OpticalIllusionNode import OpticalIllusionNode
NODE_CLASS_MAPPINGS["OpticalIllusionNode"] = OpticalIllusionNode
NODE_DISPLAY_NAME_MAPPINGS["OpticalIllusionNode"] = "Optical Illusion Generator"
except (ImportError, AttributeError) as e:
print(f"[illusion_node] Avertissement (OpticalIllusionNode): {e}")
# OpticalGeometricNodee
try:
from .OpticalGeometricNode import OpticalGeometricNode
NODE_CLASS_MAPPINGS["OpticalGeometricNode"] = OpticalGeometricNode
NODE_DISPLAY_NAME_MAPPINGS["OpticalGeometricNode"] = "Optical Geometric Generator"
except Exception as e:
print(f"[illusion_node] OpticalGeometricNode import error: {e}")
# CheckerboardNode
try:
from .CheckerboardNode import CheckerboardNode
NODE_CLASS_MAPPINGS["CheckerboardNode"] = CheckerboardNode
NODE_DISPLAY_NAME_MAPPINGS["CheckerboardNode"] = "Checkerboard Composer"
except Exception as e:
print(f"[illusion_node] CheckerboardNode import error: {e}")
# ColorImageNode
try:
from .ColorImageNode import ColorImageNode
NODE_CLASS_MAPPINGS["ColorImageNode"] = ColorImageNode
NODE_DISPLAY_NAME_MAPPINGS["ColorImageNode"] = "Color/Gradient Image"
except Exception as e:
print(f"[illusion_node] ColorImageNode import error: {e}")
# TessellationNode
try:
from .TessellationNode import NODE_CLASS_MAPPINGS as TESS_CLASS_MAPPINGS
NODE_CLASS_MAPPINGS.update(TESS_CLASS_MAPPINGS)
from .TessellationNode import NODE_DISPLAY_NAME_MAPPINGS as TESS_DISPLAY_MAPPINGS
NODE_DISPLAY_NAME_MAPPINGS.update(TESS_DISPLAY_MAPPINGS)
except Exception as e:
print(f"[illusion_node] TessellationNode import error: {e}")
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
print("--- Chargement du pack de nœuds 'illusion_node' ---")
print(f" {len(NODE_CLASS_MAPPINGS)} classe(s) de nœud(s) trouvée(s) : {list(NODE_CLASS_MAPPINGS.keys())}")
print("--- Fin du chargement de 'illusion_node' ---")
+165
View File
@@ -0,0 +1,165 @@
import numpy as np
import torch
from PIL import Image
class AdvancedAutostereogramNode: # Le nom de la classe est AdvancedAutostereogramNode
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"depth_map": ("IMAGE",),
"pattern": ("IMAGE",),
"eye_separation_pixels": ("INT", {"default": 100, "min": 30, "max": 400, "step": 1, "tooltip": "Typical eye separation projected onto the image plane in pixels. Influences pattern period and perceived depth."}),
"depth_scale_factor": ("FLOAT", {"default": 0.5, "min": 0.01, "max": 2.0, "step": 0.01, "tooltip": "Scales the depth effect. Values around 0.3-0.7 are common. Higher values = more 'pop-out'."}),
},
}
RETURN_TYPES = ("IMAGE",)
FUNCTION = "create_advanced_autostereogram" # La fonction à appeler
CATEGORY = "illusion"
def preprocess_image_to_numpy(self, image_tensor_or_pil, target_channels=None, is_depth_map=False):
# Fonction utilitaire pour convertir l'entrée IMAGE en array NumPy HWC, float32 [0,1]
if isinstance(image_tensor_or_pil, torch.Tensor):
img_tensor = image_tensor_or_pil.clone() # Cloner pour éviter de modifier l'original
if img_tensor.ndim == 4: # B,H,W,C
img_np = img_tensor[0].cpu().numpy()
elif img_tensor.ndim == 3: # H,W,C
img_np = img_tensor.cpu().numpy()
elif img_tensor.ndim == 2: # H,W (grayscale)
img_np = img_tensor.cpu().numpy()
else:
raise ValueError(f"Unsupported tensor dimensions: {img_tensor.shape}")
elif isinstance(image_tensor_or_pil, Image.Image):
# Convertir PIL en NumPy HWC, float32 [0,1]
if is_depth_map or (target_channels == 1 and image_tensor_or_pil.mode != 'RGB' and image_tensor_or_pil.mode != 'RGBA'):
img_pil = image_tensor_or_pil.convert("L")
img_np = np.array(img_pil).astype(np.float32) / 255.0
else:
img_pil = image_tensor_or_pil.convert("RGB")
img_np = np.array(img_pil).astype(np.float32) / 255.0
else:
raise TypeError(f"Input must be a torch.Tensor or PIL.Image. Got {type(image_tensor_or_pil)}")
# S'assurer que les valeurs sont bien entre 0 et 1 si elles ne le sont pas déjà
if img_np.max() > 1.1 and not (img_np.min() >=0 and img_np.max() <=1.01): # Vérifier si déjà normalisé avant de re-normaliser
img_np = np.clip(img_np.astype(np.float32) / 255.0, 0.0, 1.0)
else:
img_np = np.clip(img_np.astype(np.float32), 0.0, 1.0)
# Gestion des canaux et de la forme finale
if is_depth_map: # Pour les cartes de profondeur, on veut (H,W) puis on ajoutera le canal
if img_np.ndim == 3 and img_np.shape[2] > 1: # Si c'est HWC (ex: RGB), prendre la moyenne pour grayscale
img_np = np.mean(img_np, axis=2)
img_np = img_np[..., np.newaxis] # Assurer H, W, 1
elif target_channels:
current_channels = img_np.shape[2] if img_np.ndim == 3 else 1 if img_np.ndim == 2 else 0
if img_np.ndim == 2: # H,W -> H,W,C
if target_channels == 1:
img_np = img_np[..., np.newaxis]
elif target_channels == 3:
img_np = np.stack([img_np]*target_channels, axis=-1)
elif img_np.ndim == 3: # H,W,Cin -> H,W,Cout
if current_channels == 1 and target_channels == 3:
img_np = np.repeat(img_np, 3, axis=2)
elif current_channels == 3 and target_channels == 1:
img_np = np.mean(img_np, axis=2, keepdims=True)
elif current_channels == 4 and target_channels == 3: # RGBA -> RGB
img_np = img_np[..., :3]
elif current_channels != target_channels:
# Tentative de gestion simple si pas match parfait
print(f"Warning: Channel mismatch for pattern. Input {current_channels}, target {target_channels}. Attempting basic conversion.")
if target_channels == 3:
img_np = np.mean(img_np, axis=2, keepdims=True) # D'abord grayscale
img_np = np.repeat(img_np, 3, axis=2) # Puis RGB
elif target_channels == 1:
img_np = np.mean(img_np, axis=2, keepdims=True) # Grayscale
return img_np
def create_advanced_autostereogram(self, depth_map, pattern, eye_separation_pixels, depth_scale_factor):
depth_map_np = self.preprocess_image_to_numpy(depth_map, is_depth_map=True) # H, W, 1, float [0,1]
pattern_np = self.preprocess_image_to_numpy(pattern, target_channels=3) # PatH, PatW, 3, float [0,1]
h, w, _ = depth_map_np.shape
pat_h, pat_w, pat_c = pattern_np.shape
if pat_w == 0:
raise ValueError("Pattern width cannot be zero.")
if eye_separation_pixels <=0:
raise ValueError("Eye separation in pixels must be positive.")
stereogram = np.zeros((h, w, pat_c), dtype=np.float32)
links = np.full(w, -1, dtype=int) # Stores the source pattern column index for each stereogram column
# Période du motif à utiliser pour les liens. Devrait être eye_separation_pixels.
# Mais le motif fourni (pattern_np) a sa propre largeur pat_w.
# On va utiliser eye_separation_pixels comme la "largeur virtuelle" du motif de base pour les calculs de liens,
# et ensuite on mapperax % eye_separation_pixels à une colonne dans le motif réel (pat_w).
effective_pattern_period = eye_separation_pixels
for y in range(h):
links.fill(-1)
pattern_row_tile = pattern_np[y % pat_h, :, :] # (pat_w, C)
for x in range(w):
# Depth_value: 0.0 (loin, sur le plan de l'écran), 1.0 (proche, sort le plus)
depth_value = depth_map_np[y, x, 0]
# Separation: combien de pixels le point correspondant à l'oeil droit est décalé par rapport à l'oeil gauche
# Si depth_value = 0, separation = 0 (points sur l'écran)
# Si depth_value = 1, separation = max_separation (points les plus proches)
# max_separation est une fraction (depth_scale_factor) de eye_separation_pixels.
# Par exemple, si eye_separation_pixels = 100 et depth_scale_factor = 0.5, max_separation = 50.
# Cela signifie que pour les objets les plus proches, l'oeil gauche voit le pixel x,
# et l'oeil droit voit le pixel x + 50. Les deux doivent avoir la même couleur.
separation = int(round(depth_value * depth_scale_factor * effective_pattern_period))
# Le pixel x de l'autostéréogramme est vu par l'un des yeux (disons l'oeil gauche).
# Le pixel "frère" correspondant (qui devrait avoir la même couleur de motif) est à:
# x_linked = x - effective_pattern_period + separation
# (Formule classique: S(i) = S(i - P + s(D(i))), où P=period, s(D)=separation)
x_linked = x - effective_pattern_period + separation
if 0 <= x_linked < w and links[x_linked] != -1:
# Si x_linked est valide et a déjà une source de motif assignée (via links[x_linked]),
# alors x doit utiliser la même source de motif.
source_col_in_virtual_pattern = links[x_linked]
# On mappe cette colonne du "motif virtuel" (de période effective_pattern_period)
# à une colonne du motif réel (de largeur pat_w).
actual_col_in_real_pattern = source_col_in_virtual_pattern % pat_w
stereogram[y, x, :] = pattern_row_tile[actual_col_in_real_pattern, :]
links[x] = source_col_in_virtual_pattern # On propage le lien au motif virtuel
else:
# Pas de lien à gauche, ou le pixel lié n'a pas encore de source de motif.
# x devient un point d'ancrage. Sa source de motif est déterminée par sa position
# dans le "motif virtuel" de période effective_pattern_period.
source_col_in_virtual_pattern = x % effective_pattern_period
actual_col_in_real_pattern = source_col_in_virtual_pattern % pat_w
stereogram[y, x, :] = pattern_row_tile[actual_col_in_real_pattern, :]
links[x] = source_col_in_virtual_pattern
output_tensor = torch.from_numpy(stereogram.astype(np.float32)).unsqueeze(0)
return (output_tensor,)
# --- Mappings pour ComfyUI ---
# Assurez-vous que le nom de la classe ici correspond à celui défini ci-dessus.
NODE_CLASS_MAPPINGS = {
"AdvancedAutostereogramNode": AdvancedAutostereogramNode
# Si vous voulez que l'ancien workflow fonctionne sans changer le nom du noeud dans le JSON:
# "AutostereogramNode": AdvancedAutostereogramNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AdvancedAutostereogramNode": "Autostereogram Creator (Advanced)"
# Ou pour correspondre à la clé ci-dessus si vous l'avez changée :
# "AutostereogramNode": "Autostereogram Creator (Adv.)"
}
+12
View File
@@ -0,0 +1,12 @@
# Core dependencies (essentiels)
numpy>=1.21.0
scipy>=1.7.0
opencv-python>=4.5.0
pillow>=8.0.0
# Image processing
scikit-image>=0.18.0
imageio>=2.9.0
# Performance
numba>=0.56.0