351 lines
15 KiB
Markdown
351 lines
15 KiB
Markdown
|
|
# 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
|
|
```python
|
|
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,)
|
|
|
|
```
|