Add files via upload

feat: initial public release (v0.1.0)
This commit is contained in:
The Kraken
2025-08-25 14:15:10 -07:00
committed by GitHub
commit dd4eb5cec8
9 changed files with 711 additions and 0 deletions
+8
View File
@@ -0,0 +1,8 @@
# Changelog
## [0.1.0] - 2025-08-25
- Initial public release.
- `KrakenFluxEmptyLatentImage` with preset strings + preview.
- `KrakenUpscaleTileCalc` with `min_upscale_policy` (auto/off/1.5x/2.0x/custom), tile/overlap suggestions, preview.
- `KrakenResolutionHelper` with exact-size guarantee and corrected `allow_upscale=False` behavior.
- Front-end `web/kraken.js` previews for latent + tile calc.
+14
View File
@@ -0,0 +1,14 @@
# Contributing
## Dev setup
- Place this repo under `ComfyUI/custom_nodes/ComfyUI-KrakenTools`.
- Restart ComfyUI after changes to Python or `web/kraken.js`.
## Coding guidelines
- Python: docstrings, type hints where helpful, deterministic behavior.
- Avoid heavy deps; rely on `numpy` and `Pillow`.
- Prefer tiling over giant tensors to be VRAM-friendly.
## Pull requests
- One focused change per PR.
- Update `CHANGELOG.md` when appropriate.
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 The Kraken
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the “Software”), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
+61
View File
@@ -0,0 +1,61 @@
# 🦑 ComfyUI-KrakenTools
High-utility nodes for ComfyUI with a focus on **Flux 1 Dev** workflows and **Ultimate SD Upscale** enhancement loops.
**Why this exists:** when your “final” resolution is relatively small (e.g., 736×1344 portraits), running *no* real upscale in Ultimate SD Upscale (USDU) often doesn’t add detail. The Kraken approach enforces a true enlarge pass (e.g., 1.5×–2×), then **downscales back** to your exact target with high-quality filtering — preserving micro-detail and cleaning seams.
## ✨ Nodes
- **🦑 Kraken Flux Empty Latent Image** (`KrakenFluxEmptyLatentImage`)
- Builds a Flux-style latent and exposes canonical **preset** + **resolution** strings for downstream nodes.
- Outputs a small UI preview so you always see the intended canvas size.
- **🦑 Kraken Upscale & Tile Calc** (`KrakenUpscaleTileCalc`)
- Computes a uniform upscale factor to *cover* your target, aligns to model-friendly multiples, and suggests tile sizes + overlaps for USDU.
- **New:** `min_upscale_policy` (auto/off/1.5x/2.0x/custom) guarantees USDU *actually* upscales, even when target==source.
- **🦑 Kraken Resolution Helper (Exact)** (`KrakenResolutionHelper`)
- Exact-size finisher: **keep proportion** (contain + pad), **pad** (with anchors), **fill/crop** (cover), or **stretch**.
- Downscales without cropping and **guarantees final WxH**. With `allow_upscale=False`, it will never enlarge in the final step.
## 🛠 Installation
1. Locate your ComfyUI `custom_nodes` directory.
2. Clone or copy this repo into it:
```bash
cd /path/to/ComfyUI/custom_nodes
git clone https://github.com/<your-user>/ComfyUI-KrakenTools.git
```
3. Restart ComfyUI.
### Requirements
- ComfyUI (latest)
- Python deps: `torch`, `numpy`, `Pillow` (bundled in ComfyUI)
- Ultimate SD Upscale node pack (for USDU) installed separately
## 🚀 Usage (portrait example 736×1344)
1. **Flux latent** → add `KrakenFluxEmptyLatentImage` and pick your preset (e.g., 736×1344).
Outputs: `LATENT`, `resolution` (e.g., "736 x 1344"), `preset_out` (e.g., "736x1344"), width/height, preview
2. **Upscale planning** → add `KrakenUpscaleTileCalc`
- `resolution_in` ← latent.`resolution`
- `preset_in` ← latent.`preset_out`
- `min_upscale_policy` = **auto** (2× for small canvases), or force **"2.0x"**
3. **USDU** → add *Ultimate SD Upscale* (regular node)
- `upscale_by` ← tile calc.`upscale_by`
- Tile/overlap settings ← tile calc outputs
- Denoise ~0.2–0.35
4. **Final sizing** → add `KrakenResolutionHelper`
- `image` ← USDU output
- `preset_in` ← tile calc.`target_resolution` (or latent.`preset_out`)
- `mode` = **keep proportion**, `interpolation` = **lanczos**, `allow_upscale` = **False**
- Output is **exact 736 × 1344** with extra micro-detail
## 🧭 Contributing
See **CONTRIBUTING.md**.
## 🪪 License
MIT © 2025 **The Kraken** — X: @KrakenUnbound · YouTube: /@KrakenUnbound
+25
View File
@@ -0,0 +1,25 @@
"""
ComfyUI-KrakenTools
MIT License © 2025 The Kraken (@KrakenUnbound)
https://github.com/<your-user>/ComfyUI-KrakenTools
"""
from .flux_latent import KrakenFluxEmptyLatentImage
from .resolution_helper import KrakenResolutionHelper
from .upscale_tile_calc import KrakenUpscaleTileCalc
NODE_CLASS_MAPPINGS = {
"KrakenFluxEmptyLatentImage": KrakenFluxEmptyLatentImage,
"KrakenResolutionHelper": KrakenResolutionHelper,
"KrakenUpscaleTileCalc": KrakenUpscaleTileCalc,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"KrakenFluxEmptyLatentImage": "🦑 Kraken Flux Empty Latent Image",
"KrakenResolutionHelper": "🦑 Kraken Resolution Helper (Exact)",
"KrakenUpscaleTileCalc": "🦑 Kraken Upscale & Tile Calc",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
WEB_DIRECTORY = "./web"
+266
View File
@@ -0,0 +1,266 @@
"""
Kraken Flux Empty Latent Image
MIT License © 2025 The Kraken (@KrakenUnbound)
This node produces a Flux-style empty latent with a reliable WxH preview and
emits both a normalized `preset_out` string ("WxH") and a human `resolution` ("W x H").
Designed for clean hand-offs into Upscale & Tile Calc and Resolution Helper.
"""
import torch
import math
import os
import time
import numpy as np
from PIL import Image, ImageDraw, ImageFont
import folder_paths
counter = 0 # Global counter for unique filenames
class KrakenFluxEmptyLatentImage:
@classmethod
def INPUT_TYPES(cls):
megapixel_options = ["0.5", "0.75", "1.0", "1.25", "1.5", "1.75", "2.0", "2.25", "2.5", "2.75", "3.0", "Custom"]
# Combined aspect_ratio presets with use-case resolutions
aspect_options = [
"Custom",
"1:1 (Perfect Square)",
"2:1 (Panorama Landscape)",
"2:3 (Classic Portrait)",
"3:4 (Golden Ratio)",
"3:5 (Elegant Vertical)",
"4:5 (Artistic Frame)",
"5:7 (Balanced Portrait)",
"5:8 (Tall Portrait)",
"7:9 (Modern Portrait)",
"9:16 (Slim Vertical)",
"9:19 (Tall Slim)",
"9:21 (Ultra Tall)",
"9:32 (Skyline)",
"3:2 (Golden Landscape)",
"4:3 (Classic Landscape)",
"5:3 (Wide Horizon)",
"5:4 (Balanced Frame)",
"7:5 (Elegant Landscape)",
"8:5 (Cinematic View)",
"9:7 (Artful Horizon)",
"16:9 (Panorama)",
"19:9 (Cinematic Ultrawide)",
"21:9 (Epic Ultrawide)",
"32:9 (Extreme Ultrawide)",
"Screen: iPhone Wallpaper - 1170x2532 (9:16)",
"Screen: iPhone Pro Wallpaper - 1290x2796 (9:19.5)",
"Screen: Android HD Wallpaper - 1080x1920 (9:16)",
"Screen: Android QHD Wallpaper - 1440x3200 (9:20)",
"Screen: Desktop HD Wallpaper - 1920x1080 (16:9)",
"Screen: Desktop QHD Wallpaper - 2560x1440 (16:9)",
"Screen: Dual Monitor Wallpaper - 5120x1440 (32:9)",
"Screen: 4K Monitor - 3840x2160 (16:9)",
"Screen: Ultrawide QHD - 3440x1440 (21:9)",
"Screen: Ultrawide 4K - 3840x1600 (12:5)",
"Video: KlingAI Landscape - 1920x1080 (16:9)",
"Video: KlingAI Portrait - 1080x1920 (9:16)",
"Video: KlingAI Square - 1024x1024 (1:1)",
"Video: KlingAI Wide - 1344x768 (3:2)",
"Video: KlingAI Tall - 768x1344 (2:3)",
"Social: Instagram Post Square - 1080x1080 (1:1)",
"Social: Instagram Post Portrait - 1080x1350 (4:5)",
"Social: Instagram Story - 1080x1920 (9:16)",
"Social: YouTube Thumbnail - 1280x720 (16:9)",
"Social: Facebook Post - 1200x1200 (1:1)",
"Social: Facebook Banner - 1200x628 (1.91:1)",
"Social: Twitter/X Post - 1200x675 (16:9)",
"Social: LinkedIn Post - 1200x1200 (1:1)",
"Advertising: Leaderboard Banner - 728x90 (8:1)",
"Advertising: Medium Rectangle Banner - 300x250 (1.2:1)",
"Advertising: Large Rectangle Banner - 336x280 (1.2:1)",
"Advertising: Full Banner - 468x60 (7.8:1)",
"Advertising: Wide Skyscraper Banner - 160x600 (1:3.75)",
"Advertising: Half Page Banner - 300x600 (1:2)",
"Advertising: Large Leaderboard Banner - 970x90 (10.8:1)",
"Advertising: Square Banner - 250x250 (1:1)",
"Print: Movie Poster - 8100x12000 (27x40in at 300 DPI)",
"Print: Flyer Letter - 2550x3300 (8.5x11in at 300 DPI)",
"Print: Flyer Legal - 2550x4200 (8.5x14in at 300 DPI)",
"Print: Business Card - 1050x600 (3.5x2in at 300 DPI)",
"Print: Poster A3 - 3508x4961 (11.7x16.5in at 300 DPI)",
"Print: Poster A2 - 4961x7016 (16.5x23.4in at 300 DPI)",
"Print: Poster A1 - 7016x9933 (23.4x33.1in at 300 DPI)",
"Print: Poster A0 - 9933x14043 (33.1x46.8in at 300 DPI)"
]
divisible_options = ["Auto", 8, 16, 32, 64, 128, 256]
return {
"required": {
"megapixel": (megapixel_options, {"default": "1.0"}),
"preset": (aspect_options, {"default": "Custom"}),
"use_custom_aspect": ("BOOLEAN", {"default": False}),
"custom_aspect": ("STRING", {"default": "1:1"}),
"width": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 8}),
"height": ("INT", {"default": 1024, "min": 64, "max": 4096, "step": 8}),
"divisible_by": (divisible_options, {"default": "Auto"}),
"batch_size": ("INT", {"default": 1, "min": 1, "max": 64}),
}
}
RETURN_TYPES = ("LATENT", "INT", "INT", "STRING", "STRING")
RETURN_NAMES = ("latent", "width_out", "height_out", "resolution", "preset_out")
FUNCTION = "generate"
CATEGORY = "kraken/latent"
def create_preview_image(self, width, height, goal_str, ratio_display):
# Preview rendered at EXACT latent size so ComfyUI shows correct WxH at the bottom.
from PIL import Image, ImageDraw, ImageFont
def load_font(sz):
for name in ("DejaVuSans.ttf", "Arial.ttf", "arial.ttf"):
try:
return ImageFont.truetype(name, sz)
except Exception:
continue
return ImageFont.load_default()
def text_wh(draw, text, font):
try:
l, t, r, b = draw.textbbox((0, 0), text, font=font)
return (r - l, b - t)
except Exception:
try:
return font.getsize(text)
except Exception:
sz = getattr(font, "size", 18)
return (int(len(text) * sz * 0.6), int(sz * 1.2))
w, h = int(width), int(height)
image = Image.new('RGB', (w, h), (24, 24, 24))
draw = ImageDraw.Draw(image)
# Border
draw.rectangle([0, 0, w - 1, h - 1], outline=(160, 160, 160), width=2)
# Font sizes relative to canvas
base = max(20, int(min(w, h) * 0.14)) # big center size text
sub = max(14, int(base * 0.60)) # info text
font_main = load_font(base)
font_sub = load_font(sub)
# Big centered latent size
center_text = f"{w}x{h}"
tw, th = text_wh(draw, center_text, font_main)
cx, cy = w // 2, h // 2
# outline for readability
for off in ((-2,-2),(2,2),(-2,2),(2,-2)):
draw.text((cx - tw//2 + off[0], cy - th//2 + off[1]), center_text, fill=(0,0,0), font=font_main)
draw.text((cx - tw//2, cy - th//2), center_text, fill=(245,245,245), font=font_main)
# Top-left info block: show target if present; otherwise show ratio
lines = []
if goal_str:
lines.append(f"target {goal_str}") # e.g., 1080x1920
# Show ratio if it looks like A:B or matches known form
if isinstance(ratio_display, str) and (':' in ratio_display or 'x' in ratio_display):
lines.append(f"ratio {ratio_display}")
y = 10 # add more padding so it doesn't collide with border
for line in lines:
lw, lh = text_wh(draw, line, font_sub)
pad = 5
# dark backdrop
draw.rectangle([10 - pad, y - pad, 10 + lw + pad, y + lh + pad], fill=(0,0,0))
# draw text
draw.text((10, y), line, fill=(220,220,220), font=font_sub)
y += lh + 8
return image
def generate(self, megapixel, preset, use_custom_aspect, custom_aspect, width, height, divisible_by, batch_size):
ar = 1.0
ratio_display = f"{width}x{height}"
if use_custom_aspect:
ratio_display = custom_aspect
if ':' in custom_aspect:
w_part, h_part = map(float, custom_aspect.split(":"))
ar = w_part / h_part
else:
ar = float(custom_aspect)
elif preset != "Custom":
if " - " in preset:
# Specific resolution preset
size_str = preset.split(" - ")[1].split(" (")[0]
width, height = map(int, size_str.split("x"))
ar = width / height
ratio_display = size_str # Use just WIDTHxHEIGHT for display
else:
# Ratio-based preset
ratio_str = preset.split(" (")[0]
w_part, h_part = map(float, ratio_str.split(":"))
ar = w_part / h_part
ratio_display = f"{w_part}:{h_part}"
else:
ar = width / height
ratio_display = f"{width}x{height}"
if megapixel == "Custom":
mp = (width * height) / 1_000_000
use_calculated = False
else:
mp = float(megapixel)
use_calculated = True
if use_calculated:
base_width = math.sqrt(mp * 1_000_000 * ar)
base_height = math.sqrt(mp * 1_000_000 / ar)
else:
base_width = width
base_height = height
if divisible_by == "Auto":
if mp > 1.5:
divisible_by = 64
elif mp > 0.75:
divisible_by = 32
else:
divisible_by = 16
else:
divisible_by = int(divisible_by)
width = round(base_width / divisible_by) * divisible_by
height = round(base_height / divisible_by) * divisible_by
if width % 8 != 0 or height % 8 != 0:
width = (width // 8) * 8
height = (height // 8) * 8
latent = torch.zeros([batch_size, 16, height // 8, width // 8])
resolution = f"{width} x {height}"
# Determine preset_out: prefer explicit target size from preset label if present
target_preset_str = None
try:
if preset != "Custom" and " - " in preset:
target_preset_str = preset.split(" - ")[1].split(" (")[0]
except Exception:
target_preset_str = None
preset_out = target_preset_str if target_preset_str else f"{width}x{height}" # WIDTHxHEIGHT string for downstream
# Determine preset_out: prefer explicit target size from preset label if present
preview_img = self.create_preview_image(width, height, target_preset_str or '', ratio_display)
global counter
counter += 1
full_output_folder = folder_paths.get_temp_directory()
current_time = time.time()
file = f"kraken_preview_{current_time}_{counter:05}.png"
file_path = os.path.join(full_output_folder, file)
preview_img.save(file_path, compress_level=4)
images = [{"filename": file, "subfolder": "", "type": "temp"}]
return {
"ui": {"images": images},
"result": ({"samples": latent}, width, height, resolution, preset_out)
}
+141
View File
@@ -0,0 +1,141 @@
"""
Kraken Resolution Helper (Exact)
MIT License © 2025 The Kraken (@KrakenUnbound)
"""
import math, re
from typing import Optional, Tuple
import numpy as np, torch
from PIL import Image as PILImage
def _parse_wxH(s: str) -> Optional[Tuple[int, int]]:
if not s: return None
s = str(s).strip().lower()
m = re.match(r"^\s*(\d+)\s*[xX]\s*(\d+)\s*$", s)
if not m: return None
w, h = int(m.group(1)), int(m.group(2))
if w <= 0 or h <= 0: return None
return w, h
def _tensor_to_pil(img) -> PILImage.Image:
if isinstance(img, torch.Tensor):
t = img[0] if img.ndim == 4 else img
t = t.clamp(0.0,1.0).detach().cpu()
if t.shape[-1] == 1: t = t.repeat(1,1,3)
if t.shape[-1] == 4: t = t[..., :3]
arr = (t.numpy()*255.0+0.5).astype(np.uint8)
return PILImage.fromarray(arr, mode="RGB")
if isinstance(img, np.ndarray):
a = img[0] if img.ndim == 4 else img
a = np.clip(a,0.0,1.0).astype(np.float32)
if a.ndim == 2: a = np.stack([a,a,a], axis=-1)
if a.shape[-1] == 1: a = np.repeat(a,3,axis=-1)
if a.shape[-1] == 4: a = a[..., :3]
arr = (a*255.0+0.5).astype(np.uint8)
return PILImage.fromarray(arr, mode="RGB")
raise TypeError("Unsupported image type for _tensor_to_pil")
def _pil_to_tensor(img: PILImage.Image) -> torch.Tensor:
arr = np.array(img).astype(np.float32)/255.0
if arr.ndim == 2: arr = np.stack([arr,arr,arr], axis=-1)
if arr.shape[-1] == 4: arr = arr[..., :3]
return torch.from_numpy(arr).unsqueeze(0)
def _cover_scale(wi,hi,wt,ht): return max(wt/wi, ht/hi)
def _contain_scale(wi,hi,wt,ht): return min(wt/wi, ht/hi)
def _round_up(x,step): import math; return math.ceil(x/step)*step
def _ceil_to_multiple(x,m): import math; return x if m<=1 else int(math.ceil(x/m)*m)
class KrakenResolutionHelper:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",),
"mode": (["fill / crop","pad","keep proportion","stretch"], {"default": "keep proportion"}),
"interpolation": (["lanczos","bicubic","bilinear","nearest"], {"default": "lanczos"}),
"allow_upscale": ("BOOLEAN", {"default": False}),
"preset_in": ("STRING", {"default": ""}),
},
"optional": {
"target_resolution_in": ("STRING", {"default": ""}),
"width": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
"height": ("INT", {"default": 1024, "min": 1, "max": 8192, "step": 1}),
"anchor_x": (["left","center","right"], {"default": "center"}),
"anchor_y": (["top","center","bottom"], {"default": "center"}),
"crop_margin_px": ("INT", {"default": 32, "min": 0, "max": 512, "step": 1}),
"scale_step": ("FLOAT", {"default": 0.01, "min": 0.001, "max": 0.25, "step": 0.001}),
"multiple_of": ("INT", {"default": 0, "min": 0, "max": 128, "step": 1}),
},
}
RETURN_TYPES = ("IMAGE","STRING")
RETURN_NAMES = ("image","size")
FUNCTION = "apply"
CATEGORY = "kraken/resize"
def _goal(self, preset_in, target_resolution_in, width, height):
r = _parse_wxH(target_resolution_in) or _parse_wxH(preset_in)
return r if r else (max(1,int(width)), max(1,int(height)))
def _anchor_offsets(self,big_w,big_h,small_w,small_h,ax,ay):
off_x = 0 if ax=="left" else (big_w-small_w if ax=="right" else (big_w-small_w)//2)
off_y = 0 if ay=="top" else (big_h-small_h if ay=="bottom" else (big_h-small_h)//2)
return max(0,off_x), max(0,off_y)
def apply(self,image,mode="keep proportion",interpolation="lanczos",allow_upscale=False,
preset_in="",target_resolution_in="",width=1024,height=1024,
anchor_x="center",anchor_y="center",crop_margin_px=32,scale_step=0.01,multiple_of=0):
Wt,Ht = self._goal(preset_in, target_resolution_in, width, height)
if isinstance(image, torch.Tensor) and image.ndim==4: _,Hi,Wi,_ = image.shape
else: Hi,Wi = image.shape[0], image.shape[1]
pil = _tensor_to_pil(image)
interp = {"lanczos":PILImage.LANCZOS,"bicubic":PILImage.BICUBIC,"bilinear":PILImage.BILINEAR,"nearest":PILImage.NEAREST}.get(interpolation,PILImage.LANCZOS)
if mode=="stretch":
result = pil.resize((Wt,Ht), interp)
elif mode=="keep proportion":
s = _contain_scale(Wi,Hi,Wt,Ht)
if s>1.0 and not allow_upscale: s=1.0
new_w,new_h = max(1,int(Wi*s)), max(1,int(Hi*s))
if multiple_of>1:
new_w=_ceil_to_multiple(new_w,multiple_of); new_h=_ceil_to_multiple(new_h,multiple_of)
if not allow_upscale:
new_w=min(new_w,Wt); new_h=min(new_h,Ht)
scaled = pil.resize((new_w,new_h), interp)
canvas = PILImage.new("RGB", (Wt,Ht), (0,0,0))
off_x,off_y = self._anchor_offsets(Wt,Ht,new_w,new_h,"center","center")
canvas.paste(scaled,(off_x,off_y))
result = canvas
elif mode=="pad":
s = _contain_scale(Wi,Hi,Wt,Ht)
if s>1.0 and not allow_upscale: s=1.0
new_w,new_h = max(1,int(Wi*s)), max(1,int(Hi*s))
if multiple_of>1:
new_w=_ceil_to_multiple(new_w,multiple_of); new_h=_ceil_to_multiple(new_h,multiple_of)
if not allow_upscale:
new_w=min(new_w,Wt); new_h=min(new_h,Ht)
scaled = pil.resize((new_w,new_h), interp)
canvas = PILImage.new("RGB", (Wt,Ht), (0,0,0))
off_x,off_y = self._anchor_offsets(Wt,Ht,new_w,new_h,anchor_x,anchor_y)
canvas.paste(scaled,(off_x,off_y))
result = canvas
else: # fill / crop
margin=max(0,int(crop_margin_px))
s_req=_cover_scale(Wi,Hi,Wt+2*margin,Ht+2*margin)
s=_round_up(s_req,max(0.001,float(scale_step)))
if s>1.0 and not allow_upscale: s=1.0
new_w,max_w = max(Wt,int(Wi*s)), Wt
new_h,max_h = max(Ht,int(Hi*s)), Ht
if multiple_of>1:
new_w=_ceil_to_multiple(new_w,multiple_of); new_h=_ceil_to_multiple(new_h,multiple_of)
scaled = pil.resize((new_w,new_h), interp)
left,top = self._anchor_offsets(new_w,new_h,Wt,Ht,anchor_x,anchor_y)
result = scaled.crop((left,top,left+Wt,top+Ht))
out = _pil_to_tensor(result)
return out, f"{result.width} x {result.height}"
NODE_CLASS_MAPPINGS = {"KrakenResolutionHelper": KrakenResolutionHelper}
NODE_DISPLAY_NAME_MAPPINGS = {"KrakenResolutionHelper": "🦑 Kraken Resolution Helper (Exact)"}
+143
View File
@@ -0,0 +1,143 @@
"""
Kraken Upscale & Tile Calc
MIT License © 2025 The Kraken (@KrakenUnbound)
"""
import math, os, re, time, logging
import numpy as np, torch
from PIL import Image, ImageDraw
import folder_paths
counter = 0
def _round_to(x, base): return int(round(x / base) * base)
def _ceil_to(x, base): return int(math.ceil(x / base) * base)
def _clamp(v, lo, hi): return max(lo, min(hi, v))
def _blank_image_tensor(w=8, h=8): return torch.zeros((1, h, w, 3), dtype=torch.float32)
class KrakenUpscaleTileCalc:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {"resolution_in": ("STRING", {"default": "1024 x 1024"})},
"optional": {
"preset_in": ("STRING", {"default": "Custom"}),
"image": ("IMAGE",),
"scale_step": ("FLOAT", {"default": 0.01, "min": 0.005, "max": 0.25, "step": 0.005}),
"crop_margin_px": ("INT", {"default": 32, "min": 0, "max": 128, "step": 1}),
"mask_blur": ("INT", {"default": 16, "min": 0, "max": 64, "step": 1}),
"tile_padding": ("INT", {"default": 16, "min": 0, "max": 256, "step": 1}),
"seam_fix_width": ("INT", {"default": 64, "min": 0, "max": 256, "step": 1}),
"seam_fix_mask_blur": ("INT", {"default": 16, "min": 0, "max": 64, "step": 1}),
"seam_fix_padding": ("INT", {"default": 32, "min": 0, "max": 256, "step": 1}),
"min_upscale_policy": (["auto","off","1.5x","2.0x","custom"], {"default": "auto"}),
"min_upscale_custom": ("FLOAT", {"default": 1.5, "min": 1.0, "max": 4.0, "step": 0.1}),
}
}
RETURN_TYPES = ("IMAGE","FLOAT","INT","INT","INT","INT","INT","INT","INT","STRING","STRING","STRING")
RETURN_NAMES = ("image_out","upscale_by","tile_width","tile_height","mask_blur","tile_padding","seam_fix_width","seam_fix_mask_blur","seam_fix_padding","predicted_resolution","target_resolution","target_preset")
FUNCTION = "calculate"
CATEGORY = "kraken/upscale"
def _parse_resolution_ws(self, s):
s = (s or "").lower().replace(" ", "")
if "x" in s:
try:
w, h = s.split("x"); return (int(w), int(h))
except Exception: return None
return None
def _parse_resolution_from_label(self, label):
s = str(label or ""); m = re.search(r"(\d+)\s*[xX]\s*(\d+)", s)
return (int(m.group(1)), int(m.group(2))) if m else None
def _image_size(self, image, resolution_in):
if image is not None:
if isinstance(image, torch.Tensor):
if image.ndim == 4: _, h, w, _ = image.shape
else: h, w = image.shape[0], image.shape[1]
return (w, h)
if isinstance(image, np.ndarray):
if image.ndim == 4: _, h, w, _ = image.shape
else: h, w = image.shape[0], image.shape[1]
return (w, h)
return self._parse_resolution_ws(resolution_in) or (1024,1024)
def _choose_min_upscale(self, wi, hi, Wt, Ht, policy, custom):
policy = (policy or "auto").lower()
if policy == "off": return 1.0
if policy == "1.5x": return 1.5
if policy == "2.0x": return 2.0
if policy == "custom":
try: return max(1.0, min(4.0, float(custom)))
except Exception: return 1.5
src_mp = (wi*hi)/1_000_000.0; tgt_mp = (Wt*Ht)/1_000_000.0
if max(Wt,Ht) <= 1024 or tgt_mp <= 1.0: return 2.0
if src_mp <= 2.0: return 2.0
return 1.5
def _make_preview(self, pred_w, pred_h, Wt, Ht):
w, h = max(pred_w, Wt), max(pred_h, Ht)
scale = max(1, min(512 / w, 512 / h))
canvas = Image.new("RGB", (int(w*scale), int(h*scale)), (24,24,24))
draw = ImageDraw.Draw(canvas)
draw.rectangle([0,0,canvas.size[0]-1, canvas.size[1]-1], outline=(160,160,160), width=2)
tw, th = int(Wt*scale), int(Ht*scale)
x0, y0 = (canvas.size[0]-tw)//2, (canvas.size[1]-th)//2
draw.rectangle([x0,y0,x0+tw-1,y0+th-1], outline=(0,200,120), width=2)
draw.text((6,6), f"pred {pred_w}x{pred_h}", fill=(220,220,220))
draw.text((6,24), f"target {Wt}x{Ht}", fill=(0,200,120))
return canvas
def calculate(self, resolution_in, preset_in="Custom", image=None, scale_step=0.01, crop_margin_px=32,
mask_blur=16, tile_padding=16, seam_fix_width=64, seam_fix_mask_blur=16, seam_fix_padding=32,
min_upscale_policy="auto", min_upscale_custom=1.5):
wi, hi = self._image_size(image, resolution_in)
tgt = self._parse_resolution_ws(preset_in) or self._parse_resolution_from_label(preset_in) or self._parse_resolution_ws(resolution_in) or (wi, hi)
Wt, Ht = tgt
crop_margin_px = int(crop_margin_px); crop_margin_px = _clamp(crop_margin_px, 0, max(Wt,Ht)//10)
Wm, Hm = Wt + 2*crop_margin_px, Ht + 2*crop_margin_px
required = max(Wm/wi, Hm/hi)
s = max(1.0, round((required/scale_step)) * scale_step)
min_s = self._choose_min_upscale(wi, hi, Wt, Ht, min_upscale_policy, min_upscale_custom)
if min_s > 1.0:
s = max(s, round((min_s/scale_step)) * scale_step)
s = round(float(s), 3)
pred_align = 8
pred_w = _ceil_to(wi * s, pred_align)
pred_h = _ceil_to(hi * s, pred_align)
bump_guard = 0
while (pred_w < Wt or pred_h < Ht) and bump_guard < 4:
s = round(s + scale_step, 3)
pred_w = _ceil_to(wi * s, pred_align)
pred_h = _ceil_to(hi * s, pred_align)
bump_guard += 1
overlap = _clamp(int(round(min(pred_w, pred_h) * 0.015)), 32, 96)
tile_w = _round_to(_clamp(pred_w // 2 + overlap, 512, 2048), 64)
tile_h = _round_to(_clamp(pred_h // 2 + overlap, 512, 2048), 64)
predicted_resolution = f"{pred_w} x {pred_h}"
target_resolution = f"{Wt} x {Ht}"
target_preset = f"{Wt}x{Ht}"
global counter; counter += 1
from folder_paths import get_temp_directory
file = f"kraken_tilecalc_{time.time()}_{counter:05}.png"
try:
preview_img = self._make_preview(pred_w, pred_h, Wt, Ht)
preview_img.save(os.path.join(get_temp_directory(), file), compress_level=4)
images = [{"filename": file, "subfolder": "", "type": "temp"}]
except Exception as e:
logging.warning(f"Preview generation failed: {e}"); images = []
image_out = image if image is not None else _blank_image_tensor()
return {"ui": {"images": images},
"result": (image_out, s, tile_w, tile_h, mask_blur, tile_padding, seam_fix_width, seam_fix_mask_blur,
seam_fix_padding, predicted_resolution, target_resolution, target_preset)}
+32
View File
@@ -0,0 +1,32 @@
import { app } from "../../../scripts/app.js";
app.registerExtension({
name: "kraken.tools",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
const attachPreview = () => {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, []) : undefined;
const imgElement = document.createElement("img");
imgElement.style = "width: 100%; height: auto; display: block; margin: 5px 0;";
this.previewWidget = this.addDOMWidget("preview", "custom", imgElement, { serialize: false });
return r;
};
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
if (onExecuted) onExecuted.apply(this, [message]);
if (message && message.images && message.images.length > 0 && this.previewWidget) {
const img = this.previewWidget.value;
const { filename, subfolder, type } = message.images[0];
const url = `/view?filename=${encodeURIComponent(filename)}&type=${encodeURIComponent(type || "temp")}&subfolder=${encodeURIComponent(subfolder || "")}&rand=${Math.random()}`;
img.src = url;
}
};
};
if (nodeData.name === "KrakenFluxEmptyLatentImage") attachPreview();
if (nodeData.name === "KrakenUpscaleTileCalc") attachPreview();
},
});