""" 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", }