diff --git a/konabot/docs/user/fx.md b/konabot/docs/user/fx.txt similarity index 97% rename from konabot/docs/user/fx.md rename to konabot/docs/user/fx.txt index 1b389aa..397cd52 100644 --- a/konabot/docs/user/fx.md +++ b/konabot/docs/user/fx.txt @@ -51,6 +51,8 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx RGB分离 <偏移量=5>``` * ```fx 叠加颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]> <叠加模式=overlay>``` * ```fx 像素抖动 <最大偏移量=2>``` +* ```fx 半调 <半径=5>``` +* ```fx 描边 <半径=5> <颜色=black>``` ### 几何变换滤镜 * ```fx 平移 ``` diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index 012e8b3..17e6b66 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -1,5 +1,5 @@ import random -from PIL import Image, ImageFilter +from PIL import Image, ImageFilter, ImageDraw, ImageStat from PIL import ImageEnhance from PIL import ImageChops from PIL import ImageOps @@ -412,6 +412,13 @@ class ImageFilterImplement: if valid_mask.any(): # 使用花式索引复制像素 result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]] + + # 计算裁剪边界 + ys, xs = np.where(valid_mask) + min_y, max_y = ys.min(), ys.max() + 1 + min_x, max_x = xs.min(), xs.max() + 1 + # 裁剪结果图像 + result = result[min_y:max_y, min_x:max_x] return Image.fromarray(result, 'RGBA') @@ -1103,6 +1110,86 @@ class ImageFilterImplement: return Image.fromarray(result, 'RGBA') + # 描边 + @staticmethod + def apply_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image: + if image.mode != 'RGBA': + image = image.convert('RGBA') + + # 基于图像的 alpha 通道创建描边 + alpha = image.split()[3] + # 创建描边图像 + stroke_image = Image.new('RGBA', image.size, (0, 0, 0, 0)) + # 根据 alpha 通道的值,以每个像素为中心,扩大描边区域 + expanded_alpha = alpha.filter(ImageFilter.MaxFilter(size=stroke_width*2+1)) + draw = Image.new('RGBA', image.size, ColorHandle.parse_color(stroke_color) + (255,)) + stroke_image.paste(draw, (0, 0), expanded_alpha) + # 将描边和原图合并 + result = Image.alpha_composite(stroke_image, image) + return result + + # 半调 + @staticmethod + def apply_halftone(image: Image.Image, dot_size: int = 5) -> Image.Image: + if image.mode != 'L': + grayscale = image.convert('L') + else: + grayscale = image.copy() + + # 获取图像尺寸 + width, height = grayscale.size + + # 计算网格数量 + grid_width = math.ceil(width / dot_size) + grid_height = math.ceil(height / dot_size) + + # 创建输出图像(白色背景) + output = Image.new('RGB', (width, height), 'white') + draw = ImageDraw.Draw(output) + + # 遍历每个网格单元 + for gy in range(grid_height): + for gx in range(grid_width): + # 计算当前网格的边界 + left = gx * dot_size + top = gy * dot_size + right = min(left + dot_size, width) + bottom = min(top + dot_size, height) + + # 获取当前网格的区域 + grid_region = grayscale.crop((left, top, right, bottom)) + + # 计算网格内像素的平均亮度(0-255) + stat = ImageStat.Stat(grid_region) + avg_brightness = stat.mean[0] + + # 将亮度转换为网点半径 + # 亮度越高(越白),网点越小;亮度越低(越黑),网点越大 + max_radius = dot_size / 2 + radius = max_radius * (1 - avg_brightness / 255) + + # 如果半径太小,则不绘制网点 + if radius > 0.5: + # 计算网点中心坐标 + center_x = left + (right - left) / 2 + center_y = top + (bottom - top) / 2 + + # 绘制黑色网点 + draw.ellipse([ + center_x - radius + 0.5, + center_y - radius + 0.5, + center_x + radius + 0.5, + center_y + radius + 0.5 + ], fill='black') + + # 叠加 alpha 通道 + if image.mode == 'RGBA': + alpha = image.split()[3] + output.putalpha(alpha) + + return output + + class ImageFilterEmpty: # 空滤镜,不做任何处理,形式化参数用 @staticmethod diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index 3c1d653..176a1d3 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -47,6 +47,8 @@ class ImageFilterManager: "晃动": ImageFilterImplement.apply_random_wiggle, "动图": ImageFilterEmpty.empty_filter_param, "像素抖动": ImageFilterImplement.apply_pixel_jitter, + "描边": ImageFilterImplement.apply_stroke, + "半调": ImageFilterImplement.apply_halftone, # 图像处理 "存入图像": ImageFilterStorage.store_image, "读取图像": ImageFilterStorage.load_image,