简介:一份面向LOL英雄联盟角色检测任务的高质量标注数据集资源,素材规模约3000张游戏截图,覆盖队友小兵、己方小兵、敌方小兵、防御塔、LUX、VAYNE 6个类别,标注框总数达24665个。资源包共2000个文件,核心为1999个Pascal VOC格式的XML标注文件,另附1个说明TXT,整体压缩后约135.85MB,便于直接下载与解压使用。标注过程采用labelImg工具按矩形框绘制,类别分布清晰,例如敌方小兵10973框、友方小兵7339框、VN 3669框等,可用于目标检测模型的训练、验证与对比实验,也可作为YOLO、SSD等算法的入门练习数据。该数据集已有464人学习,适合游戏智能、目标检测算法研究者及深度学习初学者用于实践。
1. 做游戏画面目标检测,为什么这份LOL英雄联盟数据集值得拆开细看
录屏回放、直播画面分析、赛事数据复盘这类应用,第一步永远是让程序在一帧游戏画面上回答“谁在哪”:哪个是队友、哪边是敌方小兵、防御塔在什么位置。LOL英雄联盟角色检测数据集这份3000张、6类的目标检测数据集,做的事情正是这件事——把召唤师峡谷画面里的核心单位切成6个可直接训练的语义类别,让模型学会在复杂UI背景里定位真实游戏单位。它适合两类人:想用YOLOv8训练自己的目标检测数据集、又不想从爬图和手动标注入门的新手,以及要评估“游戏画面AI视觉方案能不能落地”的开发者。先说结论:这类项目的难点从来不在网络结构,而在标注语义、类别不均衡和小目标漏检这些数据侧的坑,后面每一章都是冲着这些坑去的。
2. 把LOL检测数据集拆开:6类语义、标注习惯与解压后的三件事
2.1 六类目标的语义边界与标注习惯
标题里写着6类,括号中点名的纯语义只有5个:队友、己方小兵、敌方小兵、防御塔、韦恩。这种打包方式在游戏数据集里不罕见:单个英雄“韦恩”被单独拆出来,说明原始需求大概率是“针对某个英雄的专项检测”,比如对线期预警或者高光集锦切片;剩下的敌方英雄统一收进一个兜底类,比如enemy_hero或other_hero,总数这才凑成6。至于是不是这样,解压后先看包内classes.txt或readme,这一步别省,后面所有环节都要依赖这个类名清单。
六类目标的语义边界值得在动手前先对齐,否则后面画框、改标注都会反复。队友指的是己方英雄单位,韦恩如果出现在敌方阵营,应该归进兜底类而不是标成自己的名字;己方小兵和敌方小兵包含远程兵、近战兵和炮车,游戏里三者的外形不同,但这个数据集把它们算成一类,标注时不需要区分;防御塔从一塔到高地塔都算,标注时框塔身加底座即可,别把攻击弹道线框进去,那是典型的多余像素。
标注习惯上,游戏画面数据集常见两种画法:一种只框英雄或小兵的本体,另一种把头顶血条也框进去。我偏向后者。游戏单位本体小、技能特效又容易遮挡,血条是逐帧稳定的可见特征,模型早期收敛明显更快。但代价是把血条框进来后,模型很容易抓住“蓝色条=队友、红色条=敌方”这条颜色捷径,后面第5章单独讲这个坑。
这种垂直场景数据集的路数和通用Benchmark不一样。像CrowdHuman这类密集人群数据集里最头疼的遮挡问题,在兵线上同样存在;像CCPD这种只做车牌的垂类数据集,价值恰恰在于把单一场景做到可用;而ReID数据集里“同一个身份跨帧”的思路,可以迁移到“同一个英雄在连续几十帧里保持ID”这件事。游戏画面还有一个优势:所有目标都是同一套引擎渲染出来的,外观分布比自然场景稳得多,不像鸟类目标检测数据集要考虑姿态、光照、遮挡的无限变化。所以只要标注规范对,3000张完全可以训出一个能用的baseline。
2.2 解压后的第一件事:清点每类框数
拿到zip,先别急着转格式开训。我会先写一个最短脚本把每类目标的标注框数量统计出来,确认类别分布和标题描述是否一致。以常见的VOC XML结构为例:
from glob import glob import xml.etree.ElementTree as ET stats = {} for xml_file in glob("Annotations/*.xml"): root = ET.parse(xml_file).getroot() for obj in root.findall("object"): name = obj.find("name").text stats[name] = stats.get(name, 0) + 1 for name, cnt in sorted(stats.items(), key=lambda x: -x[1]): print(f"{name}: {cnt}")这段代码的逻辑很简单:遍历Annotations目录下所有XML,每个object节点对应一个标注框,取name字段累加计数,最后按数量倒序打印。如果你解压出来的是YOLO txt,把解析段换成“读txt第一列的类别ID,再对照classes.txt映射成类名”;如果是COCO JSON,用json库读annotations里的category_id同样能算。这个统计的最大价值是暴露出类别不均衡程度:游戏画面里小兵一帧就是十几个,防御塔一局也就那几座,我见过不少同类数据集里塔类占比不到百分之五。记住这个数字,第5章还要用到。
注意:这个统计脚本跑完,如果输出里出现了不在classes.txt里的名字,说明标注文件里有脏数据,先清理再继续,否则后面转格式时这些目标会被静默丢掉。
2.3 解压后的第二件事:把标注画回图片
统计数字只能说明有多少框,不能说明框得准不准。我一般会随机抽20到30张图,把标注框画回去逐张看。这个习惯能在半小时内发现一半的标注问题,尤其适合判断框有没有紧贴目标、类别名和框内目标对不对得上。
import cv2 import xml.etree.ElementTree as ET img = cv2.imread("images/0001.jpg") root = ET.parse("Annotations/0001.xml").getroot() for obj in root.findall("object"): name = obj.find("name").text bbox = obj.find("bndbox") xmin = int(float(bbox.find("xmin").text)) ymin = int(float(bbox.find("ymin").text)) xmax = int(float(bbox.find("xmax").text)) ymax = int(float(bbox.find("ymax").text)) cv2.rectangle(img, (xmin, ymin), (xmax, ymax), (0, 255, 0), 2) cv2.putText(img, name, (xmin, max(ymin - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite("check_0001.jpg", img)这里有两个容易翻车的细节:XML里坐标字段是字符串,必须先用float再int转一次,否则OpenCV画框直接报类型错误;框坐标有时会越出图片边界,画的时候先做clip,训练前在转换脚本里还要钳制一次。画完看什么?一看框是否紧贴目标,二看类别名和框内目标是否对得上,三看有没有把技能特效、弹道线、装备图标这些非目标物框进来。我见过最典型的问题是英雄脚下阴影被切掉一半,导致框底边不在身体底部,这类系统性偏差会在小目标上放大漏检。
2.4 解压后的第三件事:统一命名与镜像目录
第三件事是统一文件命名。图片和标注文件必须同名同前缀:0001.jpg对应0001.txt或0001.xml,后缀可以不同,前缀不能错位。同时把jpg、jpeg、png统一成一种格式,避免后续训练脚本里图像解码分支一堆。如果包里混着中文文件名,建议一并改成纯英文数字前缀,YOLO生态对中文路径的兼容性在Windows上经常埋雷。这三件事做完了,再进入转换环节,后面每一分钟都不会浪费在数据错位上。
3. 统一成YOLO格式:从VOC/COCO到txt的转换脚本与三个边界坑
3.1 为什么先统一成YOLO txt再动手
常见的目标检测标注格式有三种:VOC XML每个图像一个XML文件,COCO JSON所有标注收在一个JSON里,YOLO txt每张图一个txt、每行一个框。拿到手先转YOLO格式,不是因为YOLO一定最好,而是因为yolov5和yolov8的训练生态默认吃txt,data.yaml只要指定图像目录,标签目录按相同结构放好就能跑。另一个原因是排查成本低:一个txt就是一张图的标注,格式错了打开看一眼就知道问题在哪,COCO JSON一旦结构出错,报错信息能把人绕晕。
| 格式 | 形态 | 适合做什么 | 不适合什么 |
|---|---|---|---|
| VOC XML | 一图一XML,坐标绝对像素 | 人工检查、通用工具多 | 训练直接吃麻烦、文件碎 |
| COCO JSON | 全部标注一个JSON | 大规模数据集、跨任务复用 | 类别调整麻烦、结构复杂 |
| YOLO txt | 一图一txt,每行类别+归一化坐标 | YOLO系训练默认、增强管线友好 | 没有类别字典时难读 |
另外一个连带考虑是后续的旋转目标检测需求。像遥感那边用mmrotate训练DOTA数据集时会做旋转框,因为舰船、飞机密集排列且方向任意;游戏画面里的英雄、小兵、防御塔都是直立单位,水平框完全够用,没有必要引入OBB标注,省掉一整套标注格式转换的麻烦。
3.2 VOC XML转YOLO txt:转换脚本与参数说明
假设压缩包里是VOC XML结构,我一般用下面这个脚本统一转换。类名按我这边的命名习惯演示:ally_hero对应队友,ally_minion对应己方小兵,enemy_minion对应敌方小兵,turret对应防御塔,vayne对应韦恩,enemy_hero是敌方英雄兜底。实际类名以你的压缩包为准,关键是顺序和data.yaml保持一致。
import os import xml.etree.ElementTree as ET def voc_to_yolo(xml_path, out_dir, class_names): tree = ET.parse(xml_path) root = tree.getroot() size = root.find("size") img_w = int(size.find("width").text) img_h = int(size.find("height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_names: continue cls_id = class_names.index(name) box = obj.find("bndbox") xmin = float(box.find("xmin").text) ymin = float(box.find("ymin").text) xmax = float(box.find("xmax").text) ymax = float(box.find("ymax").text) x_center = (xmin + xmax) / 2 / img_w y_center = (ymin + ymax) / 2 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{cls_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") if not lines: return out_path = os.path.join(out_dir, os.path.splitext(os.path.basename(xml_path))[0] + ".txt") with open(out_path, "w") as f: f.write("\n".join(lines)) xml_dir = "Annotations" out_dir = "labels" os.makedirs(out_dir, exist_ok=True) class_names = ["ally_hero", "ally_minion", "enemy_minion", "turret", "vayne", "enemy_hero"] for xml_file in os.listdir(xml_dir): if not xml_file.endswith(".xml"): continue voc_to_yolo(os.path.join(xml_dir, xml_file), out_dir, class_names)核心逻辑是三步:从XML的size节点拿到图像宽高,然后对每个object取类名和bndbox绝对坐标,最后把绝对坐标换算成归一化的中心点坐标和宽高。换算公式是x_center等于xmin加xmax的一半再除以图像宽,宽直接除以图像宽,高度同理。需要特别注意class_names这个列表的顺序,它决定了每个类名对应的ID,后续data.yaml里的names顺序必须和它完全一致。
注意:顺序一旦错位,训练出来的模型类别全是乱的,而且框的位置是对的,单看图像很难发现。
参数上值得调整的有三处:一是不在class_names里的目标目前是跳过,如果这类目标数量多,应该回头核对类名拼写,比如“turret”被标成“tower”就是最常见的错位;二是归一化后做了min取1.0的钳制,防止坐标越界导致训练时算loss出现负宽高;三是空标注文件直接return,但训练时这类图片最好还是保留,YOLO会当成纯背景处理,对抑制误检有帮助。转完后对比labels目录的txt数量和Annotations目录的xml数量,差太多说明有类名被整体跳过。
3.3 训练集/验证集划分:80/20与稀有类保护
转完格式,接下来划分数据集。常见做法是80%训练、20%验证,游戏截图同质化高,这个比例够用。我一般用脚本随机划分,同时保证同一局游戏的不同帧尽量不拆散,否则验证集会因为见过同一局而虚高。
import os import random from collections import defaultdict random.seed(42) images = [f for f in os.listdir("images") if f.endswith(".jpg")] random.shuffle(images) split_idx = int(len(images) * 0.8) train, val = images[:split_idx], images[split_idx:] def label_names(img_name): txt = os.path.join("labels", os.path.splitext(img_name)[0] + ".txt") if not os.path.exists(txt): return set() with open(txt) as f: return {line.split()[0] for line in f} val_stats = defaultdict(int) for img in val: for cid in label_names(img): val_stats[cid] += 1 print("val class distribution:", dict(val_stats))这个脚本里random.seed(42)固定随机种子,保证二次划分结果可复现;label_names函数统计验证集里每个类别出现的标注框数量,用来检查稀有类是否被均匀覆盖。一个常见的翻车点是:防御塔只在某几局排位里出现,随机划分后这些局全进了训练集,验证集里一个塔都没有,最后mAP看起来不错,上线一测塔全丢。所以划分前先看第2章的统计,如果稀有类的图像只有几十张,可以先手动把它们按比例放进两个集合,再随机填充其余图片。
另外强调一点数据泄漏:任何复制增强都要在划分之后做,绝不能让同源的增强副本同时出现在训练集和验证集。复制粘贴的增强图如果进了val,模型等于开卷考试,报告出来的精度没有任何参考价值。
4. 用YOLOv8训练LOL检测数据集:data.yaml、选型与超参数
4.1 先锁data.yaml的names顺序
训练前第一步不是选模型,而是把data.yaml写对。YOLOv8和YOLOv5在这一点上完全一致,都是通过data.yaml同时指定图像目录、类别数和类别名。
train: dataset/images/train val: dataset/images/val nc: 6 names: 0: ally_hero 1: ally_minion 2: enemy_minion 3: turret 4: vayne 5: enemy_herotrain和val指向第3章划分出来的目录,nc必须和names列表长度一致,names顺序必须和第3章转换脚本里的class_names顺序一致。这里顺序不是靠自觉,而是靠文本对比:打开两个文件,一行一行对。我在这上面吃过亏,class_names里turret排第三、data.yaml里turret排第四,模型训完所有塔的预测类别都错了一位,测试时还没发现,因为框的位置是对的,只是类别标签错了。后来我养成习惯:转换完先打印一次txt里出现过的类别ID集合,再和data.yaml的names对齐,确保ID集合是0到5且每个都有类名。
4.2 YOLOv8训练命令与网络选型
写好后直接命令行训练。以1080p游戏截图、单卡12GB显存为例:
yolo detect train \ data=data.yaml \ model=yolov8n.pt \ imgsz=640 \ epochs=120 \ batch=16 \ workers=4 \ patience=20 \ project=run_lol \ name=yolov8n_640参数说明:model用yolov8n.pt做预训练权重,n是nano,速度最快、显存最小,适合先验证pipeline;后面想提精度再换成yolov8s.pt或yolov8m.pt,显存占用依次上升。imgsz=640是第一轮baseline的常用值,如果小目标漏检严重,再提到1280,这一点第5章会展开。batch=16是12GB显存在640输入下的相对稳妥值,显存不足先降到8。patience=20表示连续20轮验证集没提升就早停,避免挂机浪费算力。训练完看run_lol/yolov8n_640目录,results.png包含loss、mAP、PR曲线,先看val/box_loss是否还在降,再看mAP50有没有往上走,两个指标都正常再谈后续优化。
4.3 类别不均衡时不要一上来就调loss权重
第2章统计过每类框数,如果发现防御塔或者韦恩的框数只有小兵的零头,训练前先别急着动损失权重——YOLOv8的类别权重需要自己改训练脚本,调试成本高,而且容易把模型带偏。我一般会按这个顺序处理:第一步过采样,把包含稀有类的图片在训练集里复制两到三份,复制时可以配合同一份图像做HSV颜色扰动、轻微缩放和翻转,相当于人工扩充;第二步针对稀有类做几何增强,防御塔大多数是静态建筑,水平翻转、小角度旋转都能用;第三步才是考虑改损失权重。顺序反过来很容易出问题:权重调太大,稀有类是拉起来了,小兵类开始疯狂误检,验证集上一片红。
分类别过采样还有一个隐藏好处:它顺带缓解了“一帧里小兵框太多导致loss被小兵主导”的问题,让模型每一轮更新时都能看到足够多的塔和英雄。3000张图的量级下,过采样加增强通常比换更大的backbone更有效,因为游戏画面目标分布稳定,数据侧多做文章比堆参数更划算。
5. 避坑指南:LOL游戏画面检测的5个高频翻车点
5.1 漏检重灾区:远处的韦恩和敌方小兵只有十几个像素
现象:训练时mAP看着正常,一到实际录屏,画面远端出现的英雄框直接消失,敌方小兵在镜头拉远时一整片漏掉,检测框数量比真值少了将近一半。
原因:游戏截图是1080p全尺寸,远处角色在图像里的高度经常只有二三十像素,imgsz=640下采样后只剩十几个像素,骨干网络最后一层特征图上目标只有1到2个格子,特征几乎被池化抹平。
解决:第一优先把imgsz提到1280,训练和推理都用同一分辨率,实测召回率能明显回升;如果还不行,用切图推理,把1080p截图按640窗口带重叠地切成4到6块,分别推理再合并,重合区域用NMS去重。代价是推理时间乘以切块数量,离线分析场景可以接受,实时场景要掂量一下。调参时先跑一轮1280看漏检改善幅度,再决定要不要上切图,别一上来就全上。
5.2 颜色捷径:模型只认蓝条红条,不认英雄和小兵
现象:验证集上己方小兵大量被预测成“队友”,敌方小兵被预测成“敌方英雄”,框的位置没错,类别全串了。
原因:蓝方血条全是蓝色,红方血条全是红色,颜色是整个数据集里最稳定的判别特征。模型不需要学英雄外形就能把蓝条目标全归成队友,把红条目标全归成敌方,损失已经很低,英雄和小兵的形状差异反而没学到。
解决:训练时把HSV颜色增强的强度调大,我一般会把hsv_h从默认的0.015调到0.05左右,尤其加大色调H的扰动幅度,让血条颜色在同一类里反复变化,逼模型回去学外形。标注上也尽量统一,包含血条的话所有同类目标都包含,不要有的框带血条有的不带。另一个难例思路是收集带增益状态、不同皮肤、不同阵营外观的目标图片单独校验,这类图片最容易暴露模型到底在学什么。
5.3 小地图误检:缩略图标成了新的正样本
现象:模型在画面边角的小地图区域输出大量高置信度框,把英雄图标、小兵图标甚至防御塔图标都当成真实目标,主战场反而漏掉一部分。
原因:小地图上的角色图标尺寸小但特征清晰,和真实单位的语义边界在模型眼里是模糊的,尤其图标和真实英雄颜色一致时极其容易激活。
解决:标注阶段就在小地图区域打上“忽略”标记,或者预处理时直接把固定的小地图区域裁掉,从根上消除这一条;推理阶段如果业务只关心主战场画面,同样直接裁剪。还有一个更省事的办法:把带小地图的截图里小地图区域填充成纯色背景再入train,模型见过这里是空白之后,误检会明显下降。这个坑几乎每个游戏画面检测项目都会遇到,早处理早干净。
5.4 类别不均衡:防御塔类拉不动Recall
现象:训练曲线正常,confusion_matrix里turret那一行几乎全被预测成别类或直接漏检,mAP卡在某个值上不去,跑多少轮都不动。
原因:一局游戏防御塔只有个位数,3000张图里塔类框占比可能低于百分之五,损失函数长期被小兵和英雄主导,塔类的梯度信号被淹没。
解决:回到第2章的统计,按框数排序看看倒数第一第二是谁;对含塔图片做2到4倍过采样配增强;还不行再单独给塔类提高损失权重。判断标准很简单:每训完一轮看一眼验证集塔类的召回率,没动就是增强没起作用,动了继续。这个问题不会自己消失,越晚处理,后面调参越痛苦,所以我把第2章的统计当作开工前必做项。
5.5 mAP很高,录屏实测却翻车
现象:验证集mAP@0.5到了0.9以上,剪一段真实的游戏录屏逐帧测,结果框乱跳、闪烁、时有时无,远不如验证集表现。
原因:训练截图的采集设备和录屏码率不一致,边缘锯齿和压缩噪声不同;游戏UI版本、镜头缩放、画质设置也可能不一样。这些都是典型的域差异,验证集来自同一分布,所以完全暴露不出来。
解决:训练时加入轻度的高斯模糊、JPEG压缩模拟增强,让模型对画质退化不那么敏感;实测用与训练采集一致的分辨率和画质设置;同时定期抽最新的录屏帧回流训练集当难例。游戏更新版本后UI变动,旧的测试结论要重新跑一遍,这不是玄学,是常态。数据集的保质期比你想象得短,游戏项目尤其明显。
6. 从mAP到录屏实测:模型能不能用的最后一道验证
6.1 训练完先看结果目录里的三样东西
训练结束后,run_lol/yolov8n_640目录里会留下confusion_matrix.png、results.png和val_batch1_pred.jpg。我的习惯是先看混淆矩阵而不是mAP数值:对角线够不够亮,turret那一行是不是稀稀拉拉落到别的类上,这能直接告诉你第5章哪种翻车在验证集上已经出现;再看val_batch1_pred.jpg里预测框和真值框的重合、有没有多余的假框。之后对一张训练时没见过的截图单独预测:
yolo detect predict \ model=run_lol/yolov8n_640/weights/best.pt \ source=test_shot.jpg \ imgsz=1280 \ conf=0.25conf=0.25是偏低一点的阈值,游戏里小目标多的场景下,宁可多看几个误检框,也不要把真目标漏掉。误检可以靠帧间去抖处理。
6.2 录屏抽帧测试加一个帧间IoU去抖
单张图测试过关只是第一步,录屏测试才能暴露框乱跳的问题。把录屏按每秒抽几帧做批量推理,统计漏检率和误检率的波动;对同一目标跨帧的稳定性,我会用一个极简的帧间IoU去抖逻辑:
def iou(a, b): x1, y1 = max(a[0], b[0]), max(a[1], b[1]) x2, y2 = min(a[2], b[2]), min(a[3], b[3]) inter = max(0, x2 - x1) * max(0, y2 - y1) area_a = (a[2] - a[0]) * (a[3] - a[1]) area_b = (b[2] - b[0]) * (b[3] - b[1]) return inter / (area_a + area_b - inter + 1e-6)用法是:对相邻两帧中类别相同的检测框算IoU,IoU大于0.3就认为是同一个目标,给它继承上一帧的ID;单帧出现且跟前后帧都没有匹配的框,暂存两三帧再决定是否输出,连续匹配不上就当作误检抑制掉。这个函数只是骨架,实际要加上类别约束和消失帧数的计数,但已经是把YOLO输出从“一帧一堆框”变成“一路稳定框”最省事的办法。
做这类游戏数据集项目,我最后悔的一次是拿到数据就直接开训,跑了两天才发现类别ID在转换脚本和data.yaml里错了一位,返工重训等于白烧两天电。后来我把第2章那套清点、抽检、画框回看当成铁律,省下的时间远超多跑几次实验。数据侧的坑永远比模型侧的坑贵,希望帮到你。
本文还有配套的精品资源,点击获取