From 1d763dfc3cb344431737cf3b04409def06c52a5f Mon Sep 17 00:00:00 2001 From: MixBadGun <1059129006@qq.com> Date: Wed, 10 Dec 2025 19:28:34 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=80=9A=E9=81=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- konabot/plugins/fx_process/fx_handle.py | 59 +++++++++++ konabot/plugins/fx_process/fx_manager.py | 2 + konabot/plugins/fx_process/math_helper.py | 120 ++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 konabot/plugins/fx_process/math_helper.py diff --git a/konabot/plugins/fx_process/fx_handle.py b/konabot/plugins/fx_process/fx_handle.py index 7580387..bad582a 100644 --- a/konabot/plugins/fx_process/fx_handle.py +++ b/konabot/plugins/fx_process/fx_handle.py @@ -3,6 +3,7 @@ from PIL import Image, ImageFilter, ImageDraw, ImageStat from PIL import ImageEnhance from PIL import ImageChops from PIL import ImageOps +import cv2 from konabot.plugins.fx_process.color_handle import ColorHandle @@ -12,6 +13,7 @@ from konabot.plugins.fx_process.gradient import GradientGenerator import numpy as np from konabot.plugins.fx_process.image_storage import ImageStorager +from konabot.plugins.fx_process.math_helper import expand_contours from konabot.plugins.fx_process.types import SenderInfo, StoredInfo class ImageFilterImplement: @@ -1126,6 +1128,45 @@ class ImageFilterImplement: result = Image.alpha_composite(stroke_image, image) return result + # 基于形状的描边 + @staticmethod + def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image: + if image.mode != 'RGBA': + image = image.convert('RGBA') + + img = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA) + + # 提取alpha通道 + alpha = img[:, :, 3] + + # 应用阈值创建二值掩码 + _, binary_mask = cv2.threshold(alpha, 0.5, 255, cv2.THRESH_BINARY) + + # 寻找轮廓 + contours, hierarchy = cv2.findContours( + binary_mask, + cv2.RETR_EXTERNAL, + cv2.CHAIN_APPROX_SIMPLE + ) + + # # 减少轮廓点数,以实现尖角效果 + # epsilon = 0.01 * cv2.arcLength(contours[0], True) + # contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours] + + # 将轮廓点沿法线方向外扩 + expanded_contours = expand_contours(contours, stroke_width) + + # 创建描边图像 + stroke_img = np.zeros_like(img) + cv2.fillPoly(stroke_img, expanded_contours, ColorHandle.parse_color(stroke_color) + (255,)) + + # 轮廓图像转为PIL格式 + stroke_pil = Image.fromarray(cv2.cvtColor(stroke_img, cv2.COLOR_BGRA2RGBA)) + # 合并描边和原图 + result = Image.alpha_composite(stroke_pil, image) + + return result + # 半调 @staticmethod def apply_halftone(image: Image.Image, dot_size: int = 5) -> Image.Image: @@ -1186,6 +1227,24 @@ class ImageFilterImplement: output.putalpha(alpha) return output + + # 设置通道 + @staticmethod + def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'R', value: int = 255) -> Image.Image: + if image.mode != 'RGBA': + image = image.convert('RGBA') + + if apply_image.mode != 'RGBA': + apply_image = apply_image.convert('RGBA') + + # 将 apply_image 的通道设置给 image + image_arr = np.array(image) + apply_arr = np.array(apply_image.resize(image.size, Image.Resampling.LANCZOS)) + channel_index = {'R':0, 'G':1, 'B':2, 'A':3}.get(channel.upper(), 0) + image_arr[:, :, channel_index] = apply_arr[:, :, channel_index] + + return Image.fromarray(image_arr, 'RGBA') + class ImageFilterEmpty: diff --git a/konabot/plugins/fx_process/fx_manager.py b/konabot/plugins/fx_process/fx_manager.py index 176a1d3..7075a42 100644 --- a/konabot/plugins/fx_process/fx_manager.py +++ b/konabot/plugins/fx_process/fx_manager.py @@ -48,7 +48,9 @@ class ImageFilterManager: "动图": ImageFilterEmpty.empty_filter_param, "像素抖动": ImageFilterImplement.apply_pixel_jitter, "描边": ImageFilterImplement.apply_stroke, + "形状描边": ImageFilterImplement.apply_shape_stroke, "半调": ImageFilterImplement.apply_halftone, + "设置通道": ImageFilterImplement.apply_set_channel, # 图像处理 "存入图像": ImageFilterStorage.store_image, "读取图像": ImageFilterStorage.load_image, diff --git a/konabot/plugins/fx_process/math_helper.py b/konabot/plugins/fx_process/math_helper.py new file mode 100644 index 0000000..4e8d955 --- /dev/null +++ b/konabot/plugins/fx_process/math_helper.py @@ -0,0 +1,120 @@ +import cv2 +from nonebot import logger +import numpy as np + +from shapely.geometry import Polygon +from shapely.ops import unary_union + +def fix_with_shapely(contour: 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) + +def expand_contours(contours, stroke_width): + """ + 将轮廓向外扩展指定宽度 + + 参数: + contours: OpenCV轮廓列表 + stroke_width: 扩展宽度(像素) + + 返回: + 扩展后的轮廓列表 + """ + expanded_contours = [] + + for cnt in contours: + # 将轮廓转换为点列表 + points = cnt.reshape(-1, 2).astype(np.float32) + n = len(points) + + if n < 3: + continue # 至少需要3个点才能形成多边形 + + expanded_points = [] + + for i in range(n): + # 获取当前点、前一个点和后一个点 + p_curr = points[i] + p_prev = points[(i - 1) % n] + p_next = points[(i + 1) % n] + + # 计算两条边的向量 + v1 = p_curr - p_prev # 前一条边(从prev到curr) + v2 = p_next - p_curr # 后一条边(从curr到next) + + # 归一化 + norm1 = np.linalg.norm(v1) + norm2 = np.linalg.norm(v2) + + if norm1 == 0 or norm2 == 0: + # 如果有零向量,直接沿着法线方向扩展 + edge_dir = np.array([0, 0]) + if norm1 > 0: + edge_dir = v1 / norm1 + elif norm2 > 0: + edge_dir = v2 / norm2 + normal = np.array([-edge_dir[1], edge_dir[0]]) + expanded_point = p_curr + normal * stroke_width + else: + # 归一化向量 + v1_norm = v1 / norm1 + v2_norm = v2 / norm2 + + # 计算两条边的单位法向量(指向多边形外部) + n1 = np.array([-v1_norm[1], v1_norm[0]]) + n2 = np.array([-v2_norm[1], v2_norm[0]]) + + # 计算角平分线方向(两个法向量的和) + bisector = n1 + n2 + + # 计算平分线的长度 + bisector_norm = np.linalg.norm(bisector) + + if bisector_norm == 0: + # 如果两条边平行(同向或反向),取任一法线方向 + expanded_point = p_curr + n1 * stroke_width + else: + # 归一化平分线 + bisector_normalized = bisector / bisector_norm + + # 计算偏移距离(考虑夹角) + # 使用余弦定理计算正确的偏移距离 + cos_angle = np.dot(v1_norm, v2_norm) + angle = np.arccos(np.clip(cos_angle, -1.0, 1.0)) + + if abs(np.pi - angle) < 1e-6: # 近似平角 + # 接近直线的情况 + offset_distance = stroke_width + else: + # 计算正确的偏移距离 + offset_distance = stroke_width / np.sin(angle / 2) + + # 计算扩展点 + expanded_point = p_curr + bisector_normalized * offset_distance + + expanded_points.append(expanded_point) + + # 将扩展后的点转换为整数坐标 + expanded_cnt = np.array(expanded_points, dtype=np.float32).reshape(-1, 1, 2) + expanded_contours.append(expanded_cnt.astype(np.int32)) + + 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