2023-02-12 07:42:19 +00:00
|
|
|
"""
|
|
|
|
image utils.
|
|
|
|
|
|
|
|
Library format cheat sheet:
|
|
|
|
|
|
|
|
Library Dim Order Channel Order Value Range Type
|
|
|
|
Pillow R, G, B, A 0-255 PIL.Image.Image
|
|
|
|
OpenCV B, G, R, A 0-255 np.ndarray
|
|
|
|
Torch (B), C, H, W R, G, B -1.0-1.0 torch.Tensor
|
|
|
|
|
|
|
|
"""
|
2022-09-24 05:58:48 +00:00
|
|
|
from typing import Sequence
|
|
|
|
|
|
|
|
import numpy as np
|
|
|
|
import PIL
|
|
|
|
import torch
|
|
|
|
from einops import rearrange, repeat
|
2023-01-29 01:16:47 +00:00
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
2022-09-24 05:58:48 +00:00
|
|
|
|
2023-01-29 01:16:47 +00:00
|
|
|
from imaginairy.schema import LazyLoadingImage
|
2022-09-24 05:58:48 +00:00
|
|
|
from imaginairy.utils import get_device
|
2023-12-20 20:32:29 +00:00
|
|
|
from imaginairy.utils.named_resolutions import normalize_image_size
|
2023-12-15 21:40:10 +00:00
|
|
|
from imaginairy.utils.paths import PKG_ROOT
|
2022-09-24 05:58:48 +00:00
|
|
|
|
|
|
|
|
2023-01-01 22:54:49 +00:00
|
|
|
def pillow_fit_image_within(
|
2023-12-12 06:29:36 +00:00
|
|
|
image: PIL.Image.Image | LazyLoadingImage,
|
|
|
|
max_height=512,
|
|
|
|
max_width=512,
|
|
|
|
convert="RGB",
|
|
|
|
snap_size=8,
|
|
|
|
) -> PIL.Image.Image:
|
2023-01-01 22:54:49 +00:00
|
|
|
image = image.convert(convert)
|
2022-09-24 05:58:48 +00:00
|
|
|
w, h = image.size
|
2022-10-06 06:13:48 +00:00
|
|
|
resize_ratio = 1
|
2022-09-24 21:41:25 +00:00
|
|
|
if w > max_width or h > max_height:
|
|
|
|
resize_ratio = min(max_width / w, max_height / h)
|
2022-10-06 06:13:48 +00:00
|
|
|
elif w < max_width and h < max_height:
|
|
|
|
# it's smaller than our target image, enlarge
|
|
|
|
resize_ratio = max(max_width / w, max_height / h)
|
|
|
|
|
|
|
|
if resize_ratio != 1:
|
2022-09-24 21:41:25 +00:00
|
|
|
w, h = int(w * resize_ratio), int(h * resize_ratio)
|
2023-01-24 06:25:56 +00:00
|
|
|
# resize to integer multiple of snap_size
|
|
|
|
w -= w % snap_size
|
|
|
|
h -= h % snap_size
|
2023-01-02 04:14:22 +00:00
|
|
|
|
2022-10-06 06:13:48 +00:00
|
|
|
if (w, h) != image.size:
|
2022-09-24 21:41:25 +00:00
|
|
|
image = image.resize((w, h), resample=Image.Resampling.LANCZOS)
|
2022-09-24 07:29:45 +00:00
|
|
|
return image
|
2022-09-24 05:58:48 +00:00
|
|
|
|
|
|
|
|
2023-12-12 06:29:36 +00:00
|
|
|
def pillow_img_to_torch_image(
|
|
|
|
img: PIL.Image.Image | LazyLoadingImage, convert="RGB"
|
|
|
|
) -> torch.Tensor:
|
2023-02-15 16:02:36 +00:00
|
|
|
if convert:
|
|
|
|
img = img.convert(convert)
|
2023-12-11 02:29:47 +00:00
|
|
|
img_np = np.array(img).astype(np.float32) / 255.0
|
2023-02-12 07:42:19 +00:00
|
|
|
# b, h, w, c => b, c, h, w
|
2023-12-11 02:29:47 +00:00
|
|
|
img_np = img_np[None].transpose(0, 3, 1, 2)
|
|
|
|
img_t = torch.from_numpy(img_np)
|
|
|
|
return 2.0 * img_t - 1.0
|
2022-09-24 05:58:48 +00:00
|
|
|
|
|
|
|
|
2023-12-12 06:29:36 +00:00
|
|
|
def pillow_mask_to_latent_mask(
|
|
|
|
mask_img: PIL.Image.Image | LazyLoadingImage, downsampling_factor
|
|
|
|
) -> torch.Tensor:
|
2023-02-15 16:02:36 +00:00
|
|
|
mask_img = mask_img.resize(
|
|
|
|
(
|
|
|
|
mask_img.width // downsampling_factor,
|
|
|
|
mask_img.height // downsampling_factor,
|
|
|
|
),
|
|
|
|
resample=Image.Resampling.LANCZOS,
|
|
|
|
)
|
|
|
|
|
|
|
|
mask = np.array(mask_img).astype(np.float32) / 255.0
|
|
|
|
mask = mask[None, None]
|
2023-12-12 06:29:36 +00:00
|
|
|
mask_t = torch.from_numpy(mask)
|
|
|
|
return mask_t
|
2023-02-12 08:52:50 +00:00
|
|
|
|
|
|
|
|
2023-12-12 06:29:36 +00:00
|
|
|
def pillow_img_to_opencv_img(img: PIL.Image.Image | LazyLoadingImage):
|
2022-09-24 05:58:48 +00:00
|
|
|
open_cv_image = np.array(img)
|
|
|
|
# Convert RGB to BGR
|
|
|
|
open_cv_image = open_cv_image[:, :, ::-1].copy()
|
|
|
|
return open_cv_image
|
|
|
|
|
|
|
|
|
2023-12-11 02:29:47 +00:00
|
|
|
def torch_image_to_openvcv_img(img: torch.Tensor) -> np.ndarray:
|
2023-02-12 07:42:19 +00:00
|
|
|
img = (img + 1) / 2
|
2023-12-11 02:29:47 +00:00
|
|
|
img_np = img.detach().cpu().numpy()
|
2023-02-12 07:42:19 +00:00
|
|
|
# assert there is only one image
|
2023-12-11 02:29:47 +00:00
|
|
|
assert img_np.shape[0] == 1
|
|
|
|
img_np = img_np[0]
|
|
|
|
img_np = img_np.transpose(1, 2, 0)
|
|
|
|
img_np = (img_np * 255).astype(np.uint8)
|
2023-02-12 07:42:19 +00:00
|
|
|
# RGB to BGR
|
2023-12-11 02:29:47 +00:00
|
|
|
img_np = img_np[:, :, ::-1]
|
|
|
|
return img_np
|
2023-02-12 07:42:19 +00:00
|
|
|
|
|
|
|
|
2023-12-12 06:29:36 +00:00
|
|
|
def torch_img_to_pillow_img(img_t: torch.Tensor) -> PIL.Image.Image:
|
2023-02-15 20:43:19 +00:00
|
|
|
img_t = img_t.to(torch.float32).detach().cpu()
|
2023-02-15 16:02:36 +00:00
|
|
|
if len(img_t.shape) == 3:
|
|
|
|
img_t = img_t.unsqueeze(0)
|
|
|
|
if img_t.shape[0] != 1:
|
|
|
|
raise ValueError("Only batch size 1 supported")
|
|
|
|
if img_t.shape[1] == 1:
|
|
|
|
colorspace = "L"
|
|
|
|
elif img_t.shape[1] == 3:
|
|
|
|
colorspace = "RGB"
|
|
|
|
else:
|
2023-09-29 08:13:50 +00:00
|
|
|
msg = (
|
2023-02-12 02:23:45 +00:00
|
|
|
f"Unsupported colorspace. {img_t.shape[1]} channels in {img_t.shape} shape"
|
|
|
|
)
|
2023-09-29 08:13:50 +00:00
|
|
|
raise ValueError(msg)
|
2023-02-15 16:02:36 +00:00
|
|
|
img_t = rearrange(img_t, "b c h w -> b h w c")
|
|
|
|
img_t = torch.clamp((img_t + 1.0) / 2.0, min=0.0, max=1.0)
|
|
|
|
img_np = (255.0 * img_t).cpu().numpy().astype(np.uint8)[0]
|
|
|
|
if colorspace == "L":
|
|
|
|
img_np = img_np[:, :, 0]
|
|
|
|
return Image.fromarray(img_np, colorspace)
|
|
|
|
|
|
|
|
|
|
|
|
def model_latent_to_pillow_img(latent: torch.Tensor) -> PIL.Image.Image:
|
2023-12-15 21:42:45 +00:00
|
|
|
from imaginairy.utils.model_manager import get_current_diffusion_model
|
2022-09-24 05:58:48 +00:00
|
|
|
|
2023-02-15 16:02:36 +00:00
|
|
|
if len(latent.shape) == 3:
|
|
|
|
latent = latent.unsqueeze(0)
|
|
|
|
if latent.shape[0] != 1:
|
|
|
|
raise ValueError("Only batch size 1 supported")
|
2022-10-23 21:46:45 +00:00
|
|
|
model = get_current_diffusion_model()
|
2023-11-16 03:46:56 +00:00
|
|
|
img_t = model.lda.decode(latent)
|
2023-02-15 16:02:36 +00:00
|
|
|
return torch_img_to_pillow_img(img_t)
|
|
|
|
|
|
|
|
|
|
|
|
def model_latents_to_pillow_imgs(latents: torch.Tensor) -> Sequence[PIL.Image.Image]:
|
|
|
|
return [model_latent_to_pillow_img(latent) for latent in latents]
|
2022-09-24 05:58:48 +00:00
|
|
|
|
|
|
|
|
2023-12-12 06:29:36 +00:00
|
|
|
def pillow_img_to_model_latent(
|
|
|
|
model, img: PIL.Image.Image | LazyLoadingImage, batch_size=1, half=True
|
|
|
|
):
|
2022-09-24 05:58:48 +00:00
|
|
|
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))
|
2023-01-22 01:36:47 +00:00
|
|
|
|
|
|
|
|
2023-01-29 01:16:47 +00:00
|
|
|
def imgpaths_to_imgs(imgpaths):
|
|
|
|
imgs = []
|
|
|
|
for imgpath in imgpaths:
|
|
|
|
if isinstance(imgpath, str):
|
|
|
|
img = LazyLoadingImage(filepath=imgpath)
|
|
|
|
imgs.append(img)
|
|
|
|
else:
|
|
|
|
imgs.append(imgpath)
|
|
|
|
|
|
|
|
return imgs
|
|
|
|
|
|
|
|
|
|
|
|
def add_caption_to_image(
|
2023-12-12 06:29:36 +00:00
|
|
|
img: PIL.Image.Image | LazyLoadingImage,
|
|
|
|
caption,
|
|
|
|
font_size=16,
|
|
|
|
font_path=f"{PKG_ROOT}/data/DejaVuSans.ttf",
|
2023-01-29 01:16:47 +00:00
|
|
|
):
|
2023-12-12 06:29:36 +00:00
|
|
|
img_pil = img.as_pillow() if isinstance(img, LazyLoadingImage) else img
|
|
|
|
draw = ImageDraw.Draw(img_pil)
|
2023-01-29 01:16:47 +00:00
|
|
|
|
|
|
|
font = ImageFont.truetype(font_path, font_size)
|
|
|
|
|
|
|
|
x = 15
|
2023-12-12 06:29:36 +00:00
|
|
|
y = img_pil.height - 15 - font_size
|
2023-01-22 01:36:47 +00:00
|
|
|
|
2023-01-29 01:16:47 +00:00
|
|
|
draw.text(
|
|
|
|
(x, y),
|
|
|
|
caption,
|
|
|
|
font=font,
|
|
|
|
fill=(255, 255, 255),
|
|
|
|
stroke_width=3,
|
|
|
|
stroke_fill=(0, 0, 0),
|
2023-01-22 01:36:47 +00:00
|
|
|
)
|
2023-12-17 06:08:19 +00:00
|
|
|
|
|
|
|
|
|
|
|
def create_halo_effect(
|
|
|
|
bw_image: PIL.Image.Image, background_color: tuple
|
|
|
|
) -> PIL.Image.Image:
|
|
|
|
from PIL import Image, ImageFilter
|
|
|
|
|
|
|
|
# Step 1: Make white portion of the image transparent
|
|
|
|
transparent_image = bw_image.convert("RGBA")
|
|
|
|
datas = transparent_image.getdata()
|
|
|
|
new_data = []
|
|
|
|
for item in datas:
|
|
|
|
# Change all white (also shades of whites)
|
|
|
|
# to transparent
|
|
|
|
if item[0] > 200 and item[1] > 200 and item[2] > 200:
|
|
|
|
new_data.append((255, 255, 255, 0))
|
|
|
|
else:
|
|
|
|
new_data.append(item)
|
|
|
|
transparent_image.putdata(new_data) # type: ignore
|
|
|
|
|
|
|
|
# Step 2: Make a copy of the image
|
|
|
|
eroded_image = transparent_image.copy()
|
|
|
|
|
|
|
|
# Step 3: Erode and blur the copy
|
|
|
|
# eroded_image = ImageOps.invert(eroded_image.convert("L")).convert("1")
|
|
|
|
# eroded_image = eroded_image.filter(ImageFilter.MinFilter(3)) # Erode
|
|
|
|
eroded_image = eroded_image.filter(ImageFilter.GaussianBlur(radius=25))
|
|
|
|
|
|
|
|
# Step 4: Create new canvas
|
|
|
|
new_canvas = Image.new("RGBA", bw_image.size, color=background_color)
|
|
|
|
|
|
|
|
# Step 5: Paste the blurred copy on the new canvas
|
|
|
|
new_canvas.paste(eroded_image, (0, 0), eroded_image)
|
|
|
|
|
|
|
|
# Step 6: Paste the original sharp image on the new canvas
|
|
|
|
new_canvas.paste(transparent_image, (0, 0), transparent_image)
|
|
|
|
|
|
|
|
return new_canvas
|
2023-12-20 20:32:29 +00:00
|
|
|
|
|
|
|
|
|
|
|
def combine_image(original_img, generated_img, mask_img):
|
|
|
|
"""Combine the generated image with the original image using the mask image."""
|
|
|
|
from PIL import Image
|
|
|
|
|
|
|
|
from imaginairy.utils.log_utils import log_img
|
|
|
|
|
|
|
|
generated_img = generated_img.resize(
|
|
|
|
original_img.size,
|
|
|
|
resample=Image.Resampling.LANCZOS,
|
|
|
|
)
|
|
|
|
|
|
|
|
mask_for_orig_size = mask_img.resize(
|
|
|
|
original_img.size,
|
|
|
|
resample=Image.Resampling.LANCZOS,
|
|
|
|
)
|
|
|
|
log_img(mask_for_orig_size, "mask for original image size")
|
|
|
|
|
|
|
|
rebuilt_orig_img = Image.composite(
|
|
|
|
original_img,
|
|
|
|
generated_img,
|
|
|
|
mask_for_orig_size,
|
|
|
|
)
|
|
|
|
return rebuilt_orig_img
|
|
|
|
|
|
|
|
|
|
|
|
def calc_scale_to_fit_within(height: int, width: int, max_size) -> float:
|
|
|
|
max_width, max_height = normalize_image_size(max_size)
|
|
|
|
if width <= max_width and height <= max_height:
|
|
|
|
return 1
|
|
|
|
|
|
|
|
width_ratio = max_width / width
|
|
|
|
height_ratio = max_height / height
|
|
|
|
|
|
|
|
return min(width_ratio, height_ratio)
|