简介:面向LOL英雄联盟角色检测任务,数据集包含3000张对局截图,提供Pascal VOC与YOLO两种标注格式,覆盖己方小兵、敌方小兵、己方防御塔、敌方防御塔、LUX、VAYNE共6类目标,总计24665个标注框,适合训练YOLO系列、Faster R-CNN等模型,也可作为游戏AI视觉识别的教学样例。压缩包共2000个文件(以XML标注文件和说明TXT为主),大小约135.85MB;标注由labelImg以矩形框完成,类别划分清晰,每张图片均对应VOC和YOLO两套标注,可直接用于模型训练与评估。资源中还包含各类别框数统计,便于分析样本分布、做数据均衡处理。目前已有464人学习下载,适合目标检测学习者与LOL图像分析开发者快速获取已标注数据,有效节省手动标注时间。
1. LOL目标检测数据集:先搞清楚它长什么样、能拿来干什么
搜“LOL英雄联盟角色检测数据集”的人,多数不是玩家,而是缺一个中间尺度、能快速验证目标检测训练流程的数据集。这个包号称3000张、6类,涵盖队友、己方小兵、敌方小兵、防御塔、韦恩,既做了目标检测里最基础的多类识别,也命中现实场景里最难的两件事:小目标密集和类别不均衡。把检测框喂给对局分析或行为模型,是这类数据最常用的落地方式。适合想用YOLOv8跑通自定义目标检测全流程的入门者、做游戏视频分析但不想自己打标的工程师、以及关注小目标精度的算法同学。3000张到底够不够,后面从分布讲到训练再讲到排错,你会得到一个比“够”或“不够”更具体的答案。
2. 3000张6类的构成拆解:类目语义、分布陷阱与“韦恩”类问题
2.1 六类目标的语义和容易混淆的视觉特征
标题括号里写的是队友、己方小兵、敌方小兵、防御塔、韦恩,一共五个名字却号称六类。这通常不是标题漏写,而是包里还有“敌方英雄”这个第6类,只是命名上比较零散——有的标注成enemy_hero,有的直接按具体英雄名拆。我拿到这种包的第一步是数labels文件夹里的class个数,而不是对着文件名猜。这一步看似简单,实际卡过很多人:一张截图里“队友”和“敌方英雄”站在同一条兵线上,头顶血条分色容易被标注工具合并成一个类,后续训练出来的模型分不清敌我。
六个目标在游戏截屏里的物理形态差异很大,先做一张速查表,后面所有转换脚本和增强策略都按这里的类ID对齐。
| 包内常见名 | 含义 | 典型视觉特征 | 最容易混淆的对象 |
|---|---|---|---|
| ally_hero / 队友 | 本方英雄角色 | 人物模型大、跟随玩家视角 | 敌方英雄 |
| ally_minion / 己方小兵 | 己方推进的小兵 | 体积小、密集、蓝色血条 | 敌方小兵 |
| enemy_minion / 敌方小兵 | 敌方推进的小兵 | 体积小、密集、红色血条 | 己方小兵 |
| tower / 防御塔 | 防御塔或基地塔 | 塔身细长、框比较大 | 塔下小兵 |
| vayne / 韦恩 | 特定射手英雄 | 单角色、弩箭配色 | 队友模型 |
| enemy_hero / 第6类 | 敌方英雄 | 出现频率低、模型较大 | 队友 |
注意这个分类轴并不统一:前四类是“阵营+单位类型”,韦恩是具体英雄名,第6类又是阵营粒度。这意味着模型对韦恩的学习本质上是在记一套固定配色和轮廓,换皮肤、换版本都可能失效。做对局分析的人常把这类混合分类数据当成“角色识别”的第一阶段,后续再接一个分类头专门区分英雄,这是更稳的工程方案。
2.2 3000张的分布与长尾:不能只按随机比例切分
这种游戏截图数据集,大概率是从回放录像抽帧生成的。按这个来源推断,统计特征会表现为:每张图的目标数差异巨大,有的整张图只有两个英雄,有的兵线交汇处有二三十个小兵;韦恩这个单类目标占框总量往往极低,三千张里可能只有几百个韦恩框;帧与帧之间高度相关,同一波团战里截出来的图背景几乎一致。
这对训练的影响比想象大。目标检测训练最基础的动作是train/val随机切分,但在这个场景下,随机切分会让val“偷看”训练分布,造成mAP虚高。比数据泄漏更直观的是类别不均衡:YOLO系模型对每个类别独立计算BCE loss,模型发现“韦恩”很少出现,就会把这一类的预测概率整体压低,训练日志里对应AP掉到接近0。所以拿到包先做一次框级统计,我一般会写几十行脚本,打印每个类的框数、每张图目标数、框面积分布,看一眼就能决定要不要做采样。
2.3 标注格式与坐标归一化:统一到YOLO坐标系再做训练
常见的游戏截图数据集包,标注格式可能是VOC XML、COCO JSON或YOLO txt。无论包内用什么格式,第一步都先统一成YOLO的归一化bbox,因为后接YOLOv5/YOLOv8最省事。YOLO txt每行是class_id x_center y_center width height,五列全部是相对于原图宽高的比值。坐标换算只有一个公式要记:
- x_center = (xmin + xmax) / (2 * image_width)
- y_center = (ymin + ymax) / (2 * image_height)
- width = (xmax - xmin) / image_width
- height = (ymax - ymin) / image_height
这里有一个容易想歪的点:如果截图带UI血条和技能栏,转换时不要先裁掉UI再算坐标,而是直接按原图宽高归一化。因为训练时YOLO内部会对输入做letterbox,缩放填充是在代码里完成的,标注存的必须是原始图像坐标系下的归一化坐标。裁图改尺寸属于预处理,要改就整条数据链路一起改,只改标注文件会造成框位整体偏移。
3. 把LOL数据集跑成YOLOv8基线:解压检查、格式转换、数据切分
3.1 开箱检查:先看目录结构,再写任何脚本
解压zip后不要急着训练,先摸清目录长什么样。常见做法是images和labels平铺,但也有的包在子目录里套train/val,标注文件后缀可能是txt、xml或json。用几行命令把结构扫一遍:
unzip 'LOL角色检测数据集.zip' -d lol_dataset find lol_dataset -maxdepth 2 -type d | head -20 ls lol_dataset/images | head -5 ls lol_dataset/labels | head -5 wc -l lol_dataset/labels/*.txt | tail -1- find限定了maxdepth 2,只看两层目录,结构特别深的包需要加深深度;
- ls的头几行用来确认命名规律,比如是否按game_id_frame_no这种格式组织;
- wc -l统计label文件行数,能快速知道每张图平均目标数。我见过有人跳过这一步,直接写转换脚本,结果images是png、labels是xml,脚本跑一半报错,浪费的时间比检查多得多。
如果发现labels里的类型和images不是一一对应,先用这个命令找出没有标注的图:
for img in lol_dataset/images/*.png; do label="lol_dataset/labels/$(basename "$img" .png).txt" [ ! -f "$label" ] && echo "$img" done没有bbox文件的图要么删除,要么归到“背景帧”单独存放。游戏截屏里纯背景帧不多,但对后期调阈值和抑制误报很有用。
3.2 把VOC/COCO标注转成YOLO txt:转换脚本与参数说明
如果包内是VOC XML,我一般直接用一个几十行的Python脚本转。核心地方在于类名映射、越界裁剪和输出格式:
import xml.etree.ElementTree as ET import os CATEGORY_ID = { 'ally_hero': 0, # 队友 'ally_minion': 1, # 己方小兵 'enemy_minion': 2, # 敌方小兵 'tower': 3, # 防御塔 'vayne': 4, # 韦恩 'enemy_hero': 5, # 敌方英雄 } def voc2yolo(xml_path, out_path): root = ET.parse(xml_path).getroot() width = int(root.find('size/width').text) height = int(root.find('size/height').text) lines = [] for obj in root.findall('object'): name = obj.find('name').text if name not in CATEGORY_ID: continue 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 / width y_center = (ymin + ymax) / 2 / height w = (xmax - xmin) / width h = (ymax - ymin) / height # 防止浮点误差或标注出界导致坐标跑到[0,1]之外 x_center = min(max(x_center, 0), 1) y_center = min(max(y_center, 0), 1) w = min(w, 1 - x_center) h = min(h, 1 - y_center) lines.append(f"{CATEGORY_ID[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(out_path, 'w') as f: f.write('\n'.join(lines))这段代码有几个关键点。CATEGORY_ID的映射必须和后面data.yaml保持一致,顺序错了整个训练就废了。输出格式里class_id在最前面,YOLOv8读取时不会帮你做类名容错。最后两行对坐标做clip是为了吞掉个别标注出界的脏数据,但不要依赖这一点去掩盖粗糙标注,后面你还是得回去查那些xmax大于图像宽度的XML。
COCO JSON的转换逻辑类似,只是字段不同:annotations里的bbox字段本身就是[x, y, width, height],category_id已经存在,不用再拼字符串。唯一要注意的是COCO的坐标是绝对像素,同样要除以原图宽高做归一化。
3.3 数据切分:按帧随机切分是回放型数据的泄漏源头
转换完成后的第一步切分,很多人直接random.shuffle再按比例切。对独立拍摄的照片数据集这样没问题,但回放抽帧型数据强烈建议先分组再切。我遇到的一个真实案例:训练集里第15帧和第16帧几乎一样,val里恰好也抽到了同波团战的第17帧,验证mAP高出实际水平一大截,模型一上完整回放就露馅。
先按文件名前缀分组,再按组随机切分,代码不复杂:
import os import random from collections import defaultdict img_dir = 'lol_dataset/images' groups = defaultdict(list) for fname in sorted(os.listdir(img_dir)): game_id = fname.split('_')[0] # 假设文件名是 gameid_frameno.png groups[game_id].append(fname) keys = list(groups.keys()) random.seed(42) random.shuffle(keys) split = int(len(keys) * 0.8) train_txt, val_txt = 'train.txt', 'val.txt' for out_txt, subset_keys in [ (train_txt, keys[:split]), (val_txt, keys[split:]) ]: with open(out_txt, 'w') as f: for key in subset_keys: for fname in groups[key]: f.write(os.path.join(img_dir, fname) + '\n')这里的group key是按文件名前缀推测的,如果你的文件名不是这个格式,先看3.1的ls输出,找到真正的分组字段。另一个容易忽略的点是训练集里必须保证每个类都有框,尤其是韦恩这种少样本类。如果分组切完后发现某个类的标注全部掉进val,需要把这个组整体挪回训练集,而不是重新随机一次碰运气。
3.4 写data.yaml:类别名和路径经常对不上
模型训练前还要写一个data.yaml,内容极其简单,但80%的人第一次跑都会在这里翻车:
path: /绝对路径/lol_dataset train: train.txt val: val.txt names: 0: ally_hero 1: ally_minion 2: enemy_minion 3: tower 4: vayne 5: enemy_herotrain和val字段指向3.3生成的txt文件路径,不是images目录本身。names的键是CATEGORY_ID里写死的数字,顺序不能乱。path写绝对路径最稳,但换机器后要改,我一般会在训练脚本里用os.path.abspath拼出来,避免复制到服务器上路径失效。
4. 训练参数与数据策略:模型选型、输入分辨率、增强开关
4.1 YOLOv8n还是YOLOv8s:3000张数据撑不起大模型
3000张、6类属于典型的小数据集,我建议基线直接跑YOLOv8n,然后对比YOLOv8s,而不是一上来就上l或x。模型容量越大,在这个规模下的过拟合越明显。训练命令可以这样起:
cd ultralytics yolo detect train \ model=yolov8n.pt \ data=/path/to/lol_dataset/data.yaml \ epochs=100 \ imgsz=640 \ batch=16 \ workers=4 \ optimizer=auto \ project=lol_logs \ name=exp001参数按重要性排序:batch=16在16G显存上跑n没问题,换s建议降到8;epochs=100是一个能稳定看到loss平台期的数字,3000张跑300轮反而容易让val loss抬升;optimizer=auto让Ultralytics根据模型自己选AdamW或SGD,不用手动干预。workers=4够用,如果是在Windows上开很多worker容易卡死,调成2更稳妥。
为什么不用更大的模型?有人会觉得游戏截图语义简单,大模型能更快刷高mAP。实际经验是:小模型在这个数据量下泛化更好,部署到视频流里帧率也更高。如果你为了写论文凑对比,可以单独留一条l的线,但作为交付基线,n和s就已经覆盖大部分场景了。
4.2 imgsz设置顺序:先用640跑通,再用1280喂小目标
LOL截屏里的小兵可能只有20x20像素,极端情况下甚至不足10x10。imgsz=640时,小兵在降采样后可能只剩几个像素,检测器基本看不到纹理,只能靠颜色和位置硬猜。提高输入分辨率是解决小目标最直接的路径,但代价是显存和训练时间。我的实践流程是:
- 先用imgsz=640跑通全流程,确认数据流、loss下降、eval正常;
- 再开一组imgsz=1280的训练,batch降到8或4,对比两个实验的逐类AP;
- 如果显存只有8G,优先保1280而不是保大batch,小目标检测对batch size没那么敏感。
这个顺序很重要是因为可以反向排查问题:640跑挂了,先解决数据问题,再谈小目标精度;直接上1280遇到loss爆炸,你会分不清是分辨率问题还是标注问题。
4.3 增强开关:Mosaic和MixUp在游戏截图的取舍
YOLOv8默认开Mosaic增强,原理是把四张图拼成一张,增加目标上下文多样性。但游戏截图里的目标分布是“高密度小兵”,四张图拼接会让一屏里出现几十上百个框,大量目标被切到拼接缝上,标注框变成残框,训练信号反而变脏。对小目标来说,Mosaic还会把目标缩得更小,加剧检测难度。MixUp类似,它是把两张图半透明叠加,密集小兵叠加后视觉上完全混在一起,模型学到的是噪声。
所以针对这个数据集,第一批实验我建议先关掉Mosaic和MixUp,用命令行的超参覆盖:
yolo detect train \ model=yolov8n.pt \ data=/path/to/lol_dataset/data.yaml \ imgsz=640 \ epochs=100 \ batch=16 \ mosaic=0.0 \ mixup=0.0命令行传的mosaic和mixup是0到1的概率,0.0表示关闭。这样能先拿到一个干净基线的mAP,后面再单独开Mosaic对比它对6类分别的影响。大多数时候你会发现,关掉Mosaic后小兵的召回率上去了,代价是整体mAP略降,但降的幅度远小于小目标精度的提升。
对韦恩这种少样本类,比MixUp更安全的增强是Copy-Paste:从包含韦恩的图中抠出目标框,随机贴到另一张没有重叠的地方,同时复制对应的标注行。这类图像级复制粘贴不会产生Mosaic那种拼接缝截断问题,对单类目标的数量补充非常有效。增强参数没有银弹,同一个值在不同类上表现可能完全相反,所以实验记录里最好按类拆开看AP,而不是只看一个总mAP。
5. LOL数据集训练避坑:五个高频翻车现象与排查手段
5.1 韦恩类AP接近0
现象:训练结束,韦恩这一类的AP几乎为0,其他五类都正常。
原因通常是两个叠加:框数量太少,模型训练时看到这一类的机会远低于小兵;划分方式把本来就不多的韦恩框全部切进了训练集或验证集,导致验证集里没有真值,AP无法计算。
解决方法分两步。第一步写脚本统计每个类在train和val的框数,确认val里是否至少有一个韦恩框;第二步对包含韦恩的图做过采样,把它们在训练列表里重复出现两到三遍,但不要提前把同一局回放的数据同时塞进train和val,必须沿用3.3的先分组再划分逻辑。做过采样后韦恩AP会从0爬到可看的水平,如果还是不稳,就需要收集更多韦恩出场的对局截图了。
5.2 防御塔被识别成小兵
现象:推理结果里防御塔的置信度很低,大量防御塔位置被标成小兵,尤其是塔下有兵线交汇的时候。
原因在于防御塔周围总有大量小兵进出,标注框互相重叠,NMS会把得分较低的大框直接抑制掉;另一个常见原因是标注质量本身有问题,某些框把塔和小兵一起圈进去了,模型学着学着就把塔学成了“有很多小兵的地方”。
排查时先看一眼标签里防御塔框的宽高比,如果大部分是接近正方形的框,说明标注时把塔身和塔下单位混在一起了。解决方向上不要试图靠调低置信度阈值来救,因为错检的置信度往往也不低。最有效的是把这部分重叠标注修掉,要么缩小塔框只包塔身,要么把塔下的小兵单独标成小兵,保证两个类别不共用同一块像素区域。
5.3 训练loss正常但回放推理漏检严重
现象:val mAP在0.7以上,但拿一局完整回放去跑,团战场景漏检一大片,小兵和英雄频繁闪烁。
原因大概率是训练集和真实推理场景的分布不一致。回放抽帧时如果只抽了特定时间段的帧,模型对这类图的过拟合超过对分布的学习是常有的事。不要对这种问题感到奇怪,训练和推理存在分布漂移是目标检测的老问题,只是游戏截图里技能粒子特效、装备栏UI、动态模糊会让它更明显。
解决方法是先确认训练集的帧来源:如果全来自某几局,就补充不同对局、不同分路、不同时间点的样本;同时把val划分改成按局划分,让验证集来自训练没见过的对局,mAP会回归真实水平。还有一个小技巧是收集一些“无目标”的纯地图帧,作为背景负样本加入训练,可以让模型学会不在地图纹理上乱报。
5.4 loss曲线先降后升,模型在悄悄过拟合
现象:训练集loss一直在降,val loss降到最低点后开始回升,这个拐点在训练日志里看得非常清楚。
原因就是epochs太多,3000张的小数据集配YOLOv8l最多几十轮就开始背训练集细节。这个数据规模下,做大epoch没意义,反而浪费算力。
解决办法是在训练命令里直接加early stopping参数,Ultralytics的patience参数控制连续多少个epoch验证指标不提升就自动停止:
yolo detect train \ model=yolov8s.pt \ data=/path/to/lol_dataset/data.yaml \ epochs=100 \ patience=20 \ imgsz=1280 \ batch=8patience=20表示val mAP连续20个epoch没有提升就停。不要只盯loss,要看val mAP的拐点。如果还想再防一层过拟合,把weight_decay调大一点,比如默认的0.0005改成0.001,对小数据集通常有帮助。
5.5 转换后txt行数比XML对象数少
现象:跑完3.2的转换脚本,统计labels目录的行数,发现比XML里的object总数少了一截。
原因通常是标注里有difficult=1或occluded=1的对象被脚本continue吞掉了。很多人会顺手把它们过滤掉,但游戏截图里最难检出的目标往往正是这些被标记为遮挡困难的对象。把它们全部丢弃,训练数据会缺失遮挡场景,推理时遇到兵线重叠、塔下遮挡就全崩。
处理方式不是简单地把difficult对象放回去,而是先打印看一下被过滤的是哪些类、长什么样。如果是真实且可见的目标,保留并手动确认框位是否正确;如果是完全不可见的标注错误,丢弃合理。关键是把过滤逻辑从静默改为可见,给用户一个明确日志,避免黑匣子式的数据清洗。
6. 只看总mAP不够:逐类验证与回放视频的稳定性检查
总mAP是一个会被“多数类”带偏的指标。这个数据集里小兵框可能占70%,小兵AP一高,总mAP就好看,韦恩和防御塔的问题会被彻底掩盖。我一般用一段极短代码把6类AP拆开看,每类单独跑验证:
from ultralytics import YOLO model = YOLO('lol_logs/exp001/weights/best.pt') for cls_id in range(6): results = model.val(split='val', classes=[cls_id], conf=0.05, iou=0.5) print(f"class {cls_id}: mAP50 = {results.box.map50:.3f}")注意这里conf=0.05,不是推理时的0.25。验证时要用低置信度去看模型到底有没有召回能力,等确认每类的召回率都合理后,再调高推理阈值来压误报。如果某类在conf=0.05下AP仍然极低,那问题在训练数据或增强,不在阈值。
逐类AP检查通过后,还要做一次回放验证。检测mAP回答的是“框找不找得准”,不回答“框稳不稳”。我第一次把videomAP调得不错后,放到一条四分钟团战录像上,发现检测框在帧与帧之间乱跳,同一个目标这一帧在左边,下一帧就跑到右边。后来给框坐标做了轻量的指数平滑:
smoothed = 0.9 * prev_bbox + 0.1 * curr_bbox也就是把上一帧的预测框和当前帧的预测框做加权平均,权重根据帧间稳定性调。0.9/0.1适用于摄像头静止的回放场景,如果镜头在快速跟随英雄,这个系数要调成0.6/0.4,否则框会拖尾。再进阶一点的做法是接ByteTrack这类跟踪器,用运动特征把跨帧检测结果关联成轨迹,做对局分析时轨迹比单帧框有用得多。
这个收尾环节我现在每次都保留:指标按类拆开看,参数按场景微调。希望帮到你。
本文还有配套的精品资源,点击获取