Files
konabot/konabot/plugins/fx_process/fx_handle.py

1417 lines
51 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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)
# 向量化合并(只合并 RGBA 保持不变)
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