From 9d1c610b8975b13ff3b311d5db7b06a583f14cd9 Mon Sep 17 00:00:00 2001 From: flyingshutter Date: Thu, 6 Apr 2023 19:02:28 +0200 Subject: [PATCH 01/27] make LoadImagesMask work with non RGBA images --- nodes.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nodes.py b/nodes.py index 187d54a11..d33760dfc 100644 --- a/nodes.py +++ b/nodes.py @@ -915,6 +915,8 @@ class LoadImageMask: input_dir = folder_paths.get_input_directory() image_path = os.path.join(input_dir, image) i = Image.open(image_path) + if i.getbands() != ("R", "G", "B", "A"): + i = i.convert("RGBA") mask = None c = channel[0].upper() if c in i.getbands(): From 022a9f271b677291c4a4988397695bd3a91666b5 Mon Sep 17 00:00:00 2001 From: mligaintart <> Date: Wed, 5 Apr 2023 19:52:39 -0400 Subject: [PATCH 02/27] Adds masking to Latent Composite, and provides new masking utilities to allow better compositing. --- comfy_extras/nodes_mask.py | 237 +++++++++++++++++++++++++++++++++++++ nodes.py | 87 ++++++++------ 2 files changed, 291 insertions(+), 33 deletions(-) create mode 100644 comfy_extras/nodes_mask.py diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py new file mode 100644 index 000000000..ba39680a7 --- /dev/null +++ b/comfy_extras/nodes_mask.py @@ -0,0 +1,237 @@ +import torch + +from nodes import MAX_RESOLUTION + +class LatentCompositeMasked: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "destination": ("LATENT",), + "source": ("LATENT",), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + }, + "optional": { + "mask": ("MASK",), + } + } + RETURN_TYPES = ("LATENT",) + FUNCTION = "composite" + + CATEGORY = "latent" + + def composite(self, destination, source, x, y, mask = None): + output = destination.copy() + destination = destination["samples"].clone() + source = source["samples"] + + left, top = (x // 8, y // 8) + right, bottom = (left + source.shape[3], top + source.shape[2],) + + + if mask is None: + mask = torch.ones_like(source) + else: + mask = mask.clone() + mask = torch.nn.functional.interpolate(mask[None, None], size=(source.shape[2], source.shape[3]), mode="bilinear") + mask = mask.repeat((source.shape[0], source.shape[1], 1, 1)) + + # calculate the bounds of the source that will be overlapping the destination + # this prevents the source trying to overwrite latent pixels that are out of bounds + # of the destination + visible_width, visible_height = (destination.shape[3] - left, destination.shape[2] - top,) + + mask = mask[:, :, :visible_height, :visible_width] + inverse_mask = torch.ones_like(mask) - mask + + source_portion = mask * source[:, :, :visible_height, :visible_width] + destination_portion = inverse_mask * destination[:, :, top:bottom, left:right] + + destination[:, :, top:bottom, left:right] = source_portion + destination_portion + + output["samples"] = destination + + return (output,) + +class MaskToImage: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("IMAGE",) + + FUNCTION = "convert" + + def convert(self, mask): + image = torch.cat([torch.reshape(mask.clone(), [1, mask.shape[0], mask.shape[1], 1,])] * 3, 3) + + return (image,) + +class SolidMask: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "value": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), + "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + + FUNCTION = "solid" + + def solid(self, value, width, height): + out = torch.full((height, width), value, dtype=torch.float32, device="cpu") + return (out,) + +class InvertMask: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + + FUNCTION = "invert" + + def invert(self, mask): + out = 1.0 - mask + return (out,) + +class CropMask: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "width": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + "height": ("INT", {"default": 512, "min": 1, "max": MAX_RESOLUTION, "step": 1}), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + + FUNCTION = "crop" + + def crop(self, mask, x, y, width, height): + out = mask[y:y + height, x:x + width] + return (out,) + +class MaskComposite: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "destination": ("MASK",), + "source": ("MASK",), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "operation": (["multiply", "add", "subtract"],), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + + FUNCTION = "combine" + + def combine(self, destination, source, x, y, operation): + output = destination.clone() + + left, top = (x, y,) + right, bottom = (min(left + source.shape[1], destination.shape[1]), min(top + source.shape[0], destination.shape[0])) + visible_width, visible_height = (right - left, bottom - top,) + + source_portion = source[:visible_height, :visible_width] + destination_portion = destination[top:bottom, left:right] + + match operation: + case "multiply": + output[top:bottom, left:right] = destination_portion * source_portion + case "add": + output[top:bottom, left:right] = destination_portion + source_portion + case "subtract": + output[top:bottom, left:right] = destination_portion - source_portion + + output = torch.clamp(output, 0.0, 1.0) + + return (output,) + +class FeatherMask: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mask": ("MASK",), + "left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + "bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 1}), + } + } + + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + + FUNCTION = "feather" + + def feather(self, mask, left, top, right, bottom): + output = mask.clone() + + left = min(left, output.shape[1]) + right = min(right, output.shape[1]) + top = min(top, output.shape[0]) + bottom = min(bottom, output.shape[0]) + + for x in range(left): + feather_rate = (x + 1.0) / left + output[:, x] *= feather_rate + + for x in range(right): + feather_rate = (x + 1) / right + output[:, -x] *= feather_rate + + for y in range(top): + feather_rate = (y + 1) / top + output[y, :] *= feather_rate + + for y in range(bottom): + feather_rate = (y + 1) / bottom + output[-y, :] *= feather_rate + + return (output,) + + + +NODE_CLASS_MAPPINGS = { + "LatentCompositeMasked": LatentCompositeMasked, + "MaskToImage": MaskToImage, + "SolidMask": SolidMask, + "InvertMask": InvertMask, + "CropMask": CropMask, + "MaskComposite": MaskComposite, + "FeatherMask": FeatherMask, +} + diff --git a/nodes.py b/nodes.py index 187d54a11..eac232d5f 100644 --- a/nodes.py +++ b/nodes.py @@ -553,44 +553,64 @@ class LatentFlip: class LatentComposite: @classmethod def INPUT_TYPES(s): - return {"required": { "samples_to": ("LATENT",), - "samples_from": ("LATENT",), - "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - }} + return { + "required": { + "samples_to": ("LATENT",), + "samples_from": ("LATENT",), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + } + } RETURN_TYPES = ("LATENT",) FUNCTION = "composite" CATEGORY = "latent" - def composite(self, samples_to, samples_from, x, y, composite_method="normal", feather=0): - x = x // 8 - y = y // 8 - feather = feather // 8 - samples_out = samples_to.copy() - s = samples_to["samples"].clone() - samples_to = samples_to["samples"] - samples_from = samples_from["samples"] - if feather == 0: - s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] - else: - samples_from = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] - mask = torch.ones_like(samples_from) - for t in range(feather): - if y != 0: - mask[:,:,t:1+t,:] *= ((1.0/feather) * (t + 1)) + def composite(self, samples_to, samples_from, x, y, feather): + output = samples_to.copy() + destination = samples_to["samples"].clone() + source = samples_from["samples"] - if y + samples_from.shape[2] < samples_to.shape[2]: - mask[:,:,mask.shape[2] -1 -t: mask.shape[2]-t,:] *= ((1.0/feather) * (t + 1)) - if x != 0: - mask[:,:,:,t:1+t] *= ((1.0/feather) * (t + 1)) - if x + samples_from.shape[3] < samples_to.shape[3]: - mask[:,:,:,mask.shape[3]- 1 - t: mask.shape[3]- t] *= ((1.0/feather) * (t + 1)) - rev_mask = torch.ones_like(mask) - mask - s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] * mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] * rev_mask - samples_out["samples"] = s - return (samples_out,) + left, top = (x // 8, y // 8) + right, bottom = (left + source.shape[3], top + source.shape[2],) + feather = feather // 8 + + + + # calculate the bounds of the source that will be overlapping the destination + # this prevents the source trying to overwrite latent pixels that are out of bounds + # of the destination + visible_width, visible_height = (destination.shape[3] - left, destination.shape[2] - top,) + + mask = torch.ones_like(source) + + for f in range(feather): + feather_rate = (f + 1.0) / feather + + if left > 0: + mask[:, :, :, f] *= feather_rate + + if right < destination.shape[3] - 1: + mask[:, :, :, -f] *= feather_rate + + if top > 0: + mask[:, :, f, :] *= feather_rate + + if bottom < destination.shape[2] - 1: + mask[:, :, -f, :] *= feather_rate + + mask = mask[:, :, :visible_height, :visible_width] + inverse_mask = torch.ones_like(mask) - mask + + source_portion = mask * source[:, :, :visible_height, :visible_width] + destination_portion = inverse_mask * destination[:, :, top:bottom, left:right] + + destination[:, :, top:bottom, left:right] = source_portion + destination_portion + + output["samples"] = destination + + return (output,) class LatentCrop: @classmethod @@ -907,7 +927,7 @@ class LoadImageMask: "channel": (["alpha", "red", "green", "blue"], ),} } - CATEGORY = "image" + CATEGORY = "mask" RETURN_TYPES = ("MASK",) FUNCTION = "load_image" @@ -1114,3 +1134,4 @@ def init_custom_nodes(): load_custom_nodes() load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_post_processing.py")) + load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_mask.py")) From 2dc7257e292cad08876e7f188e2fbb2f2abb6644 Mon Sep 17 00:00:00 2001 From: omar92 Date: Sat, 8 Apr 2023 18:58:47 +0200 Subject: [PATCH 03/27] Allow connect premitive Node to "comfyiUI-nodes that have forceInput setting" --- web/extensions/core/widgetInputs.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index 865af7763..f4d2d22de 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -233,7 +233,9 @@ app.registerExtension({ // Fires before the link is made allowing us to reject it if it isn't valid // No widget, we cant connect - if (!input.widget) return false; + if (!input.widget) { + if (!(input.type in ComfyWidgets)) return false; + } if (this.outputs[slot].links?.length) { return this.#isValidConnection(input); @@ -252,9 +254,18 @@ app.registerExtension({ const input = theirNode.inputs[link.target_slot]; if (!input) return; - const widget = input.widget; - const { type, linkType } = getWidgetType(widget.config); + var _widget; + if (!input.widget) { + if (!(input.type in ComfyWidgets)) return; + _widget = { "name": input.name, "config": [input.type, {}] }//fake widget + } else { + _widget = input.widget; + } + + const widget = _widget; + const { type, linkType } = getWidgetType(widget.config); + console.log({ "input": input }); // Update our output to restrict to the widget type this.outputs[0].type = linkType; this.outputs[0].name = type; @@ -274,7 +285,7 @@ app.registerExtension({ if (type in ComfyWidgets) { widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget; } else { - widget = this.addWidget(type, "value", null, () => {}, {}); + widget = this.addWidget(type, "value", null, () => { }, {}); } if (node?.widgets && widget) { From 9d095c52f3d9fc65477abae380cf8ba6d8b271dd Mon Sep 17 00:00:00 2001 From: omar92 Date: Sat, 8 Apr 2023 19:05:22 +0200 Subject: [PATCH 04/27] handle double click create primitive widget --- web/extensions/core/widgetInputs.js | 43 +++++++++++++++-------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index f4d2d22de..28c5aee1d 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -159,27 +159,31 @@ app.registerExtension({ const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined; const input = this.inputs[slot]; - if (input.widget && !input[ignoreDblClick]) { - const node = LiteGraph.createNode("PrimitiveNode"); - app.graph.add(node); - - // Calculate a position that wont directly overlap another node - const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; - while (isNodeAtPos(pos)) { - pos[1] += LiteGraph.NODE_TITLE_HEIGHT; - } - - node.pos = pos; - node.connect(0, this, slot); - node.title = input.name; - - // Prevent adding duplicates due to triple clicking - input[ignoreDblClick] = true; - setTimeout(() => { - delete input[ignoreDblClick]; - }, 300); + if (!input.widget || !input[ignoreDblClick])// Not a widget input or already handled input + { + if (!(input.type in ComfyWidgets)) return r;//also Not a ComfyWidgets input (do nothing) } + // Create a primitive node + const node = LiteGraph.createNode("PrimitiveNode"); + app.graph.add(node); + + // Calculate a position that wont directly overlap another node + const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]]; + while (isNodeAtPos(pos)) { + pos[1] += LiteGraph.NODE_TITLE_HEIGHT; + } + + node.pos = pos; + node.connect(0, this, slot); + node.title = input.name; + + // Prevent adding duplicates due to triple clicking + input[ignoreDblClick] = true; + setTimeout(() => { + delete input[ignoreDblClick]; + }, 300); + return r; }; }, @@ -265,7 +269,6 @@ app.registerExtension({ const widget = _widget; const { type, linkType } = getWidgetType(widget.config); - console.log({ "input": input }); // Update our output to restrict to the widget type this.outputs[0].type = linkType; this.outputs[0].name = type; From 40ad2d4a102106b3e0fd8438af24277c3e7cb5cd Mon Sep 17 00:00:00 2001 From: EllangoK Date: Tue, 11 Apr 2023 01:08:01 -0400 Subject: [PATCH 05/27] use variables in css stylesheet --- web/style.css | 45 ++++++++++++++++++++++++++------------------- 1 file changed, 26 insertions(+), 19 deletions(-) diff --git a/web/style.css b/web/style.css index d00a2fbe2..d1bd730b8 100644 --- a/web/style.css +++ b/web/style.css @@ -1,6 +1,13 @@ :root { --fg-color: #000; --bg-color: #fff; + --comfy-menu-bg: #353535; + --comfy-input-bg: #222; + --input-text: #ddd; + --descrip-text: #999; + --drag-text: #ccc; + --error-text: #ff4444; + --border-color: #4e4e4e; } @media (prefers-color-scheme: dark) { @@ -39,8 +46,8 @@ body { position: fixed; /* Stay in place */ z-index: 100; /* Sit on top */ padding: 30px 30px 10px 30px; - background-color: #353535; /* Modal background */ - color: #ff4444; + background-color: var(--comfy-menu-bg); /* Modal background */ + color: var(--error-text); box-shadow: 0px 0px 20px #888888; border-radius: 10px; top: 50%; @@ -82,8 +89,8 @@ body { display: flex; flex-direction: column; align-items: center; - color: #999; - background-color: #353535; + color: var(--descrip-text); + background-color: var(--comfy-menu-bg); font-family: sans-serif; padding: 10px; border-radius: 0 8px 8px 8px; @@ -103,7 +110,7 @@ body { .comfy-menu-btns button { font-size: 10px; width: 50%; - color: #999 !important; + color: var(--descrip-text) !important; } .comfy-menu > button { @@ -114,10 +121,10 @@ body { .comfy-menu-btns button, .comfy-menu .comfy-list button, .comfy-modal button{ - color: #ddd; - background-color: #222; + color: var(--input-text); + background-color: var(--comfy-input-bg); border-radius: 8px; - border-color: #4e4e4e; + border-color: var(--border-color); border-style: solid; margin-top: 2px; } @@ -136,7 +143,7 @@ body { font-size: 12px; font-family: sans-serif; letter-spacing: 2px; - color: #cccccc; + color: var(--drag-text); text-shadow: 1px 0 1px black; position: absolute; top: 0; @@ -152,7 +159,7 @@ body { } .comfy-list { - color: #999; + color: var(--descrip-text); background-color: #333; margin-bottom: 10px; border-color: #4e4e4e; @@ -163,7 +170,7 @@ body { overflow-y: scroll; max-height: 100px; min-height: 25px; - background-color: #222; + background-color: var(--comfy-input-bg); padding: 5px; } @@ -206,16 +213,16 @@ button.comfy-queue-btn { .comfy-modal.comfy-manage-templates { text-align: center; font-family: sans-serif; - color: #999; + color: var(--descrip-text); z-index: 99; } .comfy-modal input, .comfy-modal select { - color: #ddd; - background-color: #222; + color: var(--input-text); + background-color: var(--comfy-input-bg); border-radius: 8px; - border-color: #4e4e4e; + border-color: var(--border-color); border-style: solid; font-size: inherit; } @@ -240,7 +247,7 @@ button.comfy-queue-btn { .graphdialog .name { font-size: 14px; font-family: sans-serif; - color: #999999; + color: var(--descrip-text); } .graphdialog button { @@ -251,10 +258,10 @@ button.comfy-queue-btn { } .graphdialog input, .graphdialog textarea, .graphdialog select { - background-color: #222; + background-color: var(--comfy-input-bg); border: 2px solid; - border-color: #444444; - color: #ddd; + border-color: var(--border-color); + color: var(--input-text); border-radius: 12px 0 0 12px; } From 73c4ba11fafb7088da6504f8be551b763e5474bb Mon Sep 17 00:00:00 2001 From: EllangoK Date: Tue, 11 Apr 2023 11:38:55 -0400 Subject: [PATCH 06/27] colorPalette modifies comfyUI as well --- web/extensions/core/colorPalette.js | 38 +++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index a08d46684..31d918366 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -45,6 +45,17 @@ const colorPalettes = { "EVENT_LINK_COLOR": "#A86", "CONNECTING_LINK_COLOR": "#AFA", }, + "comfy_base": { + "fg-color": "#000", + "bg-color": "#fff", + "comfy-menu-bg": "#353535", + "comfy-input-bg": "#222", + "input-text": "#ddd", + "descrip-text": "#999", + "drag-text": "#ccc", + "error-text": "#ff4444", + "border-color": "#4e4e4e" + } }, }, "solarized": { @@ -88,6 +99,17 @@ const colorPalettes = { "EVENT_LINK_COLOR": "#268bd2", "CONNECTING_LINK_COLOR": "#859900", }, + "comfy_base": { + "fg-color": "#fdf6e3", // Base3 + "bg-color": "#002b36", // Base03 + "comfy-menu-bg": "#073642", // Base02 + "comfy-input-bg": "#002b36", // Base03 + "input-text": "#93a1a1", // Base1 + "descrip-text": "#586e75", // Base01 + "drag-text": "#839496", // Base0 + "error-text": "#dc322f", // Solarized Red + "border-color": "#657b83" // Base00 + } }, } }; @@ -251,6 +273,22 @@ app.registerExtension({ } } } + if (colorPalette.colors.comfy_base) { + const stylesheet = document.styleSheets[1]; + + for (let i = 0; i < stylesheet.cssRules.length; i++) { + const rule = stylesheet.cssRules[i]; + const selectorText = rule.selectorText; + + if (selectorText && selectorText === ":root") { + console.log("Found :root rule"); + for (const key in colorPalette.colors.comfy_base) { + rule.style.setProperty('--' + key, colorPalette.colors.comfy_base[key]); + } + break; + } + } + } app.canvas.draw(true, true); } }; From c975fef6206e8f87e3f35c4738848e9d893e0d21 Mon Sep 17 00:00:00 2001 From: EllangoK Date: Tue, 11 Apr 2023 12:09:15 -0400 Subject: [PATCH 07/27] fix node slot colors for solarized previously many dupes, and same colors as base --- web/extensions/core/colorPalette.js | 52 ++++++++++++++--------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 31d918366..33c03ca18 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -63,41 +63,41 @@ const colorPalettes = { "name": "Solarized", "colors": { "node_slot": { - "CLIP": "#859900", // Green - "CLIP_VISION": "#6c71c4", // Indigo - "CLIP_VISION_OUTPUT": "#859900", // Green - "CONDITIONING": "#d33682", // Magenta - "CONTROL_NET": "#cb4b16", // Orange - "IMAGE": "#dc322f", // Red - "LATENT": "#268bd2", // Blue - "MASK": "#073642", // Base02 - "MODEL": "#cb4b16", // Orange - "STYLE_MODEL": "#073642", // Base02 - "UPSCALE_MODEL": "#6c71c4", // Indigo - "VAE": "#586e75", // Base1 + "CLIP": "#2AB7CA", // light blue + "CLIP_VISION": "#6c71c4", // blue violet + "CLIP_VISION_OUTPUT": "#859900", // olive green + "CONDITIONING": "#d33682", // magenta + "CONTROL_NET": "#d1ffd7", // light mint green + "IMAGE": "#5940bb", // deep blue violet + "LATENT": "#268bd2", // blue + "MASK": "#CCC9E7", // light purple-gray + "MODEL": "#dc322f", // red + "STYLE_MODEL": "#1a998a", // teal + "UPSCALE_MODEL": "#054A29", // dark green + "VAE": "#facfad", // light pink-orange }, "litegraph_base": { - "NODE_TITLE_COLOR": "#fdf6e3", - "NODE_SELECTED_TITLE_COLOR": "#b58900", + "NODE_TITLE_COLOR": "#fdf6e3", // Base3 + "NODE_SELECTED_TITLE_COLOR": "#A9D400", "NODE_TEXT_SIZE": 14, - "NODE_TEXT_COLOR": "#657b83", + "NODE_TEXT_COLOR": "#657b83", // Base00 "NODE_SUBTEXT_SIZE": 12, - "NODE_DEFAULT_COLOR": "#586e75", - "NODE_DEFAULT_BGCOLOR": "#073642", - "NODE_DEFAULT_BOXCOLOR": "#839496", + "NODE_DEFAULT_COLOR": "#094656", + "NODE_DEFAULT_BGCOLOR": "#073642", // Base02 + "NODE_DEFAULT_BOXCOLOR": "#839496", // Base0 "NODE_DEFAULT_SHAPE": "box", - "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", + "NODE_BOX_OUTLINE_COLOR": "#fdf6e3", // Base3 "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)", "DEFAULT_GROUP_FONT": 24, - "WIDGET_BGCOLOR": "#002b36", - "WIDGET_OUTLINE_COLOR": "#839496", - "WIDGET_TEXT_COLOR": "#fdf6e3", - "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", + "WIDGET_BGCOLOR": "#002b36", // Base03 + "WIDGET_OUTLINE_COLOR": "#839496", // Base0 + "WIDGET_TEXT_COLOR": "#fdf6e3", // Base3 + "WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1", // Base1 - "LINK_COLOR": "#2aa198", - "EVENT_LINK_COLOR": "#268bd2", - "CONNECTING_LINK_COLOR": "#859900", + "LINK_COLOR": "#2aa198", // Solarized Cyan + "EVENT_LINK_COLOR": "#268bd2", // Solarized Blue + "CONNECTING_LINK_COLOR": "#859900", // Solarized Green }, "comfy_base": { "fg-color": "#fdf6e3", // Base3 From eae159eb4c061e7de5680008690fef838eef8e20 Mon Sep 17 00:00:00 2001 From: EllangoK Date: Tue, 11 Apr 2023 13:11:39 -0400 Subject: [PATCH 08/27] adds light theme, fixes multiline css --- web/extensions/core/colorPalette.js | 61 +++++++++++++++++++++++++++-- web/style.css | 4 +- 2 files changed, 59 insertions(+), 6 deletions(-) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 33c03ca18..5f5cc0355 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -5,9 +5,9 @@ import { api } from "/scripts/api.js"; // Manage color palettes const colorPalettes = { - "palette_1": { - "id": "palette_1", - "name": "Palette 1", + "dark": { + "id": "dark", + "name": "Dark (Default)", "colors": { "node_slot": { "CLIP": "#FFD500", // bright yellow @@ -58,6 +58,59 @@ const colorPalettes = { } }, }, + "light": { + "id": "light", + "name": "Light", + "colors": { + "node_slot": { + "CLIP": "#FFA726", // orange + "CLIP_VISION": "#5C6BC0", // indigo + "CLIP_VISION_OUTPUT": "#8D6E63", // brown + "CONDITIONING": "#EF5350", // red + "CONTROL_NET": "#66BB6A", // green + "IMAGE": "#42A5F5", // blue + "LATENT": "#AB47BC", // purple + "MASK": "#9CCC65", // light green + "MODEL": "#7E57C2", // deep purple + "STYLE_MODEL": "#D4E157", // lime + "VAE": "#FF7043", // deep orange + }, + "litegraph_base": { + "NODE_TITLE_COLOR": "#222", + "NODE_SELECTED_TITLE_COLOR": "#000", + "NODE_TEXT_SIZE": 14, + "NODE_TEXT_COLOR": "#444", + "NODE_SUBTEXT_SIZE": 12, + "NODE_DEFAULT_COLOR": "#F7F7F7", + "NODE_DEFAULT_BGCOLOR": "#F5F5F5", + "NODE_DEFAULT_BOXCOLOR": "#CCC", + "NODE_DEFAULT_SHAPE": "box", + "NODE_BOX_OUTLINE_COLOR": "#000", + "DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.1)", + "DEFAULT_GROUP_FONT": 24, + + "WIDGET_BGCOLOR": "#D4D4D4", + "WIDGET_OUTLINE_COLOR": "#999", + "WIDGET_TEXT_COLOR": "#222", + "WIDGET_SECONDARY_TEXT_COLOR": "#555", + + "LINK_COLOR": "#4CAF50", + "EVENT_LINK_COLOR": "#FF9800", + "CONNECTING_LINK_COLOR": "#2196F3", + }, + "comfy_base": { + "fg-color": "#222", + "bg-color": "#FFF", + "comfy-menu-bg": "#F5F5F5", + "comfy-input-bg": "#C9C9C9", + "input-text": "#222", + "descrip-text": "#444", + "drag-text": "#555", + "error-text": "#F44336", + "border-color": "#CCC" + } + }, + }, "solarized": { "id": "solarized", "name": "Solarized", @@ -116,7 +169,7 @@ const colorPalettes = { const id = "Comfy.ColorPalette"; const idCustomColorPalettes = "Comfy.CustomColorPalettes"; -const defaultColorPaletteId = "palette_1"; +const defaultColorPaletteId = "dark"; const els = {} // const ctxMenu = LiteGraph.ContextMenu; app.registerExtension({ diff --git a/web/style.css b/web/style.css index d1bd730b8..34e31726c 100644 --- a/web/style.css +++ b/web/style.css @@ -32,8 +32,8 @@ body { } .comfy-multiline-input { - background-color: var(--bg-color); - color: var(--fg-color); + background-color: var(--comfy-input-bg); + color: var(--input-text); overflow: hidden; overflow-y: auto; padding: 2px; From 19ce3df8c009b5b4f8e91005bb314371c211d5ce Mon Sep 17 00:00:00 2001 From: EllangoK Date: Tue, 11 Apr 2023 13:24:32 -0400 Subject: [PATCH 09/27] simplify setting color of root, fixes fg and bg --- web/extensions/core/colorPalette.js | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 5f5cc0355..55aded62a 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -46,8 +46,8 @@ const colorPalettes = { "CONNECTING_LINK_COLOR": "#AFA", }, "comfy_base": { - "fg-color": "#000", - "bg-color": "#fff", + "fg-color": "#fff", + "bg-color": "#202020", "comfy-menu-bg": "#353535", "comfy-input-bg": "#222", "input-text": "#ddd", @@ -311,10 +311,12 @@ app.registerExtension({ const loadColorPalette = async (colorPalette) => { colorPalette = await completeColorPalette(colorPalette); if (colorPalette.colors) { + // Sets the colors of node slots and links if (colorPalette.colors.node_slot) { Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot); Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot); } + // Sets the colors of the LiteGraph objects if (colorPalette.colors.litegraph_base) { // Everything updates correctly in the loop, except the Node Title and Link Color for some reason app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR; @@ -326,20 +328,11 @@ app.registerExtension({ } } } + // Sets the color of ComfyUI elements if (colorPalette.colors.comfy_base) { - const stylesheet = document.styleSheets[1]; - - for (let i = 0; i < stylesheet.cssRules.length; i++) { - const rule = stylesheet.cssRules[i]; - const selectorText = rule.selectorText; - - if (selectorText && selectorText === ":root") { - console.log("Found :root rule"); - for (const key in colorPalette.colors.comfy_base) { - rule.style.setProperty('--' + key, colorPalette.colors.comfy_base[key]); - } - break; - } + const rootStyle = document.documentElement.style; + for (const key in colorPalette.colors.comfy_base) { + rootStyle.setProperty('--' + key, colorPalette.colors.comfy_base[key]); } } app.canvas.draw(true, true); From e12fb88b1b84e354872c0d761544558479bcfad2 Mon Sep 17 00:00:00 2001 From: missionfloyd Date: Tue, 11 Apr 2023 16:49:39 -0600 Subject: [PATCH 10/27] Image/mask conversion nodes --- nodes.py | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/nodes.py b/nodes.py index 14a73bcd7..ecd931d69 100644 --- a/nodes.py +++ b/nodes.py @@ -1059,6 +1059,43 @@ class ImagePadForOutpaint: return (new_image, mask) +class ImageToMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "channel": (["red", "green", "blue"],), + } + } + + CATEGORY = "image" + + RETURN_TYPES = ("MASK",) + FUNCTION = "image_to_mask" + + def image_to_mask(self, image, channel): + channels = ["red", "green", "blue"] + mask = torch.select(image[0], 2, channels.index(channel)) + return (mask,) + +class MaskToImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask": ("MASK",), + } + } + + CATEGORY = "image" + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "mask_to_image" + + def mask_to_image(self, mask): + result = mask[None, :, :, None].expand(-1, -1, -1, 3) + return (result,) NODE_CLASS_MAPPINGS = { "KSampler": KSampler, @@ -1102,6 +1139,8 @@ NODE_CLASS_MAPPINGS = { "unCLIPCheckpointLoader": unCLIPCheckpointLoader, "CheckpointLoader": CheckpointLoader, "DiffusersLoader": DiffusersLoader, + "ImageToMask": ImageToMask, + "MaskToImage": MaskToImage, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1147,6 +1186,8 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ImageUpscaleWithModel": "Upscale Image (using Model)", "ImageInvert": "Invert Image", "ImagePadForOutpaint": "Pad Image for Outpainting", + "ImageToMask": "Convert Image to Mask", + "MaskToImage": "Convert Mask to Image", # _for_testing "VAEDecodeTiled": "VAE Decode (Tiled)", "VAEEncodeTiled": "VAE Encode (Tiled)", From e1d289c1ec6894e15af0b57b6630b853341c61fa Mon Sep 17 00:00:00 2001 From: missionfloyd Date: Tue, 11 Apr 2023 20:26:24 -0600 Subject: [PATCH 11/27] use slice instead of torch.select() --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index ecd931d69..815631f58 100644 --- a/nodes.py +++ b/nodes.py @@ -1076,7 +1076,7 @@ class ImageToMask: def image_to_mask(self, image, channel): channels = ["red", "green", "blue"] - mask = torch.select(image[0], 2, channels.index(channel)) + mask = image[0, :, :, channels.index(channel)] return (mask,) class MaskToImage: From 6c69853afd447ec33b146f57dc3b28999c8537ec Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 11 Apr 2023 23:23:06 -0400 Subject: [PATCH 12/27] Change colour of background in light theme. --- web/extensions/core/colorPalette.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 55aded62a..94bea9ab3 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -100,7 +100,7 @@ const colorPalettes = { }, "comfy_base": { "fg-color": "#222", - "bg-color": "#FFF", + "bg-color": "#DDD", "comfy-menu-bg": "#F5F5F5", "comfy-input-bg": "#C9C9C9", "input-text": "#222", From d6a3c0d424c7e49bd114a019b333d8dadb68ef0f Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:49:32 +0100 Subject: [PATCH 13/27] Add support for dropping images from urls --- web/scripts/app.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index 0399ac722..b1892fc2a 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -362,8 +362,20 @@ class ComfyApp { if (n && n.onDragDrop && (await n.onDragDrop(event))) { return; } - + // Dragging from Chrome->Firefox there is a file but its a bmp, so ignore that + if (event.dataTransfer.files.length && event.dataTransfer.files[0].type !== "image/bmp") { await this.handleFile(event.dataTransfer.files[0]); + } else { + // Try loading the first URI in the transfer list + const validTypes = ["text/uri-list", "text/x-moz-url"]; + const match = [...event.dataTransfer.types].find((t) => validTypes.find(v => t === v)); + if (match) { + const uri = event.dataTransfer.getData(match)?.split("\n")?.[0]; + if (uri) { + await this.handleFile(await (await fetch(uri)).blob()); + } + } + } }); // Always clear over node on drag leave @@ -1090,7 +1102,7 @@ class ComfyApp { importA1111(this.graph, pngInfo.parameters); } } - } else if (file.type === "application/json" || file.name.endsWith(".json")) { + } else if (file.type === "application/json" || file.name?.endsWith(".json")) { const reader = new FileReader(); reader.onload = () => { this.loadGraphData(JSON.parse(reader.result)); From a3516225f9488b82c6c309d7f00dc3705607733d Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 12 Apr 2023 13:52:19 +0100 Subject: [PATCH 14/27] Changed default name to be the node type not title --- web/extensions/core/saveImageExtraOutput.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/extensions/core/saveImageExtraOutput.js b/web/extensions/core/saveImageExtraOutput.js index ce97b5491..6032d4cc7 100644 --- a/web/extensions/core/saveImageExtraOutput.js +++ b/web/extensions/core/saveImageExtraOutput.js @@ -90,7 +90,7 @@ app.registerExtension({ const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; if (!this.properties || !("Node name for S&R" in this.properties)) { - this.addProperty("Node name for S&R", this.title, "string"); + this.addProperty("Node name for S&R", this.constructor.type, "string"); } return r; From e3566679bd18af0da00004472eec1836639d036e Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 12 Apr 2023 17:40:52 -0400 Subject: [PATCH 15/27] Update litegraph from upstream. --- web/lib/litegraph.core.js | 142 +++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 49 deletions(-) diff --git a/web/lib/litegraph.core.js b/web/lib/litegraph.core.js index c3efa22a9..4189a48c0 100644 --- a/web/lib/litegraph.core.js +++ b/web/lib/litegraph.core.js @@ -142,6 +142,8 @@ pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) + ctrl_shift_v_paste_connect_unselected_outputs: true, //[true!] allows ctrl + shift + v to paste nodes with the outputs of the unselected nodes connected with the inputs of the newly pasted nodes + /** * Register a node class so it can be listed when the user wants to create a new one * @method registerNodeType @@ -253,13 +255,18 @@ * @param {String|Object} type name of the node or the node constructor itself */ unregisterNodeType: function(type) { - var base_class = type.constructor === String ? this.registered_node_types[type] : type; - if(!base_class) - throw("node type not found: " + type ); - delete this.registered_node_types[base_class.type]; - if(base_class.constructor.name) - delete this.Nodes[base_class.constructor.name]; - }, + const base_class = + type.constructor === String + ? this.registered_node_types[type] + : type; + if (!base_class) { + throw "node type not found: " + type; + } + delete this.registered_node_types[base_class.type]; + if (base_class.constructor.name) { + delete this.Nodes[base_class.constructor.name]; + } + }, /** * Save a slot type and his node @@ -267,38 +274,49 @@ * @param {String|Object} type name of the node or the node constructor itself * @param {String} slot_type name of the slot type (variable type), eg. string, number, array, boolean, .. */ - registerNodeAndSlotType: function(type,slot_type,out){ + registerNodeAndSlotType: function(type, slot_type, out){ out = out || false; - var base_class = type.constructor === String && this.registered_node_types[type] !== "anonymous" ? this.registered_node_types[type] : type; - - var sCN = base_class.constructor.type; - - if (typeof slot_type == "string"){ - var aTypes = slot_type.split(","); - }else if (slot_type == this.EVENT || slot_type == this.ACTION){ - var aTypes = ["_event_"]; - }else{ - var aTypes = ["*"]; + const base_class = + type.constructor === String && + this.registered_node_types[type] !== "anonymous" + ? this.registered_node_types[type] + : type; + + const class_type = base_class.constructor.type; + + let allTypes = []; + if (typeof slot_type === "string") { + allTypes = slot_type.split(","); + } else if (slot_type == this.EVENT || slot_type == this.ACTION) { + allTypes = ["_event_"]; + } else { + allTypes = ["*"]; } - for (var i = 0; i < aTypes.length; ++i) { - var sT = aTypes[i]; //.toLowerCase(); - if (sT === ""){ - sT = "*"; + for (let i = 0; i < allTypes.length; ++i) { + let slotType = allTypes[i]; + if (slotType === "") { + slotType = "*"; } - var registerTo = out ? "registered_slot_out_types" : "registered_slot_in_types"; - if (typeof this[registerTo][sT] == "undefined") this[registerTo][sT] = {nodes: []}; - this[registerTo][sT].nodes.push(sCN); - + const registerTo = out + ? "registered_slot_out_types" + : "registered_slot_in_types"; + if (this[registerTo][slotType] === undefined) { + this[registerTo][slotType] = { nodes: [] }; + } + if (!this[registerTo][slotType].nodes.includes(class_type)) { + this[registerTo][slotType].nodes.push(class_type); + } + // check if is a new type - if (!out){ - if (!this.slot_types_in.includes(sT.toLowerCase())){ - this.slot_types_in.push(sT.toLowerCase()); + if (!out) { + if (!this.slot_types_in.includes(slotType.toLowerCase())) { + this.slot_types_in.push(slotType.toLowerCase()); this.slot_types_in.sort(); } - }else{ - if (!this.slot_types_out.includes(sT.toLowerCase())){ - this.slot_types_out.push(sT.toLowerCase()); + } else { + if (!this.slot_types_out.includes(slotType.toLowerCase())) { + this.slot_types_out.push(slotType.toLowerCase()); this.slot_types_out.sort(); } } @@ -1616,7 +1634,8 @@ var nRet = null; for (var i = nodes_list.length - 1; i >= 0; i--) { var n = nodes_list[i]; - if (n.isPointInside(x, y, margin)) { + var skip_title = n.constructor.title_mode == LiteGraph.NO_TITLE; + if (n.isPointInside(x, y, margin, skip_title)) { // check for lesser interest nodes (TODO check for overlapping, use the top) /*if (typeof n == "LGraphGroup"){ nRet = n; @@ -3967,8 +3986,8 @@ var aSource = (type+"").toLowerCase().split(","); var aDest = aSlots[i].type=="0"||aSlots[i].type=="*"?"0":aSlots[i].type; aDest = (aDest+"").toLowerCase().split(","); - for(sI=0;sI= 0 && target_slot !== null){ //console.debug("CONNbyTYPE type "+target_slotType+" for "+target_slot) return this.connect(slot, target_node, target_slot); @@ -4072,7 +4091,7 @@ if (source_node && source_node.constructor === Number) { source_node = this.graph.getNodeById(source_node); } - source_slot = source_node.findOutputSlotByType(source_slotType, false, true); + var source_slot = source_node.findOutputSlotByType(source_slotType, false, true); if (source_slot >= 0 && source_slot !== null){ //console.debug("CONNbyTYPE OUT! type "+source_slotType+" for "+source_slot) return source_node.connect(source_slot, this, slot); @@ -5184,6 +5203,7 @@ LGraphNode.prototype.executeAction = function(action) this.editor_alpha = 1; //used for transition this.pause_rendering = false; this.clear_background = true; + this.clear_background_color = "#222"; this.read_only = false; //if set to true users cannot modify the graph this.render_only_selected = true; @@ -6986,7 +7006,7 @@ LGraphNode.prototype.executeAction = function(action) block_default = true; } - if (e.code == "KeyC" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + if ((e.keyCode === 67) && (e.metaKey || e.ctrlKey) && !e.shiftKey) { //copy if (this.selected_nodes) { this.copyToClipboard(); @@ -6994,9 +7014,9 @@ LGraphNode.prototype.executeAction = function(action) } } - if (e.code == "KeyV" && (e.metaKey || e.ctrlKey) && !e.shiftKey) { + if ((e.keyCode === 86) && (e.metaKey || e.ctrlKey)) { //paste - this.pasteFromClipboard(); + this.pasteFromClipboard(e.shiftKey); } //delete or backspace @@ -7081,15 +7101,15 @@ LGraphNode.prototype.executeAction = function(action) var target_node = this.graph.getNodeById( link_info.origin_id ); - if (!target_node || !this.selected_nodes[target_node.id]) { - //improve this by allowing connections to non-selected nodes + if (!target_node) { continue; - } //not selected + } clipboard_info.links.push([ target_node._relative_id, link_info.origin_slot, //j, node._relative_id, - link_info.target_slot + link_info.target_slot, + target_node.id ]); } } @@ -7100,7 +7120,11 @@ LGraphNode.prototype.executeAction = function(action) ); }; - LGraphCanvas.prototype.pasteFromClipboard = function() { + LGraphCanvas.prototype.pasteFromClipboard = function(isConnectUnselected = false) { + // if ctrl + shift + v is off, return when isConnectUnselected is true (shift is pressed) to maintain old behavior + if (!LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { + return; + } var data = localStorage.getItem("litegrapheditor_clipboard"); if (!data) { return; @@ -7149,7 +7173,16 @@ LGraphNode.prototype.executeAction = function(action) //create links for (var i = 0; i < clipboard_info.links.length; ++i) { var link_info = clipboard_info.links[i]; - var origin_node = nodes[link_info[0]]; + var origin_node; + var origin_node_relative_id = link_info[0]; + if (origin_node_relative_id != null) { + origin_node = nodes[origin_node_relative_id]; + } else if (LiteGraph.ctrl_shift_v_paste_connect_unselected_outputs && isConnectUnselected) { + var origin_node_id = link_info[4]; + if (origin_node_id) { + origin_node = this.graph.getNodeById(origin_node_id); + } + } var target_node = nodes[link_info[2]]; if( origin_node && target_node ) origin_node.connect(link_info[1], target_node, link_info[3]); @@ -8212,6 +8245,17 @@ LGraphNode.prototype.executeAction = function(action) this.ds.toCanvasContext(ctx); //render BG + if ( this.ds.scale < 1.5 && !bg_already_painted && this.clear_background_color ) + { + ctx.fillStyle = this.clear_background_color; + ctx.fillRect( + this.visible_area[0], + this.visible_area[1], + this.visible_area[2], + this.visible_area[3] + ); + } + if ( this.background_image && this.ds.scale > 0.5 && @@ -12274,7 +12318,7 @@ LGraphNode.prototype.executeAction = function(action) var aProps = LiteGraph.availableCanvasOptions; aProps.sort(); - for(pI in aProps){ + for(var pI in aProps){ var pX = aProps[pI]; panel.addWidget( "boolean", pX, graphcanvas[pX], {key: pX, on: "True", off: "False"}, fUpdate); } From 37beea0af55228c565b9e129928218aff45eb3f5 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 12 Apr 2023 20:31:26 -0400 Subject: [PATCH 16/27] Add missing shortcuts from litegraph doc to README. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 877f46433..449531b86 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,11 @@ Workflow examples can be found on the [Examples page](https://comfyanonymous.git - **Ctrl + A** select all nodes - **Ctrl + M** mute/unmute selected nodes - **Delete** or **Backspace** delete selected nodes +- **Space** Holding space key while moving the cursor moves the canvas around. It works when holding the mouse button down so it is easier to connect different nodes when the canvas gets too large. +- **Ctrl/Shift + Click** Add clicked node to selection. +- **Ctrl + C/Ctrl + V** - Copy and paste selected nodes, without maintaining the connection to the outputs of unselected nodes. +- **Ctrl + C/Ctrl + Shift + V** - Copy and paste selected nodes, and maintaining the connection from the outputs of unselected nodes to the inputs of the newly pasted nodes. +- Holding **Shift** and drag selected nodes - Move multiple selected nodes at the same time. # Installing From bada50f132a39568364bd0ecde2e73fa9deadbf3 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 12 Apr 2023 20:34:11 -0400 Subject: [PATCH 17/27] Add link to Arc instructions to readme. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 449531b86..77d979ac3 100644 --- a/README.md +++ b/README.md @@ -97,6 +97,11 @@ Install the dependencies by opening your terminal inside the ComfyUI folder and: After this you should have everything installed and can proceed to running ComfyUI. +### Others: + +[Intel Arc](https://github.com/comfyanonymous/ComfyUI/discussions/476) + +Mac/MPS: There is basic support in the code but until someone makes some install instruction you are on your own. ### I already have another UI for Stable Diffusion installed do I really have to install all of these dependencies? From 3f52e7cbb16c3469cc54f751d4b3fe9354b294cf Mon Sep 17 00:00:00 2001 From: FizzleDorf <1fizzledorf@gmail.com> Date: Wed, 12 Apr 2023 20:57:13 -0400 Subject: [PATCH 18/27] Seed controls added to Ksamplers (#296) Co-authored-by: flyingshutter --- .gitignore | 1 + web/extensions/core/widgetInputs.js | 8 ++-- web/scripts/app.js | 5 +++ web/scripts/widgets.js | 67 ++++++++++++++++++----------- 4 files changed, 52 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index d311a2a09..df6adbe4b 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ temp/ custom_nodes/ !custom_nodes/example_node.py.example extra_model_paths.yaml +/.vs diff --git a/web/extensions/core/widgetInputs.js b/web/extensions/core/widgetInputs.js index 865af7763..3764c9848 100644 --- a/web/extensions/core/widgetInputs.js +++ b/web/extensions/core/widgetInputs.js @@ -1,4 +1,4 @@ -import { ComfyWidgets, addRandomizeWidget } from "/scripts/widgets.js"; +import { ComfyWidgets, addValueControlWidget } from "/scripts/widgets.js"; import { app } from "/scripts/app.js"; const CONVERTED_TYPE = "converted-widget"; @@ -23,7 +23,7 @@ function hideWidget(node, widget, suffix = "") { return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; }; - // Hide any linked widgets, e.g. seed+randomize + // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { hideWidget(node, w, ":" + widget.name); @@ -40,7 +40,7 @@ function showWidget(widget) { delete widget.origComputeSize; delete widget.origSerializeValue; - // Hide any linked widgets, e.g. seed+randomize + // Hide any linked widgets, e.g. seed+seedControl if (widget.linkedWidgets) { for (const w of widget.linkedWidgets) { showWidget(w); @@ -285,7 +285,7 @@ app.registerExtension({ } if (widget.type === "number") { - addRandomizeWidget(this, widget, "Random after every gen"); + addValueControlWidget(this, widget, "fixed"); } // When our value changes, update other widgets to reflect our changes diff --git a/web/scripts/app.js b/web/scripts/app.js index b1892fc2a..2f5e73220 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -949,6 +949,11 @@ class ComfyApp { widget.value = widget.value.slice(7); } } + if (widget.name == "control_after_generate") { + if (widget.value == true) { + widget.value = "randomize"; + } + } } } } diff --git a/web/scripts/widgets.js b/web/scripts/widgets.js index d1a9c6c6e..2acc5f2c0 100644 --- a/web/scripts/widgets.js +++ b/web/scripts/widgets.js @@ -10,37 +10,54 @@ function getNumberDefaults(inputData, defaultStep) { return { val: defaultVal, config: { min, max, step: 10.0 * step } }; } -export function addRandomizeWidget(node, targetWidget, name, defaultValue = false) { - const randomize = node.addWidget("toggle", name, defaultValue, function (v) {}, { - on: "enabled", - off: "disabled", - serialize: false, // Don't include this in prompt. - }); +export function addValueControlWidget(node, targetWidget, defaultValue = "randomize", values) { + const valueControl = node.addWidget("combo", "control_after_generate", defaultValue, function (v) { }, { + values: ["fixed", "increment", "decrement", "randomize"], + serialize: false, // Don't include this in prompt. + }); + valueControl.afterQueued = () => { - randomize.afterQueued = () => { - if (randomize.value) { - const min = targetWidget.options?.min; - let max = targetWidget.options?.max; - if (min != null || max != null) { - if (max) { - // limit max to something that javascript can handle - max = Math.min(1125899906842624, max); - } - targetWidget.value = Math.floor(Math.random() * ((max ?? 9999999999) - (min ?? 0) + 1) + (min ?? 0)); - } else { - targetWidget.value = Math.floor(Math.random() * 1125899906842624); - } + var v = valueControl.value; + + let min = targetWidget.options.min; + let max = targetWidget.options.max; + // limit to something that javascript can handle + max = Math.min(1125899906842624, max); + min = Math.max(-1125899906842624, min); + let range = (max - min) / (targetWidget.options.step / 10); + + //adjust values based on valueControl Behaviour + switch (v) { + case "fixed": + break; + case "increment": + targetWidget.value += targetWidget.options.step / 10; + break; + case "decrement": + targetWidget.value -= targetWidget.options.step / 10; + break; + case "randomize": + targetWidget.value = Math.floor(Math.random() * range) * (targetWidget.options.step / 10) + min; + default: + break; } - }; - return randomize; -} + /*check if values are over or under their respective + * ranges and set them to min or max.*/ + if (targetWidget.value < min) + targetWidget.value = min; + + if (targetWidget.value > max) + targetWidget.value = max; + } + return valueControl; +}; function seedWidget(node, inputName, inputData) { const seed = ComfyWidgets.INT(node, inputName, inputData); - const randomize = addRandomizeWidget(node, seed.widget, "Random seed after every gen", true); + const seedControl = addValueControlWidget(node, seed.widget, "randomize"); - seed.widget.linkedWidgets = [randomize]; - return { widget: seed, randomize }; + seed.widget.linkedWidgets = [seedControl]; + return seed; } const MultilineSymbol = Symbol(); From 9371924e654128258cc82419e83c2a788a32e2be Mon Sep 17 00:00:00 2001 From: missionfloyd Date: Thu, 13 Apr 2023 03:11:17 -0600 Subject: [PATCH 19/27] Move mask conversion to separate file --- comfy_extras/nodes_mask_conversion.py | 54 +++++++++++++++++++++++++++ nodes.py | 42 +-------------------- 2 files changed, 55 insertions(+), 41 deletions(-) create mode 100644 comfy_extras/nodes_mask_conversion.py diff --git a/comfy_extras/nodes_mask_conversion.py b/comfy_extras/nodes_mask_conversion.py new file mode 100644 index 000000000..04dcbd0d9 --- /dev/null +++ b/comfy_extras/nodes_mask_conversion.py @@ -0,0 +1,54 @@ +import numpy as np +import torch +import torch.nn.functional as F +from PIL import Image + +import comfy.utils + +class ImageToMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "channel": (["red", "green", "blue"],), + } + } + + CATEGORY = "image" + + RETURN_TYPES = ("MASK",) + FUNCTION = "image_to_mask" + + def image_to_mask(self, image, channel): + channels = ["red", "green", "blue"] + mask = image[0, :, :, channels.index(channel)] + return (mask,) + +class MaskToImage: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "mask": ("MASK",), + } + } + + CATEGORY = "image" + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "mask_to_image" + + def mask_to_image(self, mask): + result = mask[None, :, :, None].expand(-1, -1, -1, 3) + return (result,) + +NODE_CLASS_MAPPINGS = { + "ImageToMask": ImageToMask, + "MaskToImage": MaskToImage, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ImageToMask": "Convert Image to Mask", + "MaskToImage": "Convert Mask to Image", +} diff --git a/nodes.py b/nodes.py index 325e3ba68..3ed9cf499 100644 --- a/nodes.py +++ b/nodes.py @@ -1061,43 +1061,6 @@ class ImagePadForOutpaint: return (new_image, mask) -class ImageToMask: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "channel": (["red", "green", "blue"],), - } - } - - CATEGORY = "image" - - RETURN_TYPES = ("MASK",) - FUNCTION = "image_to_mask" - - def image_to_mask(self, image, channel): - channels = ["red", "green", "blue"] - mask = image[0, :, :, channels.index(channel)] - return (mask,) - -class MaskToImage: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "mask": ("MASK",), - } - } - - CATEGORY = "image" - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "mask_to_image" - - def mask_to_image(self, mask): - result = mask[None, :, :, None].expand(-1, -1, -1, 3) - return (result,) NODE_CLASS_MAPPINGS = { "KSampler": KSampler, @@ -1141,8 +1104,6 @@ NODE_CLASS_MAPPINGS = { "unCLIPCheckpointLoader": unCLIPCheckpointLoader, "CheckpointLoader": CheckpointLoader, "DiffusersLoader": DiffusersLoader, - "ImageToMask": ImageToMask, - "MaskToImage": MaskToImage, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -1188,8 +1149,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "ImageUpscaleWithModel": "Upscale Image (using Model)", "ImageInvert": "Invert Image", "ImagePadForOutpaint": "Pad Image for Outpainting", - "ImageToMask": "Convert Image to Mask", - "MaskToImage": "Convert Mask to Image", # _for_testing "VAEDecodeTiled": "VAE Decode (Tiled)", "VAEEncodeTiled": "VAE Encode (Tiled)", @@ -1233,3 +1192,4 @@ def init_custom_nodes(): load_custom_nodes() load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_post_processing.py")) + load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_mask_conversion.py")) From ff0be60ac4c561c50fedce1ed4a0165d3ef087ce Mon Sep 17 00:00:00 2001 From: EllangoK Date: Thu, 13 Apr 2023 06:38:24 -0400 Subject: [PATCH 20/27] fix comfy list not styled, and light theme border --- web/extensions/core/colorPalette.js | 2 +- web/style.css | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js index 94bea9ab3..41541a8d8 100644 --- a/web/extensions/core/colorPalette.js +++ b/web/extensions/core/colorPalette.js @@ -107,7 +107,7 @@ const colorPalettes = { "descrip-text": "#444", "drag-text": "#555", "error-text": "#F44336", - "border-color": "#CCC" + "border-color": "#888" } }, }, diff --git a/web/style.css b/web/style.css index 34e31726c..312fc046a 100644 --- a/web/style.css +++ b/web/style.css @@ -160,9 +160,9 @@ body { .comfy-list { color: var(--descrip-text); - background-color: #333; + background-color: var(--comfy-menu-bg); margin-bottom: 10px; - border-color: #4e4e4e; + border-color: var(--border-color); border-style: solid; } From 501f200d8631b2f5d734cad5aefba8d6f4232937 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 13 Apr 2023 10:38:41 -0400 Subject: [PATCH 21/27] Fix widgets not getting converted correctly in workflows. --- web/scripts/app.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index 2f5e73220..42addc8c6 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -949,9 +949,13 @@ class ComfyApp { widget.value = widget.value.slice(7); } } + } + if (node.type == "KSampler" || node.type == "KSamplerAdvanced" || node.type == "PrimitiveNode") { if (widget.name == "control_after_generate") { - if (widget.value == true) { + if (widget.value === true) { widget.value = "randomize"; + } else if (widget.value === false) { + widget.value = "fixed"; } } } From 601edaf6ad127c46eae417d024c24d6e9ae310c4 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 13 Apr 2023 10:59:38 -0400 Subject: [PATCH 22/27] Add links to new controlnet models to colab. --- notebooks/comfyui_colab.ipynb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/notebooks/comfyui_colab.ipynb b/notebooks/comfyui_colab.ipynb index 071a89969..8b5c0badf 100644 --- a/notebooks/comfyui_colab.ipynb +++ b/notebooks/comfyui_colab.ipynb @@ -119,9 +119,20 @@ "\n", "\n", "# ControlNet\n", - "#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_depth-fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_scribble-fp16.safetensors -P ./models/controlnet/\n", - "#!wget -c https://huggingface.co/webui/ControlNet-modules-safetensors/resolve/main/control_openpose-fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_ip2p_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11e_sd15_shuffle_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_canny_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_depth_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_inpaint_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_lineart_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_mlsd_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_normalbae_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_openpose_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_scribble_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_seg_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15_softedge_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11p_sd15s2_lineart_anime_fp16.safetensors -P ./models/controlnet/\n", + "#!wget -c https://huggingface.co/comfyanonymous/ControlNet-v1-1_fp16_safetensors/resolve/main/control_v11u_sd15_tile_fp16.safetensors -P ./models/controlnet/\n", "\n", "\n", "# Controlnet Preprocessor nodes by Fannovel16\n", From d2337a86fe6fb97ed9d818635083fcf1dc2bafc0 Mon Sep 17 00:00:00 2001 From: Gavroche CryptoRUSH <95258328+CryptoRUSHGav@users.noreply.github.com> Date: Thu, 13 Apr 2023 16:38:02 -0400 Subject: [PATCH 23/27] remove extra semi-colon --- nodes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodes.py b/nodes.py index 946c66857..f13626771 100644 --- a/nodes.py +++ b/nodes.py @@ -871,7 +871,7 @@ class SaveImage: "filename": file, "subfolder": subfolder, "type": self.type - }); + }) counter += 1 return { "ui": { "images": results } } From 35a2c790b60f836371f8955c96661e929712619e Mon Sep 17 00:00:00 2001 From: comfyanonymous <121283862+comfyanonymous@users.noreply.github.com> Date: Fri, 14 Apr 2023 00:12:15 -0400 Subject: [PATCH 24/27] Update comfy_extras/nodes_mask.py Co-authored-by: missionfloyd --- comfy_extras/nodes_mask.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index ba39680a7..ab17fc509 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -9,8 +9,8 @@ class LatentCompositeMasked: "required": { "destination": ("LATENT",), "source": ("LATENT",), - "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "x": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 8}), }, "optional": { "mask": ("MASK",), @@ -26,6 +26,9 @@ class LatentCompositeMasked: destination = destination["samples"].clone() source = source["samples"] + x = max(-source.shape[3] * 8, min(x, destination.shape[3] * 8)) + y = max(-source.shape[2] * 8, min(y, destination.shape[2] * 8)) + left, top = (x // 8, y // 8) right, bottom = (left + source.shape[3], top + source.shape[2],) @@ -40,7 +43,7 @@ class LatentCompositeMasked: # calculate the bounds of the source that will be overlapping the destination # this prevents the source trying to overwrite latent pixels that are out of bounds # of the destination - visible_width, visible_height = (destination.shape[3] - left, destination.shape[2] - top,) + visible_width, visible_height = (destination.shape[3] - left + min(0, x), destination.shape[2] - top + min(0, y),) mask = mask[:, :, :visible_height, :visible_width] inverse_mask = torch.ones_like(mask) - mask From 1a7cda715b3c01ef89b16c5cc96784ca4efa313c Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Fri, 14 Apr 2023 00:14:35 -0400 Subject: [PATCH 25/27] Revert LatentComposite. --- nodes.py | 82 +++++++++++++++++++++----------------------------------- 1 file changed, 31 insertions(+), 51 deletions(-) diff --git a/nodes.py b/nodes.py index 661f879ac..6468ac6b8 100644 --- a/nodes.py +++ b/nodes.py @@ -578,64 +578,44 @@ class LatentFlip: class LatentComposite: @classmethod def INPUT_TYPES(s): - return { - "required": { - "samples_to": ("LATENT",), - "samples_from": ("LATENT",), - "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), - } - } + return {"required": { "samples_to": ("LATENT",), + "samples_from": ("LATENT",), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "feather": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + }} RETURN_TYPES = ("LATENT",) FUNCTION = "composite" CATEGORY = "latent" - def composite(self, samples_to, samples_from, x, y, feather): - output = samples_to.copy() - destination = samples_to["samples"].clone() - source = samples_from["samples"] - - left, top = (x // 8, y // 8) - right, bottom = (left + source.shape[3], top + source.shape[2],) + def composite(self, samples_to, samples_from, x, y, composite_method="normal", feather=0): + x = x // 8 + y = y // 8 feather = feather // 8 + samples_out = samples_to.copy() + s = samples_to["samples"].clone() + samples_to = samples_to["samples"] + samples_from = samples_from["samples"] + if feather == 0: + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] + else: + samples_from = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] + mask = torch.ones_like(samples_from) + for t in range(feather): + if y != 0: + mask[:,:,t:1+t,:] *= ((1.0/feather) * (t + 1)) - - - # calculate the bounds of the source that will be overlapping the destination - # this prevents the source trying to overwrite latent pixels that are out of bounds - # of the destination - visible_width, visible_height = (destination.shape[3] - left, destination.shape[2] - top,) - - mask = torch.ones_like(source) - - for f in range(feather): - feather_rate = (f + 1.0) / feather - - if left > 0: - mask[:, :, :, f] *= feather_rate - - if right < destination.shape[3] - 1: - mask[:, :, :, -f] *= feather_rate - - if top > 0: - mask[:, :, f, :] *= feather_rate - - if bottom < destination.shape[2] - 1: - mask[:, :, -f, :] *= feather_rate - - mask = mask[:, :, :visible_height, :visible_width] - inverse_mask = torch.ones_like(mask) - mask - - source_portion = mask * source[:, :, :visible_height, :visible_width] - destination_portion = inverse_mask * destination[:, :, top:bottom, left:right] - - destination[:, :, top:bottom, left:right] = source_portion + destination_portion - - output["samples"] = destination - - return (output,) + if y + samples_from.shape[2] < samples_to.shape[2]: + mask[:,:,mask.shape[2] -1 -t: mask.shape[2]-t,:] *= ((1.0/feather) * (t + 1)) + if x != 0: + mask[:,:,:,t:1+t] *= ((1.0/feather) * (t + 1)) + if x + samples_from.shape[3] < samples_to.shape[3]: + mask[:,:,:,mask.shape[3]- 1 - t: mask.shape[3]- t] *= ((1.0/feather) * (t + 1)) + rev_mask = torch.ones_like(mask) - mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] = samples_from[:,:,:samples_to.shape[2] - y, :samples_to.shape[3] - x] * mask + s[:,:,y:y+samples_from.shape[2],x:x+samples_from.shape[3]] * rev_mask + samples_out["samples"] = s + return (samples_out,) class LatentCrop: @classmethod From f48f0872e2310b1650f798d02e94264cc06afd69 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Fri, 14 Apr 2023 00:21:01 -0400 Subject: [PATCH 26/27] Refactor: move nodes_mask_convertion nodes to nodes_mask. --- comfy_extras/nodes_mask.py | 39 +++++++++++++++---- comfy_extras/nodes_mask_conversion.py | 54 --------------------------- nodes.py | 1 - 3 files changed, 31 insertions(+), 63 deletions(-) delete mode 100644 comfy_extras/nodes_mask_conversion.py diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index ab17fc509..60feea0db 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -59,23 +59,41 @@ class LatentCompositeMasked: class MaskToImage: @classmethod - def INPUT_TYPES(cls): + def INPUT_TYPES(s): return { - "required": { - "mask": ("MASK",), - } + "required": { + "mask": ("MASK",), + } } CATEGORY = "mask" RETURN_TYPES = ("IMAGE",) + FUNCTION = "mask_to_image" - FUNCTION = "convert" + def mask_to_image(self, mask): + result = mask[None, :, :, None].expand(-1, -1, -1, 3) + return (result,) - def convert(self, mask): - image = torch.cat([torch.reshape(mask.clone(), [1, mask.shape[0], mask.shape[1], 1,])] * 3, 3) +class ImageToMask: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "channel": (["red", "green", "blue"],), + } + } - return (image,) + CATEGORY = "mask" + + RETURN_TYPES = ("MASK",) + FUNCTION = "image_to_mask" + + def image_to_mask(self, image, channel): + channels = ["red", "green", "blue"] + mask = image[0, :, :, channels.index(channel)] + return (mask,) class SolidMask: @classmethod @@ -231,6 +249,7 @@ class FeatherMask: NODE_CLASS_MAPPINGS = { "LatentCompositeMasked": LatentCompositeMasked, "MaskToImage": MaskToImage, + "ImageToMask": ImageToMask, "SolidMask": SolidMask, "InvertMask": InvertMask, "CropMask": CropMask, @@ -238,3 +257,7 @@ NODE_CLASS_MAPPINGS = { "FeatherMask": FeatherMask, } +NODE_DISPLAY_NAME_MAPPINGS = { + "ImageToMask": "Convert Image to Mask", + "MaskToImage": "Convert Mask to Image", +} diff --git a/comfy_extras/nodes_mask_conversion.py b/comfy_extras/nodes_mask_conversion.py deleted file mode 100644 index 04dcbd0d9..000000000 --- a/comfy_extras/nodes_mask_conversion.py +++ /dev/null @@ -1,54 +0,0 @@ -import numpy as np -import torch -import torch.nn.functional as F -from PIL import Image - -import comfy.utils - -class ImageToMask: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "image": ("IMAGE",), - "channel": (["red", "green", "blue"],), - } - } - - CATEGORY = "image" - - RETURN_TYPES = ("MASK",) - FUNCTION = "image_to_mask" - - def image_to_mask(self, image, channel): - channels = ["red", "green", "blue"] - mask = image[0, :, :, channels.index(channel)] - return (mask,) - -class MaskToImage: - @classmethod - def INPUT_TYPES(s): - return { - "required": { - "mask": ("MASK",), - } - } - - CATEGORY = "image" - - RETURN_TYPES = ("IMAGE",) - FUNCTION = "mask_to_image" - - def mask_to_image(self, mask): - result = mask[None, :, :, None].expand(-1, -1, -1, 3) - return (result,) - -NODE_CLASS_MAPPINGS = { - "ImageToMask": ImageToMask, - "MaskToImage": MaskToImage, -} - -NODE_DISPLAY_NAME_MAPPINGS = { - "ImageToMask": "Convert Image to Mask", - "MaskToImage": "Convert Mask to Image", -} diff --git a/nodes.py b/nodes.py index aff03dd43..6468ac6b8 100644 --- a/nodes.py +++ b/nodes.py @@ -1193,4 +1193,3 @@ def init_custom_nodes(): load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_post_processing.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_mask.py")) - load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_mask_conversion.py")) From d98a4de9eb6b676bfe9c172e7310934148e16dd2 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Fri, 14 Apr 2023 00:49:19 -0400 Subject: [PATCH 27/27] LatentCompositeMasked: negative x, y don't work. --- comfy_extras/nodes_mask.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/comfy_extras/nodes_mask.py b/comfy_extras/nodes_mask.py index 60feea0db..4dfb0b93e 100644 --- a/comfy_extras/nodes_mask.py +++ b/comfy_extras/nodes_mask.py @@ -9,8 +9,8 @@ class LatentCompositeMasked: "required": { "destination": ("LATENT",), "source": ("LATENT",), - "x": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 8}), - "y": ("INT", {"default": 0, "min": -MAX_RESOLUTION, "max": MAX_RESOLUTION, "step": 8}), + "x": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), + "y": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 8}), }, "optional": { "mask": ("MASK",),