fx 完善
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-02 14:45:07 +08:00
parent 8e6131473d
commit 40be5ce335
5 changed files with 313 additions and 25 deletions

51
konabot/docs/user/fx.txt Normal file
View File

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

View File

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

View File

@ -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) # 默认白色

View File

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

View File

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