最新最热

This commit is contained in:
2025-12-03 12:25:39 +08:00
parent 40be5ce335
commit f7d2168dac
5 changed files with 936 additions and 79 deletions

View File

@ -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>```

View File

@ -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())

View File

@ -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)
# 向量化合并(只合并 RGBA 保持不变)
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

View File

@ -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

View 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)