446 lines
17 KiB
Python
446 lines
17 KiB
Python
"""
|
|
LLS / Upscale
|
|
=============
|
|
功能域:图像超分辨率放大
|
|
对应功能分类总览第 5 节:图像处理与后处理 → 超分(Upscale)
|
|
|
|
包含节点:
|
|
- LLSUpscaleSwitcher : 在 upscale_model 与 PyTorch 插值之间切换
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from ..utils.model_info import info_to_json
|
|
|
|
# ---------- 从根模块导入共享工具函数 ----------
|
|
|
|
from ..nodes import (
|
|
_torch_inference_mode,
|
|
_require_torch,
|
|
_require_comfy_modules,
|
|
_clamp_image,
|
|
_sorted_unique,
|
|
_is_oom_exception,
|
|
_make_progress_bar,
|
|
_supports_argument,
|
|
)
|
|
|
|
# ---------- 可选依赖(防御性导入) ----------
|
|
|
|
try:
|
|
import torch
|
|
import torch.nn.functional as F
|
|
except Exception as exc:
|
|
torch = None
|
|
F = None
|
|
_TORCH_IMPORT_ERROR = exc
|
|
else:
|
|
_TORCH_IMPORT_ERROR = None
|
|
|
|
try:
|
|
import folder_paths
|
|
except Exception as exc:
|
|
folder_paths = None
|
|
_FOLDER_PATHS_IMPORT_ERROR = exc
|
|
else:
|
|
_FOLDER_PATHS_IMPORT_ERROR = None
|
|
|
|
try:
|
|
import comfy.model_management as model_management
|
|
import comfy.utils as comfy_utils
|
|
except Exception as exc:
|
|
model_management = None
|
|
comfy_utils = None
|
|
_COMFY_IMPORT_ERROR = exc
|
|
else:
|
|
_COMFY_IMPORT_ERROR = None
|
|
|
|
# ---------- 常量 ----------
|
|
|
|
UPSCALE_MODE_CHOICES = ["none", "interpolation", "upscale_model", "latent_upscale", "tile_upscale", "pytorch"]
|
|
INTERPOLATION_CHOICES = ["nearest", "bilinear", "bicubic", "area"]
|
|
NO_UPSCALE_MODEL_PLACEHOLDER = "(no upscale models found)"
|
|
|
|
|
|
# ---------- 内部工具函数(仅本模块专用) ----------
|
|
|
|
def _get_upscale_model_names() -> list[str]:
|
|
if folder_paths is None:
|
|
return [NO_UPSCALE_MODEL_PLACEHOLDER]
|
|
try:
|
|
names = list(folder_paths.get_filename_list("upscale_models"))
|
|
except Exception:
|
|
names = []
|
|
return names if names else [NO_UPSCALE_MODEL_PLACEHOLDER]
|
|
|
|
|
|
def _has_real_upscale_models(model_names: list[str]) -> bool:
|
|
return any(name and name != NO_UPSCALE_MODEL_PLACEHOLDER for name in model_names)
|
|
|
|
|
|
def _is_missing_upscale_model_selection(model_name: str | None) -> bool:
|
|
return model_name in (None, "", NO_UPSCALE_MODEL_PLACEHOLDER)
|
|
|
|
|
|
def _resolve_upscale_model_path(model_name: str) -> str:
|
|
_require_comfy_modules()
|
|
if model_name == NO_UPSCALE_MODEL_PLACEHOLDER:
|
|
raise RuntimeError(
|
|
"No upscale models were found in ComfyUI/models/upscale_models/. "
|
|
"Add a model there or switch mode to 'pytorch'."
|
|
)
|
|
try:
|
|
get_full_path_or_raise = getattr(folder_paths, "get_full_path_or_raise", None)
|
|
if callable(get_full_path_or_raise):
|
|
return get_full_path_or_raise("upscale_models", model_name)
|
|
model_path = folder_paths.get_full_path("upscale_models", model_name)
|
|
if model_path:
|
|
return model_path
|
|
except FileNotFoundError as exc:
|
|
raise RuntimeError(
|
|
f"Upscale model '{model_name}' was not found in ComfyUI/models/upscale_models/."
|
|
) from exc
|
|
except Exception as exc:
|
|
raise RuntimeError(
|
|
f"Failed to resolve upscale model '{model_name}': {exc}"
|
|
) from exc
|
|
raise RuntimeError(
|
|
f"Upscale model '{model_name}' was not found in ComfyUI/models/upscale_models/."
|
|
)
|
|
|
|
|
|
def _load_upscale_model_descriptor(model_name: str):
|
|
_require_comfy_modules()
|
|
model_path = _resolve_upscale_model_path(model_name)
|
|
try:
|
|
from spandrel import ImageModelDescriptor, ModelLoader
|
|
except Exception as exc:
|
|
raise RuntimeError(
|
|
"Failed to import ComfyUI's upscale model loader. "
|
|
"Make sure the node runs inside a recent ComfyUI installation."
|
|
) from exc
|
|
try:
|
|
loader = ModelLoader()
|
|
descriptor = None
|
|
if hasattr(comfy_utils, "load_torch_file") and hasattr(loader, "load_from_state_dict"):
|
|
state_dict = comfy_utils.load_torch_file(model_path, safe_load=True)
|
|
if (
|
|
isinstance(state_dict, dict)
|
|
and "module.layers.0.residual_group.blocks.0.norm1.weight" in state_dict
|
|
and hasattr(comfy_utils, "state_dict_prefix_replace")
|
|
):
|
|
state_dict = comfy_utils.state_dict_prefix_replace(state_dict, {"module.": ""})
|
|
descriptor = loader.load_from_state_dict(state_dict)
|
|
elif hasattr(loader, "load_from_file"):
|
|
descriptor = loader.load_from_file(model_path)
|
|
else:
|
|
raise RuntimeError("No supported spandrel loading API is available.")
|
|
except Exception as exc:
|
|
raise RuntimeError(f"Failed to load upscale model '{model_name}': {exc}") from exc
|
|
if isinstance(ImageModelDescriptor, type) and not isinstance(descriptor, ImageModelDescriptor):
|
|
raise RuntimeError(
|
|
f"File '{model_name}' is not a valid image upscale model supported by ComfyUI."
|
|
)
|
|
if not callable(descriptor):
|
|
raise RuntimeError(f"Loaded upscale model '{model_name}' is not callable.")
|
|
eval_fn = getattr(descriptor, "eval", None)
|
|
if callable(eval_fn):
|
|
maybe_descriptor = eval_fn()
|
|
if maybe_descriptor is not None:
|
|
descriptor = maybe_descriptor
|
|
return descriptor
|
|
|
|
|
|
def _get_upscale_amount(upscale_model: Any) -> float:
|
|
scale = getattr(upscale_model, "scale", None)
|
|
if scale is None:
|
|
scale = getattr(getattr(upscale_model, "model", None), "scale", None)
|
|
if scale is None:
|
|
raise RuntimeError("Loaded upscale model does not expose a scale factor.")
|
|
return float(scale)
|
|
|
|
|
|
def _estimate_model_memory(image_bchw: Any, upscale_model: Any) -> int:
|
|
if model_management is None:
|
|
return 0
|
|
estimate = 0
|
|
module = getattr(upscale_model, "model", None)
|
|
try:
|
|
if module is not None and hasattr(model_management, "module_size"):
|
|
estimate += int(model_management.module_size(module))
|
|
if hasattr(image_bchw, "element_size") and hasattr(image_bchw, "nelement"):
|
|
element_size = int(image_bchw.element_size())
|
|
estimate += int(
|
|
(512 * 512 * 3) * element_size * max(_get_upscale_amount(upscale_model), 1.0) * 384.0
|
|
)
|
|
estimate += int(image_bchw.nelement() * element_size)
|
|
except Exception:
|
|
return 0
|
|
return estimate
|
|
|
|
|
|
def _get_tiled_scale_steps(image_bchw: Any, tile: int, overlap: int) -> int:
|
|
if comfy_utils is None or not hasattr(comfy_utils, "get_tiled_scale_steps"):
|
|
return 1
|
|
return max(
|
|
1,
|
|
int(
|
|
comfy_utils.get_tiled_scale_steps(
|
|
image_bchw.shape[3],
|
|
image_bchw.shape[2],
|
|
tile_x=tile,
|
|
tile_y=tile,
|
|
overlap=overlap,
|
|
)
|
|
),
|
|
)
|
|
|
|
|
|
def _next_tile_size(current_tile: int) -> int:
|
|
halved = current_tile // 2
|
|
if halved < 128:
|
|
return 0
|
|
return max(128, (halved // 64) * 64)
|
|
|
|
|
|
# ---------- 节点类 ----------
|
|
|
|
class LLSUpscaleSwitcher:
|
|
"""
|
|
在 upscale_model 与 PyTorch 插值之间切换的超分节点。
|
|
|
|
- upscale_model 模式:调用 spandrel 加载 ESRGAN 等模型,支持分块推理防 OOM。
|
|
- pytorch 模式:使用 torch.nn.functional.interpolate 做普通插值放大。
|
|
"""
|
|
|
|
CATEGORY = "LLS/Upscale"
|
|
FUNCTION = "upscale"
|
|
RETURN_TYPES = ("IMAGE", "STRING")
|
|
RETURN_NAMES = ("image", "upscale_info")
|
|
DESCRIPTION = "Switch between ComfyUI upscale models and PyTorch interpolation."
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
model_names = _sorted_unique(_get_upscale_model_names())
|
|
default_mode = "upscale_model" if _has_real_upscale_models(model_names) else "interpolation"
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"mode": (UPSCALE_MODE_CHOICES, {"default": default_mode}),
|
|
"scale": ("FLOAT", {"default": 2.0, "min": 0.1, "max": 8.0, "step": 0.1}),
|
|
"interpolation": (INTERPOLATION_CHOICES, {"default": "bilinear"}),
|
|
"model_name": (model_names,),
|
|
"tile": ("INT", {"default": 512, "min": 128, "max": 2048, "step": 64}),
|
|
"overlap": ("INT", {"default": 32, "min": 0, "max": 256, "step": 8}),
|
|
},
|
|
}
|
|
|
|
def upscale(self, image, mode: str, scale: float, interpolation: str, model_name: str, tile: int, overlap: int):
|
|
requested_mode = mode
|
|
warning = None
|
|
if mode not in UPSCALE_MODE_CHOICES:
|
|
raise RuntimeError(f"Unsupported mode '{mode}'. Expected one of {UPSCALE_MODE_CHOICES}.")
|
|
if mode == "none":
|
|
return (
|
|
image,
|
|
info_to_json(
|
|
{
|
|
"mode": "none",
|
|
"requested_mode": requested_mode,
|
|
"scale": 1.0,
|
|
"upscaled": False,
|
|
}
|
|
),
|
|
)
|
|
if mode in {"interpolation", "pytorch"}:
|
|
result = self._upscale_with_pytorch(image=image, scale=scale, interpolation=interpolation)[0]
|
|
return (
|
|
result,
|
|
info_to_json(
|
|
{
|
|
"mode": "interpolation",
|
|
"requested_mode": requested_mode,
|
|
"scale": scale,
|
|
"interpolation": interpolation,
|
|
"upscaled": True,
|
|
}
|
|
),
|
|
)
|
|
if mode == "latent_upscale":
|
|
warning = "latent_upscale_not_available_on_IMAGE_input_fallback_to_interpolation"
|
|
result = self._upscale_with_pytorch(image=image, scale=scale, interpolation=interpolation)[0]
|
|
return (
|
|
result,
|
|
info_to_json(
|
|
{
|
|
"mode": "interpolation",
|
|
"requested_mode": requested_mode,
|
|
"scale": scale,
|
|
"interpolation": interpolation,
|
|
"warning": warning,
|
|
"upscaled": True,
|
|
}
|
|
),
|
|
)
|
|
if _is_missing_upscale_model_selection(model_name):
|
|
warning = "no_upscale_model_found_fallback_to_interpolation"
|
|
print(
|
|
"[LLS] WARNING: No valid upscale model is selected. "
|
|
"Falling back to interpolation mode."
|
|
)
|
|
result = self._upscale_with_pytorch(image=image, scale=scale, interpolation=interpolation)[0]
|
|
return (
|
|
result,
|
|
info_to_json(
|
|
{
|
|
"mode": "interpolation",
|
|
"requested_mode": requested_mode,
|
|
"scale": scale,
|
|
"interpolation": interpolation,
|
|
"warning": warning,
|
|
"upscaled": True,
|
|
}
|
|
),
|
|
)
|
|
result = self._upscale_with_model(image=image, model_name=model_name, tile=tile, overlap=overlap)[0]
|
|
actual_mode = "tile_upscale" if requested_mode == "tile_upscale" else "upscale_model"
|
|
return (
|
|
result,
|
|
info_to_json(
|
|
{
|
|
"mode": actual_mode,
|
|
"requested_mode": requested_mode,
|
|
"scale": scale,
|
|
"model_name": model_name,
|
|
"tile": tile,
|
|
"overlap": overlap,
|
|
"warning": warning,
|
|
"upscaled": True,
|
|
}
|
|
),
|
|
)
|
|
|
|
def _upscale_with_pytorch(self, image, scale: float, interpolation: str):
|
|
_require_torch()
|
|
if F is None:
|
|
raise RuntimeError("torch.nn.functional is not available.")
|
|
if interpolation not in INTERPOLATION_CHOICES:
|
|
raise RuntimeError(f"Unsupported interpolation '{interpolation}'. Expected one of {INTERPOLATION_CHOICES}.")
|
|
if scale <= 0:
|
|
raise RuntimeError("Scale must be greater than 0.")
|
|
try:
|
|
with _torch_inference_mode():
|
|
image_bchw = image.permute(0, 3, 1, 2).contiguous()
|
|
interpolate_kwargs: dict[str, Any] = {"scale_factor": float(scale), "mode": interpolation}
|
|
if interpolation in {"bilinear", "bicubic"}:
|
|
interpolate_kwargs["align_corners"] = False
|
|
result = F.interpolate(image_bchw, **interpolate_kwargs)
|
|
result = result.permute(0, 2, 3, 1).contiguous()
|
|
if model_management is not None and hasattr(model_management, "intermediate_dtype"):
|
|
try:
|
|
result = result.to(dtype=model_management.intermediate_dtype())
|
|
except Exception:
|
|
pass
|
|
return (_clamp_image(result),)
|
|
except Exception as exc:
|
|
if _is_oom_exception(exc):
|
|
raise RuntimeError(
|
|
"Out of memory while running PyTorch interpolation. "
|
|
"Reduce the scale value, or switch to 'upscale_model' mode and lower tile."
|
|
) from exc
|
|
raise RuntimeError(f"PyTorch interpolation upscale failed: {exc}") from exc
|
|
|
|
def _upscale_with_model(self, image, model_name: str, tile: int, overlap: int):
|
|
_require_torch()
|
|
_require_comfy_modules()
|
|
try:
|
|
upscale_model = _load_upscale_model_descriptor(model_name)
|
|
except Exception as exc:
|
|
raise RuntimeError(f"Failed to prepare upscale model '{model_name}': {exc}") from exc
|
|
|
|
with _torch_inference_mode():
|
|
image_bchw = image.permute(0, 3, 1, 2).contiguous()
|
|
device = model_management.get_torch_device()
|
|
output_device = None
|
|
if hasattr(model_management, "intermediate_device"):
|
|
try:
|
|
output_device = model_management.intermediate_device()
|
|
except Exception:
|
|
output_device = None
|
|
image_bchw = image_bchw.to(device)
|
|
if hasattr(upscale_model, "to"):
|
|
upscale_model = upscale_model.to(device)
|
|
try:
|
|
memory_required = _estimate_model_memory(image_bchw, upscale_model)
|
|
if memory_required > 0 and hasattr(model_management, "free_memory"):
|
|
try:
|
|
model_management.free_memory(memory_required, device)
|
|
except Exception:
|
|
pass
|
|
upscale_amount = _get_upscale_amount(upscale_model)
|
|
current_tile = int(tile)
|
|
last_oom: Exception | None = None
|
|
|
|
while current_tile >= 128:
|
|
steps = image_bchw.shape[0] * _get_tiled_scale_steps(image_bchw, current_tile, overlap)
|
|
progress_bar = _make_progress_bar(steps)
|
|
tiled_kwargs = {
|
|
"tile_x": current_tile,
|
|
"tile_y": current_tile,
|
|
"overlap": int(overlap),
|
|
"upscale_amount": upscale_amount,
|
|
"pbar": progress_bar,
|
|
}
|
|
tiled_scale = getattr(comfy_utils, "tiled_scale", None)
|
|
if not callable(tiled_scale):
|
|
raise RuntimeError("ComfyUI tiled_scale helper is not available.")
|
|
if _supports_argument(tiled_scale, "output_device") and output_device is not None:
|
|
tiled_kwargs["output_device"] = output_device
|
|
try:
|
|
result = tiled_scale(
|
|
image_bchw,
|
|
lambda tile_image: upscale_model(tile_image.float()),
|
|
**tiled_kwargs,
|
|
)
|
|
result = result.permute(0, 2, 3, 1).contiguous()
|
|
if model_management is not None and hasattr(model_management, "intermediate_dtype"):
|
|
try:
|
|
result = result.to(dtype=model_management.intermediate_dtype())
|
|
except Exception:
|
|
pass
|
|
return (_clamp_image(result),)
|
|
except Exception as exc:
|
|
if _is_oom_exception(exc):
|
|
last_oom = exc
|
|
next_tile = _next_tile_size(current_tile)
|
|
if next_tile <= 0 or next_tile == current_tile:
|
|
break
|
|
current_tile = next_tile
|
|
continue
|
|
raise RuntimeError(
|
|
f"Upscale model '{model_name}' failed during tiled inference: {exc}"
|
|
) from exc
|
|
|
|
raise RuntimeError(
|
|
f"Out of memory while running upscale model '{model_name}'. "
|
|
f"Try reducing tile below {tile} or lowering overlap."
|
|
) from last_oom
|
|
finally:
|
|
if hasattr(upscale_model, "to"):
|
|
try:
|
|
upscale_model.to("cpu")
|
|
except Exception:
|
|
pass
|
|
|
|
|
|
# ---------- 注册表 ----------
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"LLSUpscaleSwitcher": LLSUpscaleSwitcher,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"LLSUpscaleSwitcher": "LLS Upscale Switcher",
|
|
}
|