Files
2025-12-04 13:34:30 -08:00

516 lines
19 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Analysis and Utility Tools for TextureAlchemy
Texture analyzer, UV tools, atlas packer
"""
import torch
import torch.nn.functional as F
class TextureAnalyzer:
"""
Analyze texture properties and statistics
Resolution, color space, seamless detection, stats
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"check_seamless": ("BOOLEAN", {"default": True}),
"edge_tolerance": ("FLOAT", {
"default": 0.05,
"min": 0.0,
"max": 0.5,
"step": 0.01,
"display": "number",
"tooltip": "Tolerance for seamless detection"
}),
}
}
RETURN_TYPES = ("STRING", "IMAGE")
RETURN_NAMES = ("analysis", "visualization")
FUNCTION = "analyze"
CATEGORY = "Texture Alchemist/Analysis"
OUTPUT_NODE = True
def analyze(self, image, check_seamless, edge_tolerance):
"""Analyze texture"""
print(f"\n{'='*60}")
print(f"TEXTURE ANALYZER")
print(f"{'='*60}")
batch, height, width, channels = image.shape
# Basic info
resolution = f"{width}×{height}"
aspect_ratio = width / height
megapixels = (width * height) / 1000000
# Color analysis
mean_rgb = [image[:, :, :, i].mean().item() for i in range(min(3, channels))]
std_rgb = [image[:, :, :, i].std().item() for i in range(min(3, channels))]
min_vals = [image[:, :, :, i].min().item() for i in range(min(3, channels))]
max_vals = [image[:, :, :, i].max().item() for i in range(min(3, channels))]
# Luminosity
if channels >= 3:
lum = 0.299 * image[:, :, :, 0] + 0.587 * image[:, :, :, 1] + 0.114 * image[:, :, :, 2]
else:
lum = image[:, :, :, 0]
brightness = lum.mean().item()
contrast = lum.std().item()
# Seamless check
is_seamless_h = False
is_seamless_v = False
if check_seamless:
# Check horizontal seam
left = image[:, :, 0:5, :]
right = image[:, :, -5:, :]
h_diff = torch.abs(left - right).mean().item()
is_seamless_h = h_diff < edge_tolerance
# Check vertical seam
top = image[:, 0:5, :, :]
bottom = image[:, -5:, :, :]
v_diff = torch.abs(top - bottom).mean().item()
is_seamless_v = v_diff < edge_tolerance
# Build analysis report
report = []
report.append(f"📊 TEXTURE ANALYSIS REPORT")
report.append(f"")
report.append(f"🖼️ RESOLUTION:")
report.append(f" Size: {resolution}")
report.append(f" Aspect Ratio: {aspect_ratio:.2f}:1")
report.append(f" Megapixels: {megapixels:.2f}MP")
report.append(f" Channels: {channels}")
report.append(f"")
report.append(f"🎨 COLOR STATISTICS:")
if channels >= 3:
report.append(f" Mean RGB: ({mean_rgb[0]:.3f}, {mean_rgb[1]:.3f}, {mean_rgb[2]:.3f})")
report.append(f" Std Dev: ({std_rgb[0]:.3f}, {std_rgb[1]:.3f}, {std_rgb[2]:.3f})")
report.append(f" Range R: [{min_vals[0]:.3f}, {max_vals[0]:.3f}]")
report.append(f" Range G: [{min_vals[1]:.3f}, {max_vals[1]:.3f}]")
report.append(f" Range B: [{min_vals[2]:.3f}, {max_vals[2]:.3f}]")
report.append(f" Brightness: {brightness:.3f}")
report.append(f" Contrast: {contrast:.3f}")
report.append(f"")
if check_seamless:
report.append(f"🔄 SEAMLESS CHECK:")
report.append(f" Horizontal: {'✓ SEAMLESS' if is_seamless_h else '✗ NOT SEAMLESS'}")
report.append(f" Vertical: {'✓ SEAMLESS' if is_seamless_v else '✗ NOT SEAMLESS'}")
report.append(f" Edge difference: H={h_diff:.4f}, V={v_diff:.4f}")
report.append(f"")
# Create visualization
viz = self._create_visualization(image, mean_rgb, is_seamless_h, is_seamless_v)
report_str = "\n".join(report)
# Print to console
for line in report:
print(f" {line}")
print(f"{'='*60}\n")
return (report_str, viz)
def _create_visualization(self, image, mean_rgb, seamless_h, seamless_v):
"""Create visual analysis"""
batch, h, w, c = image.shape
# Create avg color swatch
avg_color = torch.tensor(mean_rgb + [1.0] * (3 - len(mean_rgb)),
device=image.device, dtype=image.dtype)
swatch = avg_color.view(1, 1, 1, 3).repeat(1, h//4, w//4, 1)
# Create edge visualization if checking seamless
if seamless_h or seamless_v:
edges = torch.zeros_like(image)
# Mark edges
edges[:, :, 0:3, :] = 1.0 if seamless_v else torch.tensor([1.0, 0.0, 0.0]) # Top
edges[:, :, -3:, :] = 1.0 if seamless_v else torch.tensor([1.0, 0.0, 0.0]) # Bottom
edges[:, 0:3, :, :] = 1.0 if seamless_h else torch.tensor([1.0, 0.0, 0.0]) # Left
edges[:, -3:, :, :] = 1.0 if seamless_h else torch.tensor([1.0, 0.0, 0.0]) # Right
# Blend with image
viz = image * 0.7 + edges * 0.3
else:
viz = image
# Add swatch in corner
viz[:, :h//4, :w//4, :] = swatch
return torch.clamp(viz, 0.0, 1.0)
class UVCheckerGenerator:
"""
Generate UV test patterns for checking UV mapping
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"width": ("INT", {"default": 1024, "min": 256, "max": 8192, "step": 256}),
"height": ("INT", {"default": 1024, "min": 256, "max": 8192, "step": 256}),
"pattern_type": (["grid", "checker", "numbered", "gradient"],),
"grid_size": ("INT", {
"default": 8,
"min": 2,
"max": 32,
"step": 1,
"tooltip": "Number of grid divisions"
}),
"color_mode": (["rainbow", "grayscale", "red_green"],),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("uv_checker",)
FUNCTION = "generate"
CATEGORY = "Texture Alchemist/Analysis"
def generate(self, width, height, pattern_type, grid_size, color_mode):
"""Generate UV checker pattern"""
print(f"\n{'='*60}")
print(f"UV Checker Generator - {pattern_type.upper()}")
print(f"{'='*60}")
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
dtype = torch.float32
# Create coordinate grid
y_coords = torch.linspace(0, 1, height, device=device, dtype=dtype).view(-1, 1).repeat(1, width)
x_coords = torch.linspace(0, 1, width, device=device, dtype=dtype).view(1, -1).repeat(height, 1)
# Create pattern based on type
if pattern_type == "grid":
pattern = self._grid_pattern(x_coords, y_coords, grid_size, color_mode)
elif pattern_type == "checker":
pattern = self._checker_pattern(x_coords, y_coords, grid_size)
elif pattern_type == "gradient":
pattern = torch.stack([x_coords, y_coords, torch.ones_like(x_coords) * 0.5], dim=2)
else: # numbered
pattern = self._grid_pattern(x_coords, y_coords, grid_size, color_mode)
pattern = pattern.unsqueeze(0) # Add batch dim
pattern = torch.clamp(pattern, 0.0, 1.0)
print(f"✓ UV checker generated")
print(f" Resolution: {width}×{height}")
print(f" Grid: {grid_size}×{grid_size}")
print(f"{'='*60}\n")
return (pattern,)
def _grid_pattern(self, x, y, grid_size, color_mode):
"""Create grid pattern"""
grid_x = (x * grid_size) % 1.0
grid_y = (y * grid_size) % 1.0
# Draw grid lines
line_width = 0.05
lines = ((grid_x < line_width) | (grid_y < line_width)).float()
# Create colored cells
cell_x = torch.floor(x * grid_size).long() % 3
cell_y = torch.floor(y * grid_size).long() % 3
if color_mode == "rainbow":
r = ((cell_x + cell_y) % 3) / 2.0
g = ((cell_x * 2) % 3) / 2.0
b = ((cell_y * 2) % 3) / 2.0
elif color_mode == "red_green":
r = cell_x.float() / 2.0
g = cell_y.float() / 2.0
b = torch.zeros_like(x)
else: # grayscale
val = ((cell_x + cell_y) % 2).float() * 0.5 + 0.25
r = g = b = val
# Combine
r = r * (1.0 - lines) + lines
g = g * (1.0 - lines) + lines
b = b * (1.0 - lines) + lines
return torch.stack([r, g, b], dim=2)
def _checker_pattern(self, x, y, grid_size):
"""Create checker pattern"""
cell_x = torch.floor(x * grid_size).long()
cell_y = torch.floor(y * grid_size).long()
checker = ((cell_x + cell_y) % 2).float()
return checker.unsqueeze(2).repeat(1, 1, 3)
class ImageBitDepthChecker:
"""
Check and display image bit depth information
Reports tensor dtype, effective bit depth, and memory usage
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING", "INT")
RETURN_NAMES = ("image", "bit_depth", "info", "memory_mb")
FUNCTION = "check_depth"
CATEGORY = "Texture Alchemist/Analysis"
OUTPUT_NODE = True
def check_depth(self, image):
"""Check image bit depth and properties"""
print(f"\n{'='*60}")
print(f"IMAGE BIT DEPTH CHECKER")
print(f"{'='*60}")
# Get tensor properties
dtype = image.dtype
shape = image.shape
batch, height, width, channels = shape
# Determine bit depth from dtype
dtype_map = {
torch.float16: ("16-bit float (half precision)", 16),
torch.float32: ("32-bit float (single precision)", 32),
torch.float64: ("64-bit float (double precision)", 64),
torch.uint8: ("8-bit unsigned integer", 8),
torch.int8: ("8-bit signed integer", 8),
torch.int16: ("16-bit signed integer", 16),
torch.int32: ("32-bit signed integer", 32),
torch.int64: ("64-bit signed integer", 64),
torch.bfloat16: ("16-bit bfloat (brain float)", 16),
}
dtype_name, bit_depth = dtype_map.get(dtype, ("Unknown dtype", 0))
# Calculate memory usage
element_size = image.element_size() # bytes per element
total_elements = batch * height * width * channels
memory_bytes = total_elements * element_size
memory_mb = memory_bytes / (1024 * 1024)
# Analyze value range
min_val = image.min().item()
max_val = image.max().item()
# Determine if values are normalized (0-1) or not
is_normalized = (min_val >= 0.0 and max_val <= 1.0)
# Build report
report = []
report.append(f"🔍 BIT DEPTH ANALYSIS")
report.append(f"")
report.append(f"📦 TENSOR INFO:")
report.append(f" Data Type: {dtype}")
report.append(f" Bit Depth: {bit_depth}-bit")
report.append(f" Description: {dtype_name}")
report.append(f" Bytes/Element: {element_size}")
report.append(f"")
report.append(f"📐 DIMENSIONS:")
report.append(f" Resolution: {width}×{height}")
report.append(f" Channels: {channels}")
report.append(f" Batch: {batch}")
report.append(f" Total Elements: {total_elements:,}")
report.append(f"")
report.append(f"💾 MEMORY USAGE:")
report.append(f" Memory: {memory_mb:.2f} MB")
report.append(f" Per Image: {memory_mb/batch:.2f} MB")
report.append(f"")
report.append(f"📊 VALUE RANGE:")
report.append(f" Min: {min_val:.6f}")
report.append(f" Max: {max_val:.6f}")
report.append(f" Normalized: {'✓ Yes (0-1 range)' if is_normalized else '✗ No'}")
report.append(f"")
report.append(f"💡 NOTES:")
if dtype == torch.float32:
report.append(f" • Standard ComfyUI format (32-bit float)")
report.append(f" • Full precision for processing")
report.append(f" • Use EXR format to preserve on save")
elif dtype == torch.float16:
report.append(f" • Half precision (saves memory)")
report.append(f" • May lose some precision")
report.append(f" • Good for inference")
elif dtype == torch.uint8:
report.append(f" • 8-bit integer (256 values)")
report.append(f" • Typical for loaded PNG/JPG images")
report.append(f" • Convert to float32 for processing")
if not is_normalized and dtype in [torch.float16, torch.float32, torch.float64]:
report.append(f" ⚠ Warning: Values outside 0-1 range!")
report.append(f" This may cause issues with some nodes")
# Simple bit depth string output
bit_depth_str = f"{bit_depth}-bit"
# Full info report
info_str = "\n".join(report)
# Print to console
for line in report:
print(f" {line}")
print(f"{'='*60}\n")
# Pass through the image unchanged
return (image, bit_depth_str, info_str, int(memory_mb))
class TextureAtlasBuilder:
"""
Combine multiple textures into a texture atlas
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"layout": (["2x2", "3x3", "4x4", "1x2", "1x3", "1x4", "2x1", "3x1", "4x1"],),
"spacing": ("INT", {
"default": 0,
"min": 0,
"max": 50,
"step": 1,
"tooltip": "Pixels between textures"
}),
"background_color": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 1.0,
"step": 0.01,
"display": "number"
}),
},
"optional": {
"tex1": ("IMAGE",),
"tex2": ("IMAGE",),
"tex3": ("IMAGE",),
"tex4": ("IMAGE",),
"tex5": ("IMAGE",),
"tex6": ("IMAGE",),
"tex7": ("IMAGE",),
"tex8": ("IMAGE",),
"tex9": ("IMAGE",),
"tex10": ("IMAGE",),
"tex11": ("IMAGE",),
"tex12": ("IMAGE",),
"tex13": ("IMAGE",),
"tex14": ("IMAGE",),
"tex15": ("IMAGE",),
"tex16": ("IMAGE",),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("atlas",)
FUNCTION = "build_atlas"
CATEGORY = "Texture Alchemist/Analysis"
def build_atlas(self, layout, spacing, background_color, **textures):
"""Build texture atlas"""
print(f"\n{'='*60}")
print(f"Texture Atlas Builder - {layout}")
print(f"{'='*60}")
# Parse layout
rows, cols = map(int, layout.split('x'))
max_textures = rows * cols
# Collect textures
tex_list = []
for i in range(1, max_textures + 1):
key = f"tex{i}"
if key in textures and textures[key] is not None:
tex_list.append(textures[key])
if not tex_list:
print("⚠ No textures provided!")
return (torch.zeros((1, 512, 512, 3)),)
# Find max dimensions
max_h = max([t.shape[1] for t in tex_list])
max_w = max([t.shape[2] for t in tex_list])
# Calculate atlas size
atlas_w = cols * max_w + (cols - 1) * spacing
atlas_h = rows * max_h + (rows - 1) * spacing
device = tex_list[0].device
dtype = tex_list[0].dtype
# Create atlas
atlas = torch.ones((1, atlas_h, atlas_w, 3), device=device, dtype=dtype) * background_color
# Place textures
idx = 0
for row in range(rows):
for col in range(cols):
if idx >= len(tex_list):
break
tex = tex_list[idx]
# Resize to max dimensions
if tex.shape[1:3] != (max_h, max_w):
tex = F.interpolate(
tex.permute(0, 3, 1, 2),
size=(max_h, max_w),
mode='bilinear',
align_corners=False
).permute(0, 2, 3, 1)
# Calculate position
y = row * (max_h + spacing)
x = col * (max_w + spacing)
# Place
atlas[:, y:y+max_h, x:x+max_w, :] = tex
idx += 1
print(f"✓ Atlas created")
print(f" Layout: {rows}×{cols}")
print(f" Textures: {len(tex_list)}")
print(f" Cell size: {max_w}×{max_h}")
print(f" Atlas size: {atlas_w}×{atlas_h}")
print(f"{'='*60}\n")
return (atlas,)
# Node registration
NODE_CLASS_MAPPINGS = {
"TextureAnalyzer": TextureAnalyzer,
"ImageBitDepthChecker": ImageBitDepthChecker,
"UVCheckerGenerator": UVCheckerGenerator,
"TextureAtlasBuilder": TextureAtlasBuilder,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"TextureAnalyzer": "Texture Analyzer",
"ImageBitDepthChecker": "Image Bit Depth Checker",
"UVCheckerGenerator": "UV Checker Generator",
"TextureAtlasBuilder": "Texture Atlas Builder",
}