import datetime import io import re import cv2 import numpy as np from PIL import Image, ImageDraw, ImageFont from .data_utils import * from .img_utils import * from .param import * # from img_utils import * # from data_utils import * # from param import * filepath = os.path.dirname(__file__).replace("\\", "/") # 字体 font_XXL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 72) font_XL = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 64) font_L = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 48) font_ML = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 42) font_M = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 36) font_MS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 32) font_S = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 28) font_XS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 24) font_XXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 18) font_XXXS = ImageFont.truetype(f"{filepath}/font/bf-sub-headline-bold 等宽数字.ttf", 16) font_XXL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 72) font_XL_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 64) font_L_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 48) font_ML_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 42) font_M_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 36) font_MS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 32) font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 24) font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 16) font_S_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 28) font_XS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 20) font_XXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 18) font_XXXS_CH = ImageFont.truetype(f"{filepath}/font/演示创黑FLY.ttf", 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, 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): 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(f"{filepath}/template/{game}.png", cv2.IMREAD_COLOR) # 载入头像并缩放 avatar = cv2.imread(f"{filepath}/avatar_cache/0.png", cv2.IMREAD_UNCHANGED) avatar = cv2.resize(avatar, (200, 200), interpolation=cv2.INTER_AREA) # 创建圆形蒙版 mask = np.zeros((200, 200), dtype=np.uint8) cv2.circle(mask, (100, 100), 100, 255, -1) # 绘制半径100的实心圆 # 处理透明通道 if avatar.shape[2] == 4: # 合并圆形蒙版与原有alpha通道 alpha = cv2.bitwise_and(avatar[:, :, 3], mask) avatar[:, :, 3] = alpha else: # 无透明通道时创建BGRA图像 avatar = cv2.cvtColor(avatar, cv2.COLOR_BGR2BGRA) avatar[:, :, 3] = mask # 粘贴头像(保持原有坐标) 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=rank, x_left=730, x_right=905, y=210, font=font_M, fill="white", ) # draw_centered_text(draw=draw, text=f"{time_play}H", x_left=965, x_right=1120, y=210, font=font_M, fill="white", ) # draw.text((730, 195), f"{rank}", fill="white", font=font_M) # draw.text((1000, 195), f"{time_play}", fill="white", font=font_M) # 生涯总览 # 场次 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不计算称号") else: 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, 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(f"{filepath}/template/{game}.png", cv2.IMREAD_COLOR) # 载入头像并缩放 avatar = cv2.imread(f"{filepath}/avatar_cache/0.png", cv2.IMREAD_UNCHANGED) avatar = cv2.resize(avatar, (134, 134), interpolation=cv2.INTER_AREA) # 粘贴头像(保持原有坐标) y, x = 65, 105 h, w = avatar.shape[:2] 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((740, 212), f"{rank}", fill="white", font=font_M) # 游玩时长 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) # draw_centered_text(draw=draw, text=(wins + loses), x_left=45, x_right=265, y=350, # font=font_L, # fill="white") # 生涯总览 # 胜率 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") # 底部栏 # # 最佳兵种 # logger.info(f"最佳兵种:{best_class}") # draw_centered_text(draw=draw, text=classes[best_class], x_left=140, x_right=310, y=1290, 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: ImageDraw, img: Image, 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: ImageDraw, img: Image, 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_centered_text(draw=draw, text=bf_item[game][weapon_name].upper(), x_left=1240, x_right=1750, y=1355, # font=font_MS, fill="white") 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_centered_text(draw=draw, text=bf_item[game][vehicle_name].upper(), x_left=1990, x_right=2440, y=1355, # font=font_MS, 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 add_commas(text): return re.sub(r'(?