import random from io import BytesIO from PIL import Image, ImageFilter, ImageDraw, ImageStat, ImageFont from PIL import ImageEnhance from PIL import ImageChops from PIL import ImageOps import cv2 from nonebot import logger from konabot.common.path import FONTS_PATH from konabot.plugins.fx_process.color_handle import ColorHandle import math 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: @staticmethod def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.GaussianBlur(radius)) # 马赛克 @staticmethod def apply_mosaic(image: Image.Image, pixel_size: int = 10) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') 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: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.CONTOUR) @staticmethod def apply_sharpen(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.SHARPEN) @staticmethod def apply_edge_enhance(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.EDGE_ENHANCE) @staticmethod def apply_emboss(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.EMBOSS) @staticmethod def apply_find_edges(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.FIND_EDGES) @staticmethod def apply_smooth(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') return image.filter(ImageFilter.SMOOTH) # 反色 @staticmethod def apply_invert(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 转换为 numpy 数组 arr = np.array(image) # 只反转 RGB 通道,保持 Alpha 不变 arr[:, :, :3] = 255 - arr[:, :, :3] return Image.fromarray(arr) # 黑白灰度 @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: if image.mode != 'RGBA': image = image.convert('RGBA') enhancer = ImageEnhance.Contrast(image) return enhancer.enhance(factor) # 亮度 @staticmethod def apply_brightness(image: Image.Image, factor: float = 1.5) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') enhancer = ImageEnhance.Brightness(image) return enhancer.enhance(factor) # 色彩 @staticmethod def apply_color(image: Image.Image, factor: float = 1.5) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') 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') rgb_color = ColorHandle.parse_color(color) # 转换为灰度并获取数组 gray = image.convert('L') lum = np.array(gray, dtype=np.float32) / 255.0 # 归一化到 [0,1] # 获取 alpha alpha = np.array(image.getchannel('A')) target_r = rgb_color[0] * lum target_g = rgb_color[1] * lum target_b = rgb_color[2] * lum # 堆叠通道 result_rgb = np.stack([target_r, target_g, target_b], axis=-1) result_rgb = np.clip(result_rgb, 0, 255).astype(np.uint8) # 创建结果图像 result = np.zeros((image.height, image.width, 4), dtype=np.uint8) result[:, :, :3] = result_rgb result[:, :, 3] = alpha return Image.fromarray(result, 'RGBA') # JPEG 损坏感压缩 @staticmethod def apply_jpeg_damage(image: Image.Image, quality: int = 10) -> Image.Image: quality = max(1, min(95, int(quality))) alpha = None if image.mode in ('RGBA', 'LA') or (image.mode == 'P' and 'transparency' in image.info): rgba_image = image.convert('RGBA') alpha = rgba_image.getchannel('A') rgb_image = Image.new('RGB', rgba_image.size, (255, 255, 255)) rgb_image.paste(rgba_image, mask=alpha) else: rgb_image = image.convert('RGB') output = BytesIO() rgb_image.save(output, format='JPEG', quality=quality, optimize=False) output.seek(0) damaged = Image.open(output).convert('RGB') if alpha is not None: return Image.merge('RGBA', (*damaged.split(), alpha)) return damaged.convert('RGBA') # 缩放 @staticmethod def apply_resize(image: Image.Image, scale: float = 1.5, scale_y = None) -> Image.Image: # scale 可以为负 # 如果 scale 为负,则代表翻转 if scale_y is not None: if float(scale_y) < 0: image = ImageOps.flip(image) scale_y = abs(float(scale_y)) if scale < 0: image = ImageOps.mirror(image) scale = abs(scale) new_size = (int(image.width * scale), int(image.height * float(scale_y))) return image.resize(new_size, Image.Resampling.LANCZOS) if scale < 0: image = ImageOps.mirror(image) image = ImageOps.flip(image) scale = abs(scale) 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: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image) # 创建坐标网格 y_coords, x_coords = np.mgrid[0:height, 0:width] # 计算偏移量(向量化) offset_x = (amplitude * np.sin(2 * np.pi * y_coords / wavelength)).astype(np.int32) offset_y = (amplitude * np.cos(2 * np.pi * x_coords / wavelength)).astype(np.int32) # 计算新坐标 new_x = x_coords + offset_x new_y = y_coords + offset_y # 创建有效坐标掩码 valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height) # 创建结果图像(初始为透明) result = np.zeros((height, width, 4), dtype=np.uint8) # 只复制有效像素 if valid_mask.any(): # 使用花式索引复制像素 result[valid_mask] = arr[new_y[valid_mask], new_x[valid_mask]] return Image.fromarray(result, 'RGBA') 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) arr = np.array(image) # 计算颜色距离(使用平方距离避免 sqrt) target_arr = np.array(target_rgb, dtype=np.int32) diff = arr[:, :, :3] - target_arr distance_sq = np.sum(diff * diff, axis=2) # (r-r0)² + (g-g0)² + (b-b0)² # 创建掩码(距离 <= 容差) mask = distance_sq <= (tolerance * tolerance) # 复制原图,只修改 alpha 通道 result = arr.copy() result[:, :, 3] = np.where(mask, 0, arr[:, :, 3]) # 符合条件的设为透明 return Image.fromarray(result) # 暗角 @staticmethod def apply_vignette(image: Image.Image, radius: float = 1.5) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 转换为 numpy 数组 arr = np.array(image, dtype=np.float32) height, width = arr.shape[:2] # 创建网格 y_coords, x_coords = np.ogrid[:height, :width] # 计算中心距离 center_x = width / 2 center_y = height / 2 max_distance = np.sqrt(center_x**2 + center_y**2) # 向量化距离计算 distances = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2) # 计算暗角因子 factors = 1 - (distances / max_distance) ** radius factors = np.clip(factors, 0, 1) # 应用暗角效果到 RGB 通道 arr[:, :, :3] = arr[:, :, :3] * factors[:, :, np.newaxis] # 转换回 uint8 result = np.clip(arr, 0, 255).astype(np.uint8) return Image.fromarray(result) # 发光 @staticmethod def apply_glow(image: Image.Image, intensity: float = 0.5, blur_radius: float = 15) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 创建发光图层 glow_layer = image.filter(ImageFilter.GaussianBlur(blur_radius)) # 增强亮度 enhancer = ImageEnhance.Brightness(glow_layer) glow_layer = enhancer.enhance(intensity) # 转换为 numpy 数组 img_arr = np.array(image, dtype=np.float32) # 使用 float32 避免溢出 glow_arr = np.array(glow_layer, dtype=np.float32) # 向量化合并(只合并 RGB,A 保持不变) result_arr = np.zeros_like(img_arr, dtype=np.float32) # RGB 通道相加并限制到 255 result_arr[:, :, :3] = np.clip(img_arr[:, :, :3] + glow_arr[:, :, :3], 0, 255) # Alpha 通道保持原图 result_arr[:, :, 3] = img_arr[:, :, 3] return Image.fromarray(result_arr.astype(np.uint8)) # RGB分离 @staticmethod def apply_rgb_split(image: Image.Image, offset: int = 5) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') r, g, b, a = image.split() r_offset = r.transform(r.size, Image.AFFINE, (1, 0, offset, 0, 1, 0)) g_offset = g.transform(g.size, Image.AFFINE, (1, 0, 0, 0, 1, offset)) return Image.merge('RGBA', (r_offset, g_offset, b, a)) # 光学补偿 @staticmethod def apply_optical_compensation(image: Image.Image, amount: float = 100.0, reverse: bool = False) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image) # 中心点 center_x, center_y = width / 2, height / 2 # 归一化amount amount_norm = amount / 100.0 # 创建坐标网格 y_coords, x_coords = np.mgrid[0:height, 0:width] # 计算相对中心的归一化坐标 dx = (x_coords - center_x) / center_x dy = (y_coords - center_y) / center_y # 计算距离(避免除零) distance = np.sqrt(dx**2 + dy**2) # 创建掩码:中心点和其他点 center_mask = distance == 0 other_mask = ~center_mask # 初始化缩放因子 scale_factor = np.ones_like(distance) if reverse: # 反鱼眼效果 # 对于非中心点 if other_mask.any(): # 使用arcsin进行反鱼眼映射 theta = np.arcsin(np.clip(distance[other_mask], 0, 0.999)) new_distance = np.sin(theta * amount_norm) scale_factor[other_mask] = new_distance / distance[other_mask] else: # 鱼眼效果 if other_mask.any(): # 使用sin或tanh进行鱼眼映射 theta = distance[other_mask] * amount_norm if amount_norm <= 1.0: new_distance = np.sin(theta) else: new_distance = np.tanh(theta) scale_factor[other_mask] = new_distance / distance[other_mask] # 计算源坐标 src_x = center_x + dx * center_x * scale_factor src_y = center_y + dy * center_y * scale_factor # 裁剪坐标到有效范围 src_x = np.clip(src_x, 0, width - 1) src_y = np.clip(src_y, 0, height - 1) # 准备插值 # 获取整数和小数部分 x0 = np.floor(src_x).astype(np.int32) x1 = np.minimum(x0 + 1, width - 1) y0 = np.floor(src_y).astype(np.int32) y1 = np.minimum(y0 + 1, height - 1) fx = src_x - x0 fy = src_y - y0 # 确保索引在范围内 x0 = np.clip(x0, 0, width - 1) x1 = np.clip(x1, 0, width - 1) y0 = np.clip(y0, 0, height - 1) y1 = np.clip(y1, 0, height - 1) # 双线性插值 - 向量化版本 # 获取四个角的像素值 c00 = arr[y0, x0] c01 = arr[y0, x1] c10 = arr[y1, x0] c11 = arr[y1, x1] # 扩展fx, fy用于3D广播 fx_3d = fx[:, :, np.newaxis] fy_3d = fy[:, :, np.newaxis] # 双线性插值公式 top = c00 * (1 - fx_3d) + c01 * fx_3d bottom = c10 * (1 - fx_3d) + c11 * fx_3d result_arr = top * (1 - fy_3d) + bottom * fy_3d # 转换为uint8 result_arr = np.clip(result_arr, 0, 255).astype(np.uint8) return Image.fromarray(result_arr, 'RGBA') # 球面化 @staticmethod def apply_spherize(image: Image.Image, strength: float = 0.5) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image) # 创建坐标网格 y_coords, x_coords = np.mgrid[0:height, 0:width] # 计算中心点 center_x = width / 2 center_y = height / 2 # 计算归一化坐标 norm_x = (x_coords - center_x) / (width / 2) norm_y = (y_coords - center_y) / (height / 2) radius = np.sqrt(norm_x**2 + norm_y**2) # 计算球面化偏移(向量化) factor = 1 + strength * (radius**2) new_x = (norm_x * factor) * (width / 2) + center_x new_y = (norm_y * factor) * (height / 2) + center_y new_x = new_x.astype(np.int32) new_y = new_y.astype(np.int32) # 创建有效坐标掩码 valid_mask = (new_x >= 0) & (new_x < width) & (new_y >= 0) & (new_y < height) # 创建结果图像(初始为透明) result = np.zeros((height, width, 4), dtype=np.uint8) # 只复制有效像素 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') # 平移 @staticmethod def apply_translate(image: Image.Image, x_offset: int = 10, y_offset: int = 10) -> Image.Image: return image.transform(image.size, Image.AFFINE, (1, 0, x_offset, 0, 1, y_offset)) # 拓展边缘 @staticmethod def apply_expand_edges(image: Image.Image, border_size: int = 10) -> Image.Image: # 拓展边缘,填充全透明 return ImageOps.expand(image, border=border_size, fill=(0, 0, 0, 0)) # 旋转 @staticmethod def apply_rotate(image: Image.Image, angle: float = 45) -> Image.Image: return image.rotate(angle, expand=True) # 透视变换 @staticmethod def apply_perspective_transform(image: Image.Image, coeffs: list[float]) -> Image.Image: return image.transform(image.size, Image.PERSPECTIVE, coeffs, Image.Resampling.BICUBIC) # 裁剪 @staticmethod def apply_crop(image: Image.Image, left: float = 0, upper: float = 0, right: float = 100, lower: float = 100) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 按百分比裁剪 width, height = image.size left_px = int(width * left / 100) upper_px = int(height * upper / 100) right_px = int(width * right / 100) lower_px = int(height * lower / 100) # 如果为负数,则扩展边缘 if left_px < 0 or upper_px < 0 or right_px > width or lower_px > height: border_left = max(0, -left_px) border_top = max(0, -upper_px) border_right = max(0, right_px - width) border_bottom = max(0, lower_px - height) image = ImageOps.expand(image, border=(border_left, border_top, border_right, border_bottom), fill=(0,0,0,0)) left_px += border_left upper_px += border_top right_px += border_left lower_px += border_top return image.crop((left_px, upper_px, right_px, lower_px)) # 噪点 @staticmethod def apply_noise(image: Image.Image, amount: float = 0.05) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') arr = np.array(image) noise = np.random.randint(0, 256, arr.shape, dtype=np.uint8) # 为每个像素创建掩码,然后扩展到所有通道,除了 alpha 通道 mask = np.random.rand(*arr.shape[:2]) < amount mask_3d = np.repeat(mask[:, :, np.newaxis], 4, axis=2) mask_3d[:, :, 3] = False # 保持 alpha 通道不变 # 混合噪点 result = np.where(mask_3d, noise, arr) return Image.fromarray(result, 'RGBA') # 素描 @staticmethod def apply_sketch(image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 转为灰度图 gray_image = image.convert('L') # 反相 inverted_image = ImageChops.invert(gray_image) # 高斯模糊 blurred_image = inverted_image.filter(ImageFilter.GaussianBlur(radius=10)) # 混合 def dodge(front, back): result = front * 255 / (255 - back) result[result > 255] = 255 result[back == 255] = 255 return result.astype(np.uint8) gray_arr = np.array(gray_image, dtype=np.float32) blurred_arr = np.array(blurred_image, dtype=np.float32) sketch_arr = dodge(gray_arr, blurred_arr) # 创建结果图像,保留原始 alpha 通道 alpha_channel = np.array(image.getchannel('A')) result_arr = np.zeros((image.height, image.width, 4), dtype=np.uint8) result_arr[:, :, 0] = sketch_arr result_arr[:, :, 1] = sketch_arr result_arr[:, :, 2] = sketch_arr result_arr[:, :, 3] = alpha_channel return Image.fromarray(result_arr, 'RGBA') # 两张图像混合,可指定叠加模式 @staticmethod def apply_blend(image1: Image.Image, image2: Image.Image, mode: str = 'normal', alpha: float = 0.5) -> Image.Image: if image1.mode != 'RGBA': image1 = image1.convert('RGBA') if image2.mode != 'RGBA': image2 = image2.convert('RGBA') image2 = image2.resize(image1.size, Image.Resampling.LANCZOS) arr1 = np.array(image1, dtype=np.float32) arr2 = np.array(image2, dtype=np.float32) if mode == 'normal': blended = arr1 * (1 - alpha) + arr2 * alpha elif mode == 'multiply': blended = (arr1 / 255.0) * (arr2 / 255.0) * 255.0 elif mode == 'screen': blended = 255 - (1 - arr1 / 255.0) * (1 - arr2 / 255.0) * 255.0 elif mode == 'overlay': mask = arr1 < 128 blended = np.zeros_like(arr1) blended[mask] = (2 * (arr1[mask] / 255.0) * (arr2[mask] / 255.0)) * 255.0 blended[~mask] = (1 - 2 * (1 - arr1[~mask] / 255.0) * (1 - arr2[~mask] / 255.0)) * 255.0 else: blended = arr1 blended = np.clip(blended, 0, 255).astype(np.uint8) return Image.fromarray(blended, 'RGBA') # 两张图像直接覆盖 @staticmethod def apply_overlay(image1: Image.Image, image2: Image.Image) -> Image.Image: if image1.mode != 'RGBA': image1 = image1.convert('RGBA') if image2.mode != 'RGBA': image2 = image2.convert('RGBA') image2 = image2.resize(image1.size, Image.Resampling.LANCZOS) return Image.alpha_composite(image1, image2) # 叠加渐变色 @staticmethod def apply_gradient_overlay( image: Image.Image, color_list: str = '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]', overlay_mode: str = 'overlay', ) -> Image.Image: gradient_gen = GradientGenerator() color_nodes = gradient_gen.parse_color_list(color_list) gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes) return ImageFilterImplement.apply_blend(image, gradient, mode=overlay_mode, alpha=0.5) # 生成颜色,类似固态层 @staticmethod def generate_solid( image: Image.Image, color_list: str = '[rgb(255,0,0)|(0,0),rgb(0,255,0)|(0,100),rgb(0,0,255)|(50,100)]' ): gradient_gen = GradientGenerator() color_nodes = gradient_gen.parse_color_list(color_list) gradient = gradient_gen.create_gradient(image.size[0], image.size[1], color_nodes) return gradient # 阴影 @staticmethod def apply_shadow(image: Image.Image, x_offset: int = 10, y_offset: int = 10, blur: float = 10, opacity: float = 0.5, shadow_color: str = "black") -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') offset = (-x_offset, -y_offset) # 创建阴影图层 shadow_rgb = ColorHandle.parse_color(shadow_color) logger.debug(f"Shadow color RGB: {shadow_rgb}") shadow = Image.new('RGBA', image.size, (0, 0, 0, 0)) shadow_draw = ImageDraw.Draw(shadow) # 填充阴影颜色 shadow_draw.rectangle([(0, 0), image.size], fill=(shadow_rgb[0], shadow_rgb[1], shadow_rgb[2], 255)) # 应用遮罩 alpha = image.split()[-1].point(lambda p: p * opacity) shadow.putalpha(alpha) # 移动 shadow = shadow.transform( shadow.size, Image.AFFINE, (1, 0, offset[0], 0, 1, offset[1]) ) # 应用模糊 shadow = shadow.filter(ImageFilter.GaussianBlur(blur)) # 将阴影叠加在原图下方 result = ImageFilterImplement.apply_overlay(shadow, image) return result @staticmethod def apply_radial_blur(image: Image.Image, strength: float = 3.0, samples: int = 6) -> Image.Image: """ 快速径向模糊 - 使用预计算网格和向量化 """ if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image, dtype=np.uint8) # 转换为float32并归一化 arr_float = arr.astype(np.float32) / 255.0 # 计算中心点 center_x = width / 2 center_y = height / 2 # 预计算坐标网格(只计算一次) x_indices = np.arange(width, dtype=np.float32) y_indices = np.arange(height, dtype=np.float32) x_grid, y_grid = np.meshgrid(x_indices, y_indices) # 预计算相对坐标和距离 dx = x_grid - center_x dy = y_grid - center_y distance = np.sqrt(dx*dx + dy*dy + 1e-6) # 避免除零 max_dist = np.max(distance) # 生成采样强度 if samples > 1: strengths = np.linspace(-strength/2, strength/2, samples) / 100 else: strengths = np.array([0]) # 初始化结果 result = np.zeros_like(arr_float) for s in strengths: # 计算缩放因子 scale = 1.0 + s * distance / max_dist # 计算变形坐标 new_x = np.clip(center_x + dx * scale, 0, width - 1) new_y = np.clip(center_y + dy * scale, 0, height - 1) # 快速双线性插值 x0 = np.floor(new_x).astype(np.int32) x1 = np.minimum(x0 + 1, width - 1) y0 = np.floor(new_y).astype(np.int32) y1 = np.minimum(y0 + 1, height - 1) # 计算权重 wx = new_x - x0 wy = new_y - y0 w00 = (1 - wx) * (1 - wy) w01 = wx * (1 - wy) w10 = (1 - wx) * wy w11 = wx * wy # 向量化插值所有通道 for c in range(4): result[:, :, c] += ( arr_float[y0, x0, c] * w00 + arr_float[y0, x1, c] * w01 + arr_float[y1, x0, c] * w10 + arr_float[y1, x1, c] * w11 ) # 平均并转换 result /= len(strengths) result = np.clip(result * 255, 0, 255).astype(np.uint8) return Image.fromarray(result, 'RGBA') @staticmethod def apply_spin_blur(image: Image.Image, strength: float = 30.0, samples: int = 6) -> Image.Image: """ 快速旋转模糊 """ if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image, dtype=np.uint8) arr_float = arr.astype(np.float32) / 255.0 # 计算中心点 center_x = width / 2 center_y = height / 2 # 预计算坐标网格 x_indices = np.arange(width, dtype=np.float32) y_indices = np.arange(height, dtype=np.float32) x_grid, y_grid = np.meshgrid(x_indices, y_indices) # 预计算相对坐标 dx = x_grid - center_x dy = y_grid - center_y # 生成角度采样 if samples > 1: angles = np.linspace(-strength/2, strength/2, samples) * np.pi / 180 else: angles = np.array([0]) result = np.zeros_like(arr_float) for angle in angles: # 预计算三角函数值 cos_a = math.cos(angle) sin_a = math.sin(angle) # 计算旋转坐标 new_x = center_x + dx * cos_a - dy * sin_a new_y = center_y + dx * sin_a + dy * cos_a # 边界裁剪 new_x = np.clip(new_x, 0, width - 1) new_y = np.clip(new_y, 0, height - 1) # 快速双线性插值 x0 = np.floor(new_x).astype(np.int32) x1 = np.minimum(x0 + 1, width - 1) y0 = np.floor(new_y).astype(np.int32) y1 = np.minimum(y0 + 1, height - 1) wx = new_x - x0 wy = new_y - y0 w00 = (1 - wx) * (1 - wy) w01 = wx * (1 - wy) w10 = (1 - wx) * wy w11 = wx * wy # 向量化插值 for c in range(4): result[:, :, c] += ( arr_float[y0, x0, c] * w00 + arr_float[y0, x1, c] * w01 + arr_float[y1, x0, c] * w10 + arr_float[y1, x1, c] * w11 ) result /= len(angles) result = np.clip(result * 255, 0, 255).astype(np.uint8) return Image.fromarray(result, 'RGBA') @staticmethod def apply_directional_blur(image: Image.Image, angle: float = 0.0, distance: int = 20, samples: int = 6) -> Image.Image: """ 快速方向模糊 - 使用累积缓冲区技术 """ if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image, dtype=np.uint8) arr_float = arr.astype(np.float32) / 255.0 # 计算角度和步长 rad = math.radians(angle) cos_a = math.cos(rad) sin_a = math.sin(rad) # 生成偏移位置 if samples > 1: offsets = np.linspace(-distance/2, distance/2, samples) else: offsets = np.array([0]) result = np.zeros_like(arr_float) for offset in offsets: # 计算偏移量 shift_x = offset * cos_a shift_y = offset * sin_a # 预计算变形坐标 x_indices = np.arange(width, dtype=np.float32) y_indices = np.arange(height, dtype=np.float32) x_grid, y_grid = np.meshgrid(x_indices, y_indices) new_x = x_grid - shift_x new_y = y_grid - shift_y # 边界裁剪 new_x = np.clip(new_x, 0, width - 1) new_y = np.clip(new_y, 0, height - 1) # 快速双线性插值 x0 = np.floor(new_x).astype(np.int32) x1 = np.minimum(x0 + 1, width - 1) y0 = np.floor(new_y).astype(np.int32) y1 = np.minimum(y0 + 1, height - 1) wx = new_x - x0 wy = new_y - y0 w00 = (1 - wx) * (1 - wy) w01 = wx * (1 - wy) w10 = (1 - wx) * wy w11 = wx * wy # 向量化插值 for c in range(4): result[:, :, c] += ( arr_float[y0, x0, c] * w00 + arr_float[y0, x1, c] * w01 + arr_float[y1, x0, c] * w10 + arr_float[y1, x1, c] * w11 ) result /= len(offsets) result = np.clip(result * 255, 0, 255).astype(np.uint8) return Image.fromarray(result, 'RGBA') @staticmethod def apply_zoom_blur(image: Image.Image, strength: float = 0.1, samples: int = 6) -> Image.Image: """ 快速缩放模糊 """ if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image, dtype=np.uint8) arr_float = arr.astype(np.float32) / 255.0 # 计算中心点 center_x = width / 2 center_y = height / 2 # 预计算坐标网格 x_indices = np.arange(width, dtype=np.float32) y_indices = np.arange(height, dtype=np.float32) x_grid, y_grid = np.meshgrid(x_indices, y_indices) # 预计算相对坐标 dx = x_grid - center_x dy = y_grid - center_y # 生成缩放因子 if samples > 1: scales = np.linspace(1 - strength, 1 + strength, samples) else: scales = np.array([1.0]) result = np.zeros_like(arr_float) for scale in scales: # 计算缩放坐标 new_x = center_x + dx / scale new_y = center_y + dy / scale # 边界裁剪 new_x = np.clip(new_x, 0, width - 1) new_y = np.clip(new_y, 0, height - 1) # 快速双线性插值 x0 = np.floor(new_x).astype(np.int32) x1 = np.minimum(x0 + 1, width - 1) y0 = np.floor(new_y).astype(np.int32) y1 = np.minimum(y0 + 1, height - 1) wx = new_x - x0 wy = new_y - y0 w00 = (1 - wx) * (1 - wy) w01 = wx * (1 - wy) w10 = (1 - wx) * wy w11 = wx * wy # 向量化插值 for c in range(4): result[:, :, c] += ( arr_float[y0, x0, c] * w00 + arr_float[y0, x1, c] * w01 + arr_float[y1, x0, c] * w10 + arr_float[y1, x1, c] * w11 ) result /= len(scales) result = np.clip(result * 255, 0, 255).astype(np.uint8) return Image.fromarray(result, 'RGBA') # 中心清晰,边缘模糊 @staticmethod def apply_focus_blur(image: Image.Image, radius: float = 10.0) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image) # 创建坐标网格 y_coords, x_coords = np.mgrid[0:height, 0:width] # 计算中心点 center_x = width / 2 center_y = height / 2 # 计算归一化坐标 norm_x = (x_coords - center_x) / (width / 2) norm_y = (y_coords - center_y) / (height / 2) distance = np.sqrt(norm_x**2 + norm_y**2) # 创建模糊图像 blurred_image = image.filter(ImageFilter.GaussianBlur(radius)) blurred_arr = np.array(blurred_image) # 创建结果图像(向量化) result = np.zeros_like(arr, dtype=np.float32) # 根据距离混合原图和模糊图 for c in range(4): # 对每个通道 result[:, :, c] = arr[:, :, c] * (1 - distance) + blurred_arr[:, :, c] * distance return Image.fromarray(np.clip(result, 0, 255).astype(np.uint8), 'RGBA') # 复制画面 @staticmethod def copy_area(image: Image.Image, dst_move: str = '(100,100)', scale: float = 1.0, src_area: str = '(0,0,100,100)') -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') result = image.copy() # 参数表示的是百分比 width, height = image.size # 首先裁剪出源区域 src_tuple = eval(src_area) src_area = ImageFilterImplement.apply_crop(image, src_tuple[0], src_tuple[1], src_tuple[2], src_tuple[3]) # 计算目标位置 dst_tuple = eval(dst_move) dst_x = int(width * dst_tuple[0] / 100) dst_y = int(height * dst_tuple[1] / 100) # 缩放源区域 if scale != 1.0: new_width = int(src_area.width * scale) new_height = int(src_area.height * scale) src_area = src_area.resize((new_width, new_height), Image.Resampling.LANCZOS) # 粘贴到目标位置 result.paste(src_area, (dst_x, dst_y), src_area) return result # 水平翻转 @staticmethod def apply_flip_horizontal(image: Image.Image) -> Image.Image: return image.transpose(Image.Transpose.FLIP_LEFT_RIGHT) # 垂直翻转 @staticmethod def apply_flip_vertical(image: Image.Image) -> Image.Image: return image.transpose(Image.Transpose.FLIP_TOP_BOTTOM) # 依方向镜像一半画面 @staticmethod def apply_mirror_half(image: Image.Image, angle: float = 90) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image, dtype=np.uint8) # 角度标准化 angle = angle % 360 # 计算折痕的斜率 # 注意:我们的角度是折痕与水平线的夹角 # 0° = 垂直线,90° = 水平线 rad = math.radians(angle) # 计算中心点 center_x = width / 2 center_y = height / 2 # 创建坐标网格 y_coords, x_coords = np.mgrid[0:height, 0:width] # 计算折痕的方向向量 # 折痕方向垂直于法向量 # 对于角度θ,折痕方向向量为(cosθ, sinθ) direction_x = math.cos(rad) direction_y = math.sin(rad) # 计算法向量(垂直于折痕) # 旋转90度:(-sinθ, cosθ) normal_x = -direction_y normal_y = direction_x # 计算每个像素相对于折痕的位置 # 点到直线的有向距离: (x-cx)*nx + (y-cy)*ny rel_x = x_coords - center_x rel_y = y_coords - center_y signed_distance = rel_x * normal_x + rel_y * normal_y source_mask = signed_distance >= 0 # 创建结果图像 result = arr.copy() # 获取镜像侧的像素(需要被替换的像素) mirror_mask = ~source_mask if mirror_mask.any(): # 对于镜像侧的每个像素,计算其关于折痕的镜像点 mirror_x = x_coords[mirror_mask] mirror_y = y_coords[mirror_mask] # 计算相对坐标 mirror_rel_x = mirror_x - center_x mirror_rel_y = mirror_y - center_y # 计算到折痕的距离(带符号) dist_to_line = signed_distance[mirror_mask] # 计算镜像点的相对坐标 # 镜像公式: p' = p - 2 * (p·n) * n # 其中p是相对中心点的向量,n是单位法向量 mirror_target_rel_x = mirror_rel_x - 2 * dist_to_line * normal_x mirror_target_rel_y = mirror_rel_y - 2 * dist_to_line * normal_y # 计算镜像点的绝对坐标 target_x = np.round(center_x + mirror_target_rel_x).astype(int) target_y = np.round(center_y + mirror_target_rel_y).astype(int) # 确保坐标在图像范围内 valid = (target_x >= 0) & (target_x < width) & (target_y >= 0) & (target_y < height) if valid.any(): # 将源侧的像素复制到镜像侧的位置 valid_target_x = target_x[valid] valid_target_y = target_y[valid] valid_mirror_x = mirror_x[valid] valid_mirror_y = mirror_y[valid] # 获取源侧的像素值 source_pixels = arr[valid_target_y, valid_target_x] # 复制到镜像位置 result[valid_mirror_y, valid_mirror_x] = source_pixels return Image.fromarray(result, 'RGBA') # 随机画面整体晃动,对于动图会很有效 @staticmethod def apply_random_wiggle(image: Image.Image, max_offset: int = 5, motion_blur = False) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') # 生成随机偏移 x_offset = random.randint(-max_offset, max_offset) y_offset = random.randint(-max_offset, max_offset) # 画面平移 translated_image = ImageFilterImplement.apply_translate(image, x_offset, y_offset) if motion_blur: # 添加运动模糊 translated_image = ImageFilterImplement.apply_directional_blur(translated_image, angle=random.uniform(0, 360), distance=max_offset, samples=6) return translated_image # 像素随机抖动 @staticmethod def apply_pixel_jitter(image: Image.Image, max_offset: int = 2) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') width, height = image.size arr = np.array(image) # 创建结果图像 result = np.zeros_like(arr) # numpy 向量化随机偏移 x_indices = np.arange(width) y_indices = np.arange(height) x_grid, y_grid = np.meshgrid(x_indices, y_indices) x_offsets = np.random.randint(-max_offset, max_offset + 1, size=(height, width)) y_offsets = np.random.randint(-max_offset, max_offset + 1, size=(height, width)) new_x = np.clip(x_grid + x_offsets, 0, width - 1) new_y = np.clip(y_grid + y_offsets, 0, height - 1) result[y_grid, x_grid] = arr[new_y, new_x] 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_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') 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 ) # 减少轮廓点数,以实现尖角效果 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) # 创建描边图像 stroke_img = np.zeros_like(img) # 如果没有轮廓,直接返回原图 if not expanded_contours[0].any(): return image 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: 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 # 设置通道 @staticmethod def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'A') -> 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') # 设置遮罩 def apply_set_mask(image: Image.Image, mask_image: Image.Image) -> Image.Image: if image.mode != 'RGBA': image = image.convert('RGBA') if mask_image.mode != 'L': mask_image = mask_image.convert('L') # 应用遮罩 image.putalpha(mask_image.resize(image.size, Image.Resampling.LANCZOS)) return image @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 class ImageFilterEmpty: # 空滤镜,不做任何处理,形式化参数用 @staticmethod def empty_filter(image): return image @staticmethod def empty_filter_param(image, param = 10): return image class ImageFilterStorage: # 用于暂存图像,存储在第一项的后面 @staticmethod def temp_store_image(image: Image.Image, images: list[Image.Image]) -> Image.Image: images.insert(1, image.copy()) return image # 用于交换移动索引 @staticmethod def swap_image_index(image: Image.Image, images: list[Image.Image], src: int = 2, dest: int = 1) -> Image.Image: if len(images) == 0: return image # 将二者交换 src -= 1 dest -= 1 if 0 <= src < len(images) and 0 <= dest < len(images): images[src], images[dest] = images[dest], images[src] return images[0] # 用于删除指定索引的图像 @staticmethod def delete_image_by_index(image: Image.Image, images: list[Image.Image], index: int = 1) -> Image.Image: # 以 1 为基准 index -= 1 if len(images) == 1: # 只有一张图像,不能删除 return image if 0 <= index < len(images): del images[index] return images[0] # 用于选择指定索引的图像为首图 @staticmethod def select_image_by_index(image: Image.Image, images: list[Image.Image], index: int = 2) -> Image.Image: # 以 1 为基准 index -= 1 if 0 <= index < len(images): return images[index] return image # 用于存储图像 @staticmethod def store_image(image: Image.Image, name: str, sender_info: SenderInfo) -> StoredInfo: ImageStorager.save_image_by_pil(image, name, sender_info.group_id, sender_info.qq_id) return StoredInfo(name=name) # 用于读取图像 @staticmethod def load_image(image: Image.Image, name: str, images: list[Image.Image], sender_info: SenderInfo) -> Image.Image: loaded_image = ImageStorager.load_image(name, sender_info.group_id, sender_info.qq_id) if loaded_image is not None: images.append(loaded_image) return image