Skip to content

Commit 1c0067f

Browse files
authored
Merge branch 'main' into lstein/feat/multi-gpu
2 parents c3d1252 + abb3bb9 commit 1c0067f

File tree

175 files changed

+7047
-1060
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

175 files changed

+7047
-1060
lines changed

README.md

Lines changed: 69 additions & 384 deletions
Large diffs are not rendered by default.

docs/features/CONFIGURATION.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,13 +51,11 @@ The settings in this file will override the defaults. You only need
5151
to change this file if the default for a particular setting doesn't
5252
work for you.
5353
54+
You'll find an example file next to `invokeai.yaml` that shows the default values.
55+
5456
Some settings, like [Model Marketplace API Keys], require the YAML
5557
to be formatted correctly. Here is a [basic guide to YAML files].
5658

57-
You can fix a broken `invokeai.yaml` by deleting it and running the
58-
configuration script again -- option [6] in the launcher, "Re-run the
59-
configure script".
60-
6159
#### Custom Config File Location
6260

6361
You can use any config file with the `--config` CLI arg. Pass in the path to the `invokeai.yaml` file you want to use.

invokeai/app/invocations/controlnet_image_processors.py

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,22 +35,16 @@
3535
from invokeai.app.invocations.primitives import ImageOutput
3636
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
3737
from invokeai.app.services.shared.invocation_context import InvocationContext
38+
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES, heuristic_resize
3839
from invokeai.backend.image_util.canny import get_canny_edges
3940
from invokeai.backend.image_util.depth_anything import DepthAnythingDetector
4041
from invokeai.backend.image_util.dw_openpose import DWOpenposeDetector
4142
from invokeai.backend.image_util.hed import HEDProcessor
4243
from invokeai.backend.image_util.lineart import LineartProcessor
4344
from invokeai.backend.image_util.lineart_anime import LineartAnimeProcessor
45+
from invokeai.backend.image_util.util import np_to_pil, pil_to_np
4446

45-
from .baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
46-
47-
CONTROLNET_MODE_VALUES = Literal["balanced", "more_prompt", "more_control", "unbalanced"]
48-
CONTROLNET_RESIZE_VALUES = Literal[
49-
"just_resize",
50-
"crop_resize",
51-
"fill_resize",
52-
"just_resize_simple",
53-
]
47+
from .baseinvocation import BaseInvocation, BaseInvocationOutput, Classification, invocation, invocation_output
5448

5549

5650
class ControlField(BaseModel):
@@ -641,3 +635,27 @@ def run_processor(self, image: Image.Image):
641635
resolution=self.image_resolution,
642636
)
643637
return processed_image
638+
639+
640+
@invocation(
641+
"heuristic_resize",
642+
title="Heuristic Resize",
643+
tags=["image, controlnet"],
644+
category="image",
645+
version="1.0.0",
646+
classification=Classification.Prototype,
647+
)
648+
class HeuristicResizeInvocation(BaseInvocation):
649+
"""Resize an image using a heuristic method. Preserves edge maps."""
650+
651+
image: ImageField = InputField(description="The image to resize")
652+
width: int = InputField(default=512, gt=0, description="The width to resize to (px)")
653+
height: int = InputField(default=512, gt=0, description="The height to resize to (px)")
654+
655+
def invoke(self, context: InvocationContext) -> ImageOutput:
656+
image = context.images.get_pil(self.image.image_name, "RGB")
657+
np_img = pil_to_np(image)
658+
np_resized = heuristic_resize(np_img, (self.width, self.height))
659+
resized = np_to_pil(np_resized)
660+
image_dto = context.images.save(image=resized)
661+
return ImageOutput.build(image_dto)

