image_io.py 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. import io
  2. import hashlib
  3. from typing import List
  4. import numpy as np
  5. from PIL import Image
  6. try:
  7. import torch
  8. except Exception: # pragma: no cover - ComfyUI provides torch
  9. torch = None
  10. def _ensure_torch_available():
  11. if torch is None:
  12. raise RuntimeError("PyTorch is required in ComfyUI runtime but was not found.")
  13. def tensor_to_pil_list(image_tensor) -> List[Image.Image]:
  14. """Convert ComfyUI IMAGE tensor [B,H,W,C] float32(0..1) to list of PIL.Image (RGB).
  15. Supports batch processing; returns one PIL image per batch.
  16. """
  17. _ensure_torch_available()
  18. if image_tensor is None:
  19. raise ValueError("image_tensor is None")
  20. if not isinstance(image_tensor, torch.Tensor):
  21. raise TypeError("Expected image_tensor to be a torch.Tensor")
  22. if image_tensor.ndim != 4 or image_tensor.shape[-1] != 3:
  23. raise ValueError(
  24. f"Expected image tensor shape [B,H,W,3], got {tuple(image_tensor.shape)}"
  25. )
  26. images: List[Image.Image] = []
  27. batch, height, width, channels = image_tensor.shape
  28. image_tensor = image_tensor.detach().cpu().clamp(0.0, 1.0)
  29. np_images = (image_tensor.numpy() * 255.0).astype(np.uint8)
  30. for i in range(batch):
  31. arr = np_images[i]
  32. img = Image.fromarray(arr, mode="RGB")
  33. images.append(img)
  34. return images
  35. def pil_list_to_tensor(images: List[Image.Image]):
  36. """Convert list of PIL.Image (RGB) to ComfyUI IMAGE tensor [B,H,W,C] float32(0..1)."""
  37. _ensure_torch_available()
  38. if not images:
  39. raise ValueError("images list is empty")
  40. tensors = []
  41. for img in images:
  42. if img.mode != "RGB":
  43. img = img.convert("RGB")
  44. arr = np.array(img).astype(np.float32) / 255.0
  45. t = torch.from_numpy(arr)
  46. tensors.append(t)
  47. # Stack along batch dimension; ComfyUI expects [B,H,W,C]
  48. batch_tensor = torch.stack(tensors, dim=0)
  49. return batch_tensor
  50. def pil_to_png_bytes(img: Image.Image) -> bytes:
  51. if img.mode != "RGB":
  52. img = img.convert("RGB")
  53. buf = io.BytesIO()
  54. img.save(buf, format="PNG")
  55. return buf.getvalue()
  56. def bytes_to_pil_image(data: bytes) -> Image.Image:
  57. return Image.open(io.BytesIO(data)).convert("RGB")
  58. def sha256_bytes(data: bytes) -> str:
  59. return hashlib.sha256(data).hexdigest()
  60. def hash_pil_image(img: Image.Image) -> str:
  61. return sha256_bytes(pil_to_png_bytes(img))
  62. def hash_pil_images(images: List[Image.Image]) -> str:
  63. hasher = hashlib.sha256()
  64. for img in images:
  65. hasher.update(pil_to_png_bytes(img))
  66. return hasher.hexdigest()