forked from mttu-developers/konabot
197 lines
6.7 KiB
Python
197 lines
6.7 KiB
Python
from io import BytesIO
|
||
|
||
import cv2
|
||
import numpy as np
|
||
from PIL import Image, ImageDraw, ImageFont
|
||
|
||
from konabot.common.path import ASSETS_PATH, FONTS_PATH
|
||
|
||
|
||
def text_to_transparent_image(text, font_size=40, padding=0, text_color=(0, 0, 0)):
|
||
"""
|
||
将文本转换为带透明背景的图像,图像大小刚好包含文本
|
||
"""
|
||
# 创建临时图像来计算文本尺寸
|
||
temp_image = Image.new('RGB', (1, 1), (255, 255, 255))
|
||
temp_draw = ImageDraw.Draw(temp_image)
|
||
|
||
font = ImageFont.truetype(FONTS_PATH / "montserrat.otf", font_size)
|
||
|
||
# 获取文本边界框
|
||
bbox = temp_draw.textbbox((0, 0), text, font=font)
|
||
text_width = bbox[2] - bbox[0]
|
||
text_height = bbox[3] - bbox[1]
|
||
|
||
# 计算图像大小(文本大小 + 内边距)
|
||
image_width = int(text_width + 2 * padding)
|
||
image_height = int(text_height + 2 * padding)
|
||
|
||
# 创建RGBA模式的空白图像(带透明通道)
|
||
image = Image.new('RGBA', (image_width, image_height), (0, 0, 0, 0))
|
||
draw = ImageDraw.Draw(image)
|
||
|
||
# 绘制文本(考虑内边距)
|
||
x = padding - bbox[0] # 调整起始位置
|
||
y = padding - bbox[1]
|
||
|
||
# 设置文本颜色(带透明度)
|
||
if len(text_color) == 3:
|
||
text_color = text_color + (255,) # 添加完全不透明的alpha值
|
||
|
||
draw.text((x, y), text, fill=text_color, font=font)
|
||
|
||
# 转换为OpenCV格式(BGRA)
|
||
image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGBA2BGRA)
|
||
return image_cv
|
||
|
||
def perspective_transform(image, target, corners):
|
||
"""
|
||
对图像进行透视变换(保持透明通道)
|
||
target: 画布
|
||
corners: 四个角点的坐标,顺序为 [左上, 右上, 右下, 左下]
|
||
"""
|
||
height, width = image.shape[:2]
|
||
|
||
# 源点(原始图像的四个角)
|
||
src_points = np.array([
|
||
[0, 0], # 左上
|
||
[width-1, 0], # 右上
|
||
[width-1, height-1], # 右下
|
||
[0, height-1] # 左下
|
||
], dtype=np.float32)
|
||
|
||
# 目标点(变换后的四个角)
|
||
dst_points = np.array(corners, dtype=np.float32)
|
||
|
||
# 计算透视变换矩阵
|
||
matrix = cv2.getPerspectiveTransform(src_points, dst_points)
|
||
|
||
# 获取画布大小
|
||
target_height, target_width = target.shape[:2]
|
||
|
||
# 应用透视变换(保持所有通道,包括alpha)
|
||
transformed = cv2.warpPerspective(image, matrix, (target_width, target_height), flags=cv2.INTER_LINEAR)
|
||
|
||
return transformed, matrix
|
||
|
||
def blend_with_transparency(background, foreground, position):
|
||
"""
|
||
将带透明通道的前景图像合成到背景图像上
|
||
position: 前景图像在背景图像上的位置 (x, y)
|
||
"""
|
||
bg = background.copy()
|
||
|
||
# 如果背景没有alpha通道,添加一个
|
||
if bg.shape[2] == 3:
|
||
bg = cv2.cvtColor(bg, cv2.COLOR_BGR2BGRA)
|
||
bg[:, :, 3] = 255 # 完全不透明
|
||
|
||
x, y = position
|
||
fg_height, fg_width = foreground.shape[:2]
|
||
bg_height, bg_width = bg.shape[:2]
|
||
|
||
# 确保位置在图像范围内
|
||
x = max(0, min(x, bg_width - fg_width))
|
||
y = max(0, min(y, bg_height - fg_height))
|
||
|
||
# 提取前景的alpha通道并归一化
|
||
alpha_foreground = foreground[:, :, 3] / 255.0
|
||
|
||
# 对于每个颜色通道进行合成
|
||
for c in range(3):
|
||
bg_region = bg[y:y+fg_height, x:x+fg_width, c]
|
||
fg_region = foreground[:, :, c]
|
||
|
||
# alpha混合公式
|
||
bg[y:y+fg_height, x:x+fg_width, c] = (
|
||
alpha_foreground * fg_region +
|
||
(1 - alpha_foreground) * bg_region
|
||
)
|
||
|
||
# 更新背景的alpha通道(如果需要)
|
||
bg_alpha_region = bg[y:y+fg_height, x:x+fg_width, 3]
|
||
bg[y:y+fg_height, x:x+fg_width, 3] = np.maximum(bg_alpha_region, foreground[:, :, 3])
|
||
|
||
return bg
|
||
|
||
def precise_blend_with_perspective(background, foreground, corners):
|
||
"""
|
||
精确合成:根据四个角点将前景图像透视合成到背景上
|
||
"""
|
||
# 创建与背景相同大小的空白图像
|
||
bg_height, bg_width = background.shape[:2]
|
||
|
||
# 如果背景没有alpha通道,转换为BGRA
|
||
if background.shape[2] == 3:
|
||
background_bgra = cv2.cvtColor(background, cv2.COLOR_BGR2BGRA)
|
||
else:
|
||
background_bgra = background.copy()
|
||
|
||
# 创建与背景相同大小的前景图层
|
||
foreground_layer = np.zeros((bg_height, bg_width, 4), dtype=np.uint8)
|
||
|
||
# 计算前景图像在背景中的边界框
|
||
min_x = int(min(corners[:, 0]))
|
||
max_x = int(max(corners[:, 0]))
|
||
min_y = int(min(corners[:, 1]))
|
||
max_y = int(max(corners[:, 1]))
|
||
|
||
# 将变换后的前景图像放置到对应位置
|
||
fg_height, fg_width = foreground.shape[:2]
|
||
if min_y + fg_height <= bg_height and min_x + fg_width <= bg_width:
|
||
foreground_layer[min_y:min_y+fg_height, min_x:min_x+fg_width] = foreground
|
||
|
||
# 创建掩码(只在前景有内容的地方合成)
|
||
mask = (foreground_layer[:, :, 3] > 0)
|
||
|
||
# 合成图像
|
||
result = background_bgra.copy()
|
||
for c in range(3):
|
||
result[:, :, c][mask] = foreground_layer[:, :, c][mask]
|
||
result[:, :, 3][mask] = foreground_layer[:, :, 3][mask]
|
||
|
||
return result
|
||
|
||
async def generate_dice_image(number: int) -> BytesIO:
|
||
# 将文本转换为带透明背景的图像
|
||
text = str(number)
|
||
text_image = text_to_transparent_image(
|
||
text,
|
||
font_size=60,
|
||
text_color=(0, 0, 0) # 黑色文字
|
||
)
|
||
|
||
# 定义3D变换的四个角点(透视效果)
|
||
# 顺序: [左上, 右上, 右下, 左下]
|
||
corners = np.array([
|
||
[16, 30], # 左上
|
||
[51, 5], # 右上(上移,创建透视)
|
||
[88, 33], # 右下
|
||
[49, 62] # 左下(下移)
|
||
], dtype=np.float32)
|
||
|
||
# 加载背景图像,保留透明通道
|
||
background = cv2.imread(str(ASSETS_PATH / "img" / "dice" / "template.png"), cv2.IMREAD_UNCHANGED)
|
||
|
||
|
||
# 对文本图像进行3D变换(保持透明通道)
|
||
transformed_text, transform_matrix = perspective_transform(text_image, background, corners)
|
||
|
||
min_x = int(min(corners[:, 0]))
|
||
min_y = int(min(corners[:, 1]))
|
||
final_image_simple = blend_with_transparency(background, transformed_text, (min_x, min_y))
|
||
|
||
pil_final = Image.fromarray(final_image_simple)
|
||
# 导入一系列图像
|
||
images: list[Image.Image] = [Image.open(ASSETS_PATH / "img" / "dice" / f"{i}.png") for i in range(1, 12)]
|
||
images.append(pil_final)
|
||
frame_durations = [100] * (len(images) - 1) + [100000]
|
||
# 保存为BytesIO对象
|
||
output = BytesIO()
|
||
images[0].save(output,
|
||
save_all=True,
|
||
append_images=images[1:],
|
||
duration=frame_durations,
|
||
format='GIF',
|
||
loop=1)
|
||
return output |