最新最热
This commit is contained in:
@ -27,6 +27,11 @@ fx [滤镜名称] <参数1> <参数2> ...
|
||||
* ```fx 浮雕```
|
||||
* ```fx 查找边缘```
|
||||
* ```fx 平滑```
|
||||
* ```fx 暗角 <半径=1.5>```
|
||||
* ```fx 发光 <强度=1.5> <模糊半径=15>```
|
||||
* ```fx 噪点 <数量=0.05>```
|
||||
* ```fx 素描```
|
||||
* ```fx 阴影 <x偏移量=10> <y偏移量=10> <模糊量=10> <不透明度=0.5> <阴影颜色=black>```
|
||||
|
||||
### 色彩处理滤镜
|
||||
* ```fx 反色```
|
||||
@ -36,10 +41,19 @@ fx [滤镜名称] <参数1> <参数2> ...
|
||||
* ```fx 亮度 <因子=1.5>```
|
||||
* ```fx 色彩 <因子=1.5>```
|
||||
* ```fx 色调 <颜色="rgb(255,0,0)">```
|
||||
* ```fx RGB分离 <偏移量=5>```
|
||||
* ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]> <叠加模式=overlay>```
|
||||
|
||||
### 几何变换滤镜
|
||||
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
|
||||
* ```fx 缩放 <比例=1.5>```
|
||||
* ```fx 旋转 <角度=45>```
|
||||
* ```fx 透视变换 <变换矩阵>```
|
||||
* ```fx 裁剪 <左=0> <上=0> <右=100> <下=100>(百分比)```
|
||||
* ```fx 拓展边缘 <拓展量=10>```
|
||||
* ```fx 波纹 <振幅=5> <波长=20>```
|
||||
* ```fx 光学补偿 <数量=100> <反转=false>```
|
||||
* ```fx 球面化 <强度=0.5>```
|
||||
|
||||
### 特殊效果滤镜
|
||||
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
|
||||
|
||||
@ -1,10 +1,12 @@
|
||||
import asyncio as asynkio
|
||||
from dataclasses import dataclass
|
||||
from io import BytesIO
|
||||
|
||||
from inspect import signature
|
||||
|
||||
from konabot.common.nb.extract_image import DepPILImage
|
||||
from konabot.common.nb.extract_image import DepImageBytes, DepPILImage
|
||||
from nonebot.adapters import Event as BaseEvent
|
||||
from nonebot import on_message
|
||||
from nonebot import on_message, logger
|
||||
|
||||
from nonebot_plugin_alconna import (
|
||||
UniMessage,
|
||||
@ -13,46 +15,126 @@ from nonebot_plugin_alconna import (
|
||||
|
||||
from konabot.plugins.fx_process.fx_manager import ImageFilterManager
|
||||
|
||||
from PIL import Image, ImageSequence
|
||||
|
||||
@dataclass
|
||||
class FilterItem:
|
||||
filter: callable
|
||||
args: list
|
||||
|
||||
def prase_input_args(input_str: str) -> list[FilterItem]:
|
||||
# 按分号或换行符分割参数
|
||||
args = []
|
||||
for part in input_str.replace('\n', ';').split(';'):
|
||||
part = part.strip()
|
||||
if not part:
|
||||
continue
|
||||
split_part = part.split()
|
||||
filter_name = split_part[0]
|
||||
if not ImageFilterManager.has_filter(filter_name):
|
||||
continue
|
||||
filter_func = ImageFilterManager.get_filter(filter_name)
|
||||
input_filter_args = split_part[1:]
|
||||
# 获取函数最大参数数量
|
||||
sig = signature(filter_func)
|
||||
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
|
||||
# 从 args 提取参数,并转换为适当类型
|
||||
func_args = []
|
||||
for i in range(0, min(len(input_filter_args), max_params)):
|
||||
# 尝试将参数转换为函数签名中对应的类型
|
||||
param = list(sig.parameters.values())[i + 1]
|
||||
param_type = param.annotation
|
||||
arg_value = input_filter_args[i]
|
||||
try:
|
||||
if param_type is float:
|
||||
converted_value = float(arg_value)
|
||||
elif param_type is int:
|
||||
converted_value = int(arg_value)
|
||||
else:
|
||||
converted_value = arg_value
|
||||
except Exception:
|
||||
converted_value = arg_value
|
||||
func_args.append(converted_value)
|
||||
args.append(FilterItem(filter=filter_func, args=func_args))
|
||||
return args
|
||||
|
||||
def apply_filters_to_image(img: Image, filters: list[FilterItem]) -> Image:
|
||||
for filter_item in filters:
|
||||
filter_func = filter_item.filter
|
||||
func_args = filter_item.args
|
||||
img = filter_func(img, *func_args)
|
||||
return img
|
||||
|
||||
async def apply_filters_to_bytes(image_bytes: bytes, filters: list[FilterItem]) -> BytesIO:
|
||||
# 如果 image 是动图,则逐帧处理
|
||||
img = Image.open(BytesIO(image_bytes))
|
||||
logger.debug("开始图像处理")
|
||||
output = BytesIO()
|
||||
if getattr(img, "is_animated", False):
|
||||
frames = []
|
||||
all_frames = []
|
||||
for frame in ImageSequence.Iterator(img):
|
||||
frame_copy = frame.copy()
|
||||
all_frames.append(frame_copy)
|
||||
|
||||
async def process_single_frame(frame: Image.Image, frame_idx: int) -> Image.Image:
|
||||
"""处理单帧的异步函数"""
|
||||
logger.debug(f"开始处理帧 {frame_idx}")
|
||||
result = await asynkio.to_thread(apply_filters_to_image, frame, filters)
|
||||
logger.debug(f"完成处理帧 {frame_idx}")
|
||||
return result
|
||||
|
||||
# 并发处理所有帧
|
||||
tasks = []
|
||||
for i, frame in enumerate(all_frames):
|
||||
task = process_single_frame(frame, i)
|
||||
tasks.append(task)
|
||||
|
||||
frames = await asynkio.gather(*tasks, return_exceptions=True)
|
||||
|
||||
# 检查是否有处理失败的帧
|
||||
for i, result in enumerate(frames):
|
||||
if isinstance(result, Exception):
|
||||
logger.error(f"帧 {i} 处理失败: {result}")
|
||||
# 使用原始帧作为回退
|
||||
frames[i] = all_frames[i]
|
||||
|
||||
logger.debug("保存动图")
|
||||
frames[0].save(
|
||||
output,
|
||||
format="GIF",
|
||||
save_all=True,
|
||||
append_images=frames[1:],
|
||||
loop=img.info.get("loop", 0),
|
||||
disposal=img.info.get("disposal", 2),
|
||||
duration=img.info.get("duration", 100),
|
||||
)
|
||||
logger.debug("Animated image saved")
|
||||
else:
|
||||
img = apply_filters_to_image(img, filters)
|
||||
img.save(output, format="PNG")
|
||||
logger.debug("Image processing completed")
|
||||
output.seek(0)
|
||||
return output
|
||||
|
||||
|
||||
def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool:
|
||||
txt = msg.extract_plain_text()
|
||||
if "fx" not in txt[:3]:
|
||||
if "fx" not in txt[:3].lower():
|
||||
return False
|
||||
return True
|
||||
|
||||
fx_on = on_message(rule=is_fx_mentioned)
|
||||
|
||||
@fx_on.handle()
|
||||
async def _(msg: UniMsg, event: BaseEvent, img: DepPILImage):
|
||||
async def _(msg: UniMsg, event: BaseEvent, img: DepImageBytes):
|
||||
args = msg.extract_plain_text().split()
|
||||
if len(args) < 2:
|
||||
return
|
||||
filter_name = args[1]
|
||||
filter_func = ImageFilterManager.get_filter(filter_name)
|
||||
if not filter_func:
|
||||
filters = prase_input_args(msg.extract_plain_text()[2:])
|
||||
if not filters:
|
||||
return
|
||||
# 获取函数最大参数数量
|
||||
sig = signature(filter_func)
|
||||
max_params = len(sig.parameters) - 1 # 减去第一个参数 image
|
||||
# 从 args 提取参数,并转换为适当类型
|
||||
func_args = []
|
||||
for i in range(2, min(len(args), max_params + 2)):
|
||||
# 尝试将参数转换为函数签名中对应的类型
|
||||
param = list(sig.parameters.values())[i - 1]
|
||||
param_type = param.annotation
|
||||
arg_value = args[i]
|
||||
try:
|
||||
if param_type is float:
|
||||
converted_value = float(arg_value)
|
||||
elif param_type is int:
|
||||
converted_value = int(arg_value)
|
||||
else:
|
||||
converted_value = arg_value
|
||||
except Exception:
|
||||
converted_value = arg_value
|
||||
func_args.append(converted_value)
|
||||
# 应用滤镜
|
||||
out_img = filter_func(img, *func_args)
|
||||
output = BytesIO()
|
||||
out_img.save(output, format="PNG")
|
||||
output = await apply_filters_to_bytes(img, filters)
|
||||
logger.debug("FX processing completed, sending result.")
|
||||
await fx_on.send(await UniMessage().image(raw=output).export())
|
||||
|
||||
@ -1,10 +1,15 @@
|
||||
from PIL import Image, ImageFilter
|
||||
from PIL import ImageEnhance
|
||||
from PIL import ImageChops
|
||||
from PIL import ImageOps
|
||||
|
||||
from konabot.plugins.fx_process.color_handle import ColorHandle
|
||||
|
||||
import math
|
||||
|
||||
from konabot.plugins.fx_process.gradient import GradientGenerator
|
||||
import numpy as np
|
||||
|
||||
class ImageFilterImplement:
|
||||
@staticmethod
|
||||
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
|
||||
@ -50,14 +55,16 @@ class ImageFilterImplement:
|
||||
# 反色
|
||||
@staticmethod
|
||||
def apply_invert(image: Image.Image) -> Image.Image:
|
||||
# 确保图像是RGBA模式,保留透明度通道
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
r, g, b, a = image.split()
|
||||
r = r.point(lambda i: 255 - i)
|
||||
g = g.point(lambda i: 255 - i)
|
||||
b = b.point(lambda i: 255 - i)
|
||||
return Image.merge('RGBA', (r, g, b, a))
|
||||
|
||||
# 转换为 numpy 数组
|
||||
arr = np.array(image)
|
||||
|
||||
# 只反转 RGB 通道,保持 Alpha 不变
|
||||
arr[:, :, :3] = 255 - arr[:, :, :3]
|
||||
|
||||
return Image.fromarray(arr)
|
||||
|
||||
# 黑白灰度
|
||||
@staticmethod
|
||||
@ -103,26 +110,30 @@ class ImageFilterImplement:
|
||||
def apply_to_color(image: Image.Image, color: str = 'rgb(255,0,0)') -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
# 转为灰度图
|
||||
gray = image.convert('L')
|
||||
# 获取目标颜色的RGB值
|
||||
|
||||
rgb_color = ColorHandle.parse_color(color)
|
||||
# 高光默认为白色,阴影默认为黑色
|
||||
highlight = (255, 255, 255)
|
||||
shadow = (0, 0, 0)
|
||||
# 创建新的图像
|
||||
new_image = Image.new('RGBA', image.size)
|
||||
width, height = image.size
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
lum = gray.getpixel((x, y))
|
||||
# 计算新颜色
|
||||
new_r = int((rgb_color[0] * lum + shadow[0] * (highlight[0] - lum)) / 255)
|
||||
new_g = int((rgb_color[1] * lum + shadow[1] * (highlight[1] - lum)) / 255)
|
||||
new_b = int((rgb_color[2] * lum + shadow[2] * (highlight[2] - lum)) / 255)
|
||||
a = image.getpixel((x, y))[3] # 保留原图的透明度
|
||||
new_image.putpixel((x, y), (new_r, new_g, new_b, a))
|
||||
return new_image
|
||||
|
||||
# 转换为灰度并获取数组
|
||||
gray = image.convert('L')
|
||||
lum = np.array(gray, dtype=np.float32) / 255.0 # 归一化到 [0,1]
|
||||
|
||||
# 获取 alpha
|
||||
alpha = np.array(image.getchannel('A'))
|
||||
|
||||
target_r = rgb_color[0] * lum
|
||||
target_g = rgb_color[1] * lum
|
||||
target_b = rgb_color[2] * lum
|
||||
|
||||
# 堆叠通道
|
||||
result_rgb = np.stack([target_r, target_g, target_b], axis=-1)
|
||||
result_rgb = np.clip(result_rgb, 0, 255).astype(np.uint8)
|
||||
|
||||
# 创建结果图像
|
||||
result = np.zeros((image.height, image.width, 4), dtype=np.uint8)
|
||||
result[:, :, :3] = result_rgb
|
||||
result[:, :, 3] = alpha
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 缩放
|
||||
@staticmethod
|
||||
@ -135,33 +146,424 @@ class ImageFilterImplement:
|
||||
# 波纹
|
||||
@staticmethod
|
||||
def apply_wave(image: Image.Image, amplitude: float = 5, wavelength: float = 20) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
new_image = Image.new('RGBA', (width, height))
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
offset_x = int(amplitude * math.sin(2 * math.pi * y / wavelength))
|
||||
offset_y = int(amplitude * math.cos(2 * math.pi * x / wavelength))
|
||||
new_x = x + offset_x
|
||||
new_y = y + offset_y
|
||||
if 0 <= new_x < width and 0 <= new_y < height:
|
||||
new_image.putpixel((x, y), image.getpixel((new_x, new_y)))
|
||||
else:
|
||||
new_image.putpixel((x, y), (0, 0, 0, 0)) # 透明像素
|
||||
return new_image
|
||||
arr = np.array(image)
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算偏移量(向量化)
|
||||
offset_x = (amplitude * np.sin(2 * np.pi * y_coords / wavelength)).astype(np.int32)
|
||||
offset_y = (amplitude * np.cos(2 * np.pi * x_coords / wavelength)).astype(np.int32)
|
||||
|
||||
# 计算新坐标
|
||||
new_x = x_coords + offset_x
|
||||
new_y = y_coords + offset_y
|
||||
|
||||
# 创建有效坐标掩码
|
||||
valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height)
|
||||
|
||||
# 创建结果图像(初始为透明)
|
||||
result = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
|
||||
# 只复制有效像素
|
||||
if valid_mask.any():
|
||||
# 使用花式索引复制像素
|
||||
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
def apply_color_key(image: Image.Image, target_color: str = 'rgb(255,0,0)', tolerance: int = 60) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
target_rgb = ColorHandle.parse_color(target_color)
|
||||
arr = np.array(image)
|
||||
|
||||
# 计算颜色距离(使用平方距离避免 sqrt)
|
||||
target_arr = np.array(target_rgb, dtype=np.int32)
|
||||
diff = arr[:, :, :3] - target_arr
|
||||
distance_sq = np.sum(diff * diff, axis=2) # (r-r0)² + (g-g0)² + (b-b0)²
|
||||
|
||||
# 创建掩码(距离 <= 容差)
|
||||
mask = distance_sq <= (tolerance * tolerance)
|
||||
|
||||
# 复制原图,只修改 alpha 通道
|
||||
result = arr.copy()
|
||||
result[:, :, 3] = np.where(mask, 0, arr[:, :, 3]) # 符合条件的设为透明
|
||||
|
||||
return Image.fromarray(result)
|
||||
|
||||
# 暗角
|
||||
@staticmethod
|
||||
def apply_vignette(image: Image.Image, radius: float = 1.5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
# 转换为 numpy 数组
|
||||
arr = np.array(image, dtype=np.float32)
|
||||
height, width = arr.shape[:2]
|
||||
# 创建网格
|
||||
y_coords, x_coords = np.ogrid[:height, :width]
|
||||
# 计算中心距离
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
max_distance = np.sqrt(center_x**2 + center_y**2)
|
||||
# 向量化距离计算
|
||||
distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
|
||||
# 计算暗角因子
|
||||
factors = 1 - (distances / max_distance) ** radius
|
||||
factors = np.clip(factors, 0, 1)
|
||||
# 应用暗角效果到 RGB 通道
|
||||
arr[:, :, :3] = arr[:, :, :3] * factors[:, :, np.newaxis]
|
||||
# 转换回 uint8
|
||||
result = np.clip(arr, 0, 255).astype(np.uint8)
|
||||
return Image.fromarray(result)
|
||||
|
||||
# 发光
|
||||
@staticmethod
|
||||
def apply_glow(image: Image.Image, intensity: float = 1.5, blur_radius: float = 15) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
# 创建发光图层
|
||||
glow_layer = image.filter(ImageFilter.GaussianBlur(blur_radius))
|
||||
# 增强亮度
|
||||
enhancer = ImageEnhance.Brightness(glow_layer)
|
||||
glow_layer = enhancer.enhance(intensity)
|
||||
# 转换为 numpy 数组
|
||||
img_arr = np.array(image, dtype=np.float32) # 使用 float32 避免溢出
|
||||
glow_arr = np.array(glow_layer, dtype=np.float32)
|
||||
|
||||
# 向量化合并(只合并 RGB,A 保持不变)
|
||||
result_arr = np.zeros_like(img_arr, dtype=np.float32)
|
||||
|
||||
# RGB 通道相加并限制到 255
|
||||
result_arr[:, :, :3] = np.clip(img_arr[:, :, :3] + glow_arr[:, :, :3], 0, 255)
|
||||
|
||||
# Alpha 通道保持原图
|
||||
result_arr[:, :, 3] = img_arr[:, :, 3]
|
||||
|
||||
return Image.fromarray(result_arr.astype(np.uint8))
|
||||
|
||||
# RGB分离
|
||||
@staticmethod
|
||||
def apply_rgb_split(image: Image.Image, offset: int = 5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
r, g, b, a = image.split()
|
||||
r_offset = r.transform(r.size, Image.AFFINE,
|
||||
(1, 0, offset, 0, 1, 0))
|
||||
g_offset = g.transform(g.size, Image.AFFINE,
|
||||
(1, 0, 0, 0, 1, offset))
|
||||
return Image.merge('RGBA', (r_offset, g_offset, b, a))
|
||||
|
||||
# 光学补偿
|
||||
@staticmethod
|
||||
def apply_optical_compensation(image: Image.Image,
|
||||
amount: float = 100.0, reverse: bool = False) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
new_image = Image.new('RGBA', (width, height))
|
||||
for x in range(width):
|
||||
for y in range(height):
|
||||
r, g, b, a = image.getpixel((x, y))
|
||||
# 计算与目标颜色的距离
|
||||
distance = math.sqrt((r - target_rgb[0]) ** 2 + (g - target_rgb[1]) ** 2 + (b - target_rgb[2]) ** 2)
|
||||
if distance <= tolerance:
|
||||
new_image.putpixel((x, y), (r, g, b, 0)) # 设置为透明
|
||||
arr = np.array(image)
|
||||
|
||||
# 中心点
|
||||
center_x, center_y = width / 2, height / 2
|
||||
|
||||
# 归一化amount
|
||||
amount_norm = amount / 100.0
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算相对中心的归一化坐标
|
||||
dx = (x_coords - center_x) / center_x
|
||||
dy = (y_coords - center_y) / center_y
|
||||
|
||||
# 计算距离(避免除零)
|
||||
distance = np.sqrt(dx**2 + dy**2)
|
||||
|
||||
# 创建掩码:中心点和其他点
|
||||
center_mask = distance == 0
|
||||
other_mask = ~center_mask
|
||||
|
||||
# 初始化缩放因子
|
||||
scale_factor = np.ones_like(distance)
|
||||
|
||||
if reverse:
|
||||
# 反鱼眼效果
|
||||
# 对于非中心点
|
||||
if other_mask.any():
|
||||
# 使用arcsin进行反鱼眼映射
|
||||
theta = np.arcsin(np.clip(distance[other_mask], 0, 0.999))
|
||||
new_distance = np.sin(theta * amount_norm)
|
||||
scale_factor[other_mask] = new_distance / distance[other_mask]
|
||||
else:
|
||||
# 鱼眼效果
|
||||
if other_mask.any():
|
||||
# 使用sin或tanh进行鱼眼映射
|
||||
theta = distance[other_mask] * amount_norm
|
||||
if amount_norm <= 1.0:
|
||||
new_distance = np.sin(theta)
|
||||
else:
|
||||
new_image.putpixel((x, y), (r, g, b, a)) # 保留原像素
|
||||
return new_image
|
||||
new_distance = np.tanh(theta)
|
||||
scale_factor[other_mask] = new_distance / distance[other_mask]
|
||||
|
||||
# 计算源坐标
|
||||
src_x = center_x + dx * center_x * scale_factor
|
||||
src_y = center_y + dy * center_y * scale_factor
|
||||
|
||||
# 裁剪坐标到有效范围
|
||||
src_x = np.clip(src_x, 0, width - 1)
|
||||
src_y = np.clip(src_y, 0, height - 1)
|
||||
|
||||
# 准备插值
|
||||
# 获取整数和小数部分
|
||||
x0 = np.floor(src_x).astype(np.int32)
|
||||
x1 = np.minimum(x0 + 1, width - 1)
|
||||
y0 = np.floor(src_y).astype(np.int32)
|
||||
y1 = np.minimum(y0 + 1, height - 1)
|
||||
|
||||
fx = src_x - x0
|
||||
fy = src_y - y0
|
||||
|
||||
# 确保索引在范围内
|
||||
x0 = np.clip(x0, 0, width - 1)
|
||||
x1 = np.clip(x1, 0, width - 1)
|
||||
y0 = np.clip(y0, 0, height - 1)
|
||||
y1 = np.clip(y1, 0, height - 1)
|
||||
|
||||
# 双线性插值 - 向量化版本
|
||||
# 获取四个角的像素值
|
||||
c00 = arr[y0, x0]
|
||||
c01 = arr[y0, x1]
|
||||
c10 = arr[y1, x0]
|
||||
c11 = arr[y1, x1]
|
||||
|
||||
# 扩展fx, fy用于3D广播
|
||||
fx_3d = fx[:, :, np.newaxis]
|
||||
fy_3d = fy[:, :, np.newaxis]
|
||||
|
||||
# 双线性插值公式
|
||||
top = c00 * (1 - fx_3d) + c01 * fx_3d
|
||||
bottom = c10 * (1 - fx_3d) + c11 * fx_3d
|
||||
result_arr = top * (1 - fy_3d) + bottom * fy_3d
|
||||
|
||||
# 转换为uint8
|
||||
result_arr = np.clip(result_arr, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(result_arr, 'RGBA')
|
||||
|
||||
# 球面化
|
||||
@staticmethod
|
||||
def apply_spherize(image: Image.Image, strength: float = 0.5) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
width, height = image.size
|
||||
arr = np.array(image)
|
||||
|
||||
# 创建坐标网格
|
||||
y_coords, x_coords = np.mgrid[0:height, 0:width]
|
||||
|
||||
# 计算中心点
|
||||
center_x = width / 2
|
||||
center_y = height / 2
|
||||
|
||||
# 计算归一化坐标
|
||||
norm_x = (x_coords - center_x) / (width / 2)
|
||||
norm_y = (y_coords - center_y) / (height / 2)
|
||||
radius = np.sqrt(norm_x**2 + norm_y**2)
|
||||
|
||||
# 计算球面化偏移(向量化)
|
||||
factor = 1 + strength * (radius**2)
|
||||
new_x = (norm_x * factor) * (width / 2) + center_x
|
||||
new_y = (norm_y * factor) * (height / 2) + center_y
|
||||
|
||||
new_x = new_x.astype(np.int32)
|
||||
new_y = new_y.astype(np.int32)
|
||||
|
||||
# 创建有效坐标掩码
|
||||
valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height)
|
||||
|
||||
# 创建结果图像(初始为透明)
|
||||
result = np.zeros((height, width, 4), dtype=np.uint8)
|
||||
|
||||
# 只复制有效像素
|
||||
if valid_mask.any():
|
||||
# 使用花式索引复制像素
|
||||
result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]]
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 平移
|
||||
@staticmethod
|
||||
def apply_translate(image: Image.Image, x_offset: int = 10, y_offset: int = 10) -> Image.Image:
|
||||
return image.transform(image.size, Image.AFFINE,
|
||||
(1, 0, x_offset, 0, 1, y_offset))
|
||||
|
||||
# 拓展边缘
|
||||
@staticmethod
|
||||
def apply_expand_edges(image: Image.Image, border_size: int = 10) -> Image.Image:
|
||||
# 拓展边缘,填充全透明
|
||||
return ImageOps.expand(image, border=border_size, fill=(0, 0, 0, 0))
|
||||
|
||||
# 旋转
|
||||
@staticmethod
|
||||
def apply_rotate(image: Image.Image, angle: float = 45) -> Image.Image:
|
||||
return image.rotate(angle, expand=True)
|
||||
|
||||
# 透视变换
|
||||
@staticmethod
|
||||
def apply_perspective_transform(image: Image.Image, coeffs: list[float]) -> Image.Image:
|
||||
return image.transform(image.size, Image.PERSPECTIVE, coeffs, Image.Resampling.BICUBIC)
|
||||
|
||||
# 裁剪
|
||||
@staticmethod
|
||||
def apply_crop(image: Image.Image, left: float = 0, upper: float = 0, right: float = 100, lower: float = 100) -> Image.Image:
|
||||
# 按百分比裁剪
|
||||
width, height = image.size
|
||||
left_px = int(width * left / 100)
|
||||
upper_px = int(height * upper / 100)
|
||||
right_px = int(width * right / 100)
|
||||
lower_px = int(height * lower / 100)
|
||||
# 如果为负数,则扩展边缘
|
||||
if left_px < 0 or upper_px < 0 or right_px > width or lower_px > height:
|
||||
border_left = max(0, -left_px)
|
||||
border_top = max(0, -upper_px)
|
||||
border_right = max(0, right_px - width)
|
||||
border_bottom = max(0, lower_px - height)
|
||||
image = ImageOps.expand(image, border=(border_left, border_top, border_right, border_bottom), fill=(0,0,0,0))
|
||||
left_px += border_left
|
||||
upper_px += border_top
|
||||
right_px += border_left
|
||||
lower_px += border_top
|
||||
return image.crop((left_px, upper_px, right_px, lower_px))
|
||||
|
||||
# 噪点
|
||||
@staticmethod
|
||||
def apply_noise(image: Image.Image, amount: float = 0.05) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
arr = np.array(image)
|
||||
noise = np.random.randint(0, 256, arr.shape, dtype=np.uint8)
|
||||
|
||||
# 为每个像素创建掩码,然后扩展到所有通道
|
||||
mask = np.random.rand(*arr.shape[:2]) < amount
|
||||
mask_3d = mask[:, :, np.newaxis] # 添加第三个维度
|
||||
|
||||
# 混合噪点
|
||||
result = np.where(mask_3d, noise, arr)
|
||||
|
||||
return Image.fromarray(result, 'RGBA')
|
||||
|
||||
# 素描
|
||||
@staticmethod
|
||||
def apply_sketch(image: Image.Image) -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
|
||||
# 转为灰度图
|
||||
gray_image = image.convert('L')
|
||||
|
||||
# 反相
|
||||
inverted_image = ImageChops.invert(gray_image)
|
||||
|
||||
# 高斯模糊
|
||||
blurred_image = inverted_image.filter(ImageFilter.GaussianBlur(radius=10))
|
||||
|
||||
# 混合
|
||||
def dodge(front, back):
|
||||
result = front * 255 / (255 - back)
|
||||
result[result > 255] = 255
|
||||
result[back == 255] = 255
|
||||
return result.astype(np.uint8)
|
||||
|
||||
gray_arr = np.array(gray_image, dtype=np.float32)
|
||||
blurred_arr = np.array(blurred_image, dtype=np.float32)
|
||||
|
||||
sketch_arr = dodge(gray_arr, blurred_arr)
|
||||
|
||||
# 创建结果图像,保留原始 alpha 通道
|
||||
alpha_channel = np.array(image.getchannel('A'))
|
||||
result_arr = np.zeros((image.height, image.width, 4), dtype=np.uint8)
|
||||
result_arr[:, :, 0] = sketch_arr
|
||||
result_arr[:, :, 1] = sketch_arr
|
||||
result_arr[:, :, 2] = sketch_arr
|
||||
result_arr[:, :, 3] = alpha_channel
|
||||
|
||||
return Image.fromarray(result_arr, 'RGBA')
|
||||
|
||||
# 两张图像混合,可指定叠加模式
|
||||
@staticmethod
|
||||
def apply_blend(image1: Image.Image, image2: Image.Image, mode: str = 'normal', alpha: float = 0.5) -> Image.Image:
|
||||
if image1.mode != 'RGBA':
|
||||
image1 = image1.convert('RGBA')
|
||||
if image2.mode != 'RGBA':
|
||||
image2 = image2.convert('RGBA')
|
||||
|
||||
image2 = image2.resize(image1.size, Image.Resampling.LANCZOS)
|
||||
|
||||
arr1 = np.array(image1, dtype=np.float32)
|
||||
arr2 = np.array(image2, dtype=np.float32)
|
||||
|
||||
if mode == 'normal':
|
||||
blended = arr1 * (1 - alpha) + arr2 * alpha
|
||||
elif mode == 'multiply':
|
||||
blended = (arr1 / 255.0) * (arr2 / 255.0) * 255.0
|
||||
elif mode == 'screen':
|
||||
blended = 255 - (1 - arr1 / 255.0) * (1 - arr2 / 255.0) * 255.0
|
||||
elif mode == 'overlay':
|
||||
mask = arr1 < 128
|
||||
blended = np.zeros_like(arr1)
|
||||
blended[mask] = (2 * (arr1[mask] / 255.0) * (arr2[mask] / 255.0)) * 255.0
|
||||
blended[~mask] = (1 - 2 * (1 - arr1[~mask] / 255.0) * (1 - arr2[~mask] / 255.0)) * 255.0
|
||||
else:
|
||||
blended = arr1
|
||||
|
||||
blended = np.clip(blended, 0, 255).astype(np.uint8)
|
||||
|
||||
return Image.fromarray(blended, 'RGBA')
|
||||
|
||||
# 叠加渐变色
|
||||
@staticmethod
|
||||
def apply_gradient_overlay(
|
||||
image: Image.Image,
|
||||
color_list: str = '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]',
|
||||
overlay_mode: str = 'overlay',
|
||||
) -> Image.Image:
|
||||
gradient_gen = GradientGenerator()
|
||||
color_nodes = gradient_gen.parse_color_list(color_list)
|
||||
gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes)
|
||||
return ImageFilterImplement.apply_blend(image, gradient, mode=overlay_mode, alpha=0.5)
|
||||
|
||||
# 阴影
|
||||
@staticmethod
|
||||
def apply_shadow(image: Image.Image,
|
||||
x_offset: int = 10,
|
||||
y_offset: int = 10,
|
||||
blur = 10,
|
||||
opacity = 0.5,
|
||||
shadow_color = "black") -> Image.Image:
|
||||
if image.mode != 'RGBA':
|
||||
image = image.convert('RGBA')
|
||||
offset = (x_offset, y_offset)
|
||||
# 创建阴影图层
|
||||
shadow = Image.new('RGBA', image.size, (0,0,0,0))
|
||||
shadow_rgb = ColorHandle.parse_color(shadow_color)
|
||||
shadow_draw = Image.new('RGBA', image.size, shadow_rgb + (0,))
|
||||
alpha = image.split()[3].point(lambda p: int(p * opacity))
|
||||
shadow.paste(shadow_draw, (0,0), alpha)
|
||||
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
|
||||
# 创建结果图像
|
||||
result = Image.new('RGBA', (image.width + abs(offset[0]), image.height + abs(offset[1])), (0,0,0,0))
|
||||
shadow_position = (max(offset[0],0), max(offset[1],0))
|
||||
image_position = (max(-offset[0],0), max(-offset[1],0))
|
||||
result.paste(shadow, shadow_position, shadow)
|
||||
result.paste(image, image_position, image)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@ -21,8 +21,26 @@ class ImageFilterManager:
|
||||
"缩放": ImageFilterImplement.apply_resize,
|
||||
"波纹": ImageFilterImplement.apply_wave,
|
||||
"色键": ImageFilterImplement.apply_color_key,
|
||||
"暗角": ImageFilterImplement.apply_vignette,
|
||||
"发光": ImageFilterImplement.apply_glow,
|
||||
"RGB分离": ImageFilterImplement.apply_rgb_split,
|
||||
"光学补偿": ImageFilterImplement.apply_optical_compensation,
|
||||
"球面化": ImageFilterImplement.apply_spherize,
|
||||
"旋转": ImageFilterImplement.apply_rotate,
|
||||
"透视变换": ImageFilterImplement.apply_perspective_transform,
|
||||
"裁剪": ImageFilterImplement.apply_crop,
|
||||
"噪点": ImageFilterImplement.apply_noise,
|
||||
"平移": ImageFilterImplement.apply_translate,
|
||||
"拓展边缘": ImageFilterImplement.apply_expand_edges,
|
||||
"素描": ImageFilterImplement.apply_sketch,
|
||||
"叠加颜色": ImageFilterImplement.apply_gradient_overlay,
|
||||
"阴影": ImageFilterImplement.apply_shadow,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_filter(cls, name: str) -> Optional[callable]:
|
||||
return cls.filter_map.get(name)
|
||||
return cls.filter_map.get(name)
|
||||
|
||||
@classmethod
|
||||
def has_filter(cls, name: str) -> bool:
|
||||
return name in cls.filter_map
|
||||
341
konabot/plugins/fx_process/gradient.py
Normal file
341
konabot/plugins/fx_process/gradient.py
Normal file
@ -0,0 +1,341 @@
|
||||
import re
|
||||
from konabot.plugins.fx_process.color_handle import ColorHandle
|
||||
import numpy as np
|
||||
from PIL import Image, ImageDraw
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
|
||||
class GradientGenerator:
|
||||
"""渐变生成器类"""
|
||||
|
||||
def __init__(self):
|
||||
self.has_numpy = hasattr(np, '__version__')
|
||||
|
||||
def parse_color_list(self, color_list_str: str) -> List[Dict]:
|
||||
"""解析渐变颜色列表字符串
|
||||
|
||||
Args:
|
||||
color_list_str: 格式如 '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]'
|
||||
|
||||
Returns:
|
||||
list: 包含颜色和位置信息的字典列表
|
||||
"""
|
||||
color_nodes = []
|
||||
color_list_str = color_list_str.strip('[] ').strip()
|
||||
pattern = r'([^|]+)\|\(([^)]+)\)'
|
||||
matches = re.findall(pattern, color_list_str)
|
||||
|
||||
for color_str, pos_str in matches:
|
||||
color = ColorHandle.parse_color(color_str.strip())
|
||||
|
||||
try:
|
||||
x_str, y_str = pos_str.split(',')
|
||||
x_percent = float(x_str.strip().replace('%', ''))
|
||||
y_percent = float(y_str.strip().replace('%', ''))
|
||||
x_percent = max(0, min(100, x_percent))
|
||||
y_percent = max(0, min(100, y_percent))
|
||||
except:
|
||||
x_percent = 0
|
||||
y_percent = 0
|
||||
|
||||
color_nodes.append({
|
||||
'color': color,
|
||||
'position': (x_percent / 100.0, y_percent / 100.0)
|
||||
})
|
||||
|
||||
if not color_nodes:
|
||||
color_nodes = [
|
||||
{'color': (255, 0, 0), 'position': (0, 0)},
|
||||
{'color': (0, 0, 255), 'position': (1, 1)}
|
||||
]
|
||||
|
||||
return color_nodes
|
||||
|
||||
def create_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建渐变图像
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
color_nodes: 颜色节点列表
|
||||
|
||||
Returns:
|
||||
Image.Image: 渐变图像
|
||||
"""
|
||||
if len(color_nodes) == 1:
|
||||
return Image.new('RGB', (width, height), color_nodes[0]['color'])
|
||||
elif len(color_nodes) == 2:
|
||||
return self._create_linear_gradient(width, height, color_nodes)
|
||||
else:
|
||||
return self._create_radial_gradient(width, height, color_nodes)
|
||||
|
||||
def _create_linear_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建线性渐变"""
|
||||
color1 = color_nodes[0]['color']
|
||||
color2 = color_nodes[1]['color']
|
||||
pos1 = color_nodes[0]['position']
|
||||
pos2 = color_nodes[1]['position']
|
||||
|
||||
if self.has_numpy:
|
||||
return self._create_linear_gradient_numpy(width, height, color1, color2, pos1, pos2)
|
||||
else:
|
||||
return self._create_linear_gradient_pil(width, height, color1, color2, pos1, pos2)
|
||||
|
||||
def _create_linear_gradient_numpy(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple,
|
||||
pos1: Tuple, pos2: Tuple) -> Image.Image:
|
||||
"""使用numpy创建线性渐变"""
|
||||
# 创建坐标网格
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
# 计算渐变方向
|
||||
dx = pos2[0] - pos1[0]
|
||||
dy = pos2[1] - pos1[1]
|
||||
length_sq = dx * dx + dy * dy
|
||||
|
||||
if length_sq > 0:
|
||||
# 计算投影参数
|
||||
t = ((xx - pos1[0]) * dx + (yy - pos1[1]) * dy) / length_sq
|
||||
t = np.clip(t, 0, 1)
|
||||
else:
|
||||
t = np.zeros_like(xx)
|
||||
|
||||
# 插值颜色
|
||||
r = color1[0] + (color2[0] - color1[0]) * t
|
||||
g = color1[1] + (color2[1] - color1[1]) * t
|
||||
b = color1[2] + (color2[2] - color1[2]) * t
|
||||
|
||||
# 创建图像
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
|
||||
def _create_linear_gradient_pil(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple,
|
||||
pos1: Tuple, pos2: Tuple) -> Image.Image:
|
||||
"""使用PIL创建线性渐变(没有numpy时使用)"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
# 判断渐变方向
|
||||
if abs(pos1[0] - pos2[0]) < 0.01: # 垂直渐变
|
||||
y1 = int(pos1[1] * (height - 1))
|
||||
y2 = int(pos2[1] * (height - 1))
|
||||
|
||||
if y2 < y1:
|
||||
y1, y2 = y2, y1
|
||||
color1, color2 = color2, color1
|
||||
|
||||
if y2 > y1:
|
||||
for y in range(height):
|
||||
if y <= y1:
|
||||
fill_color = color1
|
||||
elif y >= y2:
|
||||
fill_color = color2
|
||||
else:
|
||||
ratio = (y - y1) / (y2 - y1)
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
fill_color = (r, g, b)
|
||||
|
||||
draw.line([(0, y), (width, y)], fill=fill_color)
|
||||
else:
|
||||
draw.rectangle([0, 0, width, height], fill=color1)
|
||||
|
||||
elif abs(pos1[1] - pos2[1]) < 0.01: # 水平渐变
|
||||
x1 = int(pos1[0] * (width - 1))
|
||||
x2 = int(pos2[0] * (width - 1))
|
||||
|
||||
if x2 < x1:
|
||||
x1, x2 = x2, x1
|
||||
color1, color2 = color2, color1
|
||||
|
||||
if x2 > x1:
|
||||
for x in range(width):
|
||||
if x <= x1:
|
||||
fill_color = color1
|
||||
elif x >= x2:
|
||||
fill_color = color2
|
||||
else:
|
||||
ratio = (x - x1) / (x2 - x1)
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
fill_color = (r, g, b)
|
||||
|
||||
draw.line([(x, 0), (x, height)], fill=fill_color)
|
||||
else:
|
||||
draw.rectangle([0, 0, width, height], fill=color1)
|
||||
|
||||
else: # 对角渐变(简化处理为左上到右下)
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
distance = (x/width + y/height) / 2
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * distance)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * distance)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * distance)
|
||||
draw.point((x, y), fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_radial_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建径向渐变"""
|
||||
if self.has_numpy and len(color_nodes) > 2:
|
||||
return self._create_radial_gradient_numpy(width, height, color_nodes)
|
||||
else:
|
||||
return self._create_simple_gradient(width, height, color_nodes)
|
||||
|
||||
def _create_radial_gradient_numpy(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""使用numpy创建径向渐变(多色)"""
|
||||
# 创建坐标网格
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
# 提取颜色和位置
|
||||
positions = np.array([node['position'] for node in color_nodes])
|
||||
colors = np.array([node['color'] for node in color_nodes])
|
||||
|
||||
# 计算每个点到所有节点的距离
|
||||
distances = np.sqrt((xx[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 0]) ** 2 +
|
||||
(yy[:, :, np.newaxis] - positions[np.newaxis, np.newaxis, :, 1]) ** 2)
|
||||
|
||||
# 找到最近的两个节点
|
||||
sorted_indices = np.argsort(distances, axis=2)
|
||||
nearest_idx = sorted_indices[:, :, 0]
|
||||
second_idx = sorted_indices[:, :, 1]
|
||||
|
||||
# 获取对应的颜色
|
||||
nearest_colors = colors[nearest_idx]
|
||||
second_colors = colors[second_idx]
|
||||
|
||||
# 获取距离并计算权重
|
||||
nearest_dist = np.take_along_axis(distances, np.expand_dims(nearest_idx, axis=2), axis=2)[:, :, 0]
|
||||
second_dist = np.take_along_axis(distances, np.expand_dims(second_idx, axis=2), axis=2)[:, :, 0]
|
||||
|
||||
total_dist = nearest_dist + second_dist
|
||||
mask = total_dist > 0
|
||||
weight1 = np.zeros_like(nearest_dist)
|
||||
weight1[mask] = second_dist[mask] / total_dist[mask]
|
||||
weight2 = 1 - weight1
|
||||
|
||||
# 插值颜色
|
||||
r = nearest_colors[:, :, 0] * weight1 + second_colors[:, :, 0] * weight2
|
||||
g = nearest_colors[:, :, 1] * weight1 + second_colors[:, :, 1] * weight2
|
||||
b = nearest_colors[:, :, 2] * weight1 + second_colors[:, :, 2] * weight2
|
||||
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
|
||||
def _create_simple_gradient(self, width: int, height: int, color_nodes: List[Dict]) -> Image.Image:
|
||||
"""创建简化渐变(没有numpy或多色时使用)"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
if len(color_nodes) >= 2:
|
||||
# 使用第一个和最后一个颜色创建简单渐变
|
||||
color1 = color_nodes[0]['color']
|
||||
color2 = color_nodes[-1]['color']
|
||||
|
||||
# 判断节点分布
|
||||
x_positions = [node['position'][0] for node in color_nodes]
|
||||
y_positions = [node['position'][1] for node in color_nodes]
|
||||
|
||||
if all(abs(x - x_positions[0]) < 0.01 for x in x_positions):
|
||||
# 垂直渐变
|
||||
for y in range(height):
|
||||
ratio = y / (height - 1) if height > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
||||
else:
|
||||
# 水平渐变
|
||||
for x in range(width):
|
||||
ratio = x / (width - 1) if width > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(x, 0), (x, height)], fill=(r, g, b))
|
||||
else:
|
||||
# 单色
|
||||
draw.rectangle([0, 0, width, height], fill=color_nodes[0]['color'])
|
||||
|
||||
return gradient
|
||||
|
||||
def create_simple_gradient(self, width: int, height: int,
|
||||
start_color: Tuple, end_color: Tuple,
|
||||
direction: str = 'vertical') -> Image.Image:
|
||||
"""创建简单双色渐变
|
||||
|
||||
Args:
|
||||
width: 图像宽度
|
||||
height: 图像高度
|
||||
start_color: 起始颜色
|
||||
end_color: 结束颜色
|
||||
direction: 渐变方向 'vertical', 'horizontal', 'diagonal'
|
||||
|
||||
Returns:
|
||||
Image.Image: 渐变图像
|
||||
"""
|
||||
if direction == 'vertical':
|
||||
return self._create_vertical_gradient(width, height, start_color, end_color)
|
||||
elif direction == 'horizontal':
|
||||
return self._create_horizontal_gradient(width, height, start_color, end_color)
|
||||
else: # diagonal
|
||||
return self._create_diagonal_gradient(width, height, start_color, end_color)
|
||||
|
||||
def _create_vertical_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建垂直渐变"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
for y in range(height):
|
||||
ratio = y / (height - 1) if height > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(0, y), (width, y)], fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_horizontal_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建水平渐变"""
|
||||
gradient = Image.new('RGB', (width, height))
|
||||
draw = ImageDraw.Draw(gradient)
|
||||
|
||||
for x in range(width):
|
||||
ratio = x / (width - 1) if width > 1 else 0
|
||||
r = int(color1[0] + (color2[0] - color1[0]) * ratio)
|
||||
g = int(color1[1] + (color2[1] - color1[1]) * ratio)
|
||||
b = int(color1[2] + (color2[2] - color1[2]) * ratio)
|
||||
draw.line([(x, 0), (x, height)], fill=(r, g, b))
|
||||
|
||||
return gradient
|
||||
|
||||
def _create_diagonal_gradient(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""创建对角渐变"""
|
||||
if self.has_numpy:
|
||||
return self._create_diagonal_gradient_numpy(width, height, color1, color2)
|
||||
else:
|
||||
return self._create_horizontal_gradient(width, height, color1, color2) # 降级为水平渐变
|
||||
|
||||
def _create_diagonal_gradient_numpy(self, width: int, height: int,
|
||||
color1: Tuple, color2: Tuple) -> Image.Image:
|
||||
"""使用numpy创建对角渐变"""
|
||||
x = np.linspace(0, 1, width)
|
||||
y = np.linspace(0, 1, height)
|
||||
xx, yy = np.meshgrid(x, y)
|
||||
|
||||
distance = (xx + yy) / 2.0
|
||||
|
||||
r = color1[0] + (color2[0] - color1[0]) * distance
|
||||
g = color1[1] + (color2[1] - color1[1]) * distance
|
||||
b = color1[2] + (color2[2] - color1[2]) * distance
|
||||
|
||||
gradient_array = np.stack([r, g, b], axis=-1).astype(np.uint8)
|
||||
return Image.fromarray(gradient_array)
|
||||
Reference in New Issue
Block a user