diff --git a/README.en.md b/README.en.md index 5b3a249..b735a9d 100644 --- a/README.en.md +++ b/README.en.md @@ -35,6 +35,7 @@ **v1.1.7** +- Added `easy imageSplitGrid` - `easy kSamplerInpainting` added options such as different diffusion and brushnet in **additional** widget - Support for brushnet model loading - [ComfyUI-BrushNet](https://github.com/nullquant/ComfyUI-BrushNet) - Added `easy applyFooocusInpaint` - Replace FooocusInpaintLoader diff --git a/README.md b/README.md index c56b755..6bb6bd2 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ **v1.1.7** +- 增加 `easy imageSplitGrid` - 图像网格拆分 - `easy kSamplerInpainting` 的 **additional** 属性增加差异扩散和brushnet等相关选项 - 增加 brushnet模型加载的支持 - [ComfyUI-BrushNet](https://github.com/nullquant/ComfyUI-BrushNet) - 增加 `easy applyFooocusInpaint` - Fooocus内补节点 替代原有的 FooocusInpaintLoader diff --git a/py/config.py b/py/config.py index 24d9ba1..2665618 100644 --- a/py/config.py +++ b/py/config.py @@ -153,6 +153,20 @@ LAYER_DIFFUSION = { }, } +# IC Light +IC_LIGHT_DIR = os.path.join(folder_paths.models_dir, "ic_light") +IC_LIGHT_MODELS = { + "iclight_fc": { + "sd1": { + "model_url": "https://huggingface.co/lllyasviel/ic-light/resolve/main/iclight_sd15_fc.safetensors" + }, + "sdxl": { + "model_url": None + } + } +} + + # REMBG REMBG_DIR = os.path.join(folder_paths.models_dir, "rembg") REMBG_MODELS = { diff --git a/py/easyNodes.py b/py/easyNodes.py index 3db3267..ed2e4b7 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -1,4 +1,4 @@ -import sys, os, re, json, time, math, copy +import sys, os, re, json, time import torch import folder_paths import comfy.utils, comfy.sample, comfy.samplers, comfy.controlnet, comfy.model_base, comfy.model_management @@ -16,7 +16,7 @@ from PIL import Image from server import PromptServer from nodes import MAX_RESOLUTION, LatentFromBatch, RepeatLatentBatch, NODE_CLASS_MAPPINGS as ALL_NODE_CLASS_MAPPINGS, ConditioningSetMask, ConditioningConcat, CLIPTextEncode, VAEEncodeForInpaint, InpaintModelConditioning -from .config import MAX_SEED_NUM, BASE_RESOLUTIONS, RESOURCES_DIR, INPAINT_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_INPAINT_HEAD, FOOOCUS_INPAINT_PATCH, BRUSHNET_MODELS, IPADAPTER_DIR, IPADAPTER_MODELS, DYNAMICRAFTER_DIR, DYNAMICRAFTER_MODELS +from .config import MAX_SEED_NUM, BASE_RESOLUTIONS, RESOURCES_DIR, INPAINT_DIR, FOOOCUS_STYLES_DIR, FOOOCUS_INPAINT_HEAD, FOOOCUS_INPAINT_PATCH, BRUSHNET_MODELS, IPADAPTER_DIR, IPADAPTER_MODELS, DYNAMICRAFTER_DIR, DYNAMICRAFTER_MODELS, IC_LIGHT_DIR, IC_LIGHT_MODELS from .log import log_node_info, log_node_error, log_node_warn from .wildcards import process_with_loras, get_wildcard_list, process from .adv_encode import advanced_encode @@ -52,6 +52,7 @@ add_folder_path_and_extensions("ipadapter", [os.path.join(model_path, "ipadapter add_folder_path_and_extensions("dynamicrafter_models", [os.path.join(model_path, "dynamicrafter_models")], folder_paths.supported_pt_extensions) add_folder_path_and_extensions("mediapipe", [os.path.join(model_path, "mediapipe")], set(['.tflite','.pth'])) add_folder_path_and_extensions("t5", [os.path.join(model_path, "t5")], set(['.safetensors','.bin','.json'])) +add_folder_path_and_extensions("ic_light", [os.path.join(model_path, "ic_light")], set(['.safetensors','.bin','.json'])) add_folder_path_and_extensions("checkpoints_thumb", [os.path.join(model_path, "checkpoints")], image_suffixs) add_folder_path_and_extensions("loras_thumb", [os.path.join(model_path, "loras")], image_suffixs) @@ -2087,34 +2088,6 @@ class LLLiteLoader: # FooocusInpaint from .fooocus import InpaintHead, InpaintWorker inpaint_head_model = None -# class fooocusInpaintLoader: -# @classmethod -# def INPUT_TYPES(s): -# return { -# "required": { -# "head": (list(FOOOCUS_INPAINT_HEAD.keys()),), -# "patch": (list(FOOOCUS_INPAINT_PATCH.keys()),), -# } -# } -# -# RETURN_TYPES = ("INPAINT_PATCH",) -# RETURN_NAMES = ("patch",) -# CATEGORY = "EasyUse/Inpaint" -# FUNCTION = "apply" -# -# def apply(self, head, patch): -# global inpaint_head_model -# -# head_file = get_local_filepath(FOOOCUS_INPAINT_HEAD[head]["model_url"], INPAINT_DIR) -# if inpaint_head_model is None: -# inpaint_head_model = InpaintHead() -# sd = torch.load(head_file, map_location='cpu') -# inpaint_head_model.load_state_dict(sd) -# -# patch_file = get_local_filepath(FOOOCUS_INPAINT_PATCH[patch]["model_url"], INPAINT_DIR) -# inpaint_lora = comfy.utils.load_torch_file(patch_file, safe_load=True) -# -# return ((inpaint_head_model, inpaint_lora),) class applyFooocusInpaint: @classmethod @@ -2155,6 +2128,8 @@ class applyFooocusInpaint: #---------------------------------------------------------------适配器 开始----------------------------------------------------------------------# + +# 风格对齐 from .libs.styleAlign import styleAlignBatch, SHARE_NORM_OPTIONS, SHARE_ATTN_OPTIONS class styleAlignedBatchAlign: @@ -2176,6 +2151,33 @@ class styleAlignedBatchAlign: def align(self, model, share_norm, share_attn, scale): return (styleAlignBatch(model, share_norm, share_attn, scale),) +# 光照对齐 +from .ic_light.func import ICLight +class icLightApply: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "mode": (list(IC_LIGHT_MODELS.keys()),), + "model": ("MODEL",), + "latent": ("LATENT",), + }, + } + + RETURN_TYPES = ("MODEL",) + FUNCTION = "apply" + CATEGORY = "EasyUse/Adapter" + + def apply(self, mode, model, latent): + model_type = get_sd_version(model) + if model_type == 'sdxl': + raise Exception("IC Light model is not supported for SDXL") + model_path = get_local_filepath(IC_LIGHT_MODELS[mode]['sd1']["model_url"], IC_LIGHT_DIR) + m = ICLight().apply(model_path, model, latent) + return (m,) + + def insightface_loader(provider): try: from insightface.app import FaceAnalysis @@ -5687,8 +5689,7 @@ class detailerFix: del pipe if image_output in ("Hide", "Hide&Save"): - return {"ui": {}, - "result": (new_pipe, result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, result_mask, result_cnet_images )} + return (new_pipe, result_img, result_cropped_enhanced, result_cropped_enhanced_alpha, result_mask, result_cnet_images) if image_output in ("Sender", "Sender&Save"): PromptServer.instance.send_sync("img-send", {"link_id": link_id, "images": results}) @@ -7154,6 +7155,7 @@ NODE_CLASS_MAPPINGS = { "easy instantIDApply": instantIDApply, "easy instantIDApplyADV": instantIDApplyAdvanced, "easy styleAlignedBatchAlign": styleAlignedBatchAlign, + "easy icLightApply": icLightApply, # Inpaint 内补 # "easy fooocusInpaintLoader": fooocusInpaintLoader, "easy applyFooocusInpaint": applyFooocusInpaint, @@ -7261,6 +7263,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy instantIDApply": "Easy Apply InstantID", "easy instantIDApplyADV": "Easy Apply InstantID (Advanced)", "easy styleAlignedBatchAlign": "Easy Apply StyleAlign", + "easy icLightApply": "Easy Apply ICLight", # Inpaint 内补 # "easy fooocusInpaintLoader": "Load Fooocus Inpaint(Removed)", "easy applyFooocusInpaint": "Apply Fooocus Inpaint", diff --git a/py/ic_light/func.py b/py/ic_light/func.py new file mode 100644 index 0000000..799845b --- /dev/null +++ b/py/ic_light/func.py @@ -0,0 +1,96 @@ +import os +import torch +import safetensors.torch +from typing import Tuple, TypedDict, Callable, NamedTuple + +import folder_paths +import comfy.model_management +from comfy.diffusers_convert import convert_unet_state_dict +from comfy.model_patcher import ModelPatcher +from comfy.ldm.modules.diffusionmodules.openaimodel import UNetModel +from comfy.model_base import BaseModel +from comfy.conds import CONDRegular + +class UnetParams(TypedDict): + input: torch.Tensor + timestep: torch.Tensor + c: dict + cond_or_uncond: torch.Tensor + +class ICLight: + + @staticmethod + def apply_c_concat(cond, uncond, c_concat: torch.Tensor): + def write_c_concat(cond): + new_cond = [] + for t in cond: + n = [t[0], t[1].copy()] + if "model_conds" not in n[1]: + n[1]["model_conds"] = {} + n[1]["model_conds"]["c_concat"] = CONDRegular(c_concat) + new_cond.append(n) + return new_cond + + return (write_c_concat(cond), write_c_concat(uncond)) + + @staticmethod + def create_custom_conv( + original_conv: torch.nn.Module, + dtype: torch.dtype, + device=torch.device, + ) -> torch.nn.Module: + with torch.no_grad(): + new_conv_in = torch.nn.Conv2d( + 8, + original_conv.out_channels, + original_conv.kernel_size, + original_conv.stride, + original_conv.padding, + ) + new_conv_in.weight.zero_() + new_conv_in.weight[:, :4, :, :].copy_(original_conv.weight) + new_conv_in.bias = original_conv.bias + return new_conv_in.to(dtype=dtype, device=device) + + + def apply(self, model_path, model: ModelPatcher, c_concat: dict,) -> Tuple[ModelPatcher]: + + device = comfy.model_management.get_torch_device() + dtype = comfy.model_management.unet_dtype() + work_model = model.clone() + c_concat_samples: torch.Tensor = c_concat["samples"] + + def wrapped_unet(unet_apply: Callable, params: UnetParams): + # Apply concat. + sample = params["input"] + params["c"]["c_concat"] = torch.cat( + ( + [c_concat_samples.to(sample.device)] + * (sample.shape[0] // c_concat_samples.shape[0]) + ) + + params["c"].get("c_concat", []), + dim=0, + ) + return unet_apply(x=sample, t=params["timestep"], **params["c"]) + + work_model.add_object_patch( + "diffusion_model.input_blocks.0.0", + self.create_custom_conv( + original_conv=work_model.get_model_object("diffusion_model.input_blocks.0.0"), + dtype=dtype, + device=device, + ), + ) + work_model.set_model_unet_function_wrapper(wrapped_unet) + sd_offset = convert_unet_state_dict(safetensors.torch.load_file(model_path)) + + work_model.add_patches( + patches={ + ("diffusion_model." + key): ( + sd_offset[key].to(dtype=dtype, device=device), + ) + for key in sd_offset.keys() + } + ) + + return work_model \ No newline at end of file diff --git a/py/image.py b/py/image.py index f544d5d..3d0533c 100644 --- a/py/image.py +++ b/py/image.py @@ -540,6 +540,41 @@ class imageSplitList: new_images[1].append(img) return new_images +class imageSplitGrid: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "images": ("IMAGE",), + "row": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}), + "column": ("INT", {"default": 1,"min": 1,"max": 10,"step": 1,}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "doit" + CATEGORY = "EasyUse/Image" + + def crop(self, image, width, height, x, y): + x = min(x, image.shape[2] - 1) + y = min(y, image.shape[1] - 1) + to_x = width + x + to_y = height + y + img = image[:, y:to_y, x:to_x, :] + return img + + def doit(self, images, row, column): + _, height, width, _ = images.shape + sub_width = width // column + sub_height = height // row + new_images = [] + for i in range(row): + for j in range(column): + new_images.append(self.crop(images, sub_width, sub_height, j * sub_width, i * sub_height)) + + return (torch.cat(new_images, dim=0),) + class imagesSplitImage: @classmethod def INPUT_TYPES(s): @@ -558,6 +593,34 @@ class imagesSplitImage: new_images = torch.chunk(images, len(images), dim=0) return new_images + +class imageConcat: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "image1": ("IMAGE",), + "image2": ("IMAGE",), + "direction": (['right','down','left','up',],{"default": 'right'}), + "match_image_size": ("BOOLEAN", {"default": False}), + }} + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "concat" + CATEGORY = "EasyUse/image" + + def concat(self, image1, image2, direction, match_image_size): + if match_image_size: + image2 = torch.nn.functional.interpolate(image2, size=(image1.shape[2], image1.shape[3]), mode="bilinear") + if direction == 'right': + row = torch.cat((image1, image2), dim=2) + elif direction == 'down': + row = torch.cat((image1, image2), dim=1) + elif direction == 'left': + row = torch.cat((image2, image1), dim=2) + elif direction == 'up': + row = torch.cat((image2, image1), dim=1) + return (row,) + # 图片背景移除 from .briaai.rembg import BriaRMBG, preprocess_image, postprocess_image from .libs.utils import get_local_filepath, easySave, install_package @@ -633,12 +696,12 @@ class imageChooser(PreviewImage): def INPUT_TYPES(self): return { "required":{ - + "mode": (['Always Pause', 'Keep Last Selection'], {"default": "Always Pause"}), }, "optional": { "images": ("IMAGE",), }, - "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID", "extra_pnginfo": "EXTRA_PNGINFO"}, } RETURN_TYPES = ("IMAGE",) @@ -661,8 +724,7 @@ class imageChooser(PreviewImage): else: return None - def chooser(self, prompt=None, my_unique_id=None, **kwargs): - + def chooser(self, prompt=None, my_unique_id=None, extra_pnginfo=None, **kwargs): id = my_unique_id[0] if id not in ChooserMessage.stash: ChooserMessage.stash[id] = {} @@ -685,9 +747,19 @@ class imageChooser(PreviewImage): images = result['ui']['images'] PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images}) + # 获取上次选择 + mode = kwargs.pop('mode', 'Always Pause') + last_choosen = None + if mode == 'Keep Last Selection': + if id and extra_pnginfo[0] and "workflow" in extra_pnginfo[0]: + workflow = extra_pnginfo[0]["workflow"] + node = next((x for x in workflow["nodes"] if str(x["id"]) == id), None) + if node: + last_choosen = node['properties']['values'] + # wait for selection try: - selections = ChooserMessage.waitForMessage(id, asList=True) + selections = ChooserMessage.waitForMessage(id, asList=True) if last_choosen is None or len(last_choosen)<1 else last_choosen choosen = [x for x in selections if x >= 0] if len(selections)>1 else [0] except ChooserCancelled: raise comfy.model_management.InterruptProcessingException() @@ -1332,7 +1404,9 @@ NODE_CLASS_MAPPINGS = { "easy imageScaleDownToSize": imageScaleDownToSize, "easy imageRatio": imageRatio, "easy imageToMask": imageToMask, + "easy imageConcat": imageConcat, "easy imageSplitList": imageSplitList, + "easy imageSplitGrid": imageSplitGrid, "easy imagesSplitImage": imagesSplitImage, "easy imageCropFromMask": imageCropFromMask, "easy imageUncropFromBBOX": imageUncropFromBBOX, @@ -1362,7 +1436,9 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy imageRatio": "ImageRatio", "easy imageToMask": "ImageToMask", "easy imageHSVMask": "ImageHSVMask", + "easy imageConcat": "imageConcat", "easy imageSplitList": "imageSplitList", + "easy imageSplitGrid": "imageSplitGrid", "easy imagesSplitImage": "imagesSplitImage", "easy imageCropFromMask": "imageCropFromMask", "easy imageUncropFromBBOX": "imageUncropFromBBOX", diff --git a/web/css/chooser.css b/web/css/chooser.css index 3841061..0c1696b 100644 --- a/web/css/chooser.css +++ b/web/css/chooser.css @@ -27,4 +27,9 @@ } .easyuse-chooser-dialog-images img.selected{ border: 4px solid var(--success-color); +} + +.easyuse-chooser-hidden{ + display: none; + height:0; } \ No newline at end of file diff --git a/web/css/toolbar.css b/web/css/toolbar.css index 6243581..d297d65 100644 --- a/web/css/toolbar.css +++ b/web/css/toolbar.css @@ -139,7 +139,7 @@ } .easyuse-guide-dialog p{ margin:4px 0; - font-size: 10px; + font-size: 12px; font-weight: 300; } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { @@ -161,7 +161,7 @@ font-size: 14px; } .markdown-body h4{ - font-size: 12px; + font-size: 13px; } .markdown-body table { display: block; @@ -183,7 +183,7 @@ border-bottom: 1px solid var(--border-color); } .markdown-body table td { - font-size: 10px; + font-size: 12px; } .markdown-body table th:last-child, .markdown-body table td:last-child{ border-right: none; @@ -199,9 +199,6 @@ .markdown-body table th:last-child{ width:100%; } -.markdown-body a{ - margin-right: 10px; -} .markdown-body .warning{ color:var(--warning-color) } diff --git a/web/js/easy/easy.js b/web/js/easy/easy.js index 7274d3c..6bd4257 100644 --- a/web/js/easy/easy.js +++ b/web/js/easy/easy.js @@ -29,12 +29,15 @@ api.addEventListener("easyuse-toast",event=>{ let draggerEl = null let isGroupMapcanMove = true function createGroupMap(){ + let div = document.querySelector('#easyuse_groups_map') + if(div){ + div.style.display = div.style.display == 'none' ? 'flex' : 'none' + return + } let groups = app.canvas.graph._groups let nodes = app.canvas.graph._nodes let old_nodes = groups.length - let div = - document.querySelector('#easyuse_groups_map') || - document.createElement('div') + div = document.createElement('div') div.id = 'easyuse_groups_map' div.innerHTML = '' let btn = document.createElement('div') @@ -381,10 +384,11 @@ class GuideDialog { } } +const getEnableToolBar = _ => app.ui.settings.getSettingValue(toolBarId, true) const toolBarId = "Comfy.EasyUse.toolBar" -let enableToolBar = true +let enableToolBar = getEnableToolBar() let disableRenderInfo = localStorage['Comfy.Settings.Comfy.EasyUse.disableRenderInfo'] ? true : false export function addToolBar(app) { app.ui.settings.addSetting({ @@ -400,7 +404,6 @@ export function addToolBar(app) { }, }); } -const getEnableToolBar = _ => app.ui.settings.getSettingValue(toolBarId, true) let note = null let toolbar = null diff --git a/web/js/image_chooser/chooser.js b/web/js/image_chooser/chooser.js index c1ce41a..15056e3 100644 --- a/web/js/image_chooser/chooser.js +++ b/web/js/image_chooser/chooser.js @@ -72,6 +72,10 @@ class chooserImageDialog extends ComfyDialog { function progressButtonPressed() { const node = app.graph._nodes_by_id[this.node_id]; if (node) { + const selected = [...node.selected] + if(selected?.length>0){ + node.setProperty('values',selected) + } if (FlowState.paused()) { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); } @@ -79,10 +83,26 @@ function progressButtonPressed() { skip_next_restart_message(); restart_from_here(node.id).then(() => { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); }); } + const maxlength = node.imgs.length; + if (FlowState.paused_here(node.id) && selected>0) { + node.send_button_widget.name = (selected>1) ? "Progress selected (" + selected + '/' + maxlength +")" : "Progress selected image"; + } else if (FlowState.idle() && selected>0) { + node.send_button_widget.name = (selected>1) ? "Progress selected (" + selected + '/' + maxlength +")" : "Progress selected image as restart"; + } + else { + node.send_button_widget.name = ""; + } } } -function cancelButtonPressed() { if (FlowState.running()) { send_cancel(); } } +function cancelButtonPressed() { + if (FlowState.running()) { send_cancel();} + const node = app.graph._nodes_by_id[this.node_id]; + if (node) { + node.send_button_widget.name = ""; + node.cancel_button_widget.name = ""; + } +} app.registerExtension({ name:'comfy.easyuse.imageChooser', @@ -126,9 +146,12 @@ app.registerExtension({ }, async nodeCreated(node, app) { + if(node.comfyClass == 'easy imageChooser'){ node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed, {serialize: false}); node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed, {serialize: false}); + node.setProperty('values',[]) + /* Capture clicks */ const org_onMouseDown = node.onMouseDown;