invokeai/app/invocations/latent.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
from invokeai.backend.ip_adapter.ip_adapter import IPAdapter, IPAdapterPlus
5252
from invokeai.backend.lora import LoRAModelRaw
5353
from invokeai.backend.model_manager import BaseModelType, LoadedModel
54+
from invokeai.backend.model_manager.config import MainConfigBase, ModelVariantType
5455
from invokeai.backend.model_patcher import ModelPatcher
5556
from invokeai.backend.stable_diffusion import PipelineIntermediateState, set_seamless
5657
from invokeai.backend.stable_diffusion.diffusion.conditioning_data import (
@@ -185,7 +186,7 @@ class GradientMaskOutput(BaseInvocationOutput):
185186
title="Create Gradient Mask",
186187
tags=["mask", "denoise"],
187188
category="latents",
188-
version="1.0.0",
189+
version="1.1.0",
189190
)
190191
class CreateGradientMaskInvocation(BaseInvocation):
191192
"""Creates mask for denoising model run."""
@@ -198,6 +199,32 @@ class CreateGradientMaskInvocation(BaseInvocation):
198199
minimum_denoise: float = InputField(
199200
default=0.0, ge=0, le=1, description="Minimum denoise level for the coherence region", ui_order=4
200201
)
202+
image: Optional[ImageField] = InputField(
203+
default=None,
204+
description="OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE",
205+
title="[OPTIONAL] Image",
206+
ui_order=6,
207+
)
208+
unet: Optional[UNetField] = InputField(
209+
description="OPTIONAL: If the Unet is a specialized Inpainting model, masked_latents will be generated from the image with the VAE",
210+
default=None,
211+
input=Input.Connection,
212+
title="[OPTIONAL] UNet",
213+
ui_order=5,
214+
)
215+
vae: Optional[VAEField] = InputField(
216+
default=None,
217+
description="OPTIONAL: Only connect for specialized Inpainting models, masked_latents will be generated from the image with the VAE",
218+
title="[OPTIONAL] VAE",
219+
input=Input.Connection,
220+
ui_order=7,
221+
)
222+
tiled: bool = InputField(default=False, description=FieldDescriptions.tiled, ui_order=8)
223+
fp32: bool = InputField(
224+
default=DEFAULT_PRECISION == "float32",
225+
description=FieldDescriptions.fp32,
226+
ui_order=9,
227+
)
201228

202229
@torch.no_grad()
203230
def invoke(self, context: InvocationContext) -> GradientMaskOutput:
@@ -233,8 +260,27 @@ def invoke(self, context: InvocationContext) -> GradientMaskOutput:
233260
expanded_mask_image = Image.fromarray((expanded_mask.squeeze(0).numpy() * 255).astype(np.uint8), mode="L")
234261
expanded_image_dto = context.images.save(expanded_mask_image)
235262

263+
masked_latents_name = None
264+
if self.unet is not None and self.vae is not None and self.image is not None:
265+
# all three fields must be present at the same time
266+
main_model_config = context.models.get_config(self.unet.unet.key)
267+
assert isinstance(main_model_config, MainConfigBase)
268+
if main_model_config.variant is ModelVariantType.Inpaint:
269+
mask = blur_tensor
270+
vae_info: LoadedModel = context.models.load(self.vae.vae)
271+
image = context.images.get_pil(self.image.image_name)
272+
image_tensor = image_resized_to_grid_as_tensor(image.convert("RGB"))
273+
if image_tensor.dim() == 3:
274+
image_tensor = image_tensor.unsqueeze(0)
275+
img_mask = tv_resize(mask, image_tensor.shape[-2:], T.InterpolationMode.BILINEAR, antialias=False)
276+
masked_image = image_tensor * torch.where(img_mask < 0.5, 0.0, 1.0)
277+
masked_latents = ImageToLatentsInvocation.vae_encode(
278+
vae_info, self.fp32, self.tiled, masked_image.clone()
279+
)
280+
masked_latents_name = context.tensors.save(tensor=masked_latents)
281+
236282
return GradientMaskOutput(
237-
denoise_mask=DenoiseMaskField(mask_name=mask_name, masked_latents_name=None, gradient=True),
283+
denoise_mask=DenoiseMaskField(mask_name=mask_name, masked_latents_name=masked_latents_name, gradient=True),
238284
expanded_mask_area=ImageField(image_name=expanded_image_dto.image_name),
239285
)
240286

