Files
2026-01-07 15:29:35 +08:00

323 lines
8.5 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.
"""
工具函数
图像转换、Base64编码、URL下载等
"""
import io
import base64
import requests
import numpy as np
from PIL import Image
from typing import Tuple, Optional
import torch
def tensor_to_pil(tensor: torch.Tensor) -> Image.Image:
"""
将ComfyUI IMAGE tensor转换为PIL Image
ComfyUI IMAGE格式: (B, H, W, C), float32, 0-1范围
Args:
tensor: 形状为 (B, H, W, C) 的tensor,取第一张图
Returns:
PIL.Image对象
"""
# 确保在CPU上
if tensor.device.type != "cpu":
tensor = tensor.cpu()
# 取第一张图像
if len(tensor.shape) == 4:
img_np = tensor[0].numpy()
else:
img_np = tensor.numpy()
# 转换到0-255范围
img_np = np.clip(img_np * 255, 0, 255).astype(np.uint8)
# 创建PIL Image
if img_np.shape[2] == 4:
mode = "RGBA"
elif img_np.shape[2] == 3:
mode = "RGB"
else:
mode = "L"
return Image.fromarray(img_np, mode=mode)
def pil_to_tensor(image: Image.Image) -> torch.Tensor:
"""
将PIL Image转换为ComfyUI IMAGE tensor
Args:
image: PIL.Image对象
Returns:
形状为 (1, H, W, C) 的tensor,float32, 0-1范围
"""
# 确保是RGB或RGBA
if image.mode not in ["RGB", "RGBA"]:
image = image.convert("RGB")
# 转换为numpy数组
img_np = np.array(image).astype(np.float32) / 255.0
# 添加batch维度
img_tensor = torch.from_numpy(img_np).unsqueeze(0)
return img_tensor
def tensor_to_base64(tensor: torch.Tensor, format: str = "PNG") -> str:
"""
将ComfyUI IMAGE tensor转换为Base64编码字符串
Args:
tensor: ComfyUI IMAGE tensor
format: 图像格式,如 "PNG", "JPEG"
Returns:
带MIME类型前缀的Base64字符串,如 "data:image/png;base64,xxxxx"
"""
# 转换为PIL Image
pil_image = tensor_to_pil(tensor)
# 保存到内存缓冲区
buffer = io.BytesIO()
# 如果是RGBA且格式不支持透明,转换为RGB
if pil_image.mode == "RGBA" and format.upper() == "JPEG":
# 创建白色背景
background = Image.new("RGB", pil_image.size, (255, 255, 255))
background.paste(pil_image, mask=pil_image.split()[3])
pil_image = background
pil_image.save(buffer, format=format.upper())
# 编码为Base64
base64_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
# 添加MIME类型前缀
mime_type = f"image/{format.lower()}"
if format.upper() == "JPEG":
mime_type = "image/jpeg"
return f"data:{mime_type};base64,{base64_data}"
def mask_tensor_to_base64(mask_tensor: torch.Tensor, format: str = "PNG") -> str:
"""
将ComfyUI MASK tensor转换为Base64编码的黑白图像
ComfyUI MASK格式: (B, H, W), float32, 0-1范围
白色(1.0)表示要编辑的区域,黑色(0.0)表示保留区域
Args:
mask_tensor: ComfyUI MASK tensor
format: 图像格式
Returns:
带MIME类型前缀的Base64字符串
"""
# 确保在CPU上
if mask_tensor.device.type != "cpu":
mask_tensor = mask_tensor.cpu()
# 取第一张mask
if len(mask_tensor.shape) == 3:
mask_np = mask_tensor[0].numpy()
else:
mask_np = mask_tensor.numpy()
# 转换到0-255范围(白色=要编辑的区域)
mask_np = np.clip(mask_np * 255, 0, 255).astype(np.uint8)
# 创建RGB图像(API需要RGB格式的黑白图像)
mask_rgb = np.stack([mask_np, mask_np, mask_np], axis=-1)
# 创建PIL Image
pil_image = Image.fromarray(mask_rgb, mode="RGB")
# 保存到内存缓冲区
buffer = io.BytesIO()
pil_image.save(buffer, format=format.upper())
# 编码为Base64
base64_data = base64.b64encode(buffer.getvalue()).decode("utf-8")
# 添加MIME类型前缀
mime_type = f"image/{format.lower()}"
return f"data:{mime_type};base64,{base64_data}"
def download_image(url: str, timeout: int = 30) -> Image.Image:
"""
从URL下载图像
Args:
url: 图像URL
timeout: 超时时间(秒)
Returns:
PIL.Image对象
"""
response = requests.get(url, timeout=timeout)
response.raise_for_status()
return Image.open(io.BytesIO(response.content))
def download_image_as_tensor(url: str, timeout: int = 30) -> torch.Tensor:
"""
从URL下载图像并转换为ComfyUI tensor
Args:
url: 图像URL
timeout: 超时时间(秒)
Returns:
ComfyUI IMAGE tensor
"""
pil_image = download_image(url, timeout)
return pil_to_tensor(pil_image)
def get_image_size(tensor: torch.Tensor) -> Tuple[int, int]:
"""
获取图像尺寸
Args:
tensor: ComfyUI IMAGE tensor (B, H, W, C)
Returns:
(width, height)
"""
if len(tensor.shape) == 4:
_, h, w, _ = tensor.shape
else:
h, w, _ = tensor.shape
return (w, h)
def validate_image_size(tensor: torch.Tensor, min_size: int = 512, max_size: int = 4096) -> Tuple[bool, str]:
"""
验证图像尺寸是否符合API要求
Args:
tensor: ComfyUI IMAGE tensor
min_size: 最小尺寸(像素)
max_size: 最大尺寸(像素)
Returns:
(是否有效, 错误信息)
"""
w, h = get_image_size(tensor)
if w < min_size or h < min_size:
return False, f"图像尺寸过小:{w}x{h},最小要求 {min_size}x{min_size} 像素"
if w > max_size or h > max_size:
return False, f"图像尺寸过大:{w}x{h},最大支持 {max_size}x{max_size} 像素"
return True, ""
def resize_image_if_needed(
tensor: torch.Tensor,
min_size: int = 512,
max_size: int = 4096
) -> Tuple[torch.Tensor, bool]:
"""
如果图像尺寸不符合要求,则调整大小
Args:
tensor: ComfyUI IMAGE tensor
min_size: 最小尺寸
max_size: 最大尺寸
Returns:
(调整后的tensor, 是否调整过)
"""
w, h = get_image_size(tensor)
# 检查是否需要调整
if min_size <= w <= max_size and min_size <= h <= max_size:
return tensor, False
# 计算新尺寸
scale = 1.0
if w < min_size or h < min_size:
scale = max(min_size / w, min_size / h)
elif w > max_size or h > max_size:
scale = min(max_size / w, max_size / h)
new_w = int(w * scale)
new_h = int(h * scale)
# 确保在范围内
new_w = max(min_size, min(max_size, new_w))
new_h = max(min_size, min(max_size, new_h))
# 转换为PIL进行缩放
pil_image = tensor_to_pil(tensor)
pil_image = pil_image.resize((new_w, new_h), Image.Resampling.LANCZOS)
# 转回tensor
return pil_to_tensor(pil_image), True
def concat_images_horizontal(tensors: list) -> torch.Tensor:
"""
水平拼接多张图像(用于显示多个结果)
Args:
tensors: ComfyUI IMAGE tensor列表
Returns:
拼接后的tensor
"""
if len(tensors) == 1:
return tensors[0]
# 转换为PIL
pil_images = [tensor_to_pil(t) for t in tensors]
# 计算总宽度和最大高度
total_width = sum(img.width for img in pil_images)
max_height = max(img.height for img in pil_images)
# 创建新图像
mode = "RGBA" if any(img.mode == "RGBA" for img in pil_images) else "RGB"
combined = Image.new(mode, (total_width, max_height))
# 拼接
x_offset = 0
for img in pil_images:
# 垂直居中
y_offset = (max_height - img.height) // 2
combined.paste(img, (x_offset, y_offset))
x_offset += img.width
return pil_to_tensor(combined)
def batch_tensors(tensors: list) -> torch.Tensor:
"""
将多个单张图像tensor合并为批次tensor
Args:
tensors: ComfyUI IMAGE tensor列表,每个形状为 (1, H, W, C)
Returns:
形状为 (N, H, W, C) 的批次tensor
"""
if len(tensors) == 1:
return tensors[0]
return torch.cat(tensors, dim=0)