Files
2024-09-05 11:42:12 +08:00

501 lines
20 KiB
Python

import re
import numpy as np
import torch
from collections import defaultdict
import cv2
from PIL import Image, ImageOps, ImageFilter
from torchvision import transforms
from skimage.color import rgb2lab, deltaE_ciede2000
class prompt_sorting:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"string": ("STRING", {"multiline": True, "default": ""}),
"remove_duplicate_tags": ("BOOLEAN", {"default": True}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("string",)
FUNCTION = "execute"
CATEGORY = "advanced/AI_Assistant/text"
def execute(self,string,remove_duplicate_tags):
return (self.tidy_tags(string,remove_duplicate_tags),)
def tidy_tags(self,text, remove_duplicate_tags):
if not text or not isinstance(text,(str,list)):
return ""
# Convert list to string
if isinstance(text,list):
text = ",".join(map(str, text))
while True:
# Eliminate redundant commas and strip whitespace around commas
new_text = re.sub(r"[,\s\t\n]*,[,\s\t\n]*", ",", text)
# Fix double spaces
new_text = re.sub(r"\s\s+", " ", new_text)
if new_text == text:
break
text = new_text
text = text.strip(" ,\t\r\n")
# Remove duplicate tags, keeping the duplicate closest to the beginning of the prompt
if remove_duplicate_tags:
tags = text.split(',')
seen = set()
result = []
for tag in tags:
if tag not in seen:
seen.add(tag)
result.append(tag)
text = ",".join(result)
# Clean up BREAKs
# text = re.sub(r"[,\s\n]*BREAK[,\s\n]*"," BREAK ", text)
return text
class apply_lighting_effects:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"light_yaw": ("FLOAT", {
"default": 60,
"min": -180,
"max": 180,
"step": 0.1,
"display": "number"}),
"light_pitch": ("FLOAT", {
"default": -60,
"min": -90,
"max": 90,
"step": 0.1,
"display": "number"}),
"specular_power": ("FLOAT", {
"default": 30,
"min": 10,
"max": 100,
"step": 0.01,
"display": "number"}),
"normal_diffuse_strength": ("FLOAT", {
"default": 1,
"min": 0,
"max": 5,
"step": 0.01,
"display": "number"}),
"specular_highlights_strength": ("FLOAT", {
"default": 0.8,
"min": 0,
"max": 5,
"step": 0.01,
"display": "number"}),
"total_gain": ("FLOAT", {
"default": 0.6,
"min": 0,
"max": 1,
"step": 0.01,
"display": "number"}),
# "select_lighting_option": (
# [
# 'Directly Above',
# 'Directly Below',
# 'Directly Left',
# 'Directly Right',
# 'Upper Left Diagonal',
# 'Upper Right Diagonal'
# ], {
# "default": 'Directly Above'
# }),
},
}
RETURN_TYPES = ("IMAGE","STRING", )
RETURN_NAMES = ("image","show_help",)
FUNCTION = "process"
CATEGORY = "advanced/AI_Assistant/image"
DESCRIPTION = """
光源方向參考參數如下:
光源正前方=light_yaw:60 light_pitch:-60
光源左上方=light_yaw:40 light_pitch:-60
光源右上方=light_yaw:60 light_pitch:-40
光源左=light_yaw:0 light_pitch:0
光源右=light_yaw:90 light_pitch:0
光源下方=light_yaw:45 light_pitch:0
"""
def process(self, image, light_yaw, light_pitch, specular_power, normal_diffuse_strength,
specular_highlights_strength, total_gain):
show_help ="光源方向參考參數如下:\n光源正前方=light_yaw:60 light_pitch:-60\n光源左上方=light_yaw:40 light_pitch:-60\n光源右上方=light_yaw:60 light_pitch:-40\n光源左=light_yaw:0 light_pitch:0\n光源右=light_yaw:90 light_pitch:0\n光源下方=light_yaw:45 light_pitch:0\n"
return (self.lighting_effects(image, light_yaw, light_pitch, specular_power, normal_diffuse_strength,
specular_highlights_strength, total_gain),show_help,)
def lighting_effects(self, input_tensor, light_yaw, light_pitch, specular_power, normal_diffuse_strength,
specular_highlights_strength, total_gain):
# 元の次元数を記録する
original_dim = input_tensor.dim()
#print(original_dim,input_tensor.shape,input_tensor.shape[1])
# 入力テンソルが最低でも4次元であることを保証する(バッチ次元がなければ追加する)
if original_dim == 3:
input_tensor = input_tensor.unsqueeze(0) # バッチ次元を追加
# 轉換形狀為 (Batch, Channels, Height, Width)
input_tensor = input_tensor.permute(0, 3, 1, 2)
# 入力テンソルに3つ以上のチャンネルがある場合、追加のチャンネルをアルファと仮定し削除する
if input_tensor.shape[1] > 3:
input_tensor = input_tensor[:, :3, :, :] # 最初の3チャンネルのみを保持
# 入力テンソルを正規化して法線ベクトルを取得する
normal_tensor = torch.nn.functional.normalize(input_tensor, dim=1) # チャンネル次元に沿って正規化
# 光の方向ベクトルを計算する
light_direction = self.euler_to_vector(light_yaw, light_pitch, 0)
light_direction = light_direction.to(input_tensor.device) # 同じデバイスを保証する
# 光の方向とカメラの方向を入力テンソルの次元に合わせて拡張する
batch_size, _, height, width = input_tensor.shape
light_direction = light_direction.view(1, 3, 1, 1).expand(batch_size, -1, height, width)
camera_direction = torch.tensor([0, 0, 1], dtype=torch.float32, device=input_tensor.device)
camera_direction = camera_direction.view(1, 3, 1, 1).expand(batch_size, -1, height, width)
# 拡散成分を計算する
diffuse = torch.sum(normal_tensor * light_direction, dim=1, keepdim=True)
diffuse = torch.clamp(diffuse, 0, 1)
# 鏡面成分を計算する
half_vector = torch.nn.functional.normalize(light_direction + camera_direction, dim=1)
specular = torch.sum(normal_tensor * half_vector, dim=1, keepdim=True)
specular = torch.pow(torch.clamp(specular, 0, 1), specular_power)
# 拡散成分と鏡面成分を組み合わせて、強度とゲインを適用する
output_tensor = (diffuse * normal_diffuse_strength + specular * specular_highlights_strength) * total_gain
output_tensor = output_tensor.squeeze(1) # keepdim=Trueで追加されたチャンネル次元を削除
# 初めに追加されたバッチ次元があれば削除する
if original_dim == 3:
output_tensor = output_tensor.squeeze(0)
return output_tensor
def euler_to_vector(self, yaw, pitch, roll):
# オイラー角から方向ベクトルを計算
yaw_rad = np.radians(yaw)
pitch_rad = np.radians(pitch)
roll_rad = np.radians(roll)
cos_pitch = np.cos(pitch_rad)
sin_pitch = np.sin(pitch_rad)
cos_yaw = np.cos(yaw_rad)
sin_yaw = np.sin(yaw_rad)
direction = np.array([
sin_yaw * cos_pitch,
-sin_pitch,
cos_yaw * cos_pitch
])
return torch.from_numpy(direction).float()
class clean_prompt_tags:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"string": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
"Clean_Prompt": ("STRING", {"multiline": True, "default": ""}),
},
}
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("string","show_help")
FUNCTION = "execute"
CATEGORY = "advanced/AI_Assistant/text"
DESCRIPTION = """
粗略的移除工具:
EX:
base_prompt = masterpiece, best quality, monochrome, greyscale,blue_hair, blue_sky, blue_dress, pink_eyes, pink_bow, pink_panties
removeprompt = pink, red, orange, brown, yellow, green, blue, purple, blonde, colored skin, white hair,
輸出則為:masterpiece, best quality, monochrome, greyscale
"""
def execute(self,string,Clean_Prompt):
show_help ="粗略的移除工具:\n\nEX:\n\nbase_prompt = masterpiece, best quality, monochrome, greyscale,blue_hair, blue_sky, blue_dress, pink_eyes, pink_bow, pink_panties\n\nremoveprompt = pink, red, orange, brown, yellow, green, blue, purple, blonde, colored skin, white hair,\n\n輸出則為:\nmasterpiece, best quality, monochrome, greyscale"
return (self.remove_prompt(string,Clean_Prompt),show_help)
def remove_prompt(self,base_prompt, remove_prompt,):
# 移除末尾多餘的逗號並使用正規表達式來分割標籤
remove_prompt = re.sub(r',\s*$', '', remove_prompt).replace('\n', '')
# 使用正規表達式來分割標籤,忽略逗號後的多餘空格
prompt_list = re.split(r',\s*', base_prompt)
color_list = re.split(r',\s*', remove_prompt)
# カラータグを除去します。
cleaned_tags = [tag for tag in prompt_list if all(color.lower() not in tag.lower() for color in color_list)]
return ", ".join(cleaned_tags)
class prompt_blacklist:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"string": ("STRING", {"multiline": True, "default": "", "forceInput": True}),
"Blacklist_Prompt": ("STRING", {"multiline": True, "default": ""}),
},
}
RETURN_TYPES = ("STRING","STRING",)
RETURN_NAMES = ("string","show_help")
FUNCTION = "execute"
CATEGORY = "advanced/AI_Assistant/text"
DESCRIPTION = """
黑名單功能,分別輸入需移除的提示以逗號分隔即可
EX:
base_prompt= masterpiece, best quality,lineart,sketch,transparent background
Blacklist Prompt=lineart,sketch,
輸出則為:masterpiece, best quality, transparent background
"""
def execute(self,Blacklist_Prompt,string):
show_help ="黑名單功能,分別輸入需移除的提示以逗號分隔即可\n\nEX:\nbase_prompt= masterpiece, best quality,lineart,sketch,transparent background\nBlacklist Prompt=lineart,sketch,\n輸出則為:\n masterpiece, best quality, transparent background"
return (self.execute_prompt(Blacklist_Prompt,string),show_help)
def execute_prompt(self,execute_tags, base_prompt):
prompt_list = re.split(r',\s*', base_prompt)
# execute_tagsを除去
filtered_tags = [tag for tag in prompt_list if tag not in execute_tags]
# 最終的なプロンプトを生成
return ", ".join(filtered_tags)
class resize_image_sdxl_ratio:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("INT","INT","STRING",)
RETURN_NAMES = ("Target Width","Target Height","show_help")
FUNCTION = "execute"
CATEGORY = "advanced/AI_Assistant/image"
DESCRIPTION = """
自動輸出對應的SDXL目標尺寸與ImageScale(圖像縮放)搭配使用並開啟剪裁功能
"""
def execute(self,image):
width,height=self.resize_image_aspect_ratio(image)
show_help ="自動輸出對應的SDXL目標尺寸與ImageScale(圖像縮放)搭配使用並開啟剪裁功能"
return (width, height, show_help)
def ResolutionFromImage(self, image):
_, H, W, _ = image.shape
return (W, H)
def resize_image_aspect_ratio(self,image):
# 元の画像サイズを取得
original_width, original_height = self.ResolutionFromImage(image)
# アスペクト比を計算
aspect_ratio = original_width / original_height
# 標準のアスペクト比サイズを定義
sizes = {
1: (1024, 1024), # 正方形
4/3: (1152, 896), # 横長画像
3/2: (1216, 832),
16/9: (1344, 768),
21/9: (1568, 672),
3/1: (1728, 576),
1/4: (512, 2048), # 縦長画像
1/3: (576, 1728),
9/16: (768, 1344),
2/3: (832, 1216),
3/4: (896, 1152)
}
# 最も近いアスペクト比を見つける
closest_aspect_ratio = min(sizes.keys(), key=lambda x: abs(x - aspect_ratio))
target_width, target_height = sizes[closest_aspect_ratio]
return (target_width, target_height)
class noline_process:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
},
}
RETURN_TYPES = ("IMAGE","STRING", )
RETURN_NAMES = ("image","show_help",)
FUNCTION = "process"
CATEGORY = "advanced/AI_Assistant/image"
DESCRIPTION = """
將輸入圖像移除線條並將顏色取近似值用於I2I
"""
def process(self, image,):
show_help ="將輸入圖像移除線條並將顏色取近似值用於I2I\n"
return (self.noline_process(image),show_help,)
def noline_process(self, input_image):
#ToPILImage
input_image = input_image.permute(0, 3, 1, 2)
input_image = input_image.squeeze(0)
input_image = transforms.ToPILImage()(input_image)
def get_major_colors(image, threshold_percentage):
if image.mode != 'RGB':
image = image.convert('RGB')
color_count = defaultdict(int)
for pixel in image.getdata():
color_count[pixel] += 1
total_pixels = image.width * image.height
return [(color, count) for color, count in color_count.items() if (count / total_pixels) >= threshold_percentage]
def consolidate_colors(major_colors, threshold):
colors_lab = [rgb2lab(np.array([[color]], dtype=np.float32)/255.0).reshape(3) for color, _ in major_colors]
i = 0
while i < len(colors_lab):
j = i + 1
while j < len(colors_lab):
if deltaE_ciede2000(colors_lab[i], colors_lab[j]) < threshold:
if major_colors[i][1] >= major_colors[j][1]:
major_colors[i] = (major_colors[i][0], major_colors[i][1] + major_colors[j][1])
major_colors.pop(j)
colors_lab.pop(j)
else:
major_colors[j] = (major_colors[j][0], major_colors[j][1] + major_colors[i][1])
major_colors.pop(i)
colors_lab.pop(i)
continue
j += 1
i += 1
return major_colors
def generate_distant_colors(consolidated_colors, distance_threshold):
consolidated_lab = [rgb2lab(np.array([color], dtype=np.float32) / 255.0).reshape(3) for color, _ in consolidated_colors]
max_attempts = 10000
for _ in range(max_attempts):
random_rgb = np.random.randint(0, 256, size=3)
random_lab = rgb2lab(np.array([random_rgb], dtype=np.float32) / 255.0).reshape(3)
if all(deltaE_ciede2000(base_color_lab, random_lab) > distance_threshold for base_color_lab in consolidated_lab):
return tuple(random_rgb)
return (128, 128, 128)
def line_color(image, mask, new_color):
data = np.array(image)
data[mask, :3] = new_color
return Image.fromarray(data)
def replace_color(image, color_1, blur_radius=2):
data = np.array(image)
original_shape = data.shape
channels = original_shape[2] if len(original_shape) > 2 else 1
data = data.reshape(-1, channels)
color_1 = np.array(color_1)
matches = np.all(data[:, :3] == color_1, axis=1)
mask = np.zeros(data.shape[0], dtype=bool)
while np.any(matches):
new_matches = np.zeros_like(matches)
for i in range(len(data)):
if matches[i]:
x, y = divmod(i, original_shape[1])
neighbors = [(x, y-1), (x, y+1), (x-1, y), (x+1, y)]
valid_neighbors = [data[nx * original_shape[1] + ny, :3] for nx, ny in neighbors if 0 <= nx < original_shape[0] and 0 <= ny < original_shape[1] and not matches[nx * original_shape[1] + ny]]
if valid_neighbors:
new_color = np.mean(valid_neighbors, axis=0).astype(np.uint8)
data[i, :3] = new_color
mask[i] = True
else:
new_matches[i] = True
matches = new_matches
if not np.any(matches):
break
data = data.reshape(original_shape)
mask = mask.reshape(original_shape[:2])
result_image = Image.fromarray(data, 'RGBA')
blurred_image = result_image.filter(ImageFilter.GaussianBlur(radius=blur_radius))
blurred_data = np.array(blurred_image)
np.copyto(data, blurred_data, where=mask[..., None])
return Image.fromarray(data, 'RGBA')
def DoG_filter(image, kernel_size=0, sigma=1.0, k_sigma=2.0, gamma=1.5):
g1 = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma)
g2 = cv2.GaussianBlur(image, (kernel_size, kernel_size), sigma * k_sigma)
return g1 - gamma * g2
def XDoG_filter(image, kernel_size=0, sigma=1.4, k_sigma=1.6, epsilon=0, phi=10, gamma=0.98):
epsilon /= 255
dog = DoG_filter(image, kernel_size, sigma, k_sigma, gamma)
dog /= dog.max()
e = 1 + np.tanh(phi * (dog - epsilon))
e[e >= 1] = 1
return (e * 255).astype('uint8')
def binarize_image(image):
_, binarized = cv2.threshold(image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
return binarized
def process_XDoG(image):
kernel_size = 0
sigma = 1.4
k_sigma = 1.6
epsilon = 0
phi = 10
gamma = 0.98
# 將 PIL Image 對象轉換為灰度模式的 numpy 數組
image_array = np.array(image.convert("L"))
xdog_image = XDoG_filter(image_array, kernel_size, sigma, k_sigma, epsilon, phi, gamma)
binarized_image = binarize_image(xdog_image)
final_image = Image.fromarray(binarized_image)
return final_image
rgb_image = input_image.convert("RGBA")
lineart = process_XDoG(input_image).convert('L')
lineart = lineart.point(lambda x: 0 if x < 200 else 255)
lineart = ImageOps.invert(lineart)
kernel = np.ones((3, 3), np.uint8)
lineart = cv2.dilate(np.array(lineart), kernel, iterations=1)
lineart = Image.fromarray(lineart)
mask = np.array(lineart) == 255
major_colors = get_major_colors(rgb_image, threshold_percentage=0.05)
major_colors = consolidate_colors(major_colors, 10)
new_color_1 = generate_distant_colors(major_colors, 100)
filled_image = line_color(rgb_image, mask, new_color_1)
replace_color_image = replace_color(filled_image, new_color_1, 2).convert('RGB')
#ToTensor
replace_color_image=transforms.ToTensor()(replace_color_image)
replace_color_image = replace_color_image.unsqueeze(0)
replace_color_image = replace_color_image.permute(0, 2, 3, 1)
#print(replace_color_image.shape)
return replace_color_image