diff --git a/konabot/docs/user/fx.txt b/konabot/docs/user/fx.txt new file mode 100644 index 0000000..9b9355b --- /dev/null +++ b/konabot/docs/user/fx.txt @@ -0,0 +1,51 @@ +## 指令介绍 + +`fx` - 用于对图片应用各种滤镜效果的指令 + +## 格式 + +``` +fx [滤镜名称] <参数1> <参数2> ... +``` + +## 示例 + +- `fx 模糊` +- `fx 阈值 150` +- `fx 缩放 2.0` +- `fx 色彩 1.8` +- `fx 色键 rgb(0,255,0) 50` + +## 可用滤镜列表 + +### 基础滤镜 +* ```fx 模糊 <半径=10>``` +* ```fx 马赛克 <像素大小=10>``` +* ```fx 轮廓``` +* ```fx 锐化``` +* ```fx 边缘增强``` +* ```fx 浮雕``` +* ```fx 查找边缘``` +* ```fx 平滑``` + +### 色彩处理滤镜 +* ```fx 反色``` +* ```fx 黑白``` +* ```fx 阈值 <阈值=128>``` +* ```fx 对比度 <因子=1.5>``` +* ```fx 亮度 <因子=1.5>``` +* ```fx 色彩 <因子=1.5>``` +* ```fx 色调 <颜色="rgb(255,0,0)">``` + +### 几何变换滤镜 +* ```fx 缩放 <比例=1.5>``` +* ```fx 波纹 <振幅=5> <波长=20>``` + +### 特殊效果滤镜 +* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>``` + +## 颜色名称支持 +- **基本颜色**:红、绿、蓝、黄、紫、黑、白、橙、粉、灰、青、靛、棕 +- **修饰词**:浅、深、亮、暗(可组合使用,如`浅红`、`深蓝`) +- **RGB格式**:`rgb(255,0,0)`、`rgb(0,255,0)`、`(255,0,0)` 等 +- **HEX格式**:`#66ccff`等 \ No newline at end of file diff --git a/konabot/plugins/fx_process/__init__.py b/konabot/plugins/fx_process/__init__.py index 6978887..895f5c3 100644 --- a/konabot/plugins/fx_process/__init__.py +++ b/konabot/plugins/fx_process/__init__.py @@ -1,25 +1,17 @@ from io import BytesIO -from pathlib import Path from inspect import signature from konabot.common.nb.extract_image import DepPILImage -from konabot.common.nb.match_keyword import match_keyword -from konabot.plugins.fx_process.fx_handle import ImageFilterUtils - -import nonebot from nonebot.adapters import Event as BaseEvent -from nonebot import on_message, logger -from nonebot_plugin_alconna import Alconna, Args, on_alconna +from nonebot import on_message from nonebot_plugin_alconna import ( UniMessage, UniMsg ) -filter_map = { - "模糊": ImageFilterUtils.apply_blur -} +from konabot.plugins.fx_process.fx_manager import ImageFilterManager def is_fx_mentioned(evt: BaseEvent, msg: UniMsg) -> bool: txt = msg.extract_plain_text() @@ -32,17 +24,32 @@ fx_on = on_message(rule=is_fx_mentioned) @fx_on.handle() async def _(msg: UniMsg, event: BaseEvent, img: DepPILImage): args = msg.extract_plain_text().split() - if len(args) < 2 or args[1] not in filter_map: + if len(args) < 2: return filter_name = args[1] - filter_func = filter_map[filter_name] + filter_func = ImageFilterManager.get_filter(filter_name) + if not filter_func: + return # 获取函数最大参数数量 sig = signature(filter_func) max_params = len(sig.parameters) - 1 # 减去第一个参数 image - # 从 args 提取参数,不能超界 + # 从 args 提取参数,并转换为适当类型 func_args = [] for i in range(2, min(len(args), max_params + 2)): - func_args.append(args[i]) + # 尝试将参数转换为函数签名中对应的类型 + 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() diff --git a/konabot/plugins/fx_process/color_handle.py b/konabot/plugins/fx_process/color_handle.py new file mode 100644 index 0000000..ba3578e --- /dev/null +++ b/konabot/plugins/fx_process/color_handle.py @@ -0,0 +1,50 @@ +from typing import Optional +from PIL import ImageColor + +class ColorHandle: + color_name_map = { + "红": (255, 0, 0), + "绿": (0, 255, 0), + "蓝": (0, 0, 255), + "黄": (255, 255, 0), + "紫": (128, 0, 128), + "黑": (0, 0, 0), + "白": (255, 255, 255), + "橙": (255, 165, 0), + "粉": (255, 192, 203), + "灰": (128, 128, 128), + "青": (0, 255, 255), + "靛": (75, 0, 130), + "棕": (165, 42, 42), + "浅": (200, 200, 200), + "深": (50, 50, 50), + "亮": (255, 255, 224), + "暗": (47, 79, 79), + } + + @staticmethod + def set_or_blend_color(ori_color: Optional[tuple], target_color: tuple) -> tuple: + # 如果没有指定初始颜色,返回目标颜色 + if ori_color is None: + return target_color + # 混合颜色,取平均值 + blended_color = tuple((o + t) // 2 for o, t in zip(ori_color, target_color)) + return blended_color + + @staticmethod + def parse_color(color_str: str) -> tuple: + # 如果是纯括号,则加上前缀 rgb + if color_str.startswith('(') and color_str.endswith(')'): + color_str = 'rgb' + color_str + try: + return ImageColor.getrgb(color_str) + except ValueError: + pass + base_color = None + color_str = color_str.replace('色', '') + for name, rgb in ColorHandle.color_name_map.items(): + if name in color_str: + base_color = ColorHandle.set_or_blend_color(base_color, rgb) + if base_color is not None: + return base_color + return (255, 255, 255) # 默认白色 \ 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 9c36fd7..d4ee009 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -1,15 +1,167 @@ from PIL import Image, ImageFilter +from PIL import ImageEnhance -class ImageFilterUtils: +from konabot.plugins.fx_process.color_handle import ColorHandle + +import math + +class ImageFilterImplement: @staticmethod - def apply_blur(image: Image.Image, radius: float = 5) -> Image.Image: - """对图像应用模糊效果 + def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image: + return image.filter(ImageFilter.GaussianBlur(radius)) + + # 马赛克 + @staticmethod + def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image: + if pixel_size <= 0: + pixel_size = 1 + # 缩小图像 + small_image = image.resize( + (image.width // pixel_size, image.height // pixel_size), + Image.Resampling.NEAREST + ) + # 放大图像 + return small_image.resize(image.size, Image.Resampling.NEAREST) + + @staticmethod + def apply_contour(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.CONTOUR) + + @staticmethod + def apply_sharpen(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.SHARPEN) + + @staticmethod + def apply_edge_enhance(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.EDGE_ENHANCE) + + @staticmethod + def apply_emboss(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.EMBOSS) + + @staticmethod + def apply_find_edges(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.FIND_EDGES) + + @staticmethod + def apply_smooth(image: Image.Image) -> Image.Image: + return image.filter(ImageFilter.SMOOTH) + + # 反色 + @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)) + + # 黑白灰度 + @staticmethod + def apply_black_white(image: Image.Image) -> Image.Image: + # 保留透明度通道 + if image.mode != 'RGBA': + image = image.convert('RGBA') + r, g, b, a = image.split() + gray = Image.merge('RGB', (r, g, b)).convert('L') + return Image.merge('RGBA', (gray, gray, gray, a)) + + # 阈值 + @staticmethod + def apply_threshold(image: Image.Image, threshold: int = 128) -> Image.Image: + # 保留透明度通道 + if image.mode != 'RGBA': + image = image.convert('RGBA') + r, g, b, a = image.split() + gray = Image.merge('RGB', (r, g, b)).convert('L') + bw = gray.point(lambda x: 255 if x >= threshold else 0, '1') + return Image.merge('RGBA', (bw.convert('L'), bw.convert('L'), bw.convert('L'), a)) + + # 对比度 + @staticmethod + def apply_contrast(image: Image.Image, factor: float = 1.5) -> Image.Image: + enhancer = ImageEnhance.Contrast(image) + return enhancer.enhance(factor) + + # 亮度 + @staticmethod + def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image: + enhancer = ImageEnhance.Brightness(image) + return enhancer.enhance(factor) + + # 色彩 + @staticmethod + def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image: + enhancer = ImageEnhance.Color(image) + return enhancer.enhance(factor) + + # 三色调 + @staticmethod + 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 + + # 缩放 + @staticmethod + def apply_resize(image: Image.Image, scale: float = 1.5) -> Image.Image: + if scale <= 0: + scale = 1.0 + new_size = (int(image.width * scale), int(image.height * scale)) + return image.resize(new_size, Image.Resampling.LANCZOS) + + # 波纹 + @staticmethod + def apply_wave(image: Image.Image, amplitude: float = 5, wavelength: float = 20) -> Image.Image: + 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 - 参数: - image: 要处理的Pillow图像对象 - radius: 模糊半径,值越大模糊效果越明显 - - 返回: - 处理后的Pillow图像对象 - """ - return image.filter(ImageFilter.GaussianBlur(radius)) \ No newline at end of file + 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) + 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)) # 设置为透明 + else: + new_image.putpixel((x, y), (r, g, b, a)) # 保留原像素 + return new_image \ No newline at end of file diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py new file mode 100644 index 0000000..e78c504 --- /dev/null +++ b/konabot/plugins/fx_process/fx_manager.py @@ -0,0 +1,28 @@ +from typing import Optional +from konabot.plugins.fx_process.fx_handle import ImageFilterImplement + +class ImageFilterManager: + filter_map = { + "模糊": ImageFilterImplement.apply_blur, + "马赛克": ImageFilterImplement.apply_mosaic, + "轮廓": ImageFilterImplement.apply_contour, + "锐化": ImageFilterImplement.apply_sharpen, + "边缘增强": ImageFilterImplement.apply_edge_enhance, + "浮雕": ImageFilterImplement.apply_emboss, + "查找边缘": ImageFilterImplement.apply_find_edges, + "平滑": ImageFilterImplement.apply_smooth, + "反色": ImageFilterImplement.apply_invert, + "黑白": ImageFilterImplement.apply_black_white, + "阈值": ImageFilterImplement.apply_threshold, + "对比度": ImageFilterImplement.apply_contrast, + "亮度": ImageFilterImplement.apply_brightness, + "色彩": ImageFilterImplement.apply_color, + "色调": ImageFilterImplement.apply_to_color, + "缩放": ImageFilterImplement.apply_resize, + "波纹": ImageFilterImplement.apply_wave, + "色键": ImageFilterImplement.apply_color_key, + } + + @classmethod + def get_filter(cls, name: str) -> Optional[callable]: + return cls.filter_map.get(name) \ No newline at end of file