设置通道

This commit is contained in:
2025-12-10 19:28:34 +08:00
parent a829f035b3
commit 1d763dfc3c
3 changed files with 181 additions and 0 deletions

View File

@ -3,6 +3,7 @@ from PIL import Image, ImageFilter, ImageDraw, ImageStat
from PIL import ImageEnhance from PIL import ImageEnhance
from PIL import ImageChops from PIL import ImageChops
from PIL import ImageOps from PIL import ImageOps
import cv2
from konabot.plugins.fx_process.color_handle import ColorHandle from konabot.plugins.fx_process.color_handle import ColorHandle
@ -12,6 +13,7 @@ from konabot.plugins.fx_process.gradient import GradientGenerator
import numpy as np import numpy as np
from konabot.plugins.fx_process.image_storage import ImageStorager 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 from konabot.plugins.fx_process.types import SenderInfo, StoredInfo
class ImageFilterImplement: class ImageFilterImplement:
@ -1126,6 +1128,45 @@ class ImageFilterImplement:
result = Image.alpha_composite(stroke_image, image) result = Image.alpha_composite(stroke_image, image)
return result return result
# 基于形状的描边
@staticmethod
def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
img = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA)
# 提取alpha通道
alpha = img[:, :, 3]
# 应用阈值创建二值掩码
_, binary_mask = cv2.threshold(alpha, 0.5, 255, cv2.THRESH_BINARY)
# 寻找轮廓
contours, hierarchy = cv2.findContours(
binary_mask,
cv2.RETR_EXTERNAL,
cv2.CHAIN_APPROX_SIMPLE
)
# # 减少轮廓点数,以实现尖角效果
# epsilon = 0.01 * cv2.arcLength(contours[0], True)
# contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours]
# 将轮廓点沿法线方向外扩
expanded_contours = expand_contours(contours, stroke_width)
# 创建描边图像
stroke_img = np.zeros_like(img)
cv2.fillPoly(stroke_img, expanded_contours, ColorHandle.parse_color(stroke_color) + (255,))
# 轮廓图像转为PIL格式
stroke_pil = Image.fromarray(cv2.cvtColor(stroke_img, cv2.COLOR_BGRA2RGBA))
# 合并描边和原图
result = Image.alpha_composite(stroke_pil, image)
return result
# 半调 # 半调
@staticmethod @staticmethod
def apply_halftone(image: Image.Image, dot_size: int = 5) -> Image.Image: def apply_halftone(image: Image.Image, dot_size: int = 5) -> Image.Image:
@ -1186,6 +1227,24 @@ class ImageFilterImplement:
output.putalpha(alpha) output.putalpha(alpha)
return output return output
# 设置通道
@staticmethod
def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'R', value: int = 255) -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
if apply_image.mode != 'RGBA':
apply_image = apply_image.convert('RGBA')
# 将 apply_image 的通道设置给 image
image_arr = np.array(image)
apply_arr = np.array(apply_image.resize(image.size, Image.Resampling.LANCZOS))
channel_index = {'R':0, 'G':1, 'B':2, 'A':3}.get(channel.upper(), 0)
image_arr[:, :, channel_index] = apply_arr[:, :, channel_index]
return Image.fromarray(image_arr, 'RGBA')
class ImageFilterEmpty: class ImageFilterEmpty:

View File

@ -48,7 +48,9 @@ class ImageFilterManager:
"动图": ImageFilterEmpty.empty_filter_param, "动图": ImageFilterEmpty.empty_filter_param,
"像素抖动": ImageFilterImplement.apply_pixel_jitter, "像素抖动": ImageFilterImplement.apply_pixel_jitter,
"描边": ImageFilterImplement.apply_stroke, "描边": ImageFilterImplement.apply_stroke,
"形状描边": ImageFilterImplement.apply_shape_stroke,
"半调": ImageFilterImplement.apply_halftone, "半调": ImageFilterImplement.apply_halftone,
"设置通道": ImageFilterImplement.apply_set_channel,
# 图像处理 # 图像处理
"存入图像": ImageFilterStorage.store_image, "存入图像": ImageFilterStorage.store_image,
"读取图像": ImageFilterStorage.load_image, "读取图像": ImageFilterStorage.load_image,

