917 lines
22 KiB
Python
917 lines
22 KiB
Python
"""EA 系(BF1/3/4/5/6)战绩卡渲染:OpenCV 拼模板 + PIL 绘字,输出 PNG BytesIO。"""
|
||||
|
|
|
|||
|
|
import datetime
|
|||
|
|
import io
|
|||
|
|
|
|||
|
|
import cv2
|
|||
|
|
import numpy as np
|
|||
|
|
from PIL import Image, ImageDraw, ImageFont
|
|||
|
|
from nonebot import logger
|
|||
|
|
|
|||
|
|
from ..paths import RES_DIR
|
|||
|
|
from .images import (
|
|||
|
|
designation_paste,
|
|||
|
|
draw_centered_text,
|
|||
|
|
draw_right_aligned_text,
|
|||
|
|
get_icon_from_cache,
|
|||
|
|
get_save_icon,
|
|||
|
|
image_paste,
|
|||
|
|
image_paste_center,
|
|||
|
|
load_avatar,
|
|||
|
|
notice_paste,
|
|||
|
|
notice_paste_bf6,
|
|||
|
|
paste_rank_icon,
|
|||
|
|
png_resize,
|
|||
|
|
)
|
|||
|
|
from .params import bf_item, classes
|
|||
|
|
from .text import add_commas, add_commas_with_padding
|
|||
|
|
|
|||
|
|
# 字体
|
|||
|
|
_FONT_NUM = RES_DIR / "font" / "bf-sub-headline-bold 等宽数字.ttf"
|
|||
|
|
_FONT_CH = RES_DIR / "font" / "演示创黑FLY.ttf"
|
|||
|
|
|
|||
|
|
font_XXL = ImageFont.truetype(str(_FONT_NUM), 72)
|
|||
|
|
font_XL = ImageFont.truetype(str(_FONT_NUM), 64)
|
|||
|
|
font_L = ImageFont.truetype(str(_FONT_NUM), 48)
|
|||
|
|
font_ML = ImageFont.truetype(str(_FONT_NUM), 42)
|
|||
|
|
font_M = ImageFont.truetype(str(_FONT_NUM), 36)
|
|||
|
|
font_MS = ImageFont.truetype(str(_FONT_NUM), 32)
|
|||
|
|
font_S = ImageFont.truetype(str(_FONT_NUM), 28)
|
|||
|
|
font_XS = ImageFont.truetype(str(_FONT_NUM), 24)
|
|||
|
|
font_XXS = ImageFont.truetype(str(_FONT_NUM), 18)
|
|||
|
|
font_XXXS = ImageFont.truetype(str(_FONT_NUM), 16)
|
|||
|
|
|
|||
|
|
font_XXL_CH = ImageFont.truetype(str(_FONT_CH), 72)
|
|||
|
|
font_XL_CH = ImageFont.truetype(str(_FONT_CH), 64)
|
|||
|
|
font_L_CH = ImageFont.truetype(str(_FONT_CH), 48)
|
|||
|
|
font_ML_CH = ImageFont.truetype(str(_FONT_CH), 42)
|
|||
|
|
font_M_CH = ImageFont.truetype(str(_FONT_CH), 36)
|
|||
|
|
font_MS_CH = ImageFont.truetype(str(_FONT_CH), 32)
|
|||
|
|
font_S_CH = ImageFont.truetype(str(_FONT_CH), 28)
|
|||
|
|
font_XS_CH = ImageFont.truetype(str(_FONT_CH), 24)
|
|||
|
|
font_XXS_CH = ImageFont.truetype(str(_FONT_CH), 18)
|
|||
|
|
font_XXXS_CH = ImageFont.truetype(str(_FONT_CH), 16)
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def build_stats_card(
|
|||
|
|
game="bfv",
|
|||
|
|
player="Player001",
|
|||
|
|
pid=1145141919,
|
|||
|
|
kd=1.25,
|
|||
|
|
kpm=2.3,
|
|||
|
|
spm=500,
|
|||
|
|
acc="25.6",
|
|||
|
|
head_shots="10",
|
|||
|
|
rank=100,
|
|||
|
|
time_play=100.0,
|
|||
|
|
kills=100,
|
|||
|
|
kill_assists=150,
|
|||
|
|
revives=114,
|
|||
|
|
wins=10,
|
|||
|
|
loses=10,
|
|||
|
|
destroyed=514,
|
|||
|
|
best_weapon="M1907 SF",
|
|||
|
|
best_vehicle="坦克",
|
|||
|
|
best_class="Support",
|
|||
|
|
longest_head_shot=1200,
|
|||
|
|
highest_ill_streak=100,
|
|||
|
|
stat_data=None,
|
|||
|
|
level_designation=None,
|
|||
|
|
qq_id=None,
|
|||
|
|
):
|
|||
|
|
if stat_data is None:
|
|||
|
|
stat_data = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
|
|||
|
|
|
|||
|
|
# 获取当前事件并且格式化
|
|||
|
|
now = datetime.datetime.now()
|
|||
|
|
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
|
|||
|
|
# ---------------- OpenCV 部分 ----------------
|
|||
|
|
# 载入模板
|
|||
|
|
img = cv2.imread(str(RES_DIR / "template" / f"{game}.png"), cv2.IMREAD_COLOR)
|
|||
|
|
|
|||
|
|
# 载入头像(按QQ号取,取不到时使用占位头像,避免空图 resize 崩溃)
|
|||
|
|
avatar = load_avatar(qq_id, 200)
|
|||
|
|
|
|||
|
|
# 创建圆形蒙版
|
|||
|
|
mask = np.zeros((200, 200), dtype=np.uint8)
|
|||
|
|
cv2.circle(mask, (100, 100), 100, 255, -1) # 绘制半径100的实心圆
|
|||
|
|
|
|||
|
|
# 合并圆形蒙版与原有alpha通道(load_avatar 保证是BGRA四通道)
|
|||
|
|
alpha = cv2.bitwise_and(avatar[:, :, 3], mask)
|
|||
|
|
avatar[:, :, 3] = alpha
|
|||
|
|
|
|||
|
|
# 粘贴头像(保持原有坐标)
|
|||
|
|
y, x = 55, 426
|
|||
|
|
h, w = avatar.shape[:2]
|
|||
|
|
alpha = avatar[:, :, 3] / 255.0
|
|||
|
|
for c in range(3):
|
|||
|
|
img[y : y + h, x : x + w, c] = (
|
|||
|
|
alpha * avatar[:, :, c] + (1 - alpha) * img[y : y + h, x : x + w, c]
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 公告栏
|
|||
|
|
img = notice_paste(img)
|
|||
|
|
|
|||
|
|
# ---------------- PIL 部分 ----------------
|
|||
|
|
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
|||
|
|
draw = ImageDraw.Draw(pil_img)
|
|||
|
|
|
|||
|
|
# 玩家信息
|
|||
|
|
draw.text((665, 65), f"{player}", fill="white", font=font_L_CH)
|
|||
|
|
draw.text((745, 133), f"{pid}", fill="white", font=font_M_CH)
|
|||
|
|
|
|||
|
|
# 等级游玩时长
|
|||
|
|
draw.text((740, 212), f"{rank}", fill="white", font=font_M_CH)
|
|||
|
|
draw.text((985, 212), f"{time_play}H", fill="white", font=font_M_CH)
|
|||
|
|
|
|||
|
|
# 生涯总览
|
|||
|
|
|
|||
|
|
# 场次
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=(wins + loses),
|
|||
|
|
x_left=45,
|
|||
|
|
x_right=265,
|
|||
|
|
y=350,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 命中率
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=round(float(acc.replace("%", "")), 1),
|
|||
|
|
x_left=415,
|
|||
|
|
x_right=495,
|
|||
|
|
y=343,
|
|||
|
|
font=font_M_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# 爆头率
|
|||
|
|
if head_shots:
|
|||
|
|
head_shots = head_shots.replace("%", "")
|
|||
|
|
else:
|
|||
|
|
head_shots = 0
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=round(float(head_shots), 1),
|
|||
|
|
x_left=395,
|
|||
|
|
x_right=470,
|
|||
|
|
y=420,
|
|||
|
|
font=font_M_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# kd
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=kd,
|
|||
|
|
x_left=585,
|
|||
|
|
x_right=785,
|
|||
|
|
y=340,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# kpm
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=round(kpm, 1),
|
|||
|
|
x_left=825,
|
|||
|
|
x_right=1025,
|
|||
|
|
y=340,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# spm
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=round(spm, 1),
|
|||
|
|
x_left=1065,
|
|||
|
|
x_right=1265,
|
|||
|
|
y=340,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 击杀
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=kills,
|
|||
|
|
x_left=870,
|
|||
|
|
x_right=1190,
|
|||
|
|
y=595,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 助攻
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=kill_assists,
|
|||
|
|
x_left=820,
|
|||
|
|
x_right=1145,
|
|||
|
|
y=770,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 急救
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=revives,
|
|||
|
|
x_left=770,
|
|||
|
|
x_right=1090,
|
|||
|
|
y=960,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 摧毁
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=destroyed,
|
|||
|
|
x_left=720,
|
|||
|
|
x_right=1040,
|
|||
|
|
y=1140,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 底部栏
|
|||
|
|
|
|||
|
|
# 评级
|
|||
|
|
if game == "bf6":
|
|||
|
|
logger.info("bf6不计算称号")
|
|||
|
|
elif level_designation:
|
|||
|
|
level = level_designation["level"]
|
|||
|
|
des = level_designation["designation"]
|
|||
|
|
color = level_designation["color"]
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=level,
|
|||
|
|
x_left=70,
|
|||
|
|
x_right=200,
|
|||
|
|
y=1120,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill=color,
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=des,
|
|||
|
|
x_left=200,
|
|||
|
|
x_right=550,
|
|||
|
|
y=1120,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 最佳兵种
|
|||
|
|
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=classes[best_class],
|
|||
|
|
x_left=140,
|
|||
|
|
x_right=310,
|
|||
|
|
y=1290,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 最远击杀
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{longest_head_shot}m",
|
|||
|
|
x_left=455,
|
|||
|
|
x_right=625,
|
|||
|
|
y=1290,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 最高连杀
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=highest_ill_streak,
|
|||
|
|
x_left=765,
|
|||
|
|
x_right=935,
|
|||
|
|
y=1290,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 最佳⭐
|
|||
|
|
pil_img = build_best(draw, pil_img, best_weapon, best_vehicle, game)
|
|||
|
|
|
|||
|
|
# 生成时间
|
|||
|
|
draw.text(
|
|||
|
|
(1870, 1420), f"{formatted_time}", fill=(154, 132, 149), font=font_XXXS_CH
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---------------- 转回 OpenCV ----------------
|
|||
|
|
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
|
|||
|
|
|
|||
|
|
# ---------------- 雷达图 ----------------
|
|||
|
|
if game != "bf6":
|
|||
|
|
center = (311, 796)
|
|||
|
|
r = 185
|
|||
|
|
scale_min = 0.25 if min(stat_data) < 0.25 else min(stat_data)
|
|||
|
|
img = draw_radar(
|
|||
|
|
img,
|
|||
|
|
stat_data,
|
|||
|
|
center=center,
|
|||
|
|
r=r,
|
|||
|
|
buffer=0.1,
|
|||
|
|
scale_min=scale_min,
|
|||
|
|
scale_max=1.10,
|
|||
|
|
)
|
|||
|
|
# 保存
|
|||
|
|
success, buffer = cv2.imencode(".png", img)
|
|||
|
|
if success:
|
|||
|
|
b_io = io.BytesIO(buffer)
|
|||
|
|
return b_io # 就可以直接 return 二进制流
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
async def build_bf6_stats_card(
|
|||
|
|
game="bfv",
|
|||
|
|
qq_id=2931,
|
|||
|
|
player="Player001",
|
|||
|
|
pid=1145141919,
|
|||
|
|
rank=114,
|
|||
|
|
kd=1.25,
|
|||
|
|
kpm=2.3,
|
|||
|
|
spm=500,
|
|||
|
|
acc="25.6",
|
|||
|
|
head_shots="10",
|
|||
|
|
time_play=100.0,
|
|||
|
|
kills=100,
|
|||
|
|
kill_assists=150,
|
|||
|
|
revives=114,
|
|||
|
|
repairs=1000,
|
|||
|
|
captured=300,
|
|||
|
|
score=145674,
|
|||
|
|
wins=10,
|
|||
|
|
loses=10,
|
|||
|
|
destroyed=514,
|
|||
|
|
best_weapon="M1907 SF",
|
|||
|
|
best_vehicle="坦克",
|
|||
|
|
best_class="Support",
|
|||
|
|
):
|
|||
|
|
# 获取当前事件并且格式化
|
|||
|
|
now = datetime.datetime.now()
|
|||
|
|
formatted_time = now.strftime("%Y-%m-%d %H:%M:%S")
|
|||
|
|
|
|||
|
|
# ---------------- OpenCV 部分 ----------------
|
|||
|
|
# 载入模板
|
|||
|
|
img = cv2.imread(str(RES_DIR / "template" / f"{game}.png"), cv2.IMREAD_COLOR)
|
|||
|
|
|
|||
|
|
# 载入头像(按QQ号取,取不到时使用占位头像,避免空图 resize 崩溃)
|
|||
|
|
avatar = load_avatar(qq_id, 134)
|
|||
|
|
|
|||
|
|
# 粘贴头像(保持原有坐标)
|
|||
|
|
y, x = 65, 105
|
|||
|
|
h, w = avatar.shape[:2]
|
|||
|
|
if avatar.shape[2] == 4:
|
|||
|
|
# 带透明通道时按alpha混合,避免直接赋值通道数不匹配
|
|||
|
|
alpha = avatar[:, :, 3:4] / 255.0
|
|||
|
|
img[y : y + h, x : x + w] = (
|
|||
|
|
alpha * avatar[:, :, :3] + (1 - alpha) * img[y : y + h, x : x + w]
|
|||
|
|
).astype(np.uint8)
|
|||
|
|
else:
|
|||
|
|
img[y : y + h, x : x + w] = avatar
|
|||
|
|
|
|||
|
|
# 公告栏
|
|||
|
|
img = notice_paste_bf6(img)
|
|||
|
|
img = designation_paste(
|
|||
|
|
player,
|
|||
|
|
img,
|
|||
|
|
user_id=qq_id,
|
|||
|
|
start_x=104,
|
|||
|
|
start_y=215,
|
|||
|
|
target_height=30,
|
|||
|
|
spacing=10,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---------------- PIL 部分 ----------------
|
|||
|
|
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
|
|||
|
|
draw = ImageDraw.Draw(pil_img)
|
|||
|
|
|
|||
|
|
# 玩家信息 105+160
|
|||
|
|
draw.text((265, 80), f"{player}", fill="white", font=font_XL)
|
|||
|
|
draw.text((265, 140), f"UID: {pid}", fill="#CCCCCC", font=font_L)
|
|||
|
|
|
|||
|
|
# 等级
|
|||
|
|
pil_img = paste_rank_icon(int(rank), pil_img)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{rank}",
|
|||
|
|
x_left=950,
|
|||
|
|
x_right=1075,
|
|||
|
|
y=205,
|
|||
|
|
font=font_M,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 游玩时长
|
|||
|
|
draw.text((2300, 350), f"{time_play:.1f} H", fill="white", font=font_L)
|
|||
|
|
# 场次
|
|||
|
|
draw.text((2300, 450), f"{wins + loses}", fill="white", font=font_L)
|
|||
|
|
|
|||
|
|
# 生涯总览
|
|||
|
|
# 胜率
|
|||
|
|
|
|||
|
|
win_rate = round((wins / (wins + loses)) * 100, 1)
|
|||
|
|
logger.info(f"胜场{wins},负场{loses},胜率{win_rate}")
|
|||
|
|
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{win_rate}%",
|
|||
|
|
x_left=2090,
|
|||
|
|
x_right=2200,
|
|||
|
|
y=375,
|
|||
|
|
font=font_XL,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 命中率
|
|||
|
|
acc_format = round(float(acc.replace("%", "")), 1)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{acc_format}%",
|
|||
|
|
x_left=1210,
|
|||
|
|
x_right=1303,
|
|||
|
|
y=363,
|
|||
|
|
font=font_M,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# 爆头率
|
|||
|
|
if head_shots:
|
|||
|
|
head_shots = head_shots.replace("%", "")
|
|||
|
|
else:
|
|||
|
|
head_shots = 0
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{round(float(head_shots), 1)}%",
|
|||
|
|
x_left=1210,
|
|||
|
|
x_right=1303,
|
|||
|
|
y=458,
|
|||
|
|
font=font_M,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
# kd
|
|||
|
|
draw.text((1350, 355), text=str(kd), fill="white", font=font_XXL)
|
|||
|
|
# kpm
|
|||
|
|
draw.text((1610, 355), text=f"{kpm:.2f}", fill="white", font=font_XXL)
|
|||
|
|
# spm
|
|||
|
|
draw.text((1870, 355), text=f"{int(spm)}", fill="white", font=font_XXL)
|
|||
|
|
|
|||
|
|
# 击杀
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=805,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(kills)),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=805,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 急救
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=1080,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(revives)),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=1080,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 摧毁
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=1350,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(destroyed)),
|
|||
|
|
x_left=1165,
|
|||
|
|
x_right=1455,
|
|||
|
|
y=1350,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 右侧
|
|||
|
|
# 占领
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=745,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(captured)),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=745,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 得分
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=955,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(score)),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=955,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 助攻
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=1160,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(kill_assists)),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=1160,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 修理
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 10),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=1370,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#444445",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(repairs)),
|
|||
|
|
x_left=1485,
|
|||
|
|
x_right=1780,
|
|||
|
|
y=1370,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 最佳⭐
|
|||
|
|
pil_img = build_best_bf6(draw, pil_img, best_weapon, best_vehicle, best_class, game)
|
|||
|
|
|
|||
|
|
# 生成时间
|
|||
|
|
draw.text(
|
|||
|
|
(1750, 10),
|
|||
|
|
f"{formatted_time}/维护 SANSENHOSHI",
|
|||
|
|
fill=(154, 132, 149),
|
|||
|
|
font=font_XS_CH,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# ---------------- 转回 OpenCV ----------------
|
|||
|
|
img = cv2.cvtColor(np.array(pil_img), cv2.COLOR_RGB2BGR)
|
|||
|
|
|
|||
|
|
# 保存
|
|||
|
|
success, buffer = cv2.imencode(".png", img)
|
|||
|
|
if success:
|
|||
|
|
b_io = io.BytesIO(buffer)
|
|||
|
|
return b_io # 就可以直接 return 二进制流
|
|||
|
|
else:
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_best(draw, img, weapons, vehicle, game: str):
|
|||
|
|
# 最佳武器
|
|||
|
|
weapon_name = weapons["weaponName"]
|
|||
|
|
w_kills = weapons["kills"]
|
|||
|
|
w_kpm = weapons["killsPerMinute"]
|
|||
|
|
acc = weapons["accuracy"].replace("%", "")
|
|||
|
|
headshots = weapons["headshots"].replace("%", "")
|
|||
|
|
|
|||
|
|
# 最佳载具
|
|||
|
|
vehicle_name = vehicle["vehicleName"]
|
|||
|
|
v_kills = vehicle["kills"]
|
|||
|
|
v_kpm = vehicle["killsPerMinute"]
|
|||
|
|
destroyed = vehicle["destroyed"]
|
|||
|
|
|
|||
|
|
# 绘制最佳⭐
|
|||
|
|
# 武器
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=bf_item[game][weapon_name].upper(),
|
|||
|
|
x_left=1240,
|
|||
|
|
x_right=1750,
|
|||
|
|
y=1355,
|
|||
|
|
font=font_MS_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=w_kills,
|
|||
|
|
x_left=1190,
|
|||
|
|
x_right=1390,
|
|||
|
|
y=1260,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=w_kpm,
|
|||
|
|
x_left=1510,
|
|||
|
|
x_right=1635,
|
|||
|
|
y=1260,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=acc,
|
|||
|
|
x_left=1735,
|
|||
|
|
x_right=1803,
|
|||
|
|
y=1030,
|
|||
|
|
font=font_M_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=headshots,
|
|||
|
|
x_left=1695,
|
|||
|
|
x_right=1760,
|
|||
|
|
y=1200,
|
|||
|
|
font=font_M_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 载具
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=bf_item[game][vehicle_name].upper(),
|
|||
|
|
x_left=1990,
|
|||
|
|
x_right=2440,
|
|||
|
|
y=1355,
|
|||
|
|
font=font_MS_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=v_kills,
|
|||
|
|
x_left=1940,
|
|||
|
|
x_right=2150,
|
|||
|
|
y=1260,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=v_kpm,
|
|||
|
|
x_left=2295,
|
|||
|
|
x_right=2465,
|
|||
|
|
y=1055,
|
|||
|
|
font=font_XL_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=destroyed,
|
|||
|
|
x_left=2265,
|
|||
|
|
x_right=2440,
|
|||
|
|
y=1260,
|
|||
|
|
font=font_L_CH,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 图片
|
|||
|
|
weapon_url = weapons["image"]
|
|||
|
|
vehicle_url = vehicle["image"]
|
|||
|
|
wp_icon = get_save_icon(game, weapon_name, "weapon", weapon_url)
|
|||
|
|
vc_icon = get_save_icon(game, vehicle_name, "vehicles", vehicle_url)
|
|||
|
|
|
|||
|
|
img = image_paste_center(wp_icon, img, (1165, 1110))
|
|||
|
|
img = image_paste_center(vc_icon, img, (1920, 1110))
|
|||
|
|
return img
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_best_bf6(draw, img, weapons, vehicle, classes, game: str):
|
|||
|
|
# 最佳武器
|
|||
|
|
weapon_name = weapons["weaponName"]
|
|||
|
|
w_kills = weapons["kills"]
|
|||
|
|
w_kpm = weapons["killsPerMinute"]
|
|||
|
|
acc = round(float(weapons["accuracy"].replace("%", "")), 1)
|
|||
|
|
headshots = round(float(weapons["headshots"].replace("%", "")), 1)
|
|||
|
|
|
|||
|
|
# 最佳载具
|
|||
|
|
vehicle_name = vehicle["vehicleName"]
|
|||
|
|
v_kills = vehicle["kills"]
|
|||
|
|
v_kpm = vehicle["killsPerMinute"]
|
|||
|
|
destroyed = vehicle["destroyed"]
|
|||
|
|
|
|||
|
|
# 绘制最佳⭐
|
|||
|
|
# 武器
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 7),
|
|||
|
|
x_left=95,
|
|||
|
|
x_right=313,
|
|||
|
|
y=665,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#313131",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(w_kills)),
|
|||
|
|
x_left=95,
|
|||
|
|
x_right=313,
|
|||
|
|
y=665,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw, text=w_kpm, x_left=335, x_right=447, y=665, font=font_L, fill="white"
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{acc}%",
|
|||
|
|
x_left=470,
|
|||
|
|
x_right=580,
|
|||
|
|
y=670,
|
|||
|
|
font=font_M,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=f"{headshots}%",
|
|||
|
|
x_left=610,
|
|||
|
|
x_right=717,
|
|||
|
|
y=670,
|
|||
|
|
font=font_M,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 载具
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 7),
|
|||
|
|
x_left=125,
|
|||
|
|
x_right=325,
|
|||
|
|
y=1087,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#313131",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas(str(v_kills)),
|
|||
|
|
x_left=125,
|
|||
|
|
x_right=325,
|
|||
|
|
y=1087,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_centered_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=v_kpm,
|
|||
|
|
x_left=351,
|
|||
|
|
x_right=463,
|
|||
|
|
y=1087,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=add_commas_with_padding("0", 7),
|
|||
|
|
x_left=477,
|
|||
|
|
x_right=688,
|
|||
|
|
y=1087,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="#313131",
|
|||
|
|
)
|
|||
|
|
draw_right_aligned_text(
|
|||
|
|
draw=draw,
|
|||
|
|
text=destroyed,
|
|||
|
|
x_left=477,
|
|||
|
|
x_right=688,
|
|||
|
|
y=1087,
|
|||
|
|
font=font_L,
|
|||
|
|
fill="white",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# 图片
|
|||
|
|
weapon_url = weapons["image"]
|
|||
|
|
vehicle_url = vehicle["image"]
|
|||
|
|
|
|||
|
|
wp_icon = get_save_icon(game, weapon_name, "weapon", weapon_url)
|
|||
|
|
vc_icon = get_save_icon(game, vehicle_name, "vehicles", vehicle_url)
|
|||
|
|
classes_icon = get_icon_from_cache(classes, "bf6", "classes")
|
|||
|
|
wp_icon_new = png_resize(wp_icon, new_width=729 - 88)
|
|||
|
|
vc_icon_new = png_resize(vc_icon, new_width=729 - 88)
|
|||
|
|
classes_icon_new = png_resize(
|
|||
|
|
classes_icon, new_height=1165 - 326, new_width=1100 - 748
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
img = image_paste(wp_icon_new, img, (88, 325))
|
|||
|
|
img = image_paste(vc_icon_new, img, (88, 746))
|
|||
|
|
img = image_paste(classes_icon_new, img, (748, 326))
|
|||
|
|
|
|||
|
|
return img
|
|||
|
|
|
|||
|
|
|
|||
|
|
def normalize_for_radar(data, buffer=0.1, scale_min=0.0, scale_max=1.0):
|
|||
|
|
"""
|
|||
|
|
将数据动态归一化到指定区间 [scale_min, scale_max],
|
|||
|
|
并给最大值预留 buffer,让图形更饱满。
|
|||
|
|
|
|||
|
|
Args:
|
|||
|
|
data (list/ndarray): 原始数据
|
|||
|
|
buffer (float): 缓冲比例,例如 0.1 表示最大值按 1.1 倍处理
|
|||
|
|
scale_min (float): 映射区间下限
|
|||
|
|
scale_max (float): 映射区间上限
|
|||
|
|
|
|||
|
|
Returns:
|
|||
|
|
list: 归一化后的数据
|
|||
|
|
"""
|
|||
|
|
data = np.array(data, dtype=float)
|
|||
|
|
d_min, d_max = data.min(), data.max()
|
|||
|
|
|
|||
|
|
# 给最大值留 buffer
|
|||
|
|
d_max = d_max * (1 + buffer)
|
|||
|
|
|
|||
|
|
if d_max == d_min: # 避免除零
|
|||
|
|
norm = np.ones_like(data) * (scale_min + scale_max) / 2
|
|||
|
|
else:
|
|||
|
|
norm = (data - d_min) / (d_max - d_min)
|
|||
|
|
norm = scale_min + norm * (scale_max - scale_min)
|
|||
|
|
|
|||
|
|
return norm.tolist()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ---------------- 雷达图示例 ----------------
|
|||
|
|
def draw_radar(
|
|||
|
|
img, stat_data, center=(311, 796), r=185, buffer=0.1, scale_min=0.1, scale_max=1.05
|
|||
|
|
):
|
|||
|
|
# 动态归一化
|
|||
|
|
scaled_data = normalize_for_radar(stat_data, buffer, scale_min, scale_max)
|
|||
|
|
|
|||
|
|
points = []
|
|||
|
|
for i, v in enumerate(scaled_data):
|
|||
|
|
angle = 2 * np.pi / len(scaled_data) * i - np.pi / 2
|
|||
|
|
px = int(center[0] + r * v * np.cos(angle))
|
|||
|
|
py = int(center[1] + r * v * np.sin(angle))
|
|||
|
|
points.append([px, py])
|
|||
|
|
points = np.array(points, np.int32)
|
|||
|
|
|
|||
|
|
# 绘制填充
|
|||
|
|
overlay = img.copy()
|
|||
|
|
cv2.fillPoly(overlay, [points], color=(255, 255, 255, 76))
|
|||
|
|
img = cv2.addWeighted(overlay, 0.3, img, 0.7, 0)
|
|||
|
|
# 绘制边框
|
|||
|
|
cv2.polylines(img, [points], isClosed=True, color=(255, 255, 255), thickness=2)
|
|||
|
|
return img
|