最新最热

This commit is contained in:
2025-12-03 12:25:39 +08:00
parent 40be5ce335
commit f7d2168dac
5 changed files with 936 additions and 79 deletions

View File

@ -1,10 +1,15 @@
from PIL import Image, ImageFilter
from PIL import ImageEnhance
from PIL import ImageChops
from PIL import ImageOps
from konabot.plugins.fx_process.color_handle import ColorHandle
import math
from konabot.plugins.fx_process.gradient import GradientGenerator
import numpy as np
class ImageFilterImplement:
@staticmethod
def apply_blur(image: Image.Image, radius: float = 10) -> Image.Image:
@ -50,14 +55,16 @@ class ImageFilterImplement:
# 反色
@staticmethod
def apply_invert(image: Image.Image) -> Image.Image:
# 确保图像是RGBA模式保留透明度通道
if image.mode != 'RGBA':
image = image.convert('RGBA')
r, g, b, a = image.split()
r = r.point(lambda i: 255 - i)
g = g.point(lambda i: 255 - i)
b = b.point(lambda i: 255 - i)
return Image.merge('RGBA', (r, g, b, a))
# 转换为 numpy 数组
arr = np.array(image)
# 只反转 RGB 通道,保持 Alpha 不变
arr[:, :, :3] = 255 - arr[:, :, :3]
return Image.fromarray(arr)
# 黑白灰度
@staticmethod
@ -103,26 +110,30 @@ class ImageFilterImplement:
def apply_to_color(image: Image.Image, color: str = 'rgb(255,0,0)') -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
# 转为灰度图
gray = image.convert('L')
# 获取目标颜色的RGB值
rgb_color = ColorHandle.parse_color(color)
# 高光默认为白色,阴影默认为黑色
highlight = (255, 255, 255)
shadow = (0, 0, 0)
# 创建新的图像
new_image = Image.new('RGBA', image.size)
width, height = image.size
for x in range(width):
for y in range(height):
lum = gray.getpixel((x, y))
# 计算新颜色
new_r = int((rgb_color[0] * lum + shadow[0] * (highlight[0] - lum)) / 255)
new_g = int((rgb_color[1] * lum + shadow[1] * (highlight[1] - lum)) / 255)
new_b = int((rgb_color[2] * lum + shadow[2] * (highlight[2] - lum)) / 255)
a = image.getpixel((x, y))[3] # 保留原图的透明度
new_image.putpixel((x, y), (new_r, new_g, new_b, a))
return new_image
# 转换为灰度并获取数组
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')
# 缩放
@staticmethod
@ -135,33 +146,424 @@ class ImageFilterImplement:
# 波纹
@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
new_image = Image.new('RGBA', (width, height))
for x in range(width):
for y in range(height):
offset_x = int(amplitude * math.sin(2 * math.pi * y / wavelength))
offset_y = int(amplitude * math.cos(2 * math.pi * x / wavelength))
new_x = x + offset_x
new_y = y + offset_y
if 0 <= new_x < width and 0 <= new_y < height:
new_image.putpixel((x, y), image.getpixel((new_x, new_y)))
else:
new_image.putpixel((x, y), (0, 0, 0, 0)) # 透明像素
return new_image
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 = 1.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
new_image = Image.new('RGBA', (width, height))
for x in range(width):
for y in range(height):
r, g, b, a = image.getpixel((x, y))
# 计算与目标颜色的距离
distance = math.sqrt((r - target_rgb[0]) ** 2 + (g - target_rgb[1]) ** 2 + (b - target_rgb[2]) ** 2)
if distance <= tolerance:
new_image.putpixel((x, y), (r, g, b, 0)) # 设置为透明
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_image.putpixel((x, y), (r, g, b, a)) # 保留原像素
return new_image
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]]
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:
# 按百分比裁剪
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)
# 为每个像素创建掩码,然后扩展到所有通道
mask = np.random.rand(*arr.shape[:2]) < amount
mask_3d = mask[:, :, np.newaxis] # 添加第三个维度
# 混合噪点
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_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 apply_shadow(image: Image.Image,
x_offset: int = 10,
y_offset: int = 10,
blur = 10,
opacity = 0.5,
shadow_color = "black") -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
offset = (x_offset, y_offset)
# 创建阴影图层
shadow = Image.new('RGBA', image.size, (0,0,0,0))
shadow_rgb = ColorHandle.parse_color(shadow_color)
shadow_draw = Image.new('RGBA', image.size, shadow_rgb + (0,))
alpha = image.split()[3].point(lambda p: int(p * opacity))
shadow.paste(shadow_draw, (0,0), alpha)
shadow = shadow.filter(ImageFilter.GaussianBlur(blur))
# 创建结果图像
result = Image.new('RGBA', (image.width + abs(offset[0]), image.height + abs(offset[1])), (0,0,0,0))
shadow_position = (max(offset[0],0), max(offset[1],0))
image_position = (max(-offset[0],0), max(-offset[1],0))
result.paste(shadow, shadow_position, shadow)
result.paste(image, image_position, image)
return result