news 2026/9/27 5:05:12

Python基于录屏的LOLM关键数据与优劣势转折点自动分析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Python基于录屏的LOLM关键数据与优劣势转折点自动分析

基于录屏的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秒的片段,方便复盘

禁止商用转载

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/27 4:59:03

小红书客服系统:React底层Event注入,表单毫秒级填充

小红书客服系统&#xff1a;React底层Event注入&#xff0c;表单毫秒级填充 电商这行&#xff0c;谁的速度快谁吃肉。小红书的自动回复与客服&#xff0c;是店群运营中最耗人力也最容易出错的环节。 店群客服是纯人力消耗战。一个店日均50条咨询&#xff0c;20个店就是1000条。…

作者头像 李华
网站建设 2026/9/27 4:56:17

通信原理期末复习攻略:基于华工试卷PPT的考点解析与解题套路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/27 4:49:51

小红书店群自动化管理系统:C++底层指纹伪装,抹除自动化特征

小红书店群自动化管理系统&#xff1a;C底层指纹伪装&#xff0c;抹除自动化特征 在电商圈混久了就会发现&#xff0c;小红书的自动化上架&#xff0c;是店群运营中最耗人力也最容易出错的环节。 手动上架一个商品从填写标题、上传主图、设置SKU、填写详情到发布&#xff0c;熟…

作者头像 李华
网站建设 2026/9/27 4:48:10

K型热电偶低成本高精度测温设计实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华