完善形状描边,新增文本图层、空白图层生成
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2025-12-10 22:45:33 +08:00
parent ef3404b096
commit 9148073095
5 changed files with 106 additions and 28 deletions

View File

@ -53,6 +53,7 @@ fx [滤镜名称] <参数1> <参数2> ...
* ```fx 像素抖动 <最大偏移量=2>```
* ```fx 半调 <半径=5>```
* ```fx 描边 <半径=5> <颜色=black>```
* ```fx 形状描边 <半径=5> <颜色=black> <粗糙度=None>```
### 几何变换滤镜
* ```fx 平移 <x偏移量=10> <y偏移量=10>```
@ -70,6 +71,8 @@ fx [滤镜名称] <参数1> <参数2> ...
* ```fx 复制 <目标位置=(100,100)> <缩放=1.0> <源区域=(0,0,100,100)>(百分比)```
### 特殊效果滤镜
* ```fx 设置通道 <通道=A>```
* 可用 R、G、B、A。
* ```fx 色键 <目标颜色="rgb(255,0,0)"> <容差=60>```
* ```fx 晃动 <最大偏移量=5> <运动模糊=False>```
* ```fx 动图 <帧率=10>```
@ -77,7 +80,9 @@ fx [滤镜名称] <参数1> <参数2> ...
### 多图像处理器
* ```fx 存入图像 <目标名称>```
* 目标名称是图像的代名词,图像最长可存 12 小时,如果公用容量满了图像也会被删除。
* 该项仅可于首项使用。
* ```fx 读取图像 <目标名称>```
* 该项仅可于首项使用。
* ```fx 暂存图像```
* 此项默认插入存储在暂存列表中第一张图像的后面。
* ```fx 交换图像 <交换项=2> <交换项=1>```
@ -87,7 +92,11 @@ fx [滤镜名称] <参数1> <参数2> ...
### 多图像混合
* ```fx 混合图像 <模式=normal> <alpha=0.5>```
* ```fx 覆盖图像```
* ```fx 生成颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]>```
### 生成类
* ```fx 覆加颜色 <颜色列表=[rgb(255,0,0)|(0,0)+rgb(0,255,0)|(0,100)+rgb(0,0,255)|(50,100)]>```
* ```fx 生成图层 <宽度=512> <高度=512>```
* ```fx 生成文本 <文本内容=请输入文本> <字体大小=32> <文字颜色=black> <字体文件=HarmonyOS_Sans_SC_Regular.ttf>```
## 颜色名称支持
- **格式**:颜色列表采用 ```[颜色|位置+颜色|位置+颜色|位置]``` 的格式,位置是形如```(x百分比,y百分比)```的元组。

View File

@ -131,6 +131,14 @@ def copy_images_by_index(images: list[Image.Image], index: int) -> list[Image.Im
return new_images
def generate_image(images: list[Image.Image], filters: list[FilterItem]) -> Image.Image:
# 处理位于最前面的生成类滤镜
while filters and filters[0].name.strip() in ImageFilterManager.generate_filter_map:
gen_filter = filters.pop(0)
gen_func = gen_filter.filter
func_args = gen_filter.args[1:] # 去掉第一个 list 参数
gen_func(None, images, *func_args)
def save_or_load_image(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> StoredInfo | None:
stored_info = None
# 处理位于最前面的“读取图像”、“存入图像”
@ -150,8 +158,9 @@ def save_or_load_image(images: list[Image.Image], filters: list[FilterItem], sen
return stored_info
async def apply_filters_to_images(images: list[Image.Image], filters: list[FilterItem], sender_info: SenderInfo) -> BytesIO | StoredInfo:
# 先处理存取图像的操作
# 先处理存取图像、生成图像的操作
stored_info = save_or_load_image(images, filters, sender_info)
generate_image(images, filters)
if stored_info and len(filters) <= 0:
return stored_info

View File

@ -1,10 +1,11 @@
import random
from PIL import Image, ImageFilter, ImageDraw, ImageStat
from PIL import Image, ImageFilter, ImageDraw, ImageStat, ImageFont
from PIL import ImageEnhance
from PIL import ImageChops
from PIL import ImageOps
import cv2
from konabot.common.path import FONTS_PATH
from konabot.plugins.fx_process.color_handle import ColorHandle
import math
@ -1130,7 +1131,7 @@ class ImageFilterImplement:
# 基于形状的描边
@staticmethod
def apply_shape_stroke(image: Image.Image, stroke_width: int = 5, stroke_color: str = 'black') -> Image.Image:
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')
@ -1149,9 +1150,10 @@ class ImageFilterImplement:
cv2.CHAIN_APPROX_SIMPLE
)
# # 减少轮廓点数,以实现尖角效果
# epsilon = 0.01 * cv2.arcLength(contours[0], True)
# contours = [cv2.approxPolyDP(cnt, epsilon, True) for cnt in contours]
# 减少轮廓点数,以实现尖角效果
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)
@ -1230,7 +1232,7 @@ class ImageFilterImplement:
# 设置通道
@staticmethod
def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'R', value: int = 255) -> Image.Image:
def apply_set_channel(image: Image.Image, apply_image: Image.Image, channel: str = 'A') -> Image.Image:
if image.mode != 'RGBA':
image = image.convert('RGBA')
@ -1244,6 +1246,36 @@ class ImageFilterImplement:
image_arr[:, :, channel_index] = apply_arr[:, :, channel_index]
return Image.fromarray(image_arr, 'RGBA')
@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

