Files
melMass-comfy_mtb/utils.py
T
melMass 87b245c6a6 fix: ✨ deepbump update
- fixes #102
- Add support for batch in deep bump.
2023-10-04 19:28:11 +02:00

756 lines
21 KiB
Python

import functools
import math
import os
import shutil
import signal
import socket
import subprocess
import sys
import threading
import uuid
from contextlib import suppress
from pathlib import Path
from queue import Empty, Queue
from typing import List, Optional, Union
import folder_paths
import numpy as np
import requests
import torch
from PIL import Image
try:
from .log import log
except ImportError:
try:
from log import log
log.warn("Imported log without relative path")
except ImportError:
import logging
log = logging.getLogger("comfy mtb utils")
log.warn("[comfy mtb] You probably called the file outside a module.")
# region SANITY_CHECK Utilities
def make_report():
pass
# endregion
# region SERVER Utilities
class IPChecker:
def __init__(self):
self.ips = list(self.get_local_ips())
log.debug(f"Found {len(self.ips)} local ips")
self.checked_ips = set()
def get_working_ip(self, test_url_template):
for ip in self.ips:
if ip not in self.checked_ips:
self.checked_ips.add(ip)
test_url = test_url_template.format(ip)
if self._test_url(test_url):
return ip
return None
@staticmethod
def get_local_ips(prefix="192.168."):
hostname = socket.gethostname()
log.debug(f"Getting local ips for {hostname}")
for info in socket.getaddrinfo(hostname, None):
# Filter out IPv6 addresses if you only want IPv4
log.debug(info)
# if info[1] == socket.SOCK_STREAM and
if info[0] == socket.AF_INET and info[4][0].startswith(prefix):
yield info[4][0]
def _test_url(self, url):
try:
response = requests.get(url)
return response.status_code == 200
except Exception:
return False
@functools.lru_cache(maxsize=1)
def get_server_info():
from comfy.cli_args import args
ip_checker = IPChecker()
base_url = args.listen
if base_url == "0.0.0.0":
log.debug("Server set to 0.0.0.0, we will try to resolve the host IP")
base_url = ip_checker.get_working_ip(f"http://{{}}:{args.port}/history")
log.debug(f"Setting ip to {base_url}")
return (base_url, args.port)
# endregion
# region MISC Utilities
def backup_file(
fp: Path,
target: Optional[Path] = None,
backup_dir: str = ".bak",
suffix: Optional[str] = None,
prefix: Optional[str] = None,
):
if not fp.exists():
raise FileNotFoundError(f"No file found at {fp}")
backup_directory = target or fp.parent / backup_dir
backup_directory.mkdir(parents=True, exist_ok=True)
stem = fp.stem
if suffix or prefix:
new_stem = f"{prefix or ''}{stem}{suffix or ''}"
else:
new_stem = f"{stem}_{uuid.uuid4()}"
backup_file_path = backup_directory / f"{new_stem}{fp.suffix}"
# Perform the backup
shutil.copy(fp, backup_file_path)
log.debug(f"File backed up to {backup_file_path}")
def hex_to_rgb(hex_color):
try:
hex_color = hex_color.lstrip("#")
return tuple(int(hex_color[i : i + 2], 16) for i in (0, 2, 4))
except ValueError:
log.error(f"Invalid hex color: {hex_color}")
return (0, 0, 0)
def add_path(path, prepend=False):
if isinstance(path, list):
for p in path:
add_path(p, prepend)
return
if isinstance(path, Path):
path = path.resolve().as_posix()
if path not in sys.path:
if prepend:
sys.path.insert(0, path)
else:
sys.path.append(path)
def enqueue_output(out, queue):
for line in iter(out.readline, b""):
queue.put(line)
out.close()
def run_command(cmd):
if isinstance(cmd, str):
shell_cmd = cmd
elif isinstance(cmd, list):
shell_cmd = ""
for arg in cmd:
if isinstance(arg, Path):
arg = arg.as_posix()
shell_cmd += f"{arg} "
else:
raise ValueError(
"Invalid 'cmd' argument. It must be a string or a list of arguments."
)
process = subprocess.Popen(
shell_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
shell=True,
)
# Create separate threads to read standard output and standard error streams
stdout_queue = Queue()
stderr_queue = Queue()
stdout_thread = threading.Thread(
target=enqueue_output, args=(process.stdout, stdout_queue)
)
stderr_thread = threading.Thread(
target=enqueue_output, args=(process.stderr, stderr_queue)
)
stdout_thread.daemon = True
stderr_thread.daemon = True
stdout_thread.start()
stderr_thread.start()
interrupted = False
def signal_handler(signum, frame):
nonlocal interrupted
interrupted = True
print("Command execution interrupted.")
# Register the signal handler for keyboard interrupts (SIGINT)
signal.signal(signal.SIGINT, signal_handler)
# Process output from both streams until the process completes or interrupted
while not interrupted and (
process.poll() is None or not stdout_queue.empty() or not stderr_queue.empty()
):
with suppress(Empty):
stdout_line = stdout_queue.get_nowait()
if stdout_line.strip() != "":
print(stdout_line.strip())
with suppress(Empty):
stderr_line = stderr_queue.get_nowait()
if stderr_line.strip() != "":
print(stderr_line.strip())
return_code = process.returncode
if return_code == 0 and not interrupted:
print("Command executed successfully!")
else:
if not interrupted:
print(f"Command failed with return code: {return_code}")
# todo use the requirements library
reqs_map = {
"onnxruntime": "onnxruntime-gpu==1.15.1",
"basicsr": "basicsr==1.4.2",
"rembg": "rembg==2.0.50",
"qrcode": "qrcode[pil]",
"requirements": "requirements-parser==0.5.0",
}
def import_install(package_name):
from pip._internal import main as pip_main
try:
__import__(package_name)
except ImportError:
package_spec = reqs_map.get(package_name)
if package_spec is None:
print(f"Installing {package_name}")
package_spec = package_name
pip_main(["install", package_spec])
__import__(package_name)
# endregion
# region GLOBAL VARIABLES
# - detect mode
comfy_mode = None
if os.environ.get("COLAB_GPU"):
comfy_mode = "colab"
elif "python_embeded" in sys.executable:
comfy_mode = "embeded"
elif ".venv" in sys.executable:
comfy_mode = "venv"
# - Get the absolute path of the parent directory of the current script
here = Path(__file__).parent.absolute()
# - Construct the absolute path to the ComfyUI directory
comfy_dir = Path(folder_paths.base_path)
models_dir = Path(folder_paths.models_dir)
styles_dir = comfy_dir / "styles"
# - Construct the path to the font file
font_path = here / "font.ttf"
# - Add extern folder to path
extern_root = here / "extern"
add_path(extern_root)
for pth in extern_root.iterdir():
if pth.is_dir():
add_path(pth)
# - Add the ComfyUI directory and custom nodes path to the sys.path list
add_path(comfy_dir)
add_path((comfy_dir / "custom_nodes"))
PIL_FILTER_MAP = {
"nearest": Image.Resampling.NEAREST,
"box": Image.Resampling.BOX,
"bilinear": Image.Resampling.BILINEAR,
"hamming": Image.Resampling.HAMMING,
"bicubic": Image.Resampling.BICUBIC,
"lanczos": Image.Resampling.LANCZOS,
}
# endregion
# region TENSOR Utilities
def tensor2pil(image: torch.Tensor) -> List[Image.Image]:
batch_count = image.size(0) if len(image.shape) > 3 else 1
if batch_count > 1:
out = []
for i in range(batch_count):
out.extend(tensor2pil(image[i]))
return out
return [
Image.fromarray(
np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
)
]
def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor:
if isinstance(image, list):
return torch.cat([pil2tensor(img) for img in image], dim=0)
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
def np2tensor(img_np: Union[np.ndarray, List[np.ndarray]]) -> torch.Tensor:
if isinstance(img_np, list):
return torch.cat([np2tensor(img) for img in img_np], dim=0)
return torch.from_numpy(img_np.astype(np.float32) / 255.0).unsqueeze(0)
def tensor2np(tensor: torch.Tensor) -> List[np.ndarray]:
batch_count = tensor.size(0) if len(tensor.shape) > 3 else 1
if batch_count > 1:
out = []
for i in range(batch_count):
out.extend(tensor2np(tensor[i]))
return out
return [np.clip(255.0 * tensor.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)]
def pad(img, left, right, top, bottom):
pad_width = np.array(((0, 0), (top, bottom), (left, right)))
print(f"pad_width: {pad_width}, shape: {pad_width.shape}") # Debugging line
return np.pad(img, pad_width, mode="wrap")
def tiles_infer(tiles, ort_session, progress_callback=None):
"""Infer each tile with the given model. progress_callback will be called with
arguments : current tile idx and total tiles amount (used to show progress on
cursor in Blender)."""
out_channels = 3 # normal map RGB channels
tiles_nb = tiles.shape[0]
pred_tiles = np.empty((tiles_nb, out_channels, tiles.shape[2], tiles.shape[3]))
for i in range(tiles_nb):
if progress_callback != None:
progress_callback(i + 1, tiles_nb)
pred_tiles[i] = ort_session.run(
None, {"input": tiles[i : i + 1].astype(np.float32)}
)[0]
return pred_tiles
def generate_mask(tile_size, stride_size):
"""Generates a pyramidal-like mask. Used for mixing overlapping predicted tiles."""
tile_h, tile_w = tile_size
stride_h, stride_w = stride_size
ramp_h = tile_h - stride_h
ramp_w = tile_w - stride_w
mask = np.ones((tile_h, tile_w))
# ramps in width direction
mask[ramp_h:-ramp_h, :ramp_w] = np.linspace(0, 1, num=ramp_w)
mask[ramp_h:-ramp_h, -ramp_w:] = np.linspace(1, 0, num=ramp_w)
# ramps in height direction
mask[:ramp_h, ramp_w:-ramp_w] = np.transpose(
np.linspace(0, 1, num=ramp_h)[None], (1, 0)
)
mask[-ramp_h:, ramp_w:-ramp_w] = np.transpose(
np.linspace(1, 0, num=ramp_h)[None], (1, 0)
)
# Assume tiles are squared
assert ramp_h == ramp_w
# top left corner
corner = np.rot90(corner_mask(ramp_h), 2)
mask[:ramp_h, :ramp_w] = corner
# top right corner
corner = np.flip(corner, 1)
mask[:ramp_h, -ramp_w:] = corner
# bottom right corner
corner = np.flip(corner, 0)
mask[-ramp_h:, -ramp_w:] = corner
# bottom right corner
corner = np.flip(corner, 1)
mask[-ramp_h:, :ramp_w] = corner
return mask
def corner_mask(side_length):
"""Generates the corner part of the pyramidal-like mask.
Currently, only for square shapes."""
corner = np.zeros([side_length, side_length])
for h in range(0, side_length):
for w in range(0, side_length):
if h >= w:
sh = h / (side_length - 1)
corner[h, w] = 1 - sh
if h <= w:
sw = w / (side_length - 1)
corner[h, w] = 1 - sw
return corner - 0.25 * scaling_mask(side_length)
def scaling_mask(side_length):
scaling = np.zeros([side_length, side_length])
for h in range(0, side_length):
for w in range(0, side_length):
sh = h / (side_length - 1)
sw = w / (side_length - 1)
if h >= w and h <= side_length - w:
scaling[h, w] = sw
if h <= w and h <= side_length - w:
scaling[h, w] = sh
if h >= w and h >= side_length - w:
scaling[h, w] = 1 - sh
if h <= w and h >= side_length - w:
scaling[h, w] = 1 - sw
return 2 * scaling
def tiles_merge(tiles, stride_size, img_size, paddings):
"""Merges the list of tiles into one image. img_size is the original size, before
padding."""
_, tile_h, tile_w = tiles[0].shape
pad_left, pad_right, pad_top, pad_bottom = paddings
height = img_size[1] + pad_top + pad_bottom
width = img_size[2] + pad_left + pad_right
stride_h, stride_w = stride_size
# stride must be even
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
# stride must be greater or equal than half tile
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
# stride must be smaller or equal tile size
assert (stride_h <= tile_h) and (stride_w <= tile_w)
merged = np.zeros((img_size[0], height, width))
mask = generate_mask((tile_h, tile_w), stride_size)
h_range = ((height - tile_h) // stride_h) + 1
w_range = ((width - tile_w) // stride_w) + 1
idx = 0
for h in range(0, h_range):
for w in range(0, w_range):
h_from, h_to = h * stride_h, h * stride_h + tile_h
w_from, w_to = w * stride_w, w * stride_w + tile_w
merged[:, h_from:h_to, w_from:w_to] += tiles[idx] * mask
idx += 1
return merged[:, pad_top:-pad_bottom, pad_left:-pad_right]
def tiles_split(img, tile_size, stride_size):
"""Returns list of tiles from the given image and the padding used to fit the tiles
in it. Input image must have dimension C,H,W."""
log.debug(f"Splitting img: tile {tile_size}, stride {stride_size} ")
tile_h, tile_w = tile_size
stride_h, stride_w = stride_size
img_h, img_w = img.shape[0], img.shape[1]
# stride must be even
assert (stride_h % 2 == 0) and (stride_w % 2 == 0)
# stride must be greater or equal than half tile
assert (stride_h >= tile_h / 2) and (stride_w >= tile_w / 2)
# stride must be smaller or equal tile size
assert (stride_h <= tile_h) and (stride_w <= tile_w)
# find total height & width padding sizes
pad_h, pad_w = 0, 0
remainer_h = (img_h - tile_h) % stride_h
remainer_w = (img_w - tile_w) % stride_w
if remainer_h != 0:
pad_h = stride_h - remainer_h
if remainer_w != 0:
pad_w = stride_w - remainer_w
# if tile bigger than image, pad image to tile size
if tile_h > img_h:
pad_h = tile_h - img_h
if tile_w > img_w:
pad_w = tile_w - img_w
# pad image, add extra stride to padding to avoid pyramid
# weighting leaking onto the valid part of the picture
pad_left = pad_w // 2 + stride_w
pad_right = pad_left if pad_w % 2 == 0 else pad_left + 1
pad_top = pad_h // 2 + stride_h
pad_bottom = pad_top if pad_h % 2 == 0 else pad_top + 1
img = pad(img, pad_left, pad_right, pad_top, pad_bottom)
img_h, img_w = img.shape[1], img.shape[2]
# extract tiles
h_range = ((img_h - tile_h) // stride_h) + 1
w_range = ((img_w - tile_w) // stride_w) + 1
tiles = np.empty([h_range * w_range, img.shape[0], tile_h, tile_w])
idx = 0
for h in range(0, h_range):
for w in range(0, w_range):
h_from, h_to = h * stride_h, h * stride_h + tile_h
w_from, w_to = w * stride_w, w * stride_w + tile_w
tiles[idx] = img[:, h_from:h_to, w_from:w_to]
idx += 1
return tiles, (pad_left, pad_right, pad_top, pad_bottom)
# endregion
# region MODEL Utilities
def download_antelopev2():
antelopev2_url = "https://drive.google.com/uc?id=18wEUfMNohBJ4K3Ly5wpTejPfDzp-8fI8"
try:
import folder_paths
import gdown
log.debug("Loading antelopev2 model")
dest = Path(folder_paths.models_dir) / "insightface"
archive = dest / "antelopev2.zip"
final_path = dest / "models" / "antelopev2"
if not final_path.exists():
log.info(f"antelopev2 not found, downloading to {dest}")
gdown.download(
antelopev2_url,
archive.as_posix(),
resume=True,
)
log.info(f"Unzipping antelopev2 to {final_path}")
if archive.exists():
# we unzip it
import zipfile
with zipfile.ZipFile(archive.as_posix(), "r") as zip_ref:
zip_ref.extractall(final_path.parent.as_posix())
except Exception as e:
log.error(
f"Could not load or download antelopev2 model, download it manually from {antelopev2_url}"
)
raise e
# endregion
# region UV Utilities
def create_uv_map_tensor(width=512, height=512):
u = torch.linspace(0.0, 1.0, steps=width)
v = torch.linspace(0.0, 1.0, steps=height)
U, V = torch.meshgrid(u, v)
uv_map = torch.zeros(height, width, 3, dtype=torch.float32)
uv_map[:, :, 0] = U.t()
uv_map[:, :, 1] = V.t()
return uv_map.unsqueeze(0)
# endregion
# region ANIMATION Utilities
def apply_easing(value, easing_type):
if value < 0 or value > 1:
raise ValueError(f"The value should be between 0 and 1. (value is {value})")
if easing_type == "Linear":
return value
# Back easing functions
def easeInBack(t):
s = 1.70158
return t * t * ((s + 1) * t - s)
def easeOutBack(t):
s = 1.70158
return ((t - 1) * t * ((s + 1) * t + s)) + 1
def easeInOutBack(t):
s = 1.70158 * 1.525
if t < 0.5:
return (t * t * (t * (s + 1) - s)) * 2
return ((t - 2) * t * ((s + 1) * t + s) + 2) * 2
# Elastic easing functions
def easeInElastic(t):
if t == 0:
return 0
if t == 1:
return 1
p = 0.3
s = p / 4
return -(math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p))
def easeOutElastic(t):
if t == 0:
return 0
if t == 1:
return 1
p = 0.3
s = p / 4
return math.pow(2, -10 * t) * math.sin((t - s) * (2 * math.pi) / p) + 1
def easeInOutElastic(t):
if t == 0:
return 0
if t == 1:
return 1
p = 0.3 * 1.5
s = p / 4
t = t * 2
if t < 1:
return -0.5 * (
math.pow(2, 10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
)
return (
0.5 * math.pow(2, -10 * (t - 1)) * math.sin((t - 1 - s) * (2 * math.pi) / p)
+ 1
)
# Bounce easing functions
def easeInBounce(t):
return 1 - easeOutBounce(1 - t)
def easeOutBounce(t):
if t < (1 / 2.75):
return 7.5625 * t * t
elif t < (2 / 2.75):
t -= 1.5 / 2.75
return 7.5625 * t * t + 0.75
elif t < (2.5 / 2.75):
t -= 2.25 / 2.75
return 7.5625 * t * t + 0.9375
else:
t -= 2.625 / 2.75
return 7.5625 * t * t + 0.984375
def easeInOutBounce(t):
if t < 0.5:
return easeInBounce(t * 2) * 0.5
return easeOutBounce(t * 2 - 1) * 0.5 + 0.5
# Quart easing functions
def easeInQuart(t):
return t * t * t * t
def easeOutQuart(t):
t -= 1
return -(t**2 * t * t - 1)
def easeInOutQuart(t):
t *= 2
if t < 1:
return 0.5 * t * t * t * t
t -= 2
return -0.5 * (t**2 * t * t - 2)
# Cubic easing functions
def easeInCubic(t):
return t * t * t
def easeOutCubic(t):
t -= 1
return t**2 * t + 1
def easeInOutCubic(t):
t *= 2
if t < 1:
return 0.5 * t * t * t
t -= 2
return 0.5 * (t**2 * t + 2)
# Circ easing functions
def easeInCirc(t):
return -(math.sqrt(1 - t * t) - 1)
def easeOutCirc(t):
t -= 1
return math.sqrt(1 - t**2)
def easeInOutCirc(t):
t *= 2
if t < 1:
return -0.5 * (math.sqrt(1 - t**2) - 1)
t -= 2
return 0.5 * (math.sqrt(1 - t**2) + 1)
# Sine easing functions
def easeInSine(t):
return -math.cos(t * (math.pi / 2)) + 1
def easeOutSine(t):
return math.sin(t * (math.pi / 2))
def easeInOutSine(t):
return -0.5 * (math.cos(math.pi * t) - 1)
easing_functions = {
"Sine In": easeInSine,
"Sine Out": easeOutSine,
"Sine In/Out": easeInOutSine,
"Quart In": easeInQuart,
"Quart Out": easeOutQuart,
"Quart In/Out": easeInOutQuart,
"Cubic In": easeInCubic,
"Cubic Out": easeOutCubic,
"Cubic In/Out": easeInOutCubic,
"Circ In": easeInCirc,
"Circ Out": easeOutCirc,
"Circ In/Out": easeInOutCirc,
"Back In": easeInBack,
"Back Out": easeOutBack,
"Back In/Out": easeInOutBack,
"Elastic In": easeInElastic,
"Elastic Out": easeOutElastic,
"Elastic In/Out": easeInOutElastic,
"Bounce In": easeInBounce,
"Bounce Out": easeOutBounce,
"Bounce In/Out": easeInOutBounce,
}
function_ease = easing_functions.get(easing_type)
if function_ease:
return function_ease(value)
log.error(f"Unknown easing type: {easing_type}")
log.error(f"Available easing types: {list(easing_functions.keys())}")
raise ValueError(f"Unknown easing type: {easing_type}")
# endregion