1417 lines
51 KiB
Python
1417 lines
51 KiB
Python
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 |