15 KiB
15 KiB
Documentation
- Class name:
XYImage - Category:
List Stuff - Output node:
True - Repo Ref: https://github.com/comfyanonymous/ComfyUI
XYImage节点设计用于复杂的图像生成和操作任务,包括图像分割、翻转和批量堆叠。它支持在不同轴上添加标签,并能够在多维(启用z轴)环境中处理图像,使其适用于各种图像处理和可视化需求。
Input types
Required
- images
- 要处理的图像列表。这个参数至关重要,因为它是节点将通过分割、翻转和堆叠操作进行处理的主要数据。
- Comfy dtype: IMAGE
- Python dtype: List[torch.Tensor]
- splits
- 定义如何分割图像。这个参数直接影响输出图像的结构和布局,影响其分段和组织方式。
- Comfy dtype: INT
- Python dtype: List[int]
- flip_axis
- 指定应沿哪些轴翻转图像。这个参数允许调整图像方向,增强图像展示和分析的灵活性。
- Comfy dtype: COMBO[STRING]
- Python dtype: List[str]
- batch_stack_mode
- 决定如何将图像批量堆叠在一起。这个参数对定义组合图像输出的整体结构和布局至关重要。
- Comfy dtype: COMBO[STRING]
- Python dtype: List[str]
- z_enabled
- 指示是否启用z轴进行图像处理。这个参数启用多维图像处理,允许进行更复杂的图像操作和可视化。
- Comfy dtype: COMBO[STRING]
- Python dtype: List[str]
Optional
- x_main_label
- x轴的主要标签。这个可选参数允许为x轴添加描述性标签,增强图像的可解释性。
- Comfy dtype: STRING
- Python dtype: Optional[List[str]]
- y_main_label
- y轴的主要标签。与x_main_label类似,这通过为y轴添加描述性标签来增强图像的可解释性。
- Comfy dtype: STRING
- Python dtype: Optional[List[str]]
- z_main_label
- z轴的主要标签,在z_enabled为True时使用。这通过为z轴添加描述性标签来增强多维图像的可解释性。
- Comfy dtype: STRING
- Python dtype: Optional[List[str]]
- x_labels
- x轴上每个段的标签。这个参数为图像的各个部分添加详细描述,有助于其分析和理解。
- Comfy dtype: *
- Python dtype: Optional[List[str]]
- y_labels
- y轴上每个段的标签,类似于x_labels,为图像的各个部分提供详细描述。
- Comfy dtype: *
- Python dtype: Optional[List[str]]
- z_labels
- z轴上每个段的标签,在z_enabled为True时使用。这为多维图像的各个部分添加详细描述。
- Comfy dtype: *
- Python dtype: Optional[List[str]]
Output types
- Image
- 应用分割、翻转和堆叠等操作后的处理图像。这个输出很重要,因为它代表了节点图像操作能力的集大成者,可以进行进一步使用或分析。
- Comfy dtype: IMAGE
- Python dtype: List[torch.Tensor]
Usage tips
- Infra type:
GPU - Common nodes: unknown
Source code
class XYImage:
def __init__(self) -> None:
pass
@classmethod
def INPUT_TYPES(s) -> Dict[str, Dict[str, Any]]:
return {
"required": {
"images": ("IMAGE",),
"splits": ("INT", {"forceInput": True, "min": 1}),
"flip_axis": (["False", "True"], {"default": "False"}),
"batch_stack_mode": (["horizontal", "vertical"], {"default": "horizontal"}),
"z_enabled": (["False", "True"], {"default": "False"}),
},
"optional": {
"x_main_label": ("STRING", {}),
"y_main_label": ("STRING", {}),
"z_main_label": ("STRING", {}),
"x_labels": (ANY,{}),
"y_labels": (ANY,{}),
"z_labels": (ANY,{}),
}
}
RELOAD_INST = True
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("Image",)
INPUT_IS_LIST = (True,)
OUTPUT_IS_LIST = (True,)
OUTPUT_NODE = True
FUNCTION = "xy_image"
CATEGORY = "List Stuff"
MAIN_LABEL_SIZE = 60
LABEL_SIZE = 60
Z_LABEL_SIZE = 60
LABEL_COLOR = "#000"
def xy_image(
self,
images: List[Tensor],
splits: List[int],
flip_axis: List[str],
batch_stack_mode: List[str],
z_enabled: List[str],
x_main_label: Optional[List[str]] = None,
y_main_label: Optional[List[str]] = None,
z_main_label: Optional[List[str]] = None,
x_labels: Optional[List[str]] = None,
y_labels: Optional[List[str]] = None,
z_labels: Optional[List[str]] = None,
) -> Tuple[List[Tensor]]:
if len(flip_axis) != 1:
raise Exception("Only single flip_axis value supported.")
if len(batch_stack_mode) != 1:
raise Exception("Only single batch stack mode supported.")
if len(z_enabled) != 1:
raise Exception("Only single z_enabled value supported.")
if x_main_label is not None and len(x_main_label) != 1:
raise Exception("Only single x_main_label value supported.")
if y_main_label is not None and len(y_main_label) != 1:
raise Exception("Only single y_main_label value supported.")
if z_main_label is not None and len(z_main_label) != 1:
raise Exception("Only single z_main_label value supported.")
if x_main_label is not None and not isinstance(x_main_label[0], str):
try:
x_main_label[0] = str(x_main_label[0])
except:
raise Exception("x_main_label must be a string or convertible to a string.")
if y_main_label is not None and not isinstance(y_main_label[0], str):
try:
y_main_label[0] = str(y_main_label[0])
except:
raise Exception("y_main_label must be a string or convertible to a string.")
if z_main_label is not None and not isinstance(z_main_label[0], str):
try:
z_main_label[0] = str(z_main_label[0])
except:
raise Exception("z_main_label must be a string or convertible to a string.")
if x_main_label is not None and x_main_label[0] == '':
x_main_label = None
if y_main_label is not None and y_main_label[0] == '':
y_main_label = None
if z_main_label is not None and z_main_label[0] == '':
z_main_label = None
stack_direction = "horizontal"
if flip_axis[0] == "True":
stack_direction = "vertical"
x_labels, y_labels = y_labels, x_labels
x_main_label, y_main_label = y_main_label, x_main_label
batch_stack_direction = batch_stack_mode[0]
if len(splits) == 1:
splits = splits * (int(len(images) / splits[0]))
if sum(splits) != len(images):
splits.append(len(images) - sum(splits))
else:
if sum(splits) != len(images):
raise Exception("Sum of splits must equal number of images.")
batches = images
batch_size = len(batches[0])
# TODO: Some better way...
# Currently chops splits to match x_labels/y_labels and then loops over the split set over and over
num_z = 1
splits_per_z = len(splits)
images_per_z = len(images)
if z_enabled[0] == "True":
if y_labels is None or x_labels is None:
raise Exception("Must provide x_labels and y_labels when z_enabled is True.")
if stack_direction == "horizontal":
splits_per_z = len(x_labels)
else:
splits_per_z = len(y_labels)
num_z = int(len(splits) / splits_per_z)
splits = splits[:splits_per_z]
images_per_z = sum(splits)
image_h, image_w, _ = batches[0][0].size()
if batch_stack_direction == "horizontal":
batch_h = image_h
# stack horizontally
batch_w = image_w * batch_size
else:
# stack vertically
batch_h = image_h * batch_size
batch_w = image_w
if stack_direction == "horizontal":
full_w = batch_w * len(splits)
full_h = batch_h * max(splits)
else:
full_w = batch_w * max(splits)
full_h = batch_h * len(splits)
grid_w = full_w
_ = full_h
y_label_offset = 0
has_horizontal_labels = False
if x_labels is not None:
x_labels = [str(lbl) for lbl in x_labels]
if stack_direction == "horizontal":
if len(x_labels) != len(splits):
raise Exception("Number of horizontal labels must match number of splits.")
else:
if len(x_labels) != max(splits):
raise Exception("Number of horizontal labels must match maximum split size.")
full_h += self.LABEL_SIZE
y_label_offset = self.LABEL_SIZE
has_horizontal_labels = True
x_label_offset = 0
has_vertical_labels = False
if y_labels is not None:
y_labels = [str(lbl) for lbl in y_labels]
if stack_direction == "horizontal":
if len(y_labels) != max(splits):
raise Exception(f"Number of vertical labels must match maximum split size. Got {len(y_labels)} labels for {max(splits)} splits.")
else:
if len(y_labels) != len(splits):
raise Exception(f"Number of vertical labels must match number of splits. Got {len(y_labels)} labels for {len(splits)} splits.")
full_w += self.LABEL_SIZE
x_label_offset = self.LABEL_SIZE
has_vertical_labels = True
has_z_labels = False
if z_labels is not None:
has_z_labels = True
z_labels = [str(lbl) for lbl in z_labels]
if z_main_label is not None:
z_labels = [f"{z_main_label[0]}: {lbl}" for lbl in z_labels]
full_h += self.Z_LABEL_SIZE
y_label_offset += self.Z_LABEL_SIZE
if len(z_labels) != num_z:
raise Exception(f"Number of z_labels must match number of z splits. Got {len(z_labels)} labels for {num_z} splits.")
has_main_x_label = False
if x_main_label is not None:
full_h += self.MAIN_LABEL_SIZE
y_label_offset += self.MAIN_LABEL_SIZE
has_main_x_label = True
has_main_y_label = False
if y_main_label is not None:
full_w += self.MAIN_LABEL_SIZE
x_label_offset += self.MAIN_LABEL_SIZE
has_main_y_label = True
images = []
for z_idx in range(num_z):
full_image = Image.new("RGB", (full_w, full_h))
full_draw = ImageDraw.Draw(full_image)
full_draw.rectangle((0, 0, full_w, full_h), fill="#ffffff")
batch_idx = 0
active_y_offset = 0
active_x_offset = 0
if has_z_labels:
font = ImageFont.truetype(fm.findfont(fm.FontProperties()), self.Z_LABEL_SIZE)
full_draw.rectangle((0, 0, full_w, self.Z_LABEL_SIZE), fill="#ffffff")
full_draw.text((grid_w//2 + x_label_offset, 0), z_labels[z_idx], anchor='ma', fill=self.LABEL_COLOR, font=font)
active_y_offset += self.Z_LABEL_SIZE
if has_main_x_label:
assert x_main_label is not None
font = ImageFont.truetype(fm.findfont(fm.FontProperties()), self.MAIN_LABEL_SIZE)
full_draw.rectangle((0, active_y_offset, full_w, self.MAIN_LABEL_SIZE + active_y_offset), fill="#ffffff")
full_draw.text((grid_w//2 + x_label_offset, 0 + active_y_offset), x_main_label[0], anchor='ma', fill=self.LABEL_COLOR, font=font)
active_y_offset += self.MAIN_LABEL_SIZE
if has_horizontal_labels:
assert x_labels is not None
font = ImageFont.truetype(fm.findfont(fm.FontProperties()), self.LABEL_SIZE)
for label_idx, label in enumerate(x_labels):
x_offset = (batch_w * label_idx) + x_label_offset
full_draw.rectangle((x_offset, 0 + active_y_offset, x_offset + batch_w, self.LABEL_SIZE + active_y_offset), fill="#ffffff")
full_draw.text((x_offset + (batch_w / 2), 0 + active_y_offset), label, anchor='ma', fill=self.LABEL_COLOR, font=font)
if has_main_y_label:
assert y_main_label is not None
font = ImageFont.truetype(fm.findfont(fm.FontProperties()), self.MAIN_LABEL_SIZE)
img_txt = Image.new('RGB', (full_h - active_y_offset, self.MAIN_LABEL_SIZE))
draw_txt = ImageDraw.Draw(img_txt)
draw_txt.rectangle((0, 0, full_h - active_y_offset, self.MAIN_LABEL_SIZE), fill="#ffffff")
draw_txt.text(((full_h - active_y_offset)//2, 0), y_main_label[0], anchor='ma', fill=self.LABEL_COLOR, font=font)
img_txt = img_txt.rotate(90, expand=True)
full_image.paste(img_txt, (active_x_offset, active_y_offset))
active_x_offset += self.MAIN_LABEL_SIZE
if has_vertical_labels:
assert y_labels is not None
font = ImageFont.truetype(fm.findfont(fm.FontProperties()), self.LABEL_SIZE)
for label_idx, label in enumerate(y_labels):
y_offset = (batch_h * label_idx) + y_label_offset
img_txt = Image.new('RGB', (batch_h, self.LABEL_SIZE))
draw_txt = ImageDraw.Draw(img_txt)
draw_txt.rectangle((0, 0, batch_h, self.LABEL_SIZE), fill="#ffffff")
draw_txt.text((batch_h//2, 0), label, anchor='ma', fill=self.LABEL_COLOR, font=font)
img_txt = img_txt.rotate(90, expand=True)
full_image.paste(img_txt, (active_x_offset, y_offset))
for split_idx, split in enumerate(splits):
for idx_in_split in range(split):
batch_img = Image.new("RGB", (batch_w, batch_h))
batch = batches[batch_idx + idx_in_split + images_per_z * z_idx]
if batch_stack_direction == "horizontal":
for img_idx, img in enumerate(batch):
x_offset = image_w * img_idx
batch_img.paste(tensor2pil(img), (x_offset, 0))
else:
for img_idx, img in enumerate(batch):
y_offset = image_h * img_idx
batch_img.paste(tensor2pil(img), (0, y_offset))
if stack_direction == "horizontal":
x_offset = batch_w * split_idx + x_label_offset
y_offset = batch_h * idx_in_split + y_label_offset
else:
x_offset = batch_w * idx_in_split + x_label_offset
y_offset = batch_h * split_idx + y_label_offset
full_image.paste(batch_img, (x_offset, y_offset))
batch_idx += split
images.append(pil2tensor(full_image))
return (images,)