Spaces:
Running on Zero
Running on Zero
| """Препроцес G-буфер-пасів під ControlNet-Union + IP-Adapter. | |
| Тут живе ГОЛОВНА пастка Фази 0 — конвенція нормалей і depth (див. docs/01). | |
| three.js `MeshNormalMaterial` / рендер Blender кодують нормаль у своєму RGB; | |
| ControlNet-normal чекає своє. Ці функції — те місце, де ми це вирівнюємо | |
| дебаг-ручками (`normal_flip`, `normal_space`, `depth_invert`), не чіпаючи пайплайн. | |
| """ | |
| from __future__ import annotations | |
| import io | |
| import numpy as np | |
| from PIL import Image | |
| def _load_rgb(data: bytes) -> np.ndarray: | |
| return np.asarray(Image.open(io.BytesIO(data)).convert("RGB")) | |
| def _load_gray(data: bytes) -> np.ndarray: | |
| return np.asarray(Image.open(io.BytesIO(data)).convert("L")) | |
| def prep_normal( | |
| data: bytes, | |
| *, | |
| flip: dict | None = None, | |
| space: str = "view", | |
| size: tuple[int, int] | None = None, | |
| ) -> Image.Image: | |
| """view-space normal PNG (кодування (n+1)/2) → нормаль у конвенції ControlNet. | |
| `flip` = {"x":bool,"y":bool,"z":bool}: інвертує вісь ПІСЛЯ декоду в [-1,1]. | |
| Типова пастка three.js↔ControlNet — перевернути Y (і/або Z). `space` поки | |
| лише мітка (world→view вимагає viewmat; у Фазі 0 працюємо у view-space). | |
| """ | |
| flip = flip or {} | |
| rgb = _load_rgb(data).astype(np.float32) / 255.0 | |
| n = rgb * 2.0 - 1.0 # [0,1] → [-1,1] | |
| if flip.get("x"): | |
| n[..., 0] *= -1.0 | |
| if flip.get("y"): | |
| n[..., 1] *= -1.0 | |
| if flip.get("z"): | |
| n[..., 2] *= -1.0 | |
| # ре-нормалізуємо (фліпи довжину не міняють, але resize/JPEG-артефакти можуть) | |
| norm = np.linalg.norm(n, axis=-1, keepdims=True) | |
| n = n / np.clip(norm, 1e-6, None) | |
| enc = ((n + 1.0) * 0.5 * 255.0).clip(0, 255).astype(np.uint8) | |
| img = Image.fromarray(enc) | |
| if size is not None: | |
| img = img.resize(size, Image.BILINEAR) | |
| return img | |
| def prep_depth( | |
| data: bytes, | |
| *, | |
| invert: bool = False, | |
| size: tuple[int, int] | None = None, | |
| ) -> Image.Image: | |
| """depth PNG → grayscale-мапа у конвенції ControlNet-depth (MiDaS: near=bright). | |
| Нормалізуємо [0,1] по фактичному діапазону кадру (робастно, 2..98 перцентиль), | |
| `invert` якщо джерело віддає near=dark. | |
| """ | |
| d = _load_gray(data).astype(np.float32) | |
| lo, hi = np.percentile(d, [2, 98]) | |
| d = np.clip((d - lo) / max(hi - lo, 1e-6), 0.0, 1.0) | |
| if invert: | |
| d = 1.0 - d | |
| img = Image.fromarray((d * 255).astype(np.uint8)).convert("RGB") | |
| if size is not None: | |
| img = img.resize(size, Image.BILINEAR) | |
| return img | |
| def prep_beauty(data: bytes, *, size: tuple[int, int] | None = None) -> Image.Image: | |
| """lit beauty-пас → sRGB-картинка для CLIP-енкодера IP-Adapter-а.""" | |
| img = Image.fromarray(_load_rgb(data)) | |
| if size is not None: | |
| img = img.resize(size, Image.BILINEAR) | |
| return img | |
| def build_regions(seg_bytes: bytes, parts: list, size: tuple[int, int]) -> list | None: | |
| """seg-маска (кожен регіон = свій palette-колір) + parts[{color,prompt}] → | |
| список {mask: float32 (H8,W8), prompt}. Колір пікселя → НАЙБЛИЖЧИЙ у палітрі | |
| (чорний фон = index 0), тож resize/JPEG-краї не ламають межі. Маска даунсемплиться | |
| до latent-роздільності (H/8×W/8) — саме її чекає RegionalCrossAttnProcessor. | |
| Частини без промпту або невидимі в кадрі пропускаємо.""" | |
| W, H = size | |
| img = Image.open(io.BytesIO(seg_bytes)).convert("RGB").resize((W, H), Image.NEAREST) | |
| arr = np.asarray(img).astype(np.int16) # H,W,3 | |
| def _hex(h): | |
| h = h.lstrip("#") | |
| return [int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)] | |
| pal = np.array([[0, 0, 0]] + [_hex(p["color"]) for p in parts], np.int16) # (P+1,3) | |
| idx = ((arr[:, :, None, :] - pal[None, None, :, :]) ** 2).sum(-1).argmin(-1) # H,W | |
| h8, w8 = H // 8, W // 8 | |
| out = [] | |
| for i, p in enumerate(parts): | |
| m = (idx == i + 1).astype(np.uint8) * 255 | |
| m8 = np.asarray(Image.fromarray(m).resize((w8, h8), Image.NEAREST)) | |
| m8 = (m8 > 127).astype(np.float32) | |
| if m8.sum() < 1: # регіон не в кадрі | |
| continue | |
| out.append({"mask": m8, "prompt": p["prompt"]}) | |
| return out or None | |