Files
wallen0322-ComfyUI-qwenmult…/nodes.py
T

149 lines
6.0 KiB
Python

"""
Qwen Multiangle Lightning Node for ComfyUI
Extreme Stability Version: Locks Scene, Camera, Pose, and Composition.
"""
import numpy as np
from PIL import Image
import base64
import io
import hashlib
import torch
_cache = {}
_max_cache_size = 50
class QwenMultiangleLightningNode:
"""
Lighting Control Node - Extreme Stability Edition
Strictly maintains scene and character while only adjusting the lighting profile.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"light_azimuth": ("INT", {
"default": 0, "min": 0, "max": 360, "step": 1, "display": "slider"
}),
"light_elevation": ("INT", {
"default": 30, "min": -90, "max": 90, "step": 1, "display": "slider"
}),
"light_intensity": ("FLOAT", {
"default": 5.0, "min": 0.0, "max": 10.0, "step": 0.1, "display": "slider"
}),
"light_color_hex": ("COLOR", {"default": "#FFFFFF"}),
"cinematic_mode": ("BOOLEAN", {
"default": True, "display": "checkbox"
}),
},
"optional": {
"image": ("IMAGE",),
},
"hidden": {
"unique_id": "UNIQUE_ID",
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("lighting_prompt",)
FUNCTION = "generate_lighting_prompt"
CATEGORY = "image/lighting"
OUTPUT_NODE = True
def _compute_image_hash(self, image):
if image is None: return None
try:
if hasattr(image, 'cpu'):
img_np = image[0].cpu().numpy() if len(image.shape) == 4 else image.cpu().numpy()
else:
img_np = image.numpy()[0] if hasattr(image, 'numpy') and len(image.shape) == 4 else image
return hashlib.md5(img_np.tobytes()).hexdigest()
except:
return str(hash(str(image)))
def generate_lighting_prompt(self, light_azimuth, light_elevation, light_intensity, light_color_hex, cinematic_mode=True, image=None, unique_id=None):
cache_key = str(unique_id) if unique_id else "default"
image_hash = self._compute_image_hash(image)
cached = _cache.get(cache_key, {})
if (cached.get('azimuth') == light_azimuth and
cached.get('elevation') == light_elevation and
cached.get('intensity') == light_intensity and
cached.get('color') == light_color_hex and
cached.get('cinematic') == cinematic_mode and
cached.get('image_hash') == image_hash):
return cached['result']
# 1. 映射光照方位描述
az = light_azimuth % 360
if az < 22.5 or az >= 337.5: pos_desc = "light hitting from the front"
elif az < 67.5: pos_desc = "light hitting from the front-right side"
elif az < 112.5: pos_desc = "light hitting from the right (90 degrees)"
elif az < 157.5: pos_desc = "light hitting from the back-right"
elif az < 202.5: pos_desc = "backlighting, light from behind"
elif az < 247.5: pos_desc = "light hitting from the back-left"
elif az < 292.5: pos_desc = "light hitting from the left (90 degrees)"
else: pos_desc = "light hitting from the front-left side"
# 2. 映射光照高度(避免使用俯仰角相关词汇)
if light_elevation <= -60: elev_desc = "extreme low-angle light source, strong bottom-up shadow"
elif light_elevation < -30: elev_desc = "low-level light source, bottom-up shadow"
elif light_elevation < 20: elev_desc = "horizontal light source"
elif light_elevation < 60: elev_desc = "high-positioned light source"
else: elev_desc = "top-down ceiling light source"
# 3. 强度描述
if light_intensity < 3.0: int_desc = "soft ambient"
elif light_intensity < 7.0: int_desc = "bright directional"
else: int_desc = "strong dramatic contrast"
# --- 核心修改:极致场景与镜头锁定 ---
# 加入了 SCENE LOCK 和 CONSTANT BACKGROUND 等强力提示词
global_constraints = (
"SCENE LOCK, CONSTANT BACKGROUND, FIXED SCENERY. " # 锁定场景和背景
"STATIC SHOT, FIXED VIEWPOINT, NO CAMERA MOVEMENT. " # 锁定镜头
"maintaining character consistency, "
"keeping the same character pose and action, "
"maintaining the same composition, "
"RELIGHTING ONLY: only the light rays and shadows change, the scene remains untouched. " # 明确重塑光影任务
)
color_desc = f"colored light (hex: {light_color_hex})"
detail_prompt = f"{int_desc} {color_desc}, {pos_desc}, {elev_desc}"
if cinematic_mode:
prompt = f"{global_constraints}professional cinematic relighting, {detail_prompt}, raytraced shadows, realistic global illumination"
else:
prompt = f"{global_constraints}{detail_prompt}"
# 预览图处理
image_base64 = ""
if image is not None:
try:
i = 255. * image[0].cpu().numpy()
img_np = np.clip(i, 0, 255).astype(np.uint8)
pil_image = Image.fromarray(img_np)
buffer = io.BytesIO()
pil_image.save(buffer, format="PNG")
image_base64 = "data:image/png;base64," + base64.b64encode(buffer.getvalue()).decode("utf-8")
except: pass
result = {"ui": {"image_base64": [image_base64]}, "result": (prompt,)}
_cache[cache_key] = {
'azimuth': light_azimuth, 'elevation': light_elevation,
'intensity': light_intensity, 'color': light_color_hex,
'cinematic': cinematic_mode, 'image_hash': image_hash,
'result': result
}
return result
NODE_CLASS_MAPPINGS = {
"QwenMultiangleLightningNode": QwenMultiangleLightningNode,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"QwenMultiangleLightningNode": "Qwen Multiangle Lightning",
}