diff --git a/README.md b/README.md index b305b8a..2b566bc 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,27 @@ # ImagesGrid: Comfy plugin +## Preview + +### Simple grid of images + ![Image](./workflows/mini.png) -[Workflows](./workflows/mini.json) + +### XYZPlot, like in auto1111, but with more settings + ![Image](./workflows/base.png) -[Workflows](./workflows/base.json) + +Workflows: https://github.com/LEv145/images-grid-comfy-plugin/tree/main/workflows ## How to use -### Install +1. Download the latest stable release: +https://github.com/LEv145/images-grid-comfy-plugin/archive/refs/heads/main.zip -``` -cd custom_nodes # From comfy path -git clone https://github.com/LEv145/images-grid-comfy-plugin ImagesGrid -``` -### Update +2. Unpack the node to `custom_nodes`, for example in a folder `custom_nodes/ImagesGrid/` -``` -cd custom_nodes/ImagesGrid -git pull -``` + +## Source + +https://github.com/LEv145/images-grid-comfy-plugin diff --git a/__init__.py b/__init__.py index d84184d..ef94565 100644 --- a/__init__.py +++ b/__init__.py @@ -1,4 +1,10 @@ -from .src import LatentCombineNode, ImagesGridByColumnsNode, ImagesGridByRowsNode, ImageCombineNode +from .src import ( + LatentCombineNode, + ImagesGridByColumnsNode, + ImagesGridByRowsNode, + ImageCombineNode, + GridAnnotationNode, +) NODE_CLASS_MAPPINGS = { @@ -6,4 +12,5 @@ NODE_CLASS_MAPPINGS = { "ImagesGridByColumns": ImagesGridByColumnsNode, "ImagesGridByRows": ImagesGridByRowsNode, "ImageCombine": ImageCombineNode, + "GridAnnotation": GridAnnotationNode, } diff --git a/src/__init__.py b/src/__init__.py index dbab28c..225673c 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,3 +1,4 @@ from .nodes.images_grid import ImagesGridByColumnsNode, ImagesGridByRowsNode from .nodes.latent_combine import LatentCombineNode from .nodes.image_combine import ImageCombineNode +from .nodes.grid_annotation import GridAnnotationNode diff --git a/src/base.py b/src/base.py index c06dc3f..9eec7d7 100644 --- a/src/base.py +++ b/src/base.py @@ -1,9 +1,10 @@ import typing as t +from pathlib import Path + + +STATIC_PATH = Path(__file__).parent.parent / "static" class BaseNode(): CATEGORY: str = "ImagesGrid" FUNCTION: str = "execute" - - -Image = t.Any diff --git a/src/nodes/grid_annotation.py b/src/nodes/grid_annotation.py new file mode 100644 index 0000000..4b70a17 --- /dev/null +++ b/src/nodes/grid_annotation.py @@ -0,0 +1,49 @@ +import typing as t + +from PIL import ImageFont + +from ..base import BaseNode, STATIC_PATH +from ..utils import Annotation + + +class GridAnnotationNode(BaseNode): + RETURN_TYPES: tuple[str] = ("GRID_ANNOTATION",) + + @classmethod + def INPUT_TYPES(cls) -> dict[str, t.Any]: + return { + "required": { + "column_texts": ("STRING", {"multiline": False}), + "row_texts": ("STRING", {"multiline": False}), + "font_size": ("INT", {"default": 50, "min": 1}), + }, + } + + def execute( + self, + column_texts: str, + row_texts: str, + font_size: int, + ) -> tuple[Annotation]: + font = ImageFont.truetype(str(STATIC_PATH / "Roboto-Regular.ttf"), size=font_size) + column_texts_list = self._set_value_to_texts_list( + self._get_texts_from_string(column_texts), + ) + row_texts_list = self._set_value_to_texts_list( + self._get_texts_from_string(row_texts), + ) + + result = Annotation(column_texts=column_texts_list, row_texts=row_texts_list, font=font) + return (result,) + + def _get_texts_from_string(self, string: str) -> list[str]: + return [ + result + for i in string.split(";") + if (result := i.strip()) != "" + ] + + def _set_value_to_texts_list(self, texts_list: list[str]) -> list[str]: + if not texts_list: + return ["None"] + return texts_list diff --git a/src/nodes/image_combine.py b/src/nodes/image_combine.py index ce7fde0..a4c372f 100644 --- a/src/nodes/image_combine.py +++ b/src/nodes/image_combine.py @@ -2,14 +2,14 @@ import typing as t import torch -from ..base import BaseNode, Image +from ..base import BaseNode class ImageCombineNode(BaseNode): - RETURN_TYPES: t.Tuple[str] = ("IMAGE",) + RETURN_TYPES: tuple[str] = ("IMAGE",) @classmethod - def INPUT_TYPES(cls) -> t.Dict[str, t.Any]: + def INPUT_TYPES(cls) -> dict[str, t.Any]: return { "required": { "image_1": ("IMAGE",), @@ -19,14 +19,9 @@ class ImageCombineNode(BaseNode): def execute( self, - image_1: Image, - image_2: Image, - ) -> t.Tuple[Image]: - print(image_1.size()) - print(image_2.size()) - print(image_1) - + image_1: torch.Tensor, + image_2: torch.Tensor, + ) -> tuple[torch.Tensor]: result = torch.cat((image_1, image_2), 0) - print(result.size()) return (result,) diff --git a/src/nodes/images_grid.py b/src/nodes/images_grid.py index aae0184..8081412 100644 --- a/src/nodes/images_grid.py +++ b/src/nodes/images_grid.py @@ -1,30 +1,48 @@ import typing as t -from ..base import BaseNode, Image +import torch + +from ..base import BaseNode from ..utils import ( tensor_to_pillow, pillow_to_tensor, - create_image_grid_by_columns, - create_image_grid_by_rows, + create_images_grid_by_columns, + create_images_grid_by_rows, + Annotation, ) - class BaseImagesGridNode(BaseNode): - RETURN_TYPES: t.Tuple[str] = ("IMAGE",) + RETURN_TYPES: tuple[str] = ("IMAGE",) @classmethod - def _create_input_types(cls, coordinate_name: str) -> t.Dict[str, t.Any]: + def _create_input_types(cls, coordinate_name: str) -> dict[str, t.Any]: return { "required": { "images": ("IMAGE",), "gap": ("INT", {"default": 0, "min": 0}), coordinate_name: ("INT", {"default": 1, "min": 1}), + }, + "optional": { + "annotation": ("GRID_ANNOTATION",), } } - def _create_execute(self, images, function, function_kw) -> t.Tuple[Image]: + def _create_execute( + self, + function: t.Callable, + \ + images: torch.Tensor, + gap: int, + annotation: Annotation | None = None, + **kw, + ) -> tuple[torch.Tensor]: pillow_images = [tensor_to_pillow(i) for i in images] - pillow_grid = function(images=pillow_images, **function_kw) + pillow_grid = function( + images=pillow_images, + gap=gap, + annotation=annotation, + **kw, + ) tensor_grid = pillow_to_tensor(pillow_grid) return (tensor_grid,) @@ -32,17 +50,17 @@ class BaseImagesGridNode(BaseNode): class ImagesGridByColumnsNode(BaseImagesGridNode): @classmethod - def INPUT_TYPES(cls) -> t.Dict[str, t.Any]: + def INPUT_TYPES(cls) -> dict[str, t.Any]: return cls._create_input_types("max_columns") - def execute(self, images: Image, **kw) -> tuple[Image]: - return self._create_execute(images, create_image_grid_by_columns, kw) + def execute(self, **kw) -> tuple[torch.Tensor]: + return self._create_execute(create_images_grid_by_columns, **kw) class ImagesGridByRowsNode(BaseImagesGridNode): @classmethod - def INPUT_TYPES(cls) -> t.Dict[str, t.Any]: + def INPUT_TYPES(cls) -> dict[str, t.Any]: return cls._create_input_types("max_rows") - def execute(self, images: Image, **kw) -> tuple[Image]: - return self._create_execute(images, create_image_grid_by_rows, kw) + def execute(self, **kw) -> tuple[torch.Tensor]: + return self._create_execute(create_images_grid_by_rows, **kw) diff --git a/src/nodes/latent_combine.py b/src/nodes/latent_combine.py index 96e0259..6aadc40 100644 --- a/src/nodes/latent_combine.py +++ b/src/nodes/latent_combine.py @@ -2,14 +2,14 @@ import typing as t import torch -from ..base import BaseNode, Image +from ..base import BaseNode class LatentCombineNode(BaseNode): - RETURN_TYPES: t.Tuple[str] = ("LATENT",) + RETURN_TYPES: tuple[str] = ("LATENT",) @classmethod - def INPUT_TYPES(cls) -> t.Dict[str, t.Any]: + def INPUT_TYPES(cls) -> dict[str, t.Any]: return { "required": { "latent_1": ("LATENT",), @@ -19,9 +19,9 @@ class LatentCombineNode(BaseNode): def execute( self, - latent_1: t.Dict[str, t.Any], - latent_2: t.Dict[str, t.Any], - ) -> t.Tuple[t.Dict[str, t.Any]]: + latent_1: dict[str, torch.Tensor], + latent_2: dict[str, torch.Tensor], + ) -> tuple[dict[str, torch.Tensor]]: samples = torch.cat((latent_1["samples"], latent_2["samples"]), 0) return ({"samples": samples},) diff --git a/src/utils.py b/src/utils.py deleted file mode 100644 index 9f8e223..0000000 --- a/src/utils.py +++ /dev/null @@ -1,53 +0,0 @@ -import typing as t - -import torch -import numpy as np -from PIL import Image - - -def tensor_to_pillow(image): - return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) - - -def pillow_to_tensor(image): - return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) - - -def create_image_grid_by_columns( - images: t.List[Image.Image], - gap: int, - max_columns: int, -) -> Image.Image: - max_rows = (len(images) + max_columns - 1) // max_columns - return create_image_grid(images=images, gap=gap, max_columns=max_columns, max_rows=max_rows) - - -def create_image_grid_by_rows( - images: t.List[Image.Image], - gap: int, - max_rows: int, -) -> Image.Image: - max_columns = (len(images) + max_rows - 1) // max_rows - return create_image_grid(images=images, gap=gap, max_columns=max_columns, max_rows=max_rows) - - -def create_image_grid( - images: t.List[Image.Image], - gap: int, - max_columns: int, - max_rows: int, -) -> Image.Image: - size = images[0].size - - width = size[0] * max_columns + (max_columns - 1) * gap - height = size[1] * max_rows + (max_rows - 1) * gap - - grid_image = Image.new("RGB", (width, height), color="white") - - for i, image in enumerate(images): - x = (i % max_columns) * (size[0] + gap) - y = (i // max_columns) * (size[1] + gap) - - grid_image.paste(image, (x, y)) - - return grid_image diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..dc39679 --- /dev/null +++ b/src/utils/__init__.py @@ -0,0 +1,6 @@ +from .images_grid import ( + create_images_grid_by_columns, + create_images_grid_by_rows, + Annotation, +) +from .tensor_convert import tensor_to_pillow, pillow_to_tensor diff --git a/src/utils/images_grid.py b/src/utils/images_grid.py new file mode 100644 index 0000000..fa24748 --- /dev/null +++ b/src/utils/images_grid.py @@ -0,0 +1,195 @@ +import typing as t +from dataclasses import dataclass +from contextlib import suppress + +from PIL import Image, ImageDraw, ImageFont + + +@dataclass +class Annotation(): + column_texts: list[str] + row_texts: list[str] + font: ImageFont.FreeTypeFont + + +@dataclass +class _GridInfo(): + image: Image.Image + gap: int + one_image_size: tuple[int, int] + + +def create_images_grid_by_columns( + images: list[Image.Image], + gap: int, + max_columns: int, + annotation: Annotation | None = None, +) -> Image.Image: + max_rows = (len(images) + max_columns - 1) // max_columns + return _create_images_grid(images, gap, max_columns, max_rows, annotation) + + +def create_images_grid_by_rows( + images: list[Image.Image], + gap: int, + max_rows: int, + annotation: Annotation | None = None, +) -> Image.Image: + max_columns = (len(images) + max_rows - 1) // max_rows + return _create_images_grid(images, gap, max_columns, max_rows, annotation) + + +def _create_images_grid( + images: list[Image.Image], + gap: int, + max_columns: int, + max_rows: int, + annotation: Annotation | None, +) -> Image.Image: + size = images[0].size + grid_width = size[0] * max_columns + (max_columns - 1) * gap + grid_height = size[1] * max_rows + (max_rows - 1) * gap + + grid_image = Image.new("RGB", (grid_width, grid_height), color="white") + + _arrange_images_on_grid(grid_image, images=images, size=size, max_columns=max_columns, gap=gap) + + if annotation is None: + return grid_image + return _create_grid_annotations( + grid_info=_GridInfo( + image=grid_image, + gap=gap, + one_image_size=size, + ), + column_texts=annotation.column_texts, + row_texts=annotation.row_texts, + font=annotation.font, + ) + + +def _arrange_images_on_grid( + grid_image: Image.Image, + /, + images: list[Image.Image], + size: tuple[int, int], + max_columns: int, + gap: int, +): + for i, image in enumerate(images): + if image.size != size: + image = image.crop((0, 0, *size)) + x = (i % max_columns) * (size[0] + gap) + y = (i // max_columns) * (size[1] + gap) + + grid_image.paste(image, (x, y)) + + +def _create_grid_annotations( + grid_info: _GridInfo, + column_texts, + row_texts, + font: ImageFont.FreeTypeFont, +) -> Image.Image: + if not column_texts or not row_texts: + raise ValueError("Column text or row text is empty") + + grid = grid_info.image + margin = font.size // 2 + left_padding = int(max(map(font.getlength, row_texts))) + 2*margin + top_padding = font.size + 2*margin + + image = Image.new( + "RGB", + (grid.size[0] + left_padding, grid.size[1] + top_padding), + color="white", + ) + draw = ImageDraw.Draw(image) + draw.font = font # type: ignore + + _paste_image_to_lower_left_corner(image, grid) + _draw_column_text( + draw=draw, + texts=column_texts, + grid_info=grid_info, + left_padding=left_padding, + top_padding=top_padding, + ) + _draw_row_text( + draw=draw, + texts=row_texts, + grid_info=grid_info, + left_padding=left_padding, + top_padding=top_padding, + ) + + return image + + +def _draw_column_text( + draw: ImageDraw.ImageDraw, + texts: list[str], + grid_info: _GridInfo, + left_padding: int, + top_padding: int, +) -> None: + i = 0 + x0 = left_padding + y0 = 0 + x1 = left_padding + grid_info.one_image_size[0] + y1 = top_padding + while x0 != grid_info.image.size[0] + left_padding + grid_info.gap: + i = _draw_text_by_xy((x0, y0, x1, y1), i, draw=draw, texts=texts) + x0 += grid_info.one_image_size[0] + grid_info.gap + x1 += grid_info.one_image_size[0] + grid_info.gap + + +def _draw_row_text( + draw: ImageDraw.ImageDraw, + texts: list[str], + grid_info: _GridInfo, + left_padding: int, + top_padding: int, +) -> None: + i = 0 + x0 = 0 + y0 = top_padding + x1 = left_padding + y1 = top_padding + grid_info.one_image_size[1] + while y0 != grid_info.image.size[1] + top_padding + grid_info.gap: + i = _draw_text_by_xy((x0, y0, x1, y1), i, draw=draw, texts=texts) + y0 += grid_info.one_image_size[1] + grid_info.gap + y1 += grid_info.one_image_size[1] + grid_info.gap + + +def _draw_text_by_xy( + xy: tuple[int, int, int, int], + index: int, + \ + draw: ImageDraw.ImageDraw, + texts: list[str], +) -> int: + with suppress(IndexError): + _draw_center_text(draw, xy, texts[index]) + return index + 1 + + +def _draw_center_text( + draw: ImageDraw.ImageDraw, + xy: tuple[int, int, int, int], + text: str, + fill: t.Any = "black", +) -> None: + _, _, *text_size = draw.textbbox((0, 0), text) + draw.text( + ( + (xy[2] - text_size[0] + xy[0]) / 2, + (xy[3] - text_size[1] + xy[1]) / 2, + ), + text, + fill=fill, + ) + + +def _paste_image_to_lower_left_corner(base: Image.Image, image: Image.Image) -> None: + base.paste(image, (base.size[0] - image.size[0], base.size[1] - image.size[1])) diff --git a/src/utils/tensor_convert.py b/src/utils/tensor_convert.py new file mode 100644 index 0000000..d929713 --- /dev/null +++ b/src/utils/tensor_convert.py @@ -0,0 +1,13 @@ +import typing as t + +import torch +import numpy as np +from PIL import Image + + +def tensor_to_pillow(image: t.Any) -> Image.Image: + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + + +def pillow_to_tensor(image: Image.Image) -> t.Any: + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) diff --git a/static/Roboto-Regular.ttf b/static/Roboto-Regular.ttf new file mode 100644 index 0000000..3033308 Binary files /dev/null and b/static/Roboto-Regular.ttf differ diff --git a/workflows/base.json b/workflows/base.json index 7a26018..9042d9d 100644 --- a/workflows/base.json +++ b/workflows/base.json @@ -1,475 +1,23 @@ { - "last_node_id": 61, - "last_link_id": 102, + "last_node_id": 68, + "last_link_id": 112, "nodes": [ - { - "id": 25, - "type": "CLIPTextEncode", - "pos": [ - -728.3966230000002, - 324.8414170000003 - ], - "size": { - "0": 400, - "1": 200 - }, - "flags": {}, - "order": 3, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 35 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 41, - 48, - 54 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "masterpiece, best quality, 1girl, golden hair," - ] - }, - { - "id": 26, - "type": "CLIPTextEncode", - "pos": [ - -726.3966230000002, - 564.8414170000005 - ], - "size": { - "0": 400, - "1": 200 - }, - "flags": {}, - "order": 4, - "mode": 0, - "inputs": [ - { - "name": "clip", - "type": "CLIP", - "link": 36 - } - ], - "outputs": [ - { - "name": "CONDITIONING", - "type": "CONDITIONING", - "links": [ - 40, - 47, - 55 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "CLIPTextEncode" - }, - "widgets_values": [ - "(worst quality, low quality:1.4), monochrome, zombie" - ] - }, - { - "id": 28, - "type": "Reroute", - "pos": [ - -773.5473798359377, - 114.2723633500978 - ], - "size": [ - 75, - 26 - ], - "flags": {}, - "order": 2, - "mode": 0, - "inputs": [ - { - "name": "", - "type": "*", - "link": 69, - "slot_index": 0 - } - ], - "outputs": [ - { - "name": "", - "type": "MODEL", - "links": [ - 44 - ] - } - ], - "properties": { - "showOutputText": false, - "horizontal": false - } - }, - { - "id": 39, - "type": "Reroute", - "pos": [ - -770.3966230000002, - 78.84141700000006 - ], - "size": [ - 75, - 26 - ], - "flags": {}, - "order": 5, - "mode": 0, - "inputs": [ - { - "name": "", - "type": "*", - "link": 63 - } - ], - "outputs": [ - { - "name": "", - "type": "VAE", - "links": [ - 77 - ], - "slot_index": 0 - } - ], - "properties": { - "showOutputText": false, - "horizontal": false - } - }, - { - "id": 24, - "type": "CheckpointLoaderSimple", - "pos": [ - -1129.3966229999999, - 302.84141700000043 - ], - "size": { - "0": 315, - "1": 98 - }, - "flags": {}, - "order": 0, - "mode": 0, - "outputs": [ - { - "name": "MODEL", - "type": "MODEL", - "links": [ - 69 - ], - "slot_index": 0 - }, - { - "name": "CLIP", - "type": "CLIP", - "links": [ - 35, - 36 - ], - "slot_index": 1 - }, - { - "name": "VAE", - "type": "VAE", - "links": [ - 63 - ], - "slot_index": 2 - } - ], - "properties": { - "Node name for S&R": "CheckpointLoaderSimple" - }, - "widgets_values": [ - "Counterfeit-v2_5.safetensors" - ] - }, - { - "id": 40, - "type": "Reroute", - "pos": [ - 407, - 80 - ], - "size": [ - 75, - 26 - ], - "flags": {}, - "order": 8, - "mode": 0, - "inputs": [ - { - "name": "", - "type": "*", - "link": 77, - "slot_index": 0 - } - ], - "outputs": [ - { - "name": "VAE", - "type": "VAE", - "links": [ - 76 - ], - "slot_index": 0 - } - ], - "properties": { - "showOutputText": true, - "horizontal": false - } - }, - { - "id": 43, - "type": "VAEDecode", - "pos": [ - 528, - 441 - ], - "size": { - "0": 210, - "1": 46 - }, - "flags": {}, - "order": 14, - "mode": 0, - "inputs": [ - { - "name": "samples", - "type": "LATENT", - "link": 88 - }, - { - "name": "vae", - "type": "VAE", - "link": 76 - } - ], - "outputs": [ - { - "name": "IMAGE", - "type": "IMAGE", - "links": [ - 94 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "VAEDecode" - } - }, - { - "id": 38, - "type": "EmptyLatentImage", - "pos": [ - -646, - 808 - ], - "size": [ - 315, - 106 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "batch_size", - "type": "INT", - "link": 100, - "widget": { - "name": "batch_size", - "config": [ - "INT", - { - "default": 1, - "min": 1, - "max": 64 - } - ] - } - } - ], - "outputs": [ - { - "name": "LATENT", - "type": "LATENT", - "links": [ - 60, - 61, - 62 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "EmptyLatentImage" - }, - "widgets_values": [ - 512, - 512, - 4 - ] - }, - { - "id": 31, - "type": "KSampler", - "pos": [ - -213, - 598 - ], - "size": { - "0": 315, - "1": 262 - }, - "flags": { - "collapsed": false - }, - "order": 10, - "mode": 0, - "inputs": [ - { - "name": "model", - "type": "MODEL", - "link": 71 - }, - { - "name": "positive", - "type": "CONDITIONING", - "link": 48 - }, - { - "name": "negative", - "type": "CONDITIONING", - "link": 47 - }, - { - "name": "latent_image", - "type": "LATENT", - "link": 61 - } - ], - "outputs": [ - { - "name": "LATENT", - "type": "LATENT", - "links": [ - 74 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "KSampler" - }, - "widgets_values": [ - 1, - false, - 20, - 10, - "dpmpp_sde", - "karras", - 1 - ] - }, - { - "id": 27, - "type": "KSampler", - "pos": [ - -210, - 293 - ], - "size": { - "0": 315, - "1": 262 - }, - "flags": { - "collapsed": false - }, - "order": 9, - "mode": 0, - "inputs": [ - { - "name": "model", - "type": "MODEL", - "link": 70 - }, - { - "name": "positive", - "type": "CONDITIONING", - "link": 41 - }, - { - "name": "negative", - "type": "CONDITIONING", - "link": 40 - }, - { - "name": "latent_image", - "type": "LATENT", - "link": 62 - } - ], - "outputs": [ - { - "name": "LATENT", - "type": "LATENT", - "links": [ - 73 - ], - "slot_index": 0 - } - ], - "properties": { - "Node name for S&R": "KSampler" - }, - "widgets_values": [ - 1, - false, - 20, - 8, - "dpmpp_sde", - "karras", - 1 - ] - }, { "id": 36, "type": "KSampler", "pos": [ - -211, - 898 + -200, + 900 ], "size": { "0": 315, "1": 262 }, "flags": { - "collapsed": false, + "collapsed": true, "pinned": true }, - "order": 11, + "order": 12, "mode": 0, "inputs": [ { @@ -520,8 +68,8 @@ "id": 29, "type": "Reroute", "pos": [ - -360, - 117 + -380, + 120 ], "size": [ 82, @@ -530,7 +78,7 @@ "flags": { "pinned": true }, - "order": 7, + "order": 8, "mode": 0, "inputs": [ { @@ -558,99 +106,304 @@ } }, { - "id": 56, - "type": "ImagesGridByColumns", + "id": 27, + "type": "KSampler", "pos": [ - 849, - 450 + -200, + 300 ], - "size": [ - 315, - 82 - ], - "flags": {}, - "order": 15, + "size": { + "0": 315, + "1": 262 + }, + "flags": { + "collapsed": true + }, + "order": 10, "mode": 0, "inputs": [ { - "name": "images", - "type": "IMAGE", - "link": 94 + "name": "model", + "type": "MODEL", + "link": 70 }, { - "name": "max_columns", - "type": "INT", - "link": 102, - "widget": { - "name": "max_columns", - "config": [ - "INT", - { - "default": 1, - "min": 1 - } - ] - } + "name": "positive", + "type": "CONDITIONING", + "link": 41 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 40 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 62 } ], "outputs": [ { - "name": "IMAGE", - "type": "IMAGE", + "name": "LATENT", + "type": "LATENT", "links": [ - 95 + 73 ], "slot_index": 0 } ], "properties": { - "Node name for S&R": "ImagesGridByColumns" + "Node name for S&R": "KSampler" }, "widgets_values": [ - 0, - 4 - ], - "color": "#322", - "bgcolor": "#533" + 1, + false, + 20, + 8, + "dpmpp_sde", + "karras", + 1 + ] }, { - "id": 15, - "type": "PreviewImage", + "id": 31, + "type": "KSampler", "pos": [ - 1267, - 450 + -200, + 600 ], "size": { - "0": 601.5753173828125, - "1": 479.24884033203125 + "0": 315, + "1": 262 }, - "flags": {}, - "order": 16, + "flags": { + "collapsed": true + }, + "order": 11, "mode": 0, "inputs": [ { - "name": "images", - "type": "IMAGE", - "link": 95 + "name": "model", + "type": "MODEL", + "link": 71 + }, + { + "name": "positive", + "type": "CONDITIONING", + "link": 48 + }, + { + "name": "negative", + "type": "CONDITIONING", + "link": 47 + }, + { + "name": "latent_image", + "type": "LATENT", + "link": 61 + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 74 + ], + "slot_index": 0 } ], "properties": { - "Node name for S&R": "PreviewImage" + "Node name for S&R": "KSampler" + }, + "widgets_values": [ + 1, + false, + 20, + 10, + "dpmpp_sde", + "karras", + 1 + ] + }, + { + "id": 24, + "type": "CheckpointLoaderSimple", + "pos": [ + -1117, + 311 + ], + "size": { + "0": 315, + "1": 98 + }, + "flags": { + "collapsed": true + }, + "order": 0, + "mode": 0, + "outputs": [ + { + "name": "MODEL", + "type": "MODEL", + "links": [ + 69 + ], + "slot_index": 0 + }, + { + "name": "CLIP", + "type": "CLIP", + "links": [ + 35, + 36 + ], + "slot_index": 1 + }, + { + "name": "VAE", + "type": "VAE", + "links": [ + 63 + ], + "slot_index": 2 + } + ], + "properties": { + "Node name for S&R": "CheckpointLoaderSimple" + }, + "widgets_values": [ + "Counterfeit-v2_5.safetensors" + ] + }, + { + "id": 28, + "type": "Reroute", + "pos": [ + -770, + 120 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 69, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "", + "type": "MODEL", + "links": [ + 44 + ] + } + ], + "properties": { + "showOutputText": false, + "horizontal": false } }, + { + "id": 39, + "type": "Reroute", + "pos": [ + -770, + 90 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 63 + } + ], + "outputs": [ + { + "name": "", + "type": "VAE", + "links": [ + 108 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": false, + "horizontal": false + } + }, + { + "id": 25, + "type": "CLIPTextEncode", + "pos": [ + -700, + 330 + ], + "size": { + "0": 400, + "1": 200 + }, + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 35 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 41, + 48, + 54 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "masterpiece, best quality, 1girl, golden hair," + ] + }, { "id": 42, "type": "LatentCombine", "pos": [ - 266, - 446 + 250, + 430 ], "size": { "0": 210, "1": 46 }, "flags": {}, - "order": 12, + "order": 13, "mode": 0, "inputs": [ { @@ -682,65 +435,19 @@ "color": "#322", "bgcolor": "#533" }, - { - "id": 59, - "type": "PrimitiveNode", - "pos": [ - -1081, - 1086 - ], - "size": { - "0": 210, - "1": 82 - }, - "flags": { - "collapsed": false - }, - "order": 1, - "mode": 0, - "outputs": [ - { - "name": "INT", - "type": "INT", - "links": [ - 100, - 102 - ], - "slot_index": 0, - "widget": { - "name": "max_columns", - "config": [ - "INT", - { - "default": 1, - "min": 1 - } - ] - } - } - ], - "title": "Y items count", - "properties": {}, - "widgets_values": [ - 4, - false - ], - "color": "#232", - "bgcolor": "#353" - }, { "id": 44, "type": "LatentCombine", "pos": [ - 265, - 530 + 250, + 510 ], "size": { "0": 210, "1": 46 }, "flags": {}, - "order": 13, + "order": 14, "mode": 0, "inputs": [ { @@ -769,6 +476,348 @@ }, "color": "#322", "bgcolor": "#533" + }, + { + "id": 15, + "type": "PreviewImage", + "pos": [ + 1250, + 410 + ], + "size": { + "0": 601.5753173828125, + "1": 479.24884033203125 + }, + "flags": {}, + "order": 17, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 111 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } + }, + { + "id": 38, + "type": "EmptyLatentImage", + "pos": [ + -620, + 810 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "batch_size", + "type": "INT", + "link": 107, + "widget": { + "name": "batch_size", + "config": [ + "INT", + { + "default": 1, + "min": 1, + "max": 64 + } + ] + } + } + ], + "outputs": [ + { + "name": "LATENT", + "type": "LATENT", + "links": [ + 60, + 61, + 62 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "EmptyLatentImage" + }, + "widgets_values": [ + 512, + 512, + 4 + ] + }, + { + "id": 40, + "type": "Reroute", + "pos": [ + 380, + 90 + ], + "size": [ + 75, + 26 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "", + "type": "*", + "link": 108, + "slot_index": 0 + } + ], + "outputs": [ + { + "name": "VAE", + "type": "VAE", + "links": [ + 76 + ], + "slot_index": 0 + } + ], + "properties": { + "showOutputText": true, + "horizontal": false + } + }, + { + "id": 43, + "type": "VAEDecode", + "pos": [ + 530, + 510 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": { + "collapsed": true + }, + "order": 15, + "mode": 0, + "inputs": [ + { + "name": "samples", + "type": "LATENT", + "link": 88 + }, + { + "name": "vae", + "type": "VAE", + "link": 76 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 109 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VAEDecode" + } + }, + { + "id": 59, + "type": "PrimitiveNode", + "pos": [ + -1010, + 460 + ], + "size": { + "0": 210, + "1": 82 + }, + "flags": { + "collapsed": false + }, + "order": 1, + "mode": 0, + "outputs": [ + { + "name": "INT", + "type": "INT", + "links": [ + 107, + 110 + ], + "slot_index": 0, + "widget": { + "name": "max_columns", + "config": [ + "INT", + { + "default": 1, + "min": 1 + } + ] + } + } + ], + "title": "Batch size", + "properties": {}, + "widgets_values": [ + 4, + false + ], + "color": "#232", + "bgcolor": "#353" + }, + { + "id": 26, + "type": "CLIPTextEncode", + "pos": [ + -700, + 570 + ], + "size": { + "0": 400, + "1": 200 + }, + "flags": { + "collapsed": true + }, + "order": 5, + "mode": 0, + "inputs": [ + { + "name": "clip", + "type": "CLIP", + "link": 36 + } + ], + "outputs": [ + { + "name": "CONDITIONING", + "type": "CONDITIONING", + "links": [ + 40, + 47, + 55 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "CLIPTextEncode" + }, + "widgets_values": [ + "(worst quality, low quality:1.4), monochrome, zombie" + ] + }, + { + "id": 68, + "type": "GridAnnotation", + "pos": [ + 430, + 640 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 2, + "mode": 0, + "outputs": [ + { + "name": "GRID_ANNOTATION", + "type": "GRID_ANNOTATION", + "links": [ + 112 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GridAnnotation" + }, + "widgets_values": [ + "1; 2; 3; 4", + "CFG: 8; CFG: 10; CFG: 12", + 100 + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 67, + "type": "ImagesGridByColumns", + "pos": [ + 820, + 540 + ], + "size": [ + 320, + 100 + ], + "flags": {}, + "order": 16, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 109, + "slot_index": 0 + }, + { + "name": "annotation", + "type": "GRID_ANNOTATION", + "link": 112 + }, + { + "name": "max_columns", + "type": "INT", + "link": 110, + "widget": { + "name": "max_columns", + "config": [ + "INT", + { + "default": 1, + "min": 1 + } + ] + } + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 111 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImagesGridByColumns" + }, + "widgets_values": [ + 0, + 4 + ], + "color": "#322", + "bgcolor": "#533" } ], "links": [ @@ -932,14 +981,6 @@ 1, "VAE" ], - [ - 77, - 39, - 0, - 40, - 0, - "*" - ], [ 79, 36, @@ -965,23 +1006,7 @@ "LATENT" ], [ - 94, - 43, - 0, - 56, - 0, - "IMAGE" - ], - [ - 95, - 56, - 0, - 15, - 0, - "IMAGE" - ], - [ - 100, + 107, 59, 0, 38, @@ -989,12 +1014,44 @@ "INT" ], [ - 102, + 108, + 39, + 0, + 40, + 0, + "*" + ], + [ + 109, + 43, + 0, + 67, + 0, + "IMAGE" + ], + [ + 110, 59, 0, - 56, - 1, + 67, + 2, "INT" + ], + [ + 111, + 67, + 0, + 15, + 0, + "IMAGE" + ], + [ + 112, + 68, + 0, + 67, + 1, + "GRID_ANNOTATION" ] ], "groups": [], diff --git a/workflows/base.png b/workflows/base.png index 7f43905..d045ff1 100644 Binary files a/workflows/base.png and b/workflows/base.png differ diff --git a/workflows/mini.json b/workflows/mini.json index ff752a9..6556a38 100644 --- a/workflows/mini.json +++ b/workflows/mini.json @@ -1,19 +1,21 @@ { - "last_node_id": 56, - "last_link_id": 97, + "last_node_id": 70, + "last_link_id": 121, "nodes": [ { - "id": 41, + "id": 68, "type": "LoadImage", "pos": [ - 69, - 307 + -30, + 70 ], "size": { "0": 315, "1": 102 }, - "flags": {}, + "flags": { + "collapsed": true + }, "order": 0, "mode": 0, "outputs": [ @@ -21,7 +23,7 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 90 + 119 ], "slot_index": 0 }, @@ -35,22 +37,24 @@ "Node name for S&R": "LoadImage" }, "widgets_values": [ - "ComfyUI_00171_.png", + "394102.png", "image" ] }, { - "id": 42, + "id": 41, "type": "LoadImage", "pos": [ - 69, - 449 + -30, + 210 ], "size": { "0": 315, "1": 102 }, - "flags": {}, + "flags": { + "collapsed": true + }, "order": 1, "mode": 0, "outputs": [ @@ -58,7 +62,7 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 91 + 120 ], "slot_index": 0 }, @@ -72,22 +76,24 @@ "Node name for S&R": "LoadImage" }, "widgets_values": [ - "ComfyUI_00153_.png", + "394102.png", "image" ] }, { - "id": 43, + "id": 42, "type": "LoadImage", "pos": [ - 68, - 592 + -30, + 350 ], "size": { "0": 315, "1": 102 }, - "flags": {}, + "flags": { + "collapsed": true + }, "order": 2, "mode": 0, "outputs": [ @@ -95,7 +101,7 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 94 + 112 ], "slot_index": 0 }, @@ -109,12 +115,171 @@ "Node name for S&R": "LoadImage" }, "widgets_values": [ - "ComfyUI_00116_.png", + "394102.png", "image" ] }, { - "id": 54, + "id": 43, + "type": "LoadImage", + "pos": [ + -30, + 490 + ], + "size": { + "0": 315, + "1": 102 + }, + "flags": { + "collapsed": true + }, + "order": 3, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 106 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "394102.png", + "image" + ] + }, + { + "id": 62, + "type": "LoadImage", + "pos": [ + -30, + 630 + ], + "size": { + "0": 315, + "1": 102 + }, + "flags": { + "collapsed": true + }, + "order": 4, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 105 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "394102.png", + "image" + ] + }, + { + "id": 63, + "type": "LoadImage", + "pos": [ + -30, + 770 + ], + "size": { + "0": 315, + "1": 102 + }, + "flags": { + "collapsed": true + }, + "order": 5, + "mode": 0, + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 104 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + "394102.png", + "image" + ] + }, + { + "id": 65, + "type": "ImageCombine", + "pos": [ + 460, + 430 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "image_1", + "type": "IMAGE", + "link": 113 + }, + { + "name": "image_2", + "type": "IMAGE", + "link": 106 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 114 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageCombine" + }, + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 67, "type": "ImageCombine", "pos": [ 460, @@ -125,18 +290,18 @@ "1": 46 }, "flags": {}, - "order": 3, + "order": 8, "mode": 0, "inputs": [ { "name": "image_1", "type": "IMAGE", - "link": 90 + "link": 118 }, { "name": "image_2", "type": "IMAGE", - "link": 91 + "link": 112 } ], "outputs": [ @@ -144,7 +309,49 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 93 + 113 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImageCombine" + }, + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 69, + "type": "ImageCombine", + "pos": [ + 460, + 270 + ], + "size": { + "0": 210, + "1": 46 + }, + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "image_1", + "type": "IMAGE", + "link": 119 + }, + { + "name": "image_2", + "type": "IMAGE", + "link": 120 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 118 ], "slot_index": 0 } @@ -160,25 +367,25 @@ "type": "ImageCombine", "pos": [ 460, - 440 + 510 ], "size": { "0": 210, "1": 46 }, "flags": {}, - "order": 4, + "order": 10, "mode": 0, "inputs": [ { "name": "image_1", "type": "IMAGE", - "link": 93 + "link": 114 }, { "name": "image_2", "type": "IMAGE", - "link": 94 + "link": 105 } ], "outputs": [ @@ -186,7 +393,7 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 95 + 110 ], "slot_index": 0 } @@ -198,50 +405,29 @@ "bgcolor": "#533" }, { - "id": 56, - "type": "PreviewImage", + "id": 54, + "type": "ImageCombine", "pos": [ - 1059, - 350 - ], - "size": [ - 428.921914672851, - 507.1954368591306 - ], - "flags": {}, - "order": 6, - "mode": 0, - "inputs": [ - { - "name": "images", - "type": "IMAGE", - "link": 97 - } - ], - "properties": { - "Node name for S&R": "PreviewImage" - } - }, - { - "id": 53, - "type": "ImagesGridByColumns", - "pos": [ - 700, - 350 + 460, + 590 ], "size": { - "0": 315, - "1": 82 + "0": 210, + "1": 46 }, "flags": {}, - "order": 5, + "order": 11, "mode": 0, "inputs": [ { - "name": "images", + "name": "image_1", "type": "IMAGE", - "link": 95, - "slot_index": 0 + "link": 110 + }, + { + "name": "image_2", + "type": "IMAGE", + "link": 104 } ], "outputs": [ @@ -249,68 +435,226 @@ "name": "IMAGE", "type": "IMAGE", "links": [ - 97 + 115 ], "slot_index": 0 } ], "properties": { - "Node name for S&R": "ImagesGridByColumns" + "Node name for S&R": "ImageCombine" + }, + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 60, + "type": "GridAnnotation", + "pos": [ + 350, + 690 + ], + "size": { + "0": 315, + "1": 106 + }, + "flags": {}, + "order": 6, + "mode": 0, + "outputs": [ + { + "name": "GRID_ANNOTATION", + "type": "GRID_ANNOTATION", + "links": [ + 102 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "GridAnnotation" }, "widgets_values": [ - 5, - 2 + "Column 1; Column 2", + "My favorite; Others;Meow", + 400 ], "color": "#322", "bgcolor": "#533" + }, + { + "id": 61, + "type": "ImagesGridByRows", + "pos": [ + 740, + 380 + ], + "size": { + "0": 315, + "1": 102 + }, + "flags": {}, + "order": 12, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 115 + }, + { + "name": "annotation", + "type": "GRID_ANNOTATION", + "link": 102 + } + ], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 121 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "ImagesGridByRows" + }, + "widgets_values": [ + 30, + 3 + ], + "color": "#322", + "bgcolor": "#533" + }, + { + "id": 70, + "type": "PreviewImage", + "pos": [ + 1080, + 380 + ], + "size": { + "0": 353, + "1": 510 + }, + "flags": {}, + "order": 13, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 121 + } + ], + "properties": { + "Node name for S&R": "PreviewImage" + } } ], "links": [ [ - 90, - 41, + 102, + 60, 0, - 54, - 0, - "IMAGE" + 61, + 1, + "GRID_ANNOTATION" ], [ - 91, - 42, + 104, + 63, 0, 54, 1, "IMAGE" ], [ - 93, - 54, + 105, + 62, 0, 55, - 0, + 1, "IMAGE" ], [ - 94, + 106, 43, 0, - 55, + 65, 1, "IMAGE" ], [ - 95, + 110, 55, 0, - 53, + 54, 0, "IMAGE" ], [ - 97, - 53, + 112, + 42, 0, - 56, + 67, + 1, + "IMAGE" + ], + [ + 113, + 67, + 0, + 65, + 0, + "IMAGE" + ], + [ + 114, + 65, + 0, + 55, + 0, + "IMAGE" + ], + [ + 115, + 54, + 0, + 61, + 0, + "IMAGE" + ], + [ + 118, + 69, + 0, + 67, + 0, + "IMAGE" + ], + [ + 119, + 68, + 0, + 69, + 0, + "IMAGE" + ], + [ + 120, + 41, + 0, + 69, + 1, + "IMAGE" + ], + [ + 121, + 61, + 0, + 70, 0, "IMAGE" ] diff --git a/workflows/mini.png b/workflows/mini.png index 02355b6..f7e2988 100644 Binary files a/workflows/mini.png and b/workflows/mini.png differ