From 9148073095cae239b93c41f60a62d64e0b26822e Mon Sep 17 00:00:00 2001 From: MixBadGun <1059129006@qq.com> Date: Wed, 10 Dec 2025 22:45:33 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E5=BD=A2=E7=8A=B6=E6=8F=8F?= =?UTF-8?q?=E8=BE=B9=EF=BC=8C=E6=96=B0=E5=A2=9E=E6=96=87=E6=9C=AC=E5=9B=BE?= =?UTF-8?q?=E5=B1=82=E3=80=81=E7=A9=BA=E7=99=BD=E5=9B=BE=E5=B1=82=E7=94=9F?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/docs/user/fx.txt | 11 ++++- konabot/plugins/fx_process/__init__.py | 11 ++++- konabot/plugins/fx_process/fx_handle.py | 44 ++++++++++++++++--- konabot/plugins/fx_process/fx_manager.py | 16 +++++-- konabot/plugins/fx_process/math_helper.py | 52 +++++++++++++++-------- 5 files changed, 106 insertions(+), 28 deletions(-) diff --git a/konabot/docs/user/fx.txt b/konabot/docs/user/fx.txt index 397cd52..675e379 100644 --- a/konabot/docs/user/fx.txt +++ b/konabot/docs/user/fx.txt @@ -53,6 +53,7 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx 像素抖动 <最大偏移量=2>``` * ```fx 半调 <半径=5>``` * ```fx 描边 <半径=5> <颜色=black>``` +* ```fx 形状描边 <半径=5> <颜色=black> <粗糙度=None>``` ### 几何变换滤镜 * ```fx 平移 ``` @@ -70,6 +71,8 @@ fx [滤镜名称] <参数1> <参数2> ... * ```fx 复制 <目标位置=(100,100)> <缩放=1.0> <源区域=(0,0,100,100)>(百分比)``` ### 特殊效果滤镜 +* ```fx 设置通道 <通道=A>``` + * 可用 R、G、B、A。 * ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>``` * ```fx 晃动 <最大偏移量=5> <运动模糊=False>``` * ```fx 动图 <帧率=10>``` @@ -77,7 +80,9 @@ fx [滤镜名称] <参数1> <参数2> ... ### 多图像处理器 * ```fx 存入图像 <目标名称>``` * 目标名称是图像的代名词,图像最长可存 12 小时,如果公用容量满了图像也会被删除。 + * 该项仅可于首项使用。 * ```fx 读取图像 <目标名称>``` + * 该项仅可于首项使用。 * ```fx 暂存图像``` * 此项默认插入存储在暂存列表中第一张图像的后面。 * ```fx 交换图像 <交换项=2> <交换项=1>``` @@ -87,7 +92,11 @@ fx [滤镜名称] <参数1> <参数2> ... ### 多图像混合 * ```fx 混合图像 <模式=normal> ``` * ```fx 覆盖图像``` -* ```fx 生成颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]>``` + +### 生成类 +* ```fx 覆加颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]>``` +* ```fx 生成图层 <宽度=512> <高度=512>``` +* ```fx 生成文本 <文本内容=请输入文本> <字体大小=32> <文字颜色=black> <字体文件=HarmonyOS_Sans_SC_Regular.ttf>``` ## 颜色名称支持 - **格式**:颜色列表采用 ```[颜色|位置+颜色|位置+颜色|位置]``` 的格式,位置是形如```(x百分比,y百分比)```的元组。 diff --git a/konabot/plugins/fx_process/__init__.py b/konabot/plugins/fx_process/__init__.py index ff3bf33..a3dff9b 100644 --- a/konabot/plugins/fx_process/__init__.py +++ b/konabot/plugins/fx_process/__init__.py @@ -131,6 +131,14 @@ def copy_images_by_index(images: list[Image.Image], index: int) -> list[Image.Im return new_images +def generate_image(images: list[Image.Image], filters: list[FilterItem]) -> Image.Image: + # 处理位于最前面的生成类滤镜 + while filters and filters[0].name.strip() in ImageFilterManager.generate_filter_map: + gen_filter = filters.pop(0) + gen_func = gen_filter.filter + func_args = gen_filter.args[1:] # 去掉第一个 list 参数 + gen_func(None, images, *func_args) + def save_or_load_image(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> StoredInfo | None: stored_info = None # 处理位于最前面的“读取图像”、“存入图像” @@ -150,8 +158,9 @@ def save_or_load_image(images: list[Image.Image], filters: list[FilterItem], sen return stored_info async def apply_filters_to_images(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> BytesIO | StoredInfo: - # 先处理存取图像的操作 + # 先处理存取图像、生成图像的操作 stored_info = save_or_load_image(images, filters, sender_info) + generate_image(images, filters) if stored_info and len(filters) <= 0: return stored_info diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index bad582a..10d7c8a 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -1,10 +1,11 @@ import random -from PIL import Image, ImageFilter, ImageDraw, ImageStat +from PIL import Image, ImageFilter, ImageDraw, ImageStat, ImageFont from PIL import ImageEnhance from PIL import ImageChops from PIL import ImageOps import cv2 +from konabot.common.path import FONTS_PATH from konabot.plugins.fx_process.color_handle import ColorHandle import math @@ -1130,7 +1131,7 @@ class ImageFilterImplement: # 基于形状的描边 @staticmethod - def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image: + def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black', roughness: float = None) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') @@ -1149,9 +1150,10 @@ class ImageFilterImplement: cv2.CHAIN_APPROX_SIMPLE ) - # # 减少轮廓点数,以实现尖角效果 - # epsilon = 0.01 * cv2.arcLength(contours[0], True) - # contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours] + # 减少轮廓点数,以实现尖角效果 + if roughness is not None: + epsilon = roughness * cv2.arcLength(contours[0], True) + contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours] # 将轮廓点沿法线方向外扩 expanded_contours = expand_contours(contours, stroke_width) @@ -1230,7 +1232,7 @@ class ImageFilterImplement: # 设置通道 @staticmethod - def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'R', value: int = 255) -> Image.Image: + def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'A') -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') @@ -1244,6 +1246,36 @@ class ImageFilterImplement: image_arr[:, :, channel_index] = apply_arr[:, :, channel_index] return Image.fromarray(image_arr, 'RGBA') + + @staticmethod + def generate_empty(image: Image.Image, images: list[Image.Image], width: int = 512, height: int = 512) -> Image.Image: + # 生成空白图像 + empty_image = Image.new('RGBA', (width, height), (255, 255, 255, 0)) + images.append(empty_image) + return image + + @staticmethod + def generate_text(image: Image.Image, images: list[Image.Image], + text: str = "请输入文本", + font_size: int = 32, + font_color: str = "black", + font_path: str = "HarmonyOS_Sans_SC_Regular.ttf") -> Image.Image: + # 生成文本图像 + font = ImageFont.truetype(FONTS_PATH / font_path, font_size) + # 获取文本边界框 + padding = 10 + temp_draw = ImageDraw.Draw(Image.new('RGBA', (1,1))) + bbox = temp_draw.textbbox((0, 0), text, font=font) + text_width = bbox[2] - bbox[0] + padding * 2 + text_height = bbox[3] - bbox[1] + padding * 2 + # 创建文本图像 + text_image = Image.new('RGBA', (text_width, text_height), (255, 255, 255, 0)) + draw = ImageDraw.Draw(text_image) + draw_x = padding - bbox[0] + draw_y = padding - bbox[1] + draw.text((draw_x,draw_y), text, font=font, fill=ColorHandle.parse_color(font_color) + (255,)) + images.append(text_image) + return image diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index 7075a42..44e56bd 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -62,15 +62,25 @@ class ImageFilterManager: "混合图像": ImageFilterImplement.apply_blend, "覆盖图像": ImageFilterImplement.apply_overlay, # 生成式 - "生成颜色": ImageFilterImplement.generate_solid + "覆加颜色": ImageFilterImplement.generate_solid + } + + generate_filter_map = { + "生成图层": ImageFilterImplement.generate_empty, + "生成文本": ImageFilterImplement.generate_text } @classmethod def get_filter(cls, name: str) -> Optional[callable]: - return cls.filter_map.get(name) + if name in cls.filter_map: + return cls.filter_map[name] + elif name in cls.generate_filter_map: + return cls.generate_filter_map[name] + else: + return None @classmethod def has_filter(cls, name: str) -> bool: - return name in cls.filter_map + return name in cls.filter_map or name in cls.generate_filter_map \ No newline at end of file diff --git a/konabot/plugins/fx_process/math_helper.py b/konabot/plugins/fx_process/math_helper.py index 4e8d955..ca9521c 100644 --- a/konabot/plugins/fx_process/math_helper.py +++ b/konabot/plugins/fx_process/math_helper.py @@ -5,24 +5,44 @@ import numpy as np from shapely.geometry import Polygon from shapely.ops import unary_union -def fix_with_shapely(contour: list) -> np.ndarray: +def fix_with_shapely(contours: list) -> np.ndarray: """ 使用Shapely库处理复杂自相交 """ - # 转换输入为正确的格式 - contour_array = np.array(contour, dtype=np.int32).reshape(-1, 2) - # 转换为Shapely多边形 - polygon = Polygon(contour_array) - - # 修复自相交 - fixed_polygon = polygon.buffer(0) - - # 如果修复后是多部分,取最大的部分 - if fixed_polygon.geom_type == 'MultiPolygon': - fixed_polygon = max(fixed_polygon.geoms, key=lambda p: p.area) - logger.debug(f"轮廓修复后为多部分,取{fixed_polygon.area}面积最大的部分") - fixed_points = np.array(fixed_polygon.exterior.coords, dtype=np.float32) - return fixed_points.reshape(-1, 1, 2) + fixed_results = [] + for contour in contours: + # 转换输入为正确的格式 + contour_array = contour.reshape(-1, 2) + # 转换为Shapely多边形 + polygon = Polygon(contour_array) + + # 修复自相交 + if not polygon.is_valid: + polygon = polygon.buffer(0) # 修复无效多边形 + + # 提取修复后的轮廓点 + if polygon.geom_type == 'Polygon': + fixed_points = np.array(polygon.exterior.coords, dtype=np.int32) + elif polygon.geom_type == 'MultiPolygon': + # 处理多个多边形 + largest = max(polygon.geoms, key=lambda p: p.area) + fixed_points = np.array(largest.exterior.coords, dtype=np.int32) + + fixed_results.append(fixed_points.reshape(-1, 1, 2)) + # 接下来把所有轮廓合并为一个 + if len(fixed_results) > 1: + merged_polygon = unary_union([Polygon(cnt.reshape(-1, 2)) for cnt in fixed_results]) + if merged_polygon.geom_type == 'Polygon': + merged_points = np.array(merged_polygon.exterior.coords, dtype=np.int32) + elif merged_polygon.geom_type == 'MultiPolygon': + largest = max(merged_polygon.geoms, key=lambda p: p.area) + merged_points = np.array(largest.exterior.coords, dtype=np.int32) + return [merged_points.reshape(-1, 1, 2)] + elif len(fixed_results) == 1: + return [fixed_results[0]] + else: + logger.warning("No valid contours found after fixing with Shapely.") + return [np.array([], dtype=np.int32).reshape(0, 1, 2)] def expand_contours(contours, stroke_width): """ @@ -115,6 +135,4 @@ def expand_contours(contours, stroke_width): expanded_contours = fix_with_shapely(expanded_contours) - expanded_contours = [cnt.astype(np.int32) for cnt in expanded_contours] - return expanded_contours \ No newline at end of file