@@ -295,7 +341,7 @@ class DenoiseLatentsInvocation(BaseInvocation):
295341
)
296342
steps: int = InputField(default=10, gt=0, description=FieldDescriptions.steps)
297343
cfg_scale: Union[float, List[float]] = InputField(
298-
default=7.5, ge=1, description=FieldDescriptions.cfg_scale, title="CFG Scale"
344+
default=7.5, description=FieldDescriptions.cfg_scale, title="CFG Scale"
299345
)
300346
denoising_start: float = InputField(
301347
default=0.0,
@@ -517,6 +563,11 @@ def get_conditioning_data(
517563
dtype=unet.dtype,
518564
)
519565

566+
if isinstance(self.cfg_scale, list):
567+
assert (
568+
len(self.cfg_scale) == self.steps
569+
), "cfg_scale (list) must have the same length as the number of steps"
570+
520571
conditioning_data = TextConditioningData(
521572
uncond_text=uncond_text_embedding,
522573
cond_text=cond_text_embedding,

invokeai/app/invocations/mask.py

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import numpy as np
12
import torch
23

3-
from invokeai.app.invocations.baseinvocation import BaseInvocation, InvocationContext, invocation
4-
from invokeai.app.invocations.fields import InputField, TensorField, WithMetadata
4+
from invokeai.app.invocations.baseinvocation import BaseInvocation, Classification, InvocationContext, invocation
5+
from invokeai.app.invocations.fields import ImageField, InputField, TensorField, WithMetadata
56
from invokeai.app.invocations.primitives import MaskOutput
67

78

@@ -34,3 +35,86 @@ def invoke(self, context: InvocationContext) -> MaskOutput:
3435
width=self.width,
3536
height=self.height,
3637
)
38+
39+
40+
@invocation(
41+
"alpha_mask_to_tensor",
42+
title="Alpha Mask to Tensor",
43+
tags=["conditioning"],
44+
category="conditioning",
45+
version="1.0.0",
46+
classification=Classification.Beta,
47+
)
48+
class AlphaMaskToTensorInvocation(BaseInvocation):
49+
"""Convert a mask image to a tensor. Opaque regions are 1 and transparent regions are 0."""
50+
51+
image: ImageField = InputField(description="The mask image to convert.")
52+
invert: bool = InputField(default=False, description="Whether to invert the mask.")
53+
54+
def invoke(self, context: InvocationContext) -> MaskOutput:
55+
image = context.images.get_pil(self.image.image_name)
56+
mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
57+
if self.invert:
58+
mask[0] = torch.tensor(np.array(image)[:, :, 3] == 0, dtype=torch.bool)
59+
else:
60+
mask[0] = torch.tensor(np.array(image)[:, :, 3] > 0, dtype=torch.bool)
61+
62+
return MaskOutput(
63+
mask=TensorField(tensor_name=context.tensors.save(mask)),
64+
height=mask.shape[1],
65+
width=mask.shape[2],
66+
)
67+
68+
69+
@invocation(
70+
"invert_tensor_mask",
71+
title="Invert Tensor Mask",
72+
tags=["conditioning"],
73+
category="conditioning",
74+
version="1.0.0",
75+
classification=Classification.Beta,
76+
)
77+
class InvertTensorMaskInvocation(BaseInvocation):
78+
"""Inverts a tensor mask."""
79+
80+
mask: TensorField = InputField(description="The tensor mask to convert.")
81+
82+
def invoke(self, context: InvocationContext) -> MaskOutput:
83+
mask = context.tensors.load(self.mask.tensor_name)
84+
inverted = ~mask
85+
86+
return MaskOutput(
87+
mask=TensorField(tensor_name=context.tensors.save(inverted)),
88+
height=inverted.shape[1],
89+
width=inverted.shape[2],
90+
)
91+
92+
93+
@invocation(
94+
"image_mask_to_tensor",
95+
title="Image Mask to Tensor",
96+
tags=["conditioning"],
97+
category="conditioning",
98+
version="1.0.0",
99+
)
100+
class ImageMaskToTensorInvocation(BaseInvocation, WithMetadata):
101+
"""Convert a mask image to a tensor. Converts the image to grayscale and uses thresholding at the specified value."""
102+
103+
image: ImageField = InputField(description="The mask image to convert.")
104+
cutoff: int = InputField(ge=0, le=255, description="Cutoff (<)", default=128)
105+
invert: bool = InputField(default=False, description="Whether to invert the mask.")
106+
107+
def invoke(self, context: InvocationContext) -> MaskOutput:
108+
image = context.images.get_pil(self.image.image_name, mode="L")
109+
110+
mask = torch.zeros((1, image.height, image.width), dtype=torch.bool)
111+
if self.invert:
112+
mask[0] = torch.tensor(np.array(image)[:, :] >= self.cutoff, dtype=torch.bool)
113+
else:
114+
mask[0] = torch.tensor(np.array(image)[:, :] < self.cutoff, dtype=torch.bool)
115+
116+
return MaskOutput(
117+
mask=TensorField(tensor_name=context.tensors.save(mask)),
118+
height=mask.shape[1],
119+
width=mask.shape[2],
120+
)

invokeai/app/invocations/metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from pydantic import BaseModel, ConfigDict, Field
44

55
from invokeai.app.invocations.baseinvocation import BaseInvocation, BaseInvocationOutput, invocation, invocation_output
6-
from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
76
from invokeai.app.invocations.fields import (
87
FieldDescriptions,
98
ImageField,
@@ -14,6 +13,7 @@
1413
)
1514
from invokeai.app.invocations.model import ModelIdentifierField
1615
from invokeai.app.services.shared.invocation_context import InvocationContext
16+
from invokeai.app.util.controlnet_utils import CONTROLNET_MODE_VALUES, CONTROLNET_RESIZE_VALUES
1717

1818
from ...version import __version__
1919

invokeai/app/invocations/t2i_adapter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@
88
invocation,
99
invocation_output,
1010
)
11-
from invokeai.app.invocations.controlnet_image_processors import CONTROLNET_RESIZE_VALUES
1211
from invokeai.app.invocations.fields import FieldDescriptions, ImageField, Input, InputField, OutputField, UIType
1312
from invokeai.app.invocations.model import ModelIdentifierField
1413
from invokeai.app.invocations.util import validate_begin_end_step, validate_weights
1514
from invokeai.app.services.shared.invocation_context import InvocationContext
15+
from invokeai.app.util.controlnet_utils import CONTROLNET_RESIZE_VALUES
1616

