170 lines
5.3 KiB
Python
170 lines
5.3 KiB
Python
import numpy as np
|
|
import torch
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
|
|
def pil2tensor(image):
|
|
"""Convert PIL image to tensor in the correct format"""
|
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
class SFQwenResolution:
|
|
"""
|
|
A node for selecting Qwen model resolutions with visual preview.
|
|
|
|
Class methods
|
|
-------------
|
|
INPUT_TYPES (dict):
|
|
Defines the resolution selection input.
|
|
|
|
Attributes
|
|
----------
|
|
RETURN_TYPES (`tuple`):
|
|
Returns width (INT), height (INT), resolution string (STRING), and preview image (IMAGE).
|
|
RETURN_NAMES (`tuple`):
|
|
Names for the outputs: width, height, resolution, preview.
|
|
FUNCTION (`str`):
|
|
Entry-point method name: "get_dimensions".
|
|
OUTPUT_NODE (`bool`):
|
|
True - this node outputs a preview image.
|
|
CATEGORY (`str`):
|
|
UI category: "ControlAltAI Nodes/Qwen".
|
|
|
|
get_dimensions(resolution) -> tuple:
|
|
Returns the width, height, resolution string, and preview image for the selected resolution.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
"""
|
|
Return a dictionary which contains config for all input fields.
|
|
|
|
Returns: `dict`:
|
|
- Key "required": Contains required input fields config
|
|
- Value "resolution": A list selection of available resolutions
|
|
"""
|
|
return {
|
|
"required": {
|
|
"resolution": (
|
|
[
|
|
"1:1 (Square)",
|
|
"16:9 (Widescreen Landscape)",
|
|
"9:16 (Widescreen Portrait)",
|
|
"4:3 (Standard Landscape)",
|
|
"3:4 (Standard Portrait)",
|
|
"3:2 (Classic Landscape)",
|
|
"2:3 (Classic Portrait)",
|
|
],
|
|
{"default": "1:1 (Square)"},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("INT", "INT", "STRING", "IMAGE")
|
|
RETURN_NAMES = ("width", "height", "resolution", "preview")
|
|
FUNCTION = "get_dimensions"
|
|
CATEGORY = "Stillfront/Utils"
|
|
OUTPUT_NODE = True
|
|
|
|
def create_preview_image(self, width, height, resolution):
|
|
# 1024x1024 preview size
|
|
preview_size = (1024, 1024)
|
|
image = Image.new("RGB", preview_size, (0, 0, 0)) # Black background
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
# Draw grid with grey lines
|
|
grid_color = "#333333" # Dark grey for grid
|
|
grid_spacing = 50 # Adjusted grid spacing
|
|
for x in range(0, preview_size[0], grid_spacing):
|
|
draw.line([(x, 0), (x, preview_size[1])], fill=grid_color)
|
|
for y in range(0, preview_size[1], grid_spacing):
|
|
draw.line([(0, y), (preview_size[0], y)], fill=grid_color)
|
|
|
|
# Calculate preview box dimensions
|
|
preview_width = 800 # Increased size
|
|
preview_height = int(preview_width * (height / width))
|
|
|
|
# Adjust if height is too tall
|
|
if preview_height > 800: # Adjusted for larger preview
|
|
preview_height = 800
|
|
preview_width = int(preview_height * (width / height))
|
|
|
|
# Calculate center position
|
|
x_offset = (preview_size[0] - preview_width) // 2
|
|
y_offset = (preview_size[1] - preview_height) // 2
|
|
|
|
# Draw the aspect ratio box with thicker outline
|
|
draw.rectangle(
|
|
[
|
|
(x_offset, y_offset),
|
|
(x_offset + preview_width, y_offset + preview_height),
|
|
],
|
|
outline="red",
|
|
width=4, # Thicker outline
|
|
)
|
|
|
|
# Add text with larger font sizes
|
|
try:
|
|
# Draw text (centered)
|
|
text_y = y_offset + preview_height // 2
|
|
|
|
# Resolution text in red
|
|
draw.text(
|
|
(preview_size[0] // 2, text_y),
|
|
f"{width}x{height}",
|
|
fill="red",
|
|
anchor="mm",
|
|
font=ImageFont.truetype("arial.ttf", 48),
|
|
)
|
|
|
|
except:
|
|
# Fallback if font loading fails
|
|
draw.text(
|
|
(preview_size[0] // 2, text_y),
|
|
f"{width}x{height}",
|
|
fill="red",
|
|
anchor="mm",
|
|
)
|
|
|
|
# Convert to tensor using the helper function
|
|
return pil2tensor(image)
|
|
|
|
def get_dimensions(self, resolution):
|
|
# Map from aspect ratio to actual dimensions
|
|
resolution_map = {
|
|
"1:1 (Square)": (1328, 1328),
|
|
"16:9 (Widescreen Landscape)": (1664, 928),
|
|
"9:16 (Widescreen Portrait)": (928, 1664),
|
|
"4:3 (Standard Landscape)": (1472, 1104),
|
|
"3:4 (Standard Portrait)": (1104, 1472),
|
|
"3:2 (Classic Landscape)": (1584, 1056),
|
|
"2:3 (Classic Portrait)": (1056, 1584),
|
|
}
|
|
|
|
# Get dimensions from the map
|
|
width, height = resolution_map[resolution]
|
|
|
|
# Resolution as string
|
|
resolution_str = f"{width} x {height}"
|
|
|
|
# Generate preview image
|
|
preview = self.create_preview_image(width, height, resolution_str)
|
|
|
|
return width, height, resolution_str, preview
|
|
|
|
|
|
def gcd(a, b):
|
|
"""Calculate the Greatest Common Divisor of a and b."""
|
|
while b:
|
|
a, b = b, a % b
|
|
return a
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SFQwenResolution": SFQwenResolution,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SFQwenResolution": "SF Qwen Resolution",
|
|
}
|