From f7d2168dac4d4cdd11702b28b566f85074e4c4c2 Mon Sep 17 00:00:00 2001 From: MixBadGun <1059129006@qq.com> Date: Wed, 3 Dec 2025 12:25:39 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9C=80=E6=96=B0=E6=9C=80=E7=83=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/fx.txt | 14 + konabot/plugins/fx_process/__init__.py | 144 +++++-- konabot/plugins/fx_process/fx_handle.py | 496 ++++++++++++++++++++--- konabot/plugins/fx_process/fx_manager.py | 20 +- konabot/plugins/fx_process/gradient.py | 341 ++++++++++++++++ 5 files changed, 936 insertions(+), 79 deletions(-) create mode 100644 konabot/plugins/fx_process/gradient.py diff --git a/konabot/docs/user/fx.txt b/konabot/docs/user/fx.txt index 9b9355b..9361af4 100644 --- a/konabot/docs/user/fx.txt +++ b/konabot/docs/user/fx.txt @@ -27,6 +27,11 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx 浮雕``` * ```fx 查找边缘``` * ```fx 平滑``` +* ```fx 暗角 <半径=1.5>``` +* ```fx 发光 <强度=1.5> <模糊半径=15>``` +* ```fx 噪点 <数量=0.05>``` +* ```fx 素描``` +* ```fx 阴影 <模糊量=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 平移 ``` * ```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>``` diff --git a/konabot/plugins/fx_process/__init__.py b/konabot/plugins/fx_process/__init__.py index 895f5c3..914a285 100644 --- a/konabot/plugins/fx_process/__init__.py +++ b/konabot/plugins/fx_process/__init__.py @@ -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()) \ No newline at end of file diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index d4ee009..a263c90 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -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 \ No newline at end of file + 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 + diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index e78c504..f5c91ed 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -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) \ No newline at end of file + return cls.filter_map.get(name) + + @classmethod + def has_filter(cls, name: str) -> bool: + return name in cls.filter_map \ No newline at end of file diff --git a/konabot/plugins/fx_process/gradient.py b/konabot/plugins/fx_process/gradient.py new file mode 100644 index 0000000..44a1802 --- /dev/null +++ b/konabot/plugins/fx_process/gradient.py @@ -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) \ No newline at end of file