- Add ruff configuration to pyproject.toml (py310, 120 char lines) - Fix trailing whitespace (W291/W293), modernize type annotations (UP006/UP045) - Remove unused imports (F401), simplify dict iteration (SIM118) - Use f-string conversion flags (RUF010), modernize exception aliases (UP024) - Replace try/except pass with contextlib.suppress (SIM105) - Prefix unused variables with underscore (F841/RUF059) - Convert lambdas to def statements (E731) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
179 lines
6.7 KiB
Python
179 lines
6.7 KiB
Python
import os
|
|
import logging
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image, ImageOps
|
|
|
|
# Import centralized logging setup
|
|
try:
|
|
from ..utils.debug_utils import setup_logging
|
|
setup_logging()
|
|
except ImportError:
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class ImageLoader:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image_path": ("STRING", {
|
|
"default": "path/to/image.png",
|
|
"description": "Full path to the image file"
|
|
}),
|
|
"normalize": ("BOOLEAN", {
|
|
"default": True,
|
|
"description": "Normalize image values to the 0-1 range"
|
|
})
|
|
},
|
|
"hidden": {"node_id": "UNIQUE_ID"}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "MASK", "STRING")
|
|
RETURN_NAMES = ("image", "mask", "metadata")
|
|
FUNCTION = "load_regular_image"
|
|
CATEGORY = "COCO Tools/Loaders"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, **kwargs):
|
|
return float("NaN") # Always execute
|
|
|
|
def load_regular_image(
|
|
self, image_path: str, normalize: bool = True, node_id: str = None
|
|
) -> tuple[torch.Tensor, torch.Tensor, str]:
|
|
"""
|
|
Main function to load and process a regular image.
|
|
Supports formats like PNG, JPG, and WebP.
|
|
"""
|
|
if not os.path.exists(image_path):
|
|
raise FileNotFoundError(f"Image path not found: {image_path}")
|
|
|
|
try:
|
|
with Image.open(image_path) as img:
|
|
# Preserve original format before any transforms
|
|
original_format = img.format
|
|
|
|
# Apply EXIF orientation
|
|
img = ImageOps.exif_transpose(img)
|
|
|
|
# Detect bit depth from original mode BEFORE RGB conversion
|
|
original_mode = img.mode
|
|
info = self.detect_bit_depth(image_path, original_mode, original_format)
|
|
bit_depth = info["bit_depth"]
|
|
|
|
# Extract alpha channel before RGB conversion
|
|
has_alpha = original_mode in ("RGBA", "LA", "PA")
|
|
if has_alpha:
|
|
alpha_tensor = self.pil2tensor(img.split()[-1], 8).unsqueeze(0)
|
|
|
|
# High bit depth grayscale modes — bypass PIL's destructive convert("RGB")
|
|
if bit_depth > 8 and original_mode in ("I;16", "I;16B", "I;16L", "I", "F"):
|
|
image_np = np.array(img)
|
|
if image_np.ndim == 2:
|
|
image_np = np.stack([image_np, image_np, image_np], axis=-1)
|
|
rgb_tensor = self.pil2tensor_numpy(image_np, bit_depth)
|
|
else:
|
|
rgb_image = img.convert("RGB")
|
|
rgb_tensor = self.pil2tensor(rgb_image, bit_depth)
|
|
|
|
# Default opaque alpha mask matching MASK format [B,H,W]
|
|
if not has_alpha:
|
|
alpha_tensor = torch.ones(1, rgb_tensor.shape[1], rgb_tensor.shape[2])
|
|
|
|
# Apply min-max range normalization if requested
|
|
if normalize:
|
|
rgb_tensor = self.normalize_image(rgb_tensor)
|
|
alpha_tensor = alpha_tensor.clamp(0, 1)
|
|
|
|
# Prepare metadata
|
|
metadata = {
|
|
"file_path": image_path,
|
|
"tensor_shape": tuple(rgb_tensor.shape),
|
|
"format": os.path.splitext(image_path)[1].lower(),
|
|
"bit_depth": bit_depth
|
|
}
|
|
|
|
return rgb_tensor, alpha_tensor, str(metadata)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error loading image {image_path}: {e}")
|
|
raise ValueError(f"Error loading image {image_path}: {e}") from e
|
|
|
|
@staticmethod
|
|
def normalize_image(image: torch.Tensor) -> torch.Tensor:
|
|
"""
|
|
Normalize a tensor to the 0-1 range via min-max stretching.
|
|
Returns the image unchanged if all values are identical.
|
|
"""
|
|
min_val, max_val = image.min(), image.max()
|
|
if min_val == max_val:
|
|
return image
|
|
return (image - min_val) / (max_val - min_val)
|
|
|
|
@staticmethod
|
|
def detect_bit_depth(image_path: str, original_mode: str = None, original_format: str = None) -> dict:
|
|
"""
|
|
Detect the bit depth of an image from its original mode before any conversion.
|
|
"""
|
|
mode_to_bit_depth = {
|
|
"1": 1, "L": 8, "P": 8, "RGB": 8, "RGBA": 8,
|
|
"LA": 8, "PA": 8,
|
|
"I;16": 16, "I;16B": 16, "I;16L": 16,
|
|
"I": 32, "F": 32
|
|
}
|
|
|
|
if original_mode is not None:
|
|
mode = original_mode
|
|
fmt = original_format
|
|
else:
|
|
with Image.open(image_path) as img:
|
|
mode = img.mode
|
|
fmt = img.format
|
|
|
|
bit_depth = mode_to_bit_depth.get(mode, 8)
|
|
return {"bit_depth": bit_depth, "mode": mode, "format": fmt}
|
|
|
|
@staticmethod
|
|
def pil2tensor(image: Image.Image, bit_depth: int) -> torch.Tensor:
|
|
"""
|
|
Convert a PIL Image to a PyTorch tensor, scaled to the 0-1 range.
|
|
"""
|
|
image_np = np.array(image)
|
|
if bit_depth == 8:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 255.0)
|
|
elif bit_depth == 16:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 65535.0)
|
|
elif bit_depth == 32:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32))
|
|
else:
|
|
logger.warning(f"Unsupported bit depth: {bit_depth}. Defaulting to 8-bit normalization.")
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 255.0)
|
|
|
|
# Add a batch dimension if not present
|
|
if len(image_tensor.shape) == 3:
|
|
image_tensor = image_tensor.unsqueeze(0)
|
|
|
|
return image_tensor
|
|
|
|
@staticmethod
|
|
def pil2tensor_numpy(image_np: np.ndarray, bit_depth: int) -> torch.Tensor:
|
|
"""Convert a numpy array to a PyTorch tensor, scaled to 0-1 range based on bit depth."""
|
|
if bit_depth == 16:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 65535.0)
|
|
elif bit_depth == 32:
|
|
# PIL "I" mode stores int32 values; scale by dtype max to get 0-1 range
|
|
if np.issubdtype(image_np.dtype, np.integer):
|
|
max_val = np.iinfo(image_np.dtype).max
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / max_val)
|
|
else:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32))
|
|
else:
|
|
image_tensor = torch.from_numpy(image_np.astype(np.float32) / 255.0)
|
|
|
|
if len(image_tensor.shape) == 3:
|
|
image_tensor = image_tensor.unsqueeze(0)
|
|
|
|
return image_tensor
|