feature: boolean logic masks
Specify advanced text based masks using boolean logic and strength modifiers. Mask descriptions must be lowercase. Keywords uppercase. Valid symbols: `AND`, `OR`, `NOT`, `()`, and mask strength modifier `{*1.5}` where `+` can be any of `+ - * /`. Single-character boolean operators also work. When writing strength modifies know that pixel values are between 0 and 1. - feature: apply mask edits to original files - feature: auto-rotate images if exif data specifies to do so - fix: accept mask images in command linepull/26/head
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,163 @@
|
||||
# pylama:ignore=W0613
|
||||
"""
|
||||
Logic for parsing mask prompts.
|
||||
|
||||
Supports
|
||||
lower case text descriptions
|
||||
Combinations: AND OR NOT ()
|
||||
Strength Modifiers: {<operator><number>}
|
||||
|
||||
Examples:
|
||||
fruit
|
||||
fruit bowl
|
||||
fruit AND NOT pears
|
||||
fruit OR bowl
|
||||
(pears OR oranges OR peaches){*1.5}
|
||||
fruit{-0.1} OR bowl
|
||||
|
||||
"""
|
||||
import operator
|
||||
from abc import ABC
|
||||
|
||||
import pyparsing as pp
|
||||
import torch
|
||||
from pyparsing import ParserElement
|
||||
|
||||
ParserElement.enablePackrat()
|
||||
|
||||
|
||||
class Mask(ABC):
|
||||
def get_mask_for_image(self, img):
|
||||
pass
|
||||
|
||||
def gather_text_descriptions(self):
|
||||
return set()
|
||||
|
||||
def apply_masks(self, mask_cache):
|
||||
pass
|
||||
|
||||
|
||||
class SimpleMask(Mask):
|
||||
def __init__(self, text):
|
||||
self.text = text
|
||||
|
||||
@classmethod
|
||||
def from_simple_prompt(cls, instring, tokens_start, ret_tokens):
|
||||
return cls(text=ret_tokens[0])
|
||||
|
||||
def __repr__(self):
|
||||
return f"'{self.text}'"
|
||||
|
||||
def gather_text_descriptions(self):
|
||||
return {self.text}
|
||||
|
||||
def apply_masks(self, mask_cache):
|
||||
return mask_cache[self.text]
|
||||
|
||||
|
||||
class ModifiedMask(Mask):
|
||||
ops = {
|
||||
"+": operator.add,
|
||||
"-": operator.sub,
|
||||
"*": operator.mul,
|
||||
"/": operator.truediv,
|
||||
# '%': operator.mod,
|
||||
# '^': operator.xor,
|
||||
}
|
||||
|
||||
def __init__(self, mask, modifier):
|
||||
if modifier:
|
||||
modifier = modifier.strip("{}")
|
||||
self.mask = mask
|
||||
self.modifier = modifier
|
||||
self.operand = self.ops[modifier[0]]
|
||||
self.value = float(modifier[1:])
|
||||
|
||||
@classmethod
|
||||
def from_modifier_parse(cls, instring, tokens_start, ret_tokens):
|
||||
return cls(mask=ret_tokens[0][0], modifier=ret_tokens[0][1])
|
||||
|
||||
def __repr__(self):
|
||||
return f"{repr(self.mask)}{self.modifier}"
|
||||
|
||||
def gather_text_descriptions(self):
|
||||
return self.mask.gather_text_descriptions()
|
||||
|
||||
def apply_masks(self, mask_cache):
|
||||
mask = self.mask.apply_masks(mask_cache)
|
||||
return torch.clamp(self.operand(mask, self.value), 0, 1)
|
||||
|
||||
|
||||
class NestedMask(Mask):
|
||||
def __init__(self, masks, op):
|
||||
self.masks = masks
|
||||
self.op = op
|
||||
|
||||
@classmethod
|
||||
def from_or(cls, instring, tokens_start, ret_tokens):
|
||||
sub_masks = [t for t in ret_tokens[0] if isinstance(t, Mask)]
|
||||
return cls(masks=sub_masks, op="OR")
|
||||
|
||||
@classmethod
|
||||
def from_and(cls, instring, tokens_start, ret_tokens):
|
||||
sub_masks = [t for t in ret_tokens[0] if isinstance(t, Mask)]
|
||||
return cls(masks=sub_masks, op="AND")
|
||||
|
||||
@classmethod
|
||||
def from_not(cls, instring, tokens_start, ret_tokens):
|
||||
sub_masks = [t for t in ret_tokens[0] if isinstance(t, Mask)]
|
||||
assert len(sub_masks) == 1
|
||||
return cls(masks=sub_masks, op="NOT")
|
||||
|
||||
def __repr__(self):
|
||||
if self.op == "NOT":
|
||||
return f"NOT {self.masks[0]}"
|
||||
sub = f" {self.op} ".join(repr(m) for m in self.masks)
|
||||
return f"({sub})"
|
||||
|
||||
def gather_text_descriptions(self):
|
||||
return set().union(*[m.gather_text_descriptions() for m in self.masks])
|
||||
|
||||
def apply_masks(self, mask_cache):
|
||||
submasks = [m.apply_masks(mask_cache) for m in self.masks]
|
||||
mask = submasks[0]
|
||||
if self.op == "OR":
|
||||
for submask in submasks:
|
||||
mask = torch.maximum(mask, submask)
|
||||
elif self.op == "AND":
|
||||
for submask in submasks:
|
||||
mask = torch.minimum(mask, submask)
|
||||
elif self.op == "NOT":
|
||||
mask = 1 - mask
|
||||
else:
|
||||
raise ValueError(f"Invalid operand {self.op}")
|
||||
return torch.clamp(mask, 0, 1)
|
||||
|
||||
|
||||
AND = (pp.Literal("AND") | pp.Literal("&")).setName("AND").setResultsName("op")
|
||||
OR = (pp.Literal("OR") | pp.Literal("|")).setName("OR").setResultsName("op")
|
||||
NOT = (pp.Literal("NOT") | pp.Literal("!")).setName("NOT").setResultsName("op")
|
||||
|
||||
PROMPT_MODIFIER = (
|
||||
pp.Regex(r"{[*/+-]\d+\.?\d*}")
|
||||
.setName("prompt_modifier")
|
||||
.setResultsName("prompt_modifier")
|
||||
)
|
||||
PROMPT_TEXT = (
|
||||
pp.Regex(r"[a-z0-9]?[a-z0-9 -]*[a-z0-9]")
|
||||
.setName("prompt_text")
|
||||
.setResultsName("prompt_text")
|
||||
)
|
||||
SIMPLE_PROMPT = PROMPT_TEXT.setResultsName("simplePrompt")
|
||||
SIMPLE_PROMPT.setParseAction(SimpleMask.from_simple_prompt)
|
||||
|
||||
COMPLEX_PROMPT = pp.infixNotation(
|
||||
SIMPLE_PROMPT,
|
||||
[
|
||||
(PROMPT_MODIFIER, 1, pp.opAssoc.LEFT, ModifiedMask.from_modifier_parse),
|
||||
(NOT, 1, pp.opAssoc.RIGHT, NestedMask.from_not),
|
||||
(AND, 2, pp.opAssoc.LEFT, NestedMask.from_and),
|
||||
(OR, 2, pp.opAssoc.LEFT, NestedMask.from_or),
|
||||
],
|
||||
)
|
||||
MASK_PROMPT = pp.Group(COMPLEX_PROMPT).setResultsName("complexPrompt")
|
@ -0,0 +1,59 @@
|
||||
from typing import Sequence
|
||||
|
||||
import numpy as np
|
||||
import PIL
|
||||
import torch
|
||||
from einops import rearrange, repeat
|
||||
from PIL import Image
|
||||
|
||||
from imaginairy.utils import get_device
|
||||
|
||||
|
||||
def pillow_fit_image_within(image: PIL.Image.Image, max_height=512, max_width=512):
|
||||
image = image.convert("RGB")
|
||||
w, h = image.size
|
||||
resize_ratio = min(max_width / w, max_height / h)
|
||||
w, h = int(w * resize_ratio), int(h * resize_ratio)
|
||||
w, h = map(lambda x: x - x % 64, (w, h)) # resize to integer multiple of 64
|
||||
image = image.resize((w, h), resample=Image.Resampling.NEAREST)
|
||||
return image, w, h
|
||||
|
||||
|
||||
def pillow_img_to_torch_image(img: PIL.Image.Image):
|
||||
img = img.convert("RGB")
|
||||
img = np.array(img).astype(np.float32) / 255.0
|
||||
img = img[None].transpose(0, 3, 1, 2)
|
||||
img = torch.from_numpy(img)
|
||||
return 2.0 * img - 1.0
|
||||
|
||||
|
||||
def pillow_img_to_opencv_img(img: PIL.Image.Image):
|
||||
open_cv_image = np.array(img)
|
||||
# Convert RGB to BGR
|
||||
open_cv_image = open_cv_image[:, :, ::-1].copy()
|
||||
return open_cv_image
|
||||
|
||||
|
||||
def model_latents_to_pillow_imgs(latents: torch.Tensor) -> Sequence[PIL.Image.Image]:
|
||||
from imaginairy.api import load_model # noqa
|
||||
|
||||
model = load_model()
|
||||
latents = model.decode_first_stage(latents)
|
||||
latents = torch.clamp((latents + 1.0) / 2.0, min=0.0, max=1.0)
|
||||
imgs = []
|
||||
for latent in latents:
|
||||
latent = 255.0 * rearrange(latent.cpu().numpy(), "c h w -> h w c")
|
||||
img = Image.fromarray(latent.astype(np.uint8))
|
||||
imgs.append(img)
|
||||
return imgs
|
||||
|
||||
|
||||
def pillow_img_to_model_latent(model, img, batch_size=1, half=True):
|
||||
# init_image = pil_img_to_torch(img, half=half).to(device)
|
||||
init_image = pillow_img_to_torch_image(img).to(get_device())
|
||||
init_image = repeat(init_image, "1 ... -> b ...", b=batch_size)
|
||||
if half:
|
||||
return model.get_first_stage_encoding(
|
||||
model.encode_first_stage(init_image.half())
|
||||
)
|
||||
return model.get_first_stage_encoding(model.encode_first_stage(init_image))
|
After Width: | Height: | Size: 1.1 MiB |