View File

@ -62,15 +62,25 @@ class ImageFilterManager:
"混合图像": ImageFilterImplement.apply_blend,
"覆盖图像": ImageFilterImplement.apply_overlay,
# 生成式
"生成颜色": ImageFilterImplement.generate_solid
"覆加颜色": ImageFilterImplement.generate_solid
}
generate_filter_map = {
"生成图层": ImageFilterImplement.generate_empty,
"生成文本": ImageFilterImplement.generate_text
}
@classmethod
def get_filter(cls, name: str) -> Optional[callable]:
return cls.filter_map.get(name)
if name in cls.filter_map:
return cls.filter_map[name]
elif name in cls.generate_filter_map:
return cls.generate_filter_map[name]
else:
return None
@classmethod
def has_filter(cls, name: str) -> bool:
return name in cls.filter_map
return name in cls.filter_map or name in cls.generate_filter_map

View File

@ -5,24 +5,44 @@ import numpy as np
from shapely.geometry import Polygon
from shapely.ops import unary_union
def fix_with_shapely(contour: list) -> np.ndarray:
def fix_with_shapely(contours: 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)
fixed_results = []
for contour in contours:
# 转换输入为正确的格式
contour_array = contour.reshape(-1, 2)
# 转换为Shapely多边形
polygon = Polygon(contour_array)
# 修复自相交
if not polygon.is_valid:
polygon = polygon.buffer(0) # 修复无效多边形
# 提取修复后的轮廓点
if polygon.geom_type == 'Polygon':
fixed_points = np.array(polygon.exterior.coords, dtype=np.int32)
elif polygon.geom_type == 'MultiPolygon':
# 处理多个多边形
largest = max(polygon.geoms, key=lambda p: p.area)
fixed_points = np.array(largest.exterior.coords, dtype=np.int32)
fixed_results.append(fixed_points.reshape(-1, 1, 2))
# 接下来把所有轮廓合并为一个
if len(fixed_results) > 1:
merged_polygon = unary_union([Polygon(cnt.reshape(-1, 2)) for cnt in fixed_results])
if merged_polygon.geom_type == 'Polygon':
merged_points = np.array(merged_polygon.exterior.coords, dtype=np.int32)
elif merged_polygon.geom_type == 'MultiPolygon':
largest = max(merged_polygon.geoms, key=lambda p: p.area)
merged_points = np.array(largest.exterior.coords, dtype=np.int32)
return [merged_points.reshape(-1, 1, 2)]
elif len(fixed_results) == 1:
return [fixed_results[0]]
else:
logger.warning("No valid contours found after fixing with Shapely.")
return [np.array([], dtype=np.int32).reshape(0, 1, 2)]
def expand_contours(contours, stroke_width):
"""
@ -115,6 +135,4 @@ def expand_contours(contours, stroke_width):
expanded_contours = fix_with_shapely(expanded_contours)
expanded_contours = [cnt.astype(np.int32) for cnt in expanded_contours]
return expanded_contours