基于录屏的LOLM关键数据与优劣势转折点自动分析
系统架构
整个系统的处理管线可以概括为:
```
录屏视频 → 帧提取 → 屏幕区域裁剪 → OCR/目标检测识别 → 数据序列化 → 转折点检测 → 报告输出
```
核心思路是:在每一帧(或按固定间隔抽帧)中,从LOLM固定UI区域提取经济、击杀等数值,形成随时间变化的数据序列,再通过突变检测算法识别优势劣势的转折时刻。这类“视频→YOLO检测→事件跟踪→OCR过滤→时间戳”的管线已被实际项目验证可行。
完整代码实现
以下代码依赖:opencv-python、paddleocr、ultralytics、numpy。安装:
```bash
pip install opencv-python paddleocr ultralytics numpy
```
```python
"""
LOLM 录屏自动分析工具
从录屏中提取关键数据并检测优势/劣势转折点
"""
import cv2
import numpy as np
from dataclasses import dataclass, field
from typing import List, Optional, Tuple
from pathlib import Path
# ======================== 数据结构定义 ========================
@dataclass
class GameSnapshot:
"""某一时刻的游戏状态快照"""
timestamp_sec: float # 视频中的时间(秒)
blue_gold: Optional[int] = None
red_gold: Optional[int] = None
blue_kills: Optional[int] = None
red_kills: Optional[int] = None
blue_towers: Optional[int] = None
red_towers: Optional[int] = None
@property
def gold_diff(self) -> Optional[int]:
"""经济差(蓝方 - 红方),正数表示蓝方优势"""
if self.blue_gold is not None and self.red_gold is not None:
return self.blue_gold - self.red_gold
return None
@property
def kill_diff(self) -> Optional[int]:
if self.blue_kills is not None and self.red_kills is not None:
return self.blue_kills - self.red_kills
return None
@dataclass
class TurningPoint:
"""优势/劣势转折点"""
timestamp_sec: float
event_type: str # "gold_swing" / "kill_swing" / "tower_swing"
magnitude: float # 变化幅度(绝对值)
direction: str # "blue_advantage" / "red_advantage"
description: str
# ======================== 屏幕区域配置 ========================
class ScreenRegions:
"""
LOLM 屏幕各UI区域的坐标配置。
坐标系为归一化坐标 (0~1),适配不同分辨率。
重要:不同手机、不同游戏版本的UI布局有差异,
以下坐标需要根据实际录屏画面进行校准。
校准方法:截取一帧画面,用画图工具量取各区域的像素位置,
再除以画面宽高得到归一化坐标。
"""
# 顶部计分板:击杀数、经济显示区域
SCOREBOARD = {
"blue_kills": (0.38, 0.01, 0.47, 0.05),
"red_kills": (0.53, 0.01, 0.62, 0.05),
"blue_gold": (0.38, 0.06, 0.47, 0.10),
"red_gold": (0.53, 0.06, 0.62, 0.10),
}
# 小地图区域
MINIMAP = (0.01, 0.60, 0.28, 0.99)
# 防御塔计数(通常在计分板两侧)
TOWER_AREA = {
"blue_towers": (0.30, 0.01, 0.38, 0.05),
"red_towers": (0.62, 0.01, 0.70, 0.05),
}
@classmethod
def get_pixel_region(cls, norm_region: Tuple, frame_w: int, frame_h: int):
"""将归一化坐标转为像素坐标 (x1, y1, x2, y2)"""
x1 = int(norm_region[0] * frame_w)
y1 = int(norm_region[1] * frame_h)
x2 = int(norm_region[2] * frame_w)
y2 = int(norm_region[3] * frame_h)
return (x1, y1, x2, y2)
# ======================== 帧提取与OCR识别 ========================
class FrameExtractor:
"""从录屏视频中按固定间隔提取帧"""
def __init__(self, video_path: str, interval_sec: float = 2.0):
"""
Args:
video_path: 录屏文件路径
interval_sec: 抽帧间隔(秒),LOLM数据变化不需要逐帧分析
"""
self.video_path = video_path
self.interval_sec = interval_sec
self.cap = cv2.VideoCapture(video_path)
if not self.cap.isOpened():
raise FileNotFoundError(f"无法打开视频: {video_path}")
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
self.duration_sec = self.total_frames / self.fps if self.fps > 0 else 0
self.frame_w = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.frame_h = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
def extract_at(self, time_sec: float) -> Optional[np.ndarray]:
"""提取指定时间点的帧"""
frame_no = int(time_sec * self.fps)
self.cap.set(cv2.CAP_PROP_POS_FRAMES, frame_no)
ret, frame = self.cap.read()
return frame if ret else None
def iter_frames(self):
"""按间隔迭代所有帧"""
t = 0.0
while t < self.duration_sec:
frame = self.extract_at(t)
if frame is not None:
yield t, frame
t += self.interval_sec
def release(self):
self.cap.release()
class GameOCR:
"""
使用 PaddleOCR 识别屏幕上的数值。
PaddleOCR 在游戏界面识别中表现稳定,
对半透明UI和特效干扰有一定鲁棒性。
"""
def __init__(self, lang: str = "ch"):
from paddleocr import PaddleOCR
self.ocr = PaddleOCR(
use_angle_cls=False,
lang=lang,
show_log=False,
use_gpu=False,
)
def read_number(self, image_region: np.ndarray) -> Optional[int]:
"""从图像区域中读取一个整数,识别失败返回 None"""
if image_region is None or image_region.size == 0:
return None
# 预处理:灰度化 + 放大 + 二值化,提高小文字识别率
gray = cv2.cvtColor(image_region, cv2.COLOR_BGR2GRAY)
gray = cv2.resize(gray, None, fx=3, fy=3,
interpolation=cv2.INTER_CUBIC)
_, binary = cv2.threshold(gray, 0, 255,
cv2.THRESH_BINARY + cv2.THRESH_OTSU)
result = self.ocr.ocr(binary, cls=False)
if not result or not result[0]:
return None
# 从识别结果中提取数字
for line in result[0]:
text = line[1][0].strip()
# 过滤非数字字符(处理 "k" 后缀,如 "12.5k")
cleaned = text.replace(",", "").replace(" ", "")
if cleaned.endswith("k") or cleaned.endswith("K"):
try:
return int(float(cleaned[:-1]) * 1000)
except ValueError:
continue
try:
return int(cleaned)
except ValueError:
continue
return None
# ======================== 数据采集主逻辑 ========================
class LOLMDataCollector:
"""从录屏中逐帧采集游戏数据"""
def __init__(self, video_path: str,
interval_sec: float = 2.0,
use_ocr: bool = True):
self.extractor = FrameExtractor(video_path, interval_sec)
self.ocr = GameOCR() if use_ocr else None
self.snapshots: List[GameSnapshot] = []
def collect(self) -> List[GameSnapshot]:
"""执行数据采集,返回时间序列快照列表"""
w, h = self.extractor.frame_w, self.extractor.frame_h
regions = ScreenRegions()
for t, frame in self.extractor.iter_frames():
snap = GameSnapshot(timestamp_sec=round(t, 1))
# 读取顶部计分板中的击杀数和经济
for key, norm_box in regions.SCOREBOARD.items():
px = ScreenRegions.get_pixel_region(norm_box, w, h)
crop = frame[px[1]:px[3], px[0]:px[2]]
if self.ocr:
val = self.ocr.read_number(crop)
if val is not None:
setattr(snap, key, val)
# 读取防御塔计数
for key, norm_box in regions.TOWER_AREA.items():
px = ScreenRegions.get_pixel_region(norm_box, w, h)
crop = frame[px[1]:px[3], px[0]:px[2]]
if self.ocr:
val = self.ocr.read_number(crop)
if val is not None:
setattr(snap, key, val)
self.snapshots.append(snap)
self.extractor.release()
return self.snapshots
# ======================== 转折点检测 ========================
class TurningPointDetector:
"""
基于数据序列的突变检测来识别优势/劣势转折点。
核心算法参考 LoL-MDC 的思路:
计算相邻时间窗口之间指标变化量 Δ,
变化量超过阈值的时刻即为关键转折点。
"""
def __init__(self,
gold_swing_threshold: float = 3000,
kill_swing_threshold: int = 3,
window_sec: float = 60.0):
"""
Args:
gold_swing_threshold: 经济差变化超过此值判定为转折
kill_swing_threshold: 击杀差变化超过此值判定为转折
window_sec: 滑动窗口大小(秒)
"""
self.gold_threshold = gold_swing_threshold
self.kill_threshold = kill_swing_threshold
self.window_sec = window_sec
def detect(self, snapshots: List[GameSnapshot]) -> List[TurningPoint]:
"""检测所有转折点"""
points = []
points.extend(self._detect_gold_swings(snapshots))
points.extend(self._detect_kill_swings(snapshots))
# 按时间排序
points.sort(key=lambda p: p.timestamp_sec)
return points
def _detect_gold_swings(self,
snapshots: List[GameSnapshot]
) -> List[TurningPoint]:
"""检测经济差突变"""
points = []
valid = [(s.timestamp_sec, s.gold_diff)
for s in snapshots if s.gold_diff is not None]
if len(valid) < 2:
return points
for i in range(1, len(valid)):
t_prev, diff_prev = valid[i - 1]
t_curr, diff_curr = valid[i]
delta = diff_curr - diff_prev
if abs(delta) >= self.gold_threshold:
direction = ("blue_advantage" if delta > 0
else "red_advantage")
# 判断是"反超"还是"扩大优势"
crossed = (diff_prev * diff_curr < 0) # 符号相反=反超
label = "经济反超" if crossed else "经济差剧变"
side = "蓝方" if delta > 0 else "红方"
points.append(TurningPoint(
timestamp_sec=t_curr,
event_type="gold_swing",
magnitude=abs(delta),
direction=direction,
description=(
f"第 {int(t_curr // 60)}:{int(t_curr % 60):02d} "
f"{label}:{side}经济差变化 {delta:+,d} "
f"(当前经济差 {diff_curr:+,d})"
)
))
return points
def _detect_kill_swings(self,
snapshots: List[GameSnapshot]
) -> List[TurningPoint]:
"""检测击杀差突变"""
points = []
valid = [(s.timestamp_sec, s.kill_diff)
for s in snapshots if s.kill_diff is not None]
if len(valid) < 2:
return points
for i in range(1, len(valid)):
t_prev, diff_prev = valid[i - 1]
t_curr, diff_curr = valid[i]
delta = diff_curr - diff_prev
if abs(delta) >= self.kill_threshold:
side = "蓝方" if delta > 0 else "红方"
points.append(TurningPoint(
timestamp_sec=t_curr,
event_type="kill_swing",
magnitude=abs(delta),
direction=("blue_advantage" if delta > 0
else "red_advantage"),
description=(
f"第 {int(t_curr // 60)}:{int(t_curr % 60):02d} "
f"击杀差突变:{side}连获 {abs(delta)} 个人头"
)
))
return points
# ======================== 报告输出 ========================
def print_report(snapshots: List[GameSnapshot],
turning_points: List[TurningPoint],
video_duration: float):
"""打印分析报告"""
print("=" * 60)
print(" LOLM 对局分析报告")
print("=" * 60)
# 1. 对局概况
print(f"\n📊 对局概况")
print(f" 视频时长: {int(video_duration // 60)} 分 "
f"{int(video_duration % 60)} 秒")
print(f" 采集快照数: {len(snapshots)}")
valid_gold = [s for s in snapshots if s.gold_diff is not None]
if valid_gold:
diffs = [s.gold_diff for s in valid_gold]
max_blue = max(diffs)
max_red = min(diffs)
final = diffs[-1]
print(f" 蓝方最大经济领先: {max_blue:+,d}")
print(f" 红方最大经济领先: {max_red:+,d}")
print(f" 最终经济差: {final:+,d} "
f"({'蓝方' if final > 0 else '红方' if final < 0 else '持平'})")
# 2. 转折点列表
print(f"\n⚡ 优势/劣势转折点 ({len(turning_points)} 个)")
if turning_points:
for i, tp in enumerate(turning_points, 1):
print(f" [{i}] {tp.description}")
else:
print(" 本局未检测到明显的优势/劣势转折")
# 3. 经济差时间线摘要
if valid_gold:
print(f"\n📈 经济差变化时间线(每30秒采样)")
sample_interval = 30
last_t = -sample_interval
for s in valid_gold:
if s.timestamp_sec - last_t >= sample_interval:
bar_len = min(abs(s.gold_diff) // 500, 30)
if s.gold_diff >= 0:
bar = " " * 15 + "█" * bar_len + f" 蓝+{s.gold_diff}"
else:
bar = " " * max(0, 15 - bar_len) + "█" * bar_len
bar += f" 红{s.gold_diff}"
print(f" {int(s.timestamp_sec // 60):02d}:"
f"{int(s.timestamp_sec % 60):02d} {bar}")
last_t = s.timestamp_sec
print("\n" + "=" * 60)
# ======================== 入口 ========================
def analyze_lolm_recording(video_path: str,
interval_sec: float = 2.0,
gold_threshold: float = 3000,
kill_threshold: int = 3):
"""
主入口:分析LOLM录屏,输出关键数据与转折点。
Args:
video_path: 录屏文件路径
interval_sec: 抽帧间隔(秒)
gold_threshold: 经济差突变阈值
kill_threshold: 击杀差突变阈值
"""
print(f"正在加载视频: {video_path}")
# Step 1: 数据采集
collector = LOLMDataCollector(
video_path, interval_sec=interval_sec, use_ocr=True
)
snapshots = collector.collect()
print(f"采集完成,共 {len(snapshots)} 个时间点")
# Step 2: 转折点检测
detector = TurningPointDetector(
gold_swing_threshold=gold_threshold,
kill_swing_threshold=kill_threshold,
)
turning_points = detector.detect(snapshots)
# Step 3: 输出报告
print_report(snapshots, turning_points,
collector.extractor.duration_sec)
return snapshots, turning_points
if __name__ == "__main__":
import sys
if len(sys.argv) < 2:
print("用法: python lolm_analyzer.py <录屏文件路径>")
sys.exit(1)
analyze_lolm_recording(
video_path=sys.argv[1],
interval_sec=2.0,
gold_threshold=3000,
kill_threshold=3,
)
```
关键说明
屏幕区域校准(最重要的一步)
ScreenRegions 中的归一化坐标是模板值,不同设备、不同游戏版本、不同UI设置下布局会不同。例如LOLM的操作界面本身就比端游更密集,四技能加双召唤师技能使布局有较大差异。你需要:
1. 用 cv2.imread 打开一帧截图,在画图工具中量取目标区域的像素坐标
2. 将像素坐标除以画面宽高得到 (x1/W, y1/H, x2/W, y2/H)
3. 更新 SCOREBOARD 和 TOWER_AREA 中的值
OCR精度优化
游戏中特效、半透明UI容易导致识别错误,实践中常结合YOLO与PaddleOCR提升鲁棒性——YOLO负责定位UI元素位置,OCR只对裁剪出的干净区域做文字识别。如果当前OCR精度不够,建议:
· 先用YOLO训练一个小模型检测计分板区域(比固定坐标更稳健)
· 对OCR预处理加入形态学操作去除细小噪点
· 对识别结果做时序平滑(连续帧取中位数)
转折点检测算法
核心逻辑来自LoL-MDC的关键事件提取算法:对每个事件计算其前后的胜率变化 Δ,按 Δ 降序取Top-N作为关键事件。本代码中,经济差和击杀差的变化量扮演了类似“胜率变化”的角色。你可以进一步用逻辑回归模型,基于经济差、击杀差、推塔数训练一个简易的“蓝方胜率预测器”,用胜率曲线的拐点来定位转折点,会比单纯阈值判定更准确。
扩展方向
· 小地图目标检测:用YOLO检测小地图上的英雄位置,可以分析团战发生时的站位优劣。已有研究表明,合成数据预训练+真实回放数据微调的迁移学习方案在小地图英雄检测上可达到0.588 mAP
· 事件过滤层:类似NiceShot AI的做法,用OCR识别“回放中”“观战中”等状态文字,过滤掉非实时对局的帧
· 转折点视频片段导出:检测到转折点后,用FFmpeg截取前后30秒的片段,方便复盘
禁止商用转载