1717

1818
class T2IAdapterField(BaseModel):

invokeai/app/services/download/download_default.py

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,10 +318,8 @@ def _do_download(self, job: DownloadJob) -> None:
318318
in_progress_path.rename(job.download_path)
319319

320320
def _validate_filename(self, directory: str, filename: str) -> bool:
321-
pc_name_max = os.pathconf(directory, "PC_NAME_MAX") if hasattr(os, "pathconf") else 260 # hardcoded for windows
322-
pc_path_max = (
323-
os.pathconf(directory, "PC_PATH_MAX") if hasattr(os, "pathconf") else 32767
324-
) # hardcoded for windows with long names enabled
321+
pc_name_max = get_pc_name_max(directory)
322+
pc_path_max = get_pc_path_max(directory)
325323
if "/" in filename:
326324
return False
327325
if filename.startswith(".."):
@@ -419,6 +417,26 @@ def _cleanup_cancelled_job(self, job: DownloadJob) -> None:
419417
self._logger.warning(excp)
420418

421419

420+
def get_pc_name_max(directory: str) -> int:
421+
if hasattr(os, "pathconf"):
422+
try:
423+
return os.pathconf(directory, "PC_NAME_MAX")
424+
except OSError:
425+
# macOS w/ external drives raise OSError
426+
pass
427+
return 260 # hardcoded for windows
428+
429+
430+
def get_pc_path_max(directory: str) -> int:
431+
if hasattr(os, "pathconf"):
432+
try:
433+
return os.pathconf(directory, "PC_PATH_MAX")
434+
except OSError:
435+
# some platforms may not have this value
436+
pass
437+
return 32767 # hardcoded for windows with long names enabled
438+
439+
422440
# Example on_progress event handler to display a TQDM status bar
423441
# Activate with:
424442
# download_service.download(DownloadJob('http://foo.bar/baz', '/tmp', on_progress=TqdmProgress().update))

invokeai/app/services/model_install/model_install_default.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import locale
44
import os
55
import re
6-
import signal
76
import threading
87
import time
98
from hashlib import sha256
@@ -43,6 +42,7 @@
4342
from invokeai.backend.model_manager.probe import ModelProbe
4443
from invokeai.backend.model_manager.search import ModelSearch
4544
from invokeai.backend.util import InvokeAILogger
45+
from invokeai.backend.util.catch_sigint import catch_sigint
4646
from invokeai.backend.util.devices import TorchDevice
4747

4848
from .model_install_base import (
@@ -112,17 +112,6 @@ def event_bus(self) -> Optional[EventServiceBase]: # noqa D102
112112
def start(self, invoker: Optional[Invoker] = None) -> None:
113113
"""Start the installer thread."""
114114

115-
# Yes, this is weird. When the installer thread is running, the
116-
# thread masks the ^C signal. When we receive a
117-
# sigINT, we stop the thread, reset sigINT, and send a new
118-
# sigINT to the parent process.
119-
def sigint_handler(signum, frame):
120-
self.stop()
121-
signal.signal(signal.SIGINT, signal.SIG_DFL)
122-
signal.raise_signal(signal.SIGINT)
123-
124-
signal.signal(signal.SIGINT, sigint_handler)
125-
126115
with self._lock:
127116
if self._running:
128117
raise Exception("Attempt to start the installer service twice")
@@ -132,7 +121,8 @@ def sigint_handler(signum, frame):
132121
# In normal use, we do not want to scan the models directory - it should never have orphaned models.
133122
# We should only do the scan when the flag is set (which should only be set when testing).
134123
if self.app_config.scan_models_on_startup:
135-
self._register_orphaned_models()
124+
with catch_sigint():
125+
self._register_orphaned_models()
136126

137127
# Check all models' paths and confirm they exist. A model could be missing if it was installed on a volume
138128
# that isn't currently mounted. In this case, we don't want to delete the model from the database, but we do

0 commit comments

Comments
 (0)