View File

@ -0,0 +1,120 @@
import cv2
from nonebot import logger
import numpy as np
from shapely.geometry import Polygon
from shapely.ops import unary_union
def fix_with_shapely(contour: list) -> np.ndarray:
"""
使用Shapely库处理复杂自相交
"""
# 转换输入为正确的格式
contour_array = np.array(contour, dtype=np.int32).reshape(-1, 2)
# 转换为Shapely多边形
polygon = Polygon(contour_array)
# 修复自相交
fixed_polygon = polygon.buffer(0)
# 如果修复后是多部分,取最大的部分
if fixed_polygon.geom_type == 'MultiPolygon':
fixed_polygon = max(fixed_polygon.geoms, key=lambda p: p.area)
logger.debug(f"轮廓修复后为多部分,取{fixed_polygon.area}面积最大的部分")
fixed_points = np.array(fixed_polygon.exterior.coords, dtype=np.float32)
return fixed_points.reshape(-1, 1, 2)
def expand_contours(contours, stroke_width):
"""
将轮廓向外扩展指定宽度
参数:
contours: OpenCV轮廓列表
stroke_width: 扩展宽度(像素)
返回:
扩展后的轮廓列表
"""
expanded_contours = []
for cnt in contours:
# 将轮廓转换为点列表
points = cnt.reshape(-1, 2).astype(np.float32)
n = len(points)
if n < 3:
continue # 至少需要3个点才能形成多边形
expanded_points = []
for i in range(n):
# 获取当前点、前一个点和后一个点
p_curr = points[i]
p_prev = points[(i - 1) % n]
p_next = points[(i + 1) % n]
# 计算两条边的向量
v1 = p_curr - p_prev # 前一条边从prev到curr
v2 = p_next - p_curr # 后一条边从curr到next
# 归一化
norm1 = np.linalg.norm(v1)
norm2 = np.linalg.norm(v2)
if norm1 == 0 or norm2 == 0:
# 如果有零向量,直接沿着法线方向扩展
edge_dir = np.array([0, 0])
if norm1 > 0:
edge_dir = v1 / norm1
elif norm2 > 0:
edge_dir = v2 / norm2
normal = np.array([-edge_dir[1], edge_dir[0]])
expanded_point = p_curr + normal * stroke_width
else:
# 归一化向量
v1_norm = v1 / norm1
v2_norm = v2 / norm2
# 计算两条边的单位法向量(指向多边形外部)
n1 = np.array([-v1_norm[1], v1_norm[0]])
n2 = np.array([-v2_norm[1], v2_norm[0]])
# 计算角平分线方向(两个法向量的和)
bisector = n1 + n2
# 计算平分线的长度
bisector_norm = np.linalg.norm(bisector)
if bisector_norm == 0:
# 如果两条边平行(同向或反向),取任一法线方向
expanded_point = p_curr + n1 * stroke_width
else:
# 归一化平分线
bisector_normalized = bisector / bisector_norm
# 计算偏移距离(考虑夹角)
# 使用余弦定理计算正确的偏移距离
cos_angle = np.dot(v1_norm, v2_norm)
angle = np.arccos(np.clip(cos_angle, -1.0, 1.0))
if abs(np.pi - angle) < 1e-6: # 近似平角
# 接近直线的情况
offset_distance = stroke_width
else:
# 计算正确的偏移距离
offset_distance = stroke_width / np.sin(angle / 2)
# 计算扩展点
expanded_point = p_curr + bisector_normalized * offset_distance
expanded_points.append(expanded_point)
# 将扩展后的点转换为整数坐标
expanded_cnt = np.array(expanded_points, dtype=np.float32).reshape(-1, 1, 2)
expanded_contours.append(expanded_cnt.astype(np.int32))
expanded_contours = fix_with_shapely(expanded_contours)
expanded_contours = [cnt.astype(np.int32) for cnt in expanded_contours]
return expanded_contours