-
Notifications
You must be signed in to change notification settings - Fork 5
Expand file tree
/
Copy pathnodes.py
More file actions
366 lines (292 loc) · 18 KB
/
Copy pathnodes.py
File metadata and controls
366 lines (292 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
import torch
import os
from PIL import Image, ImageDraw
from .utils import get_config_folder_list, tensor2pil, pil2tensor, diffuserOutpaintSamples, get_device_by_name, get_dtype_by_name, clearVram, test_scheduler_scale_model_input
import folder_paths
from diffusers.models import UNet2DConditionModel
from diffusers import TCDScheduler
from .controlnet_union import ControlNetModel_Union
from diffusers.models.model_loading_utils import load_state_dict
from safetensors.torch import load_file
import logging
# Get the absolute path of various directories
my_dir = os.path.dirname(os.path.abspath(__file__))
def update_folder_names_and_paths(key, targets=[]):
# check for existing key
base = folder_paths.folder_names_and_paths.get(key, ([], {}))
base = base[0] if isinstance(base[0], (list, set, tuple)) else []
# find base key & add w/ fallback, sanity check + warning
target = next((x for x in targets if x in folder_paths.folder_names_and_paths), targets[0])
orig, _ = folder_paths.folder_names_and_paths.get(target, ([], {}))
folder_paths.folder_names_and_paths[key] = (orig or base, {".gguf"})
if base and base != orig:
logging.warning(f"Unknown file list already present on key {key}: {base}")
def can_expand(source_width, source_height, target_width, target_height, alignment):
"""Checks if the image can be expanded based on the alignment."""
if alignment in ("Left", "Right") and source_width >= target_width:
return False
if alignment in ("Top", "Bottom") and source_height >= target_height:
return False
return True
class PadImageForDiffusersOutpaint:
_alignment_options = ["Middle", "Left", "Right", "Top", "Bottom"]
_resize_option = ["Full", "50%", "33%", "25%", "Custom"]
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"width": ("INT", {"default": 720, "tooltip": "The width used for the image."}),
"height": ("INT", {"default": 1280, "tooltip": "The height used for the image."}),
"alignment": (s._alignment_options, {"tooltip": "Where the original image should be in the outpainted one"}),
"resize_image": (s._resize_option, {"tooltip": "Resize input image"}),
"custom_resize_image_percentage": ("INT", {"min": 1, "default": 50, "max": 100, "step": 1, "tooltip": "Custom resize (%)"}),
"mask_overlap_percentage": ("INT", {"min": 1, "default": 10, "max": 50, "step": 1, "tooltip": "Mask overlap (%)"}),
"overlap_left": ("BOOLEAN", {"default": True}),
"overlap_right": ("BOOLEAN", {"default": True}),
"overlap_top": ("BOOLEAN", {"default": True}),
"overlap_bottom": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("IMAGE", "MASK", "IMAGE")
RETURN_NAMES = ("IMAGE", "MASK", "diffuser_outpaint_cnet_image")
FUNCTION = "prepare_image_and_mask"
CATEGORY = "DiffusersOutpaint"
def prepare_image_and_mask(self, image, width, height, mask_overlap_percentage, resize_image, custom_resize_image_percentage, overlap_left, overlap_right, overlap_top, overlap_bottom, alignment="Middle"):
im=tensor2pil(image)
source=im.convert('RGB')
target_size = (width, height)
# Calculate the scaling factor to fit the image within the target size
scale_factor = min(target_size[0] / source.width, target_size[1] / source.height)
new_width = int(source.width * scale_factor)
new_height = int(source.height * scale_factor)
# Resize the source image to fit within target size
source = source.resize((new_width, new_height), Image.LANCZOS)
# Initialize new_width and new_height
new_width, new_height = source.width, source.height
# Apply resize option using percentages
if resize_image == "Full":
resize_percentage = 100
elif resize_image == "50%":
resize_percentage = 50
elif resize_image == "33%":
resize_percentage = 33
elif resize_image == "25%":
resize_percentage = 25
else: # Custom
resize_percentage = custom_resize_image_percentage
# Calculate new dimensions based on percentage
resize_factor = resize_percentage / 100
new_width = int(source.width * resize_factor)
new_height = int(source.height * resize_factor)
# Ensure minimum size of 64 pixels
new_width = max(new_width, 64)
new_height = max(new_height, 64)
# Resize the image
source = source.resize((new_width, new_height), Image.LANCZOS)
# Calculate the overlap in pixels based on the percentage
overlap_x = int(new_width * (mask_overlap_percentage / 100))
overlap_y = int(new_height * (mask_overlap_percentage / 100))
# Ensure minimum overlap of 1 pixel
overlap_x = max(overlap_x, 1)
overlap_y = max(overlap_y, 1)
# Calculate margins based on alignment
if alignment == "Middle":
margin_x = (target_size[0] - source.width) // 2
margin_y = (target_size[1] - source.height) // 2
elif alignment == "Left":
margin_x = 0
margin_y = (target_size[1] - source.height) // 2
elif alignment == "Right":
margin_x = target_size[0] - source.width
margin_y = (target_size[1] - source.height) // 2
elif alignment == "Top":
margin_x = (target_size[0] - source.width) // 2
margin_y = 0
elif alignment == "Bottom":
margin_x = (target_size[0] - source.width) // 2
margin_y = target_size[1] - source.height
# Adjust margins to eliminate gaps
margin_x = max(0, min(margin_x, target_size[0] - new_width))
margin_y = max(0, min(margin_y, target_size[1] - new_height))
# Create a new background image and paste the resized source image
background = Image.new('RGB', target_size, (255, 255, 255))
background.paste(source, (margin_x, margin_y))
image=pil2tensor(background)
#----------------------------------------------------
# Create the mask
d1, d2, d3, d4 = image.size()
left, top, bottom, right = 0, 0, 0, 0
# Image
new_image = torch.ones(
(d1, d2 + top + bottom, d3 + left + right, d4),
dtype=torch.float32,
) * 0.5
new_image[:, top:top + d2, left:left + d3, :] = image
im=tensor2pil(new_image)
pil_new_image=im.convert('RGB')
#----------------------------------------------------
# Create the mask
mask = Image.new('L', target_size, 255)
mask_draw = ImageDraw.Draw(mask)
#----------------------------------------------------
# Calculate overlap areas
white_gaps_patch = 2
left_overlap = margin_x + overlap_x if overlap_left else margin_x + white_gaps_patch
right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width - white_gaps_patch
top_overlap = margin_y + overlap_y if overlap_top else margin_y + white_gaps_patch
bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height - white_gaps_patch
#----------------------------------------------------
# Mask coordinates
if alignment == "Left":
left_overlap = margin_x + overlap_x if overlap_left else margin_x
elif alignment == "Right":
right_overlap = margin_x + new_width - overlap_x if overlap_right else margin_x + new_width
elif alignment == "Top":
top_overlap = margin_y + overlap_y if overlap_top else margin_y
elif alignment == "Bottom":
bottom_overlap = margin_y + new_height - overlap_y if overlap_bottom else margin_y + new_height
# Draw the mask
mask_draw.rectangle([
(left_overlap, top_overlap),
(right_overlap, bottom_overlap)
], fill=0)
tensor_mask=pil2tensor(mask)
#----------------------------------------------------
if not can_expand(background.width, background.height, width, height, alignment):
alignment = "Middle"
cnet_image = pil_new_image.copy() # copy background as cnet_image
cnet_image.paste(0, (0, 0), mask) # paste mask over cnet_image, cropping it a bit
tensor_cnet_image=pil2tensor(cnet_image)
return (new_image, tensor_mask, tensor_cnet_image,)
class LoadDiffuserModel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"unet_name": (folder_paths.get_filename_list("diffusion_models"), {"tooltip": "The name of the unet (model) to load."}),
"device": (["auto", "cuda", "cpu", "mps", "xpu", "meta"],{"default": "auto", "tooltip": "Device for inference, default is auto checked by comfyui"}),
"dtype": (["auto","fp16","bf16","fp32", "fp8_e4m3fn", "fp8_e4m3fnuz", "fp8_e5m2", "fp8_e5m2fnuz"],{"default":"auto", "tooltip": "Model precision for inference, default is auto checked by comfyui"}),
"model_type": (get_config_folder_list("configs"), {"default": "sdxl", "tooltip": "The json configs used for the unet. (Put unet config in \"configs/your model type/unet\", and scheduler config in \"configs/your model type/scheduler\")."}),
},
}
RETURN_TYPES = ("MODEL", "SCHEDULER")
RETURN_NAMES = ("model", "scheduler configs")
FUNCTION = "load"
CATEGORY = "DiffusersOutpaint"
def load(self, unet_name, device, dtype, model_type):
# Go 2 folders back
comfy_dir = os.path.dirname(os.path.dirname(my_dir))
unet_path = folder_paths.get_full_path_or_raise("diffusion_models", unet_name)
device = get_device_by_name(device)
dtype = get_dtype_by_name(dtype)
if model_type == "sdxl":
print("Loading sdxl unet...")
unet = UNet2DConditionModel.from_config(f"{comfy_dir}/custom_nodes/ComfyUI-DiffusersImageOutpaint/configs", subfolder=f"{model_type}/unet").to(device, dtype)
unet.load_state_dict(load_file(unet_path))
scheduler = TCDScheduler.from_config(f"{comfy_dir}/custom_nodes/ComfyUI-DiffusersImageOutpaint/configs", subfolder=f"{model_type}/scheduler")
scale_model_input_method = test_scheduler_scale_model_input(comfy_dir, model_type)
scheduler_configs = {
"scheduler": scheduler,
"scale_model_input_method": scale_model_input_method,
}
return (unet, scheduler_configs,)
class LoadDiffuserControlnet:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"controlnet_model": (folder_paths.get_filename_list("controlnet"), {"tooltip": "The controlnet model used for denoising the input latent."}),
"device": (["auto", "cuda", "cpu", "mps", "xpu", "meta"],{"default": "auto", "tooltip": "Device for inference, default is auto checked by comfyui"}),
"dtype": (["auto","fp16","bf16","fp32", "fp8_e4m3fn", "fp8_e4m3fnuz", "fp8_e5m2", "fp8_e5m2fnuz"],{"default":"auto", "tooltip": "Model precision for inference, default is auto checked by comfyui"}),
"controlnet_type": (get_config_folder_list("configs"), {"default": "controlnet-sdxl-promax", "tooltip": "The json configs used for controlnet. (Put config(s) in \"configs/your controlnet type\")."}),
},
}
RETURN_TYPES = ("CONTROL_NET",)
FUNCTION = "load"
CATEGORY = "DiffusersOutpaint"
def load(self, controlnet_model, device, dtype, controlnet_type):
# Go 2 folders back
comfy_dir = os.path.dirname(os.path.dirname(my_dir))
controlnet_path = folder_paths.get_full_path_or_raise("controlnet", controlnet_model)
device = get_device_by_name(device)
dtype = get_dtype_by_name(dtype)
if controlnet_type == "controlnet-sdxl-promax":
print("Loading controlnet-sdxl-promax...")
controlnet_model = ControlNetModel_Union.from_config(f"{comfy_dir}/custom_nodes/ComfyUI-DiffusersImageOutpaint/configs/{controlnet_type}/config_promax.json")
state_dict = load_state_dict(load_file(controlnet_path))
model, _, _, _, _ = ControlNetModel_Union._load_pretrained_model(
controlnet_model, state_dict, controlnet_path, controlnet_path
)
controlnet_model.to(device, dtype)
del model, state_dict, controlnet_path
clearVram(device)
return (controlnet_model,)
class EncodeDiffusersOutpaintPrompt:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"device": (["auto", "cuda", "cpu", "mps", "xpu", "meta"],{"default": "auto", "tooltip": "Device for inference, default is auto checked by comfyui"}),
"dtype": (["auto","fp16","bf16","fp32", "fp8_e4m3fn", "fp8_e4m3fnuz", "fp8_e5m2", "fp8_e5m2fnuz"],{"default":"auto", "tooltip": "Model precision for inference, default is auto checked by comfyui"}),
"text": ("STRING", {"multiline": True, "dynamicPrompts": True, "tooltip": "The text to be encoded."}),
"clip": ("CLIP", {"tooltip": "The CLIP model used for encoding the text."})
}
}
RETURN_TYPES = ("CONDITIONING",)
RETURN_NAMES = ("diffusers_conditioning",)
OUTPUT_TOOLTIPS = ("A conditioning containing the embedded text used to guide the diffusion model.",)
FUNCTION = "encode"
CATEGORY = "DiffusersOutpaint"
DESCRIPTION = "Encodes a text prompt using a CLIP model into an embedding that can be used to guide the diffusion model towards generating specific images."
def encode(self, device, dtype, text, clip):
device = get_device_by_name(device)
dtype = get_dtype_by_name(dtype)
text = f"{text}, high quality, 4k"
tokens = clip.tokenize(text)
output = clip.encode_from_tokens(tokens, return_pooled=True, return_dict=True)
prompt_embeds = output.pop("cond")
prompt_embeds = prompt_embeds.to(device, dtype=dtype)
pooled_prompt_embeds = output["pooled_output"].to(device, dtype=dtype)
bs_embed, seq_len, _ = prompt_embeds.shape
# duplicate text embeddings for each generation per prompt, using mps friendly method
prompt_embeds = prompt_embeds.repeat(1, 1, 1)
prompt_embeds = prompt_embeds.view(bs_embed * 1, seq_len, -1)
pooled_prompt_embeds = pooled_prompt_embeds.repeat(1, 1).view(bs_embed * 1, -1)
diffusers_conditioning = {
"prompt_embeds": prompt_embeds,
"pooled_prompt_embeds": pooled_prompt_embeds,
}
return (diffusers_conditioning,)
class DiffusersImageOutpaint:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"model": ("MODEL", {"tooltip": "The model used for denoising the input latent."}),
"scheduler_configs": ("SCHEDULER",),
"control_net": ("CONTROL_NET",),
"positive": ("CONDITIONING", {"tooltip": "The prompt describing what you want."}),
"negative": ("CONDITIONING", {"tooltip": "The prompt describing what you don't want."}),
"diffuser_outpaint_cnet_image": ("IMAGE", {"tooltip": "The image to outpaint."}),
"guidance_scale": ("FLOAT", {"default": 1.50, "min": 1.01, "max": 10, "step": 0.01, "tooltip": "The Classifier-Free Guidance scale balances creativity and adherence to the prompt. Higher values result in images more closely matching the prompt, however too high values will negatively impact quality."}),
"controlnet_strength": ("FLOAT", {"default": 1.00, "min": 0.00, "max": 10, "step": 0.01}),
"steps": ("INT", {"default": 8, "min": 4, "max": 20, "tooltip": "The number of steps used in the denoising process."}),
"device": (["auto", "cuda", "cpu", "mps", "xpu", "meta"],{"default": "auto", "tooltip": "Device for inference, default is auto checked by comfyui"}),
"dtype": (["auto","fp16","bf16","fp32", "fp8_e4m3fn", "fp8_e4m3fnuz", "fp8_e5m2", "fp8_e5m2fnuz"],{"default":"auto", "tooltip": "Model precision for inference, default is auto checked by comfyui"}),
"sequential_cpu_offload": ("BOOLEAN", {"default": False, "tooltip": "Inference by default needs around 8gb vram, if this option is on it will move controlnet and unet back and forth between cpu and vram, to have only one model loaded at a time (around 6 gb vram used), useful for gpus under 8gb but will impact inference speed."}),
},
}
RETURN_TYPES = ("LATENT",)
FUNCTION = "sample"
CATEGORY = "DiffusersOutpaint"
def sample(self, device, dtype, sequential_cpu_offload, scheduler_configs, model, control_net, positive, negative, diffuser_outpaint_cnet_image, guidance_scale, controlnet_strength, steps):
cnet_image = diffuser_outpaint_cnet_image
cnet_image=tensor2pil(cnet_image)
cnet_image=cnet_image.convert('RGB')
keep_model_device = sequential_cpu_offload
scheduler = scheduler_configs["scheduler"]
scale_model_input_method = scheduler_configs["scale_model_input_method"]
last_rgb_latent = diffuserOutpaintSamples(device, dtype, keep_model_device, scheduler, scale_model_input_method, model, control_net, positive, negative,
cnet_image, controlnet_strength, guidance_scale, steps)
return ({"samples":last_rgb_latent},)