Files
HeXi/hexi/plugins/nonebot_plugin_bf_bot/img_utils.py
T

553 lines
17 KiB
Python
Raw Normal View History

import hashlib
import os
from io import BytesIO
import aiohttp
import cv2
import numpy as np
import requests
import requests.exceptions
from PIL import Image, ImageDraw, ImageFont
from nonebot import logger
from hexi.plugins.nonebot_plugin_bf_bot.user_data.data_utils import UserManager, DesignationManager
from .param import rank_pic
filepath = os.path.dirname(__file__).replace("\\", "/")
# 圆角遮罩处理
def draw_rect(img, pos, radius, **kwargs):
trans = Image.new('RGBA', img.size, (0, 0, 0, 0))
alpha_draw = ImageDraw.Draw(trans, "RGBA")
alpha_draw.rounded_rectangle(pos, radius, **kwargs)
img.paste(Image.alpha_composite(img, trans))
return img
# 圆角处理
def circle_corner(img, radii):
"""
半透明圆角处理
:param img: 要修改的文件
:param radii: 圆角弧度
:return: 返回修改过的文件
"""
circle = Image.new('L', (radii * 2, radii * 2), 0) # 创建黑色方形
draw = ImageDraw.Draw(circle)
draw.ellipse((0, 0, radii * 2, radii * 2), fill=255) # 黑色方形内切白色圆形
img = img.convert("RGBA")
w, h = img.size
# 创建一个alpha层,存放四个圆角,使用透明度切除圆角外的图片
alpha = Image.new('L', img.size, 255)
alpha.paste(circle.crop((0, 0, radii, radii)), (0, 0)) # 左上角
alpha.paste(circle.crop((radii, 0, radii * 2, radii)),
(w - radii, 0)) # 右上角
alpha.paste(circle.crop((radii, radii, radii * 2, radii * 2)),
(w - radii, h - radii)) # 右下角
alpha.paste(circle.crop((0, radii, radii, radii * 2)),
(0, h - radii)) # 左下角
img.putalpha(alpha) # 白色区域透明可见,黑色区域不可见
# 添加圆角边框
draw = ImageDraw.Draw(img)
draw.rounded_rectangle(img.getbbox(), outline="white", width=3, radius=radii)
return img
def png_resize(source_file, new_width=0, new_height=0, resample="LANCZOS", ref_file=""):
"""
PNG 等比缩放(保持 Alpha 透明度)
:param source_file: Image.open() 得到的 Image 对象
:param new_width: 目标宽度(可选)
:param new_height: 目标高度(可选)
:param resample: NEAREST / BILINEAR / BICUBIC / LANCZOS
:param ref_file: 参考图片路径(等比适配到参考尺寸)
:return: PIL.Image (RGBA)
"""
img = source_file.convert("RGBA")
src_w, src_h = img.size
# -------------------------
# 计算目标尺寸(等比)
# -------------------------
if ref_file:
ref_img = Image.open(ref_file)
ref_w, ref_h = ref_img.size
scale = min(ref_w / src_w, ref_h / src_h)
target_w = int(src_w * scale)
target_h = int(src_h * scale)
else:
if new_width > 0 and new_height > 0:
scale = min(new_width / src_w, new_height / src_h)
target_w = int(src_w * scale)
target_h = int(src_h * scale)
elif new_width > 0:
scale = new_width / src_w
target_w = new_width
target_h = int(src_h * scale)
elif new_height > 0:
scale = new_height / src_h
target_h = new_height
target_w = int(src_w * scale)
else:
# 未指定任何尺寸,直接返回
return img
# -------------------------
# 重采样方式
# -------------------------
resample_map = {
"NEAREST": Image.NEAREST,
"BILINEAR": Image.BILINEAR,
"BICUBIC": Image.BICUBIC,
"LANCZOS": Image.LANCZOS
}
resample_method = resample_map.get(resample, Image.LANCZOS)
# -------------------------
# Alpha 安全 resize(逐通道)
# -------------------------
bands = img.split()
resized_bands = [
band.resize((target_w, target_h), resample=resample_method)
for band in bands
]
return Image.merge("RGBA", resized_bands)
# 图片粘贴
def image_paste(paste_image, under_image, pos):
"""
:param paste_image: 需要粘贴的图片
:param under_image: 底图
:param pos: 位置(x,y)坐标
:return: 返回图片
"""
# 获取需要贴入图片的透明通道
r, g, b, alpha = paste_image.split()
# 粘贴时将alpha值传递至mask属性
under_image.paste(paste_image, pos, alpha)
return under_image
def image_paste_center(paste_image: Image, under_image: Image, pos):
"""
将一张图片粘贴到另一张图片上,并使粘贴图片的Y轴中心对齐到指定y坐标
:param paste_image: 需要粘贴的图片 (RGBA)
:param under_image: 底图 (RGBA)
:param pos: 粘贴位置 (x, y),其中y表示目标对齐的中心位置
:return: 返回合成后的图片
"""
x, y = pos
# 计算上下居中偏移
paste_w, paste_h = paste_image.size
y_top = y - paste_h // 2 # 上边位置 = 中心点y - 半高
# 获取透明通道
r, g, b, alpha = paste_image.split()
under_image.paste(paste_image, (x, y_top), alpha)
return under_image
def download_icon(url):
data = download_url(url)
img = BytesIO(data)
return img
# 下载QQ头像
def download_avatar(user_id: str) -> bytes:
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=640"
data = download_url(url)
if not data or hashlib.md5(data).hexdigest() == "acef72340ac0e914090bd35799f5594e":
url = f"http://q1.qlogo.cn/g?b=qq&nk={user_id}&s=100"
data = download_url(url)
return data
# 根据URL下载文件
def download_url(url: str) -> bytes:
for i in range(3):
try:
resp = requests.get(url)
if resp.status_code != 200:
continue
return resp.content
except Exception as e:
print(f"Error downloading {url}, retry {i}/3: {str(e)}")
# 图片裁剪
def cut_image(pic_data: bytes, target_ratio: float):
try:
pic_data = Image.open(BytesIO(pic_data))
w, h = pic_data.size
pic_ratio = w / h
if pic_ratio > target_ratio:
# 宽高比大于目标比例,按高度缩放。保持宽度不变
new_h = w / target_ratio
h_delta = (h - new_h) / 2
w_delta = 0
else:
# 宽高比小于目标比例,按宽度缩放。保持高度不变
new_w = h * target_ratio
w_delta = (w - new_w) / 2
h_delta = 0
cropped = pic_data.crop((w_delta, h_delta, w - w_delta, h - h_delta))
return cropped
except Exception as e:
raise Exception(f"图片剪裁失败{str(e)}")
def draw_centered_text(draw, text, x_left, x_right, y, font, fill):
"""
在指定区域水平居中绘制文本
:param draw: ImageDraw对象
:param text: 要绘制的文本
:param x_left: 区域左边界x坐标
:param x_right: 区域右边界x坐标
:param y: 文本基线y坐标
:param font: ImageFont对象
:param fill: 文本颜色
"""
text = str(text)
text_width = font.getlength(text) # 获取文本像素宽度
area_width = x_right - x_left
x_center = x_left + (area_width - text_width) / 2
draw.text((x_center, y), text, font=font, fill=fill)
def draw_right_aligned_text(draw, text, x_left, x_right, y, font, fill):
"""
在指定区域内从右往左绘制文本(右对齐)
:param draw: ImageDraw 对象
:param text: 要绘制的文本
:param x_left: 区域左边界 x
:param x_right: 区域右边界 x(文本右侧贴齐这里)
:param y: 文本基线 y 坐标
:param font: ImageFont 对象
:param fill: 文本颜色
"""
text = str(text)
text_width = font.getlength(text)
# 右对齐:文本右边贴到 x_right
x_start = x_right - text_width
# 可选:防止越界(超出左边界时截断)
if x_start < x_left:
x_start = x_left
draw.text((x_start, y), text, font=font, fill=fill)
def get_save_icon(game, name, icon_type, url):
if game == 'bf6':
name = name.replace("/", "").upper()
else:
name = name.replace("/", "")
logger.info(f"查询的物品: {name},图标类型:{icon_type},游戏:{game}")
icon = get_icon_from_cache(name, game, icon_type)
if not icon:
if game == "bf1":
icon = get_icon_from_url(url)
if icon_type == "weapon":
icon = png_resize(icon, 500, 125)
else:
icon = png_resize(icon, 315, 79)
elif game == "bf3":
icon = get_icon_from_url(url)
if icon_type == "weapon":
icon = png_resize(icon, 500, 300)
else:
icon = png_resize(icon, 315, 200)
elif game == "bf4":
icon = get_icon_from_url(url)
if icon_type == "weapon":
icon = png_resize(icon, 500, 165)
else:
icon = png_resize(icon, 315, 80)
elif game == "bfv":
icon = get_icon_from_url(url)
if icon_type == "weapon":
icon = png_resize(icon, 500, 166)
else:
icon = png_resize(icon, 315, 315)
icon.save(f"{filepath}/img/{game}/{icon_type}/{name}.png")
logger.info(f"文件已经缓存至:{filepath}/img/{game}/{icon_type}/{name}.png")
# 上述步骤处理完再执行一次icon判空
if icon:
return icon
else:
# 针对空图片处理
if icon_type == "weapon":
icon = Image.open(filepath + "/img/wp.png").convert('RGBA')
else:
icon = Image.open(filepath + "/img/vc.png").convert('RGBA')
return icon
# 获取icon
def get_icon_from_url(url):
if url:
try:
# 添加10s超时判断,如果超时返回空
res = BytesIO(requests.get(url, timeout=10).content)
icon = Image.open(res).convert('RGBA')
return icon
except requests.exceptions.RequestException as e:
logger.error(f"请求异常:{e}")
def get_icon_from_cache(icon_name, game, icon_type):
path = f"{filepath}/img/{game}/{icon_type}"
logger.info(f"查询物品:{icon_name}物品路径:{path}")
try:
icon_list = os.listdir(path)
# logger.info(f"所有物品:{icon_list}")
if icon_name in str(icon_list):
logger.info(f"本地存在{icon_name}物品")
img = Image.open(f"{path}/{icon_name}.png").convert('RGBA')
return img
except Exception as err:
logger.error(f"获取图标失败:{str(err)}")
return None
def cutout_region(img, mask, alpha=0):
"""
在图像指定区域设置透明度
:param img: 输入图像 (BGR 或 BGRA)
:param mask: 区域定义
- 矩形: (x1, y1, x2, y2)
- 圆形: (cx, cy, r)
- 任意 mask: numpy.ndarray (0/255)
:param alpha: 设置的透明度 (0=透明, 255=不透明, 0-255之间半透明)
:return: 处理后的图像 (BGRA)
"""
# 转为 BGRA (确保有 alpha 通道)
if img.shape[2] == 3:
b, g, r = cv2.split(img)
a = np.ones(b.shape, dtype=np.uint8) * 255
img = cv2.merge((b, g, r, a))
h, w = img.shape[:2]
region_mask = np.zeros((h, w), dtype=np.uint8)
# 根据传入参数类型生成区域 mask
if isinstance(mask, tuple):
if len(mask) == 4: # 矩形
x1, y1, x2, y2 = mask
region_mask[y1:y2, x1:x2] = 255
elif len(mask) == 3: # 圆形
cx, cy, r = mask
cv2.circle(region_mask, (cx, cy), r, 255, -1)
else:
raise ValueError("不支持的 mask tuple 格式")
elif isinstance(mask, np.ndarray): # 自定义 mask
if mask.shape != (h, w):
raise ValueError("mask 尺寸必须与图像一致")
region_mask = mask
else:
raise TypeError("mask 必须是 tuple 或 numpy.ndarray")
# 修改 alpha 通道
img[region_mask > 0, 3] = alpha
return img
def paste_image_cv2(fg, bg, pos):
"""
将前景图粘贴到背景图上,支持透明度 (Alpha 混合)
:param fg: 前景图 (必须是 BGRA)
:param bg: 背景图 (BGR 或 BGRA)
:param pos: 粘贴位置 (x, y) 左上角坐标
:return: 合成后的图像 (BGRA)
"""
x, y = pos
fh, fw = fg.shape[:2]
# 确保背景有 alpha 通道
if bg.shape[2] == 3:
b, g, r = cv2.split(bg)
a = np.ones(b.shape, dtype=np.uint8) * 255
bg = cv2.merge((b, g, r, a))
# 截取 ROI 区域
roi = bg[y:y + fh, x:x + fw]
# 防止越界
if roi.shape[0] != fh or roi.shape[1] != fw:
raise ValueError("前景图超出背景范围")
# 提取 alpha 通道
alpha_fg = fg[:, :, 3] / 255.0
alpha_bg = 1.0 - alpha_fg
# 混合 RGB
for c in range(3):
roi[:, :, c] = (alpha_fg * fg[:, :, c] +
alpha_bg * roi[:, :, c])
# 更新 alpha 通道(取最大值,保证透明度正确)
roi[:, :, 3] = np.clip(fg[:, :, 3] + roi[:, :, 3], 0, 255)
# 放回背景
bg[y:y + fh, x:x + fw] = roi
return bg
def notice_paste(cv2_bg):
pos = (1176, 318)
# 获取公告
notice_path = f"{filepath}/notice/notice.png"
front_img = cv2.imread(notice_path)
mask = np.zeros((631, 1355), dtype=np.uint8)
pts = np.array([[0, 0], [167, 0], [0, 631]], np.int32)
cv2.fillPoly(mask, [pts], 255)
front_img = cutout_region(front_img, mask, alpha=0)
mix_bg = paste_image_cv2(front_img, cv2_bg, pos)
return mix_bg
def notice_paste_bf6(cv2_bg):
x, y = 0, 0
notice_path = f"{filepath}/notice/notice_bf6.png"
front_img = cv2.imread(notice_path, cv2.IMREAD_UNCHANGED)
if front_img is None:
raise RuntimeError(f"Failed to load image: {notice_path}")
h, w = front_img.shape[:2]
# ROI 边界裁剪
bg_h, bg_w = cv2_bg.shape[:2]
w = min(w, bg_w - x)
h = min(h, bg_h - y)
roi = cv2_bg[y:y + h, x:x + w]
# 有 Alpha 通道 → Alpha 混合
if front_img.shape[2] == 4:
alpha = front_img[:h, :w, 3] / 255.0
alpha = alpha[:, :, None]
cv2_bg[y:y + h, x:x + w] = (
alpha * front_img[:h, :w, :3] +
(1 - alpha) * roi
).astype(np.uint8)
# 无 Alpha → 直接覆盖
else:
cv2_bg[y:y + h, x:x + w] = front_img[:h, :w]
return cv2_bg
def designation_paste(player,
cv2_bg,
user_id,
start_x=10,
start_y=10,
target_height=40,
spacing=6
):
designation_list_path = get_designation_list_by_user_id(user_id, player)
if not designation_list_path:
return cv2_bg
icons = []
total_width = 0
for path in designation_list_path:
icon = cv2.imread(path, cv2.IMREAD_UNCHANGED)
if icon is None:
continue
h, w = icon.shape[:2]
scale = target_height / h
new_w = int(w * scale)
icon = cv2.resize(icon, (new_w, target_height))
icons.append(icon)
total_width += new_w
total_width += spacing * (len(icons) - 1)
x = start_x
y = start_y # 关键变化点:直接使用 top-left
bg_h, bg_w = cv2_bg.shape[:2]
for icon in icons:
ih, iw = icon.shape[:2]
if x + iw > bg_w or y + ih > bg_h:
break
roi = cv2_bg[y:y + ih, x:x + iw]
if icon.shape[2] == 4:
alpha = icon[:, :, 3] / 255.0
for c in range(3):
roi[:, :, c] = roi[:, :, c] * (1 - alpha) + icon[:, :, c] * alpha
else:
roi[:] = icon
cv2_bg[y:y + ih, x:x + iw] = roi
x += iw + spacing
return cv2_bg
def get_designation_list_by_user_id(user_id, player):
usermanager = UserManager()
designationmanager = DesignationManager()
user_info = usermanager.get_user_by_qq(user_id)
icon_no_list = []
if user_info is not None and user_info['ea_player_name'].upper() == player.upper():
logger.info(f"玩家标签列表:{user_info}")
icon_no_list = user_info['designation']
icon_list = designationmanager.get_designation_by_id_list(icon_no_list)
icon_path_list = []
for icon in icon_list:
icon_file_name = icon['icon_path']
icon_path_list.append(f"{filepath}/img/icon/designation/{icon_file_name}")
return icon_path_list
def paste_rank_icon(rank, img):
rank_pic_path = get_rank_pic(rank)
rank_icon_path = f"{filepath}/img/icon/rank/{rank_pic_path}"
rank_icon = Image.open(rank_icon_path)
rank_icon = png_resize(rank_icon, new_width=132, new_height=192)
fm_img = image_paste(rank_icon, img, (946, 61))
return fm_img
def get_rank_pic(rank: int) -> str:
"""
根据 rank 返回对应的 rank_pic 路径
"""
for (start, end), info in rank_pic.items():
if start <= rank <= end:
return info["path"]
return "t_ui_rankswatch_1-9 1.png"