LOL英雄联盟目标检测数据集实战:YOLOv8训练与避坑指南 简介一份面向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(), keylambda 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的训练生态默认吃txtdata.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_okTrue) 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 \ datadata.yaml \ modelyolov8n.pt \ imgsz640 \ epochs120 \ batch16 \ workers4 \ patience20 \ projectrun_lol \ nameyolov8n_640参数说明model用yolov8n.pt做预训练权重n是nano速度最快、显存最小适合先验证pipeline后面想提精度再换成yolov8s.pt或yolov8m.pt显存占用依次上升。imgsz640是第一轮baseline的常用值如果小目标漏检严重再提到1280这一点第5章会展开。batch16是12GB显存在640输入下的相对稳妥值显存不足先降到8。patience20表示连续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全尺寸远处角色在图像里的高度经常只有二三十像素imgsz640下采样后只剩十几个像素骨干网络最后一层特征图上目标只有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很高录屏实测却翻车现象验证集mAP0.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 \ modelrun_lol/yolov8n_640/weights/best.pt \ sourcetest_shot.jpg \ imgsz1280 \ conf0.25conf0.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)用法是对相邻两帧中类别相同的检测框算IoUIoU大于0.3就认为是同一个目标给它继承上一帧的ID单帧出现且跟前后帧都没有匹配的框暂存两三帧再决定是否输出连续匹配不上就当作误检抑制掉。这个函数只是骨架实际要加上类别约束和消失帧数的计数但已经是把YOLO输出从“一帧一堆框”变成“一路稳定框”最省事的办法。做这类游戏数据集项目我最后悔的一次是拿到数据就直接开训跑了两天才发现类别ID在转换脚本和data.yaml里错了一位返工重训等于白烧两天电。后来我把第2章那套清点、抽检、画框回看当成铁律省下的时间远超多跑几次实验。数据侧的坑永远比模型侧的坑贵希望帮到你。本文还有配套的精品资源点击获取