Upload salia_detailer_ezpz.py
Browse files- salia_detailer_ezpz.py +115 -31
salia_detailer_ezpz.py
CHANGED
|
@@ -4,7 +4,7 @@ from typing import Any, Dict, Tuple, Optional
|
|
| 4 |
|
| 5 |
import torch
|
| 6 |
import numpy as np
|
| 7 |
-
from PIL import Image
|
| 8 |
|
| 9 |
import folder_paths
|
| 10 |
|
|
@@ -149,45 +149,131 @@ def _load_controlnet_cached(control_net_name: str):
|
|
| 149 |
|
| 150 |
|
| 151 |
# -------------------------------------------------------------------------------------
|
| 152 |
-
#
|
| 153 |
-
# (We still lazy-call the user's LoadImage_SaliaOnline_Assets for consistent mask behavior.)
|
| 154 |
# -------------------------------------------------------------------------------------
|
| 155 |
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
try:
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
files = sorted([p.name for p in img_dir.glob("*.png")])
|
| 165 |
return files
|
| 166 |
except Exception:
|
| 167 |
return []
|
| 168 |
|
| 169 |
|
| 170 |
-
def
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 177 |
|
| 178 |
|
| 179 |
-
def
|
| 180 |
"""
|
| 181 |
-
|
| 182 |
-
|
|
|
|
|
|
|
|
|
|
| 183 |
"""
|
| 184 |
-
|
| 185 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
|
|
|
|
|
|
|
|
|
| 190 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
def _run_salia_depth(image: torch.Tensor, resolution: int) -> torch.Tensor:
|
| 193 |
"""
|
|
@@ -394,12 +480,12 @@ class Salia_Detailer_EZPZ:
|
|
| 394 |
crop_up = _resize_image_lanczos(crop_rgb, up_w, up_h)
|
| 395 |
|
| 396 |
# -------------------------
|
| 397 |
-
# 4) Load asset mask (
|
| 398 |
# -------------------------
|
| 399 |
if asset_image == "<no pngs found>":
|
| 400 |
-
raise FileNotFoundError("No PNGs found in
|
| 401 |
|
| 402 |
-
asset_mask =
|
| 403 |
if asset_mask.ndim == 2:
|
| 404 |
asset_mask = asset_mask.unsqueeze(0)
|
| 405 |
if asset_mask.ndim != 3:
|
|
@@ -466,7 +552,6 @@ class Salia_Detailer_EZPZ:
|
|
| 466 |
# -------------------------
|
| 467 |
# 9) KSampler
|
| 468 |
# -------------------------
|
| 469 |
-
# No seed input requested: derive a stable seed from inputs so changing anything changes seed.
|
| 470 |
seed_material = (
|
| 471 |
f"{ckpt_name}|{control_net_name}|{asset_image}|{x}|{y}|{s}|{up}|"
|
| 472 |
f"{steps}|{cfg}|{sampler_name}|{scheduler}|{denoise}|"
|
|
@@ -503,7 +588,6 @@ class Salia_Detailer_EZPZ:
|
|
| 503 |
join = nodes.JoinImageWithAlpha()
|
| 504 |
join_fn = getattr(join, join.FUNCTION)
|
| 505 |
|
| 506 |
-
# Some Comfy versions name the mask input "alpha", others "mask".
|
| 507 |
try:
|
| 508 |
(rgba_up,) = join_fn(image=decoded_rgb, alpha=asset_mask_up)
|
| 509 |
except TypeError:
|
|
|
|
| 4 |
|
| 5 |
import torch
|
| 6 |
import numpy as np
|
| 7 |
+
from PIL import Image, ImageOps
|
| 8 |
|
| 9 |
import folder_paths
|
| 10 |
|
|
|
|
| 149 |
|
| 150 |
|
| 151 |
# -------------------------------------------------------------------------------------
|
| 152 |
+
# Assets/images dropdown + loader (INLINED, no LoadImage_SaliaOnline_Assets dependency)
|
|
|
|
| 153 |
# -------------------------------------------------------------------------------------
|
| 154 |
|
| 155 |
+
_ASSETS_DIR_CACHE: Optional["object"] = None
|
| 156 |
+
_ASSETS_DIR_LOCK = threading.Lock()
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def _find_assets_images_dir():
|
| 160 |
+
"""
|
| 161 |
+
Find the plugin's assets/images folder by walking upward from this file.
|
| 162 |
+
This is robust even if Comfy imports modules in weird ways.
|
| 163 |
+
"""
|
| 164 |
+
from pathlib import Path
|
| 165 |
+
|
| 166 |
+
here = Path(__file__).resolve()
|
| 167 |
+
# check a few levels up; plugin root should be near
|
| 168 |
+
for parent in [here.parent] + list(here.parents)[:8]:
|
| 169 |
+
candidate = parent / "assets" / "images"
|
| 170 |
+
if candidate.is_dir():
|
| 171 |
+
return candidate
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
|
| 175 |
+
def _assets_images_dir():
|
| 176 |
+
global _ASSETS_DIR_CACHE
|
| 177 |
+
with _ASSETS_DIR_LOCK:
|
| 178 |
+
if _ASSETS_DIR_CACHE is not None:
|
| 179 |
+
# If it was found once, reuse.
|
| 180 |
+
try:
|
| 181 |
+
if _ASSETS_DIR_CACHE.is_dir():
|
| 182 |
+
return _ASSETS_DIR_CACHE
|
| 183 |
+
except Exception:
|
| 184 |
+
pass
|
| 185 |
+
|
| 186 |
+
found = _find_assets_images_dir()
|
| 187 |
+
_ASSETS_DIR_CACHE = found
|
| 188 |
+
return found
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
def _list_asset_pngs():
|
| 192 |
+
"""
|
| 193 |
+
List PNGs inside assets/images (recursive), returning paths relative to assets/images.
|
| 194 |
+
"""
|
| 195 |
+
img_dir = _assets_images_dir()
|
| 196 |
+
if img_dir is None:
|
| 197 |
+
return []
|
| 198 |
+
|
| 199 |
+
files = []
|
| 200 |
try:
|
| 201 |
+
for p in img_dir.rglob("*"):
|
| 202 |
+
if p.is_file() and p.suffix.lower() == ".png":
|
| 203 |
+
rel = p.relative_to(img_dir).as_posix()
|
| 204 |
+
files.append(rel)
|
| 205 |
+
files.sort()
|
|
|
|
| 206 |
return files
|
| 207 |
except Exception:
|
| 208 |
return []
|
| 209 |
|
| 210 |
|
| 211 |
+
def _safe_asset_path(asset_rel_path: str):
|
| 212 |
+
"""
|
| 213 |
+
Resolve a selected dropdown entry to an actual file path inside assets/images.
|
| 214 |
+
Prevents path traversal.
|
| 215 |
+
"""
|
| 216 |
+
from pathlib import Path
|
| 217 |
+
|
| 218 |
+
img_dir = _assets_images_dir()
|
| 219 |
+
if img_dir is None:
|
| 220 |
+
raise FileNotFoundError("assets/images folder not found (could not locate plugin assets).")
|
| 221 |
+
|
| 222 |
+
base = img_dir.resolve()
|
| 223 |
+
rel = Path(asset_rel_path)
|
| 224 |
+
|
| 225 |
+
if rel.is_absolute():
|
| 226 |
+
raise ValueError("Absolute paths are not allowed for asset_image.")
|
| 227 |
+
|
| 228 |
+
# Resolve and verify containment
|
| 229 |
+
full = (base / rel).resolve()
|
| 230 |
+
if base != full and base not in full.parents:
|
| 231 |
+
raise ValueError(f"Invalid asset path (path traversal blocked): {asset_rel_path}")
|
| 232 |
+
|
| 233 |
+
if not full.is_file():
|
| 234 |
+
raise FileNotFoundError(f"Asset PNG not found in assets/images: {asset_rel_path}")
|
| 235 |
+
|
| 236 |
+
if full.suffix.lower() != ".png":
|
| 237 |
+
raise ValueError(f"Asset is not a PNG: {asset_rel_path}")
|
| 238 |
+
|
| 239 |
+
return full
|
| 240 |
|
| 241 |
|
| 242 |
+
def _load_asset_image_and_mask(asset_rel_path: str):
|
| 243 |
"""
|
| 244 |
+
Load PNG from assets/images and return (IMAGE, MASK) in ComfyUI formats.
|
| 245 |
+
|
| 246 |
+
IMPORTANT: Mask semantics match ComfyUI core LoadImage:
|
| 247 |
+
- If PNG has alpha: mask = 1 - alpha
|
| 248 |
+
- If no alpha: mask = 0 (opaque)
|
| 249 |
"""
|
| 250 |
+
p = _safe_asset_path(asset_rel_path)
|
| 251 |
+
|
| 252 |
+
im = Image.open(p)
|
| 253 |
+
im = ImageOps.exif_transpose(im)
|
| 254 |
+
|
| 255 |
+
# Ensure we can extract alpha if present
|
| 256 |
+
had_alpha = ("A" in im.getbands())
|
| 257 |
+
rgba = im.convert("RGBA")
|
| 258 |
+
rgb = rgba.convert("RGB")
|
| 259 |
+
|
| 260 |
+
rgb_arr = np.array(rgb).astype(np.float32) / 255.0 # [H,W,3]
|
| 261 |
+
img_t = torch.from_numpy(rgb_arr)[None, ...]
|
| 262 |
|
| 263 |
+
if had_alpha:
|
| 264 |
+
alpha = np.array(rgba.getchannel("A")).astype(np.float32) / 255.0 # [H,W], 1=opaque
|
| 265 |
+
mask = 1.0 - alpha # Comfy MASK convention
|
| 266 |
+
else:
|
| 267 |
+
h, w = rgb.size[1], rgb.size[0]
|
| 268 |
+
mask = np.zeros((h, w), dtype=np.float32)
|
| 269 |
|
| 270 |
+
mask_t = torch.from_numpy(mask)[None, ...]
|
| 271 |
+
return img_t, mask_t
|
| 272 |
+
|
| 273 |
+
|
| 274 |
+
# -------------------------------------------------------------------------------------
|
| 275 |
+
# Salia_Depth (still lazy import, unchanged)
|
| 276 |
+
# -------------------------------------------------------------------------------------
|
| 277 |
|
| 278 |
def _run_salia_depth(image: torch.Tensor, resolution: int) -> torch.Tensor:
|
| 279 |
"""
|
|
|
|
| 480 |
crop_up = _resize_image_lanczos(crop_rgb, up_w, up_h)
|
| 481 |
|
| 482 |
# -------------------------
|
| 483 |
+
# 4) Load asset mask (INLINE assets loader) and resize to match upscaled resolution
|
| 484 |
# -------------------------
|
| 485 |
if asset_image == "<no pngs found>":
|
| 486 |
+
raise FileNotFoundError("No PNGs found in assets/images for this plugin.")
|
| 487 |
|
| 488 |
+
_asset_img_unused, asset_mask = _load_asset_image_and_mask(asset_image) # MASK is what we need
|
| 489 |
if asset_mask.ndim == 2:
|
| 490 |
asset_mask = asset_mask.unsqueeze(0)
|
| 491 |
if asset_mask.ndim != 3:
|
|
|
|
| 552 |
# -------------------------
|
| 553 |
# 9) KSampler
|
| 554 |
# -------------------------
|
|
|
|
| 555 |
seed_material = (
|
| 556 |
f"{ckpt_name}|{control_net_name}|{asset_image}|{x}|{y}|{s}|{up}|"
|
| 557 |
f"{steps}|{cfg}|{sampler_name}|{scheduler}|{denoise}|"
|
|
|
|
| 588 |
join = nodes.JoinImageWithAlpha()
|
| 589 |
join_fn = getattr(join, join.FUNCTION)
|
| 590 |
|
|
|
|
| 591 |
try:
|
| 592 |
(rgba_up,) = join_fn(image=decoded_rgb, alpha=asset_mask_up)
|
| 593 |
except TypeError:
|