FEATURED · 精选文章

YOLOv8小目标检测实战:从551张网球图像数据集到ONNX部署

发布时间 / 2026/9/15 4:18:25
来源 / 创域科博编辑部
栏目 / 资讯中心
YOLOv8小目标检测实战:从551张网球图像数据集到ONNX部署 简介面向目标检测入门与实战人群这份YOLO格式的网球与球员检测数据集可直接用于yolov5、yolov8、yolov9、yolov10、yolo11等系列模型的训练与验证。包内共1578个文件包含551张JPG原图、513个YOLO格式txt标签和513个VOC格式xml标签并额外提供data.yaml配置文件支持两种主流标注格式对照学习。数据集已提前划分好训练集与验证集且txt标注严格采用class、x_center、y_center、width、height的归一化坐标格式xml标注保留更直观的边界框信息适合快速上手目标检测全流程。整个压缩包仅27.67MB文件轻量但结构完整无需额外整理即可开始训练也适合用于理解不同标注格式之间的转换关系。目前已有138人学习是一份低门槛、易验证的算法练习数据。1. 网球场景目标检测先把551张图像的数据集盘活很多人拿到“551张图像带标签”的网球数据集第一反应是直接丢进训练脚本跑一晚上结果第二天看mAP50-95只有0.3还以为是模型不行。真实瓶颈往往不在模型而在数据没盘活标签格式不统一、类别ID偏移、坐标越界、球这种小目标的标注框比例失衡。这套数据集的核心价值在于它是“球球员”双类组合正好覆盖体育视频分析最常见的小目标难题。所以这篇文章顺着yolo目标检测流程走一遍从验证标签格式、重排目录、配置yaml到选择训练参数、导出部署用可复现的命令说明每一步为什么这么做以及遇到了问题看哪里。2. yolo目标检测流程第一步核验551张图像的标签与图片匹配2.1 拿到数据集先看目录结构与标签格式我经手的网球类数据压缩包解压后常见两种结构一种是images/与labels/平级、内部都是jpg txt另一种是单一目录里图片和标签混在一起甚至只有COCO的JSON标注文件。先花两分钟用命令行把结构看清避免后面脚本全写错路径unzip yolo算法-网球-球员数据集-551张图像带标签-球-球员.zip -d tennis_ball cd tennis_ball find . -maxdepth 2 -type f | sed s#^.*/## | cut -d. -f2 | sort | uniq -c如果输出里.txt和.jpg数量都在550上下说明大概率是标准yolo格式。找其中一个标签文件看看内容标准yolo数据标记格式应该是0 0.62109375 0.46875 0.02734375 0.03385417 1 0.53808594 0.37451172 0.18505859 0.28190104每行依次是“类别ID 中心点x 中心点y 框宽 框高”坐标全部除以原图宽高完成归一化。这里的第0类通常是球第1类是球员。行数代表这张图里有几个目标。如果同一张图里球员和球各有一个就应有两行。若发现一行里超过5列多半是从VOC XML转换时遗留了角度字段需要清洗。2.2 用一个Python脚本扫描无效标签与越界坐标肉眼只能抽查无法覆盖全部551张。我一般会写一个快速巡检脚本把空标签、类别越界、坐标越界、单边长度为0的框一次性暴露出来import os from pathlib import Path labels_dir Path(labels) allowed_cls {0, 1} # 0球, 1球员 for txt in sorted(labels_dir.glob(*.txt)): lines txt.read_text().strip().splitlines() if not lines: print(fEMPTY: {txt.name}) continue for idx, line in enumerate(lines, 1): parts line.split() if len(parts) ! 5: print(fBADLEN: {txt.name}:{idx} len{len(parts)}) continue cls int(parts[0]) cx, cy, w, h map(float, parts[1:]) if cls not in allowed_cls: print(fBADCLS: {txt.name}:{idx} cls{cls}) if not (0 cx 1 and 0 cy 1): print(fCENTER_OUT: {txt.name}:{idx} cx{cx} cy{cy}) if not (0 w 1 and 0 h 1): print(fSIZE_OUT: {txt.name}:{idx} w{w} h{h})脚本逻辑很直接长度不等于5说明格式损坏类别ID不在0和1之间说明标注工具或转换脚本把类别序列搞乱了中心点或宽高不在归一化范围内说明之前做过缩放又标了一遍框或者图像尺寸表读错。跑完没有输出才说明551张图的标签在“文件完整性”这一层是合格的。2.3 统计类别分布与球目标的大小占比除了格式更关键的是看球这个类在图像里占了多大面积。足球、排球中球也比较大网球在画面里往往只有几十像素宽模型对这类小目标天然不友好。我习惯对每个txt做一次统计看看球的框宽在整张图宽度里占多少比例awk {if($10) print $4} labels/*.txt | awk {sum$1; if($10.05) big; if($10.01) small} END{printf total%d small1%%%d big5%%%d avg%.4f\n, NR, small, big, sum/NR}这条命令把第0类球的宽度占比拉出来看。如果small1%占比超过一半说明这次训练的难点就是小目标召回后面第4章的超参数调整必须围绕它展开。同时建议顺手统计每个类别的实例总数表格如下检查项判定依据说明类别ID是否连续从0开始与data.yaml内names顺序对应常见坑COCO里player是1ball是32直接映射会错位框宽高占图比例球类平均框宽小于3%需要调整输入分辨率或数据增强策略每张图目标数量大部分图只有1球1人样本较为单一训练后要警惕过拟合到固定构图如果发现球的标注占比过小后续训练不要直接上默认配置先记住这个结论后面章节会对应处理。3. 让YOLO v8接住这批数据目录重排、类别映射与yaml配置3.1 将非标准标签转换为yolo txt格式的通用做法如果打开label文件发现是COCO JSON常见案例是annotation里categories为[{id:1,name:player},{id:2,name:ball}]需要转成yolo连续类别0/1。可以直接用下面这个精简转换脚本import json from pathlib import Path info json.loads(Path(annotations.json).read_text()) images {img[id]: img for img in info[images]} cats {cat[id]: i for i, cat in enumerate(info[categories])} Path(labels).mkdir(exist_okTrue) for ann in info[annotations]: img images[ann[image_id]] w, h img[width], img[height] x, y, bw, bh ann[bbox] # COCO bbox是左上角x,y和宽高 cx, cy x bw / 2, y bh / 2 # 转中心点表示 cls_id cats[ann[category_id]] # 类别重新映射为0/1 line f{cls_id} {cx/w:.6f} {cy/h:.6f} {bw/w:.6f} {bh/h:.6f}\n out Path(labels) / (Path(img[file_name]).stem .txt) with out.open(a) as f: f.write(line)代码先按image_id查原图宽高再把COCO的左上角格式换算为yolo的中心点格式最终把类别ID重排。值得说明的是cats这里依赖了enumerate(info[categories])的顺序如果原JSON里球排在球员前面那么球会变成0、球员变成1与第2章的约定相反。保险做法是先打印一次映射结果确认后再批量执行。3.2 目录重排与train/val切分脚本yolo训练时通常不直接拿全量551张开跑建议按82切出验证集。用Python按随机种子切分保证可复现import random, shutil from pathlib import Path random.seed(42) imgs sorted(Path(images).glob(*.jpg)) random.shuffle(imgs) split int(len(imgs) * 0.8) for tag, chunk in zip((train, val), (imgs[:split], imgs[split:])): for src in chunk: img_dst Path(dataset) / tag / images / src.name lbl_dst Path(dataset) / tag / labels / (src.stem .txt) img_dst.parent.mkdir(parentsTrue, exist_okTrue) lbl_dst.parent.mkdir(parentsTrue, exist_okTrue) shutil.copy2(src, img_dst) shutil.copy2(Path(labels) / (src.stem .txt), lbl_dst)脚本用copy2做复制而不是move原图保留一份后面重新切分再跑一次也方便。60行以内的数据集可以用复制硬盘足够如果是几万张的体育数据集我会改成os.symlink建软链节省一半空间。训练集和验证集来自同一次洗牌避免同一组画面在两边的分布偏差过大。3.3 配置data.yaml与类别映射检查YOLOv8的data.yaml是训练入口的关键路径配置可以写相对路径取决于运行时的工作目录path: ./dataset train: train/images val: val/images names: 0: ball 1: player这里最重要的是names的顺序必须与标签文件里的类别ID一一对应。网球场里只有球和球员两类顺序写反之后训练不报错但推理时会把球员当球、球当球员这类错误在部署时才暴露前期很难察觉。我的习惯是先写一个yaml再随便挑一张训练集图片用yolo predict跑一次看预测类别名是否符合常识这一步能规避绝大多数映射问题。4. yolov8训练自己的数据集从选模型到调整yolo损失相关参数4.1 模型选型yolov8n、yolov8s还是yolov8m球和球员都是目标检测网球作为小目标选模型除了考虑算力还要想清楚每层下采样的感受野。YOLOv8在COCO上的公开精度大致是官方报告的数字我只当参考基线来用模型参数量速度对网球小目标的表现倾向yolov8n约3.2M最快球漏检率高适合高帧率视频粗筛yolov8s约11.2M较快速度与精度均衡推荐先跑这一档yolov8m约25.9M中等精度上限更高需要显存更大如果只有一块消费级显卡直接用yolov8s起步比较稳。相比之下yolov8n在球只有十几个像素时很容易把球归为背景因为浅层特征图里球的信息不足以撑起分类置信度yolov8m的基础特征提取更强但训练时间几乎翻倍。551张图的数据量训练s模型完全够用不需要上m。4.2 训练命令与5个关键超参数命令行直接覆盖超参数比改yaml文件更直观也方便记录这次实验用了什么配置yolo detect train datadata.yaml \ modelyolov8s.pt \ imgsz1024 \ epochs100 \ batch16 \ patience15 \ scale0.3 \ mosaic0.5 \ copy_paste0.0 \ hsv_h0.015 \ fliplr0.5逐项拆开说。imgsz1024是最关键的一项把输入分辨率从默认640提到1024等于给球这类小目标更多的特征像素scale0.3是缩放增强幅度值设小是因为球太小再被随机缩放容易缩成一个点。mosaic0.5是马赛克拼接概率它增强背景多样性也在拼接边界制造大量无效的碎片目标网球场景里球只占画面极小部分mosaic对小球算力收益反而低保留0.5是折中。copy_paste0.0直接关掉原因是复制粘贴增强适合实例分割任务对纯框检测的球类目标增益有限还容易粘贴出不自然的球影。hsv_h0.015压低色相扰动幅度避免把网球场的绿色调乱影响球与背景的区分。提示imgsz1024会让显存占用变成640时的2.5倍以上batch16跑不动就降到8别硬跑。4.3 看懂yolo损失函数日志判断收敛而不是只看mAP训练时终端会输出box_loss、cls_loss、dfl_loss三个损失值它们分别对应yolo损失函数里的定位损失、分类损失和分布焦点损失。我判断训练是否收敛看三个信号val_box_loss是否还在下降、val_cls_loss是否在最后10个epoch还在抖动上升、以及mAP50-95是否还在增长。只看mAP50容易骗自己因为球员这类大目标很好检球拉低了整体曲线也看不出来。tail -f runs/detect/train/results.csv每行记录着当前epoch的指标。如果val/dfl_loss到后期不再下降但train/box_loss还在掉说明模型开始把训练集特征背下来了这是过拟合的典型信号。此时提前patience15会自动触发早停不用管到100轮。训练结束后看confusion_matrix.png关注“球被漏检成background”的比例这个值比总mAP更能反映小目标问题是否解决。5. 部署验证中的实用技巧导出ONNX后按类别分阈值推理5.1 导出ONNX并固定输入尺寸训练完的best.pt要部署先转ONNX格式。导出时固定imgsz为1024保持与训练一致避免部署端resize带来精度损失yolo export modelruns/detect/train/weights/best.pt formatonnx imgsz1024导出成功后同目录会出现best.onnx。这一步只做格式转换不改模型权重转换后的输出张量形状通常是(1, 84, 8400)其中84由4个坐标 80类COCO 类别数适配组合而来实际部署代码里依赖的具体尺寸建议用onnxruntime打印一次确认。5.2 球和球员分开设定置信度阈值球员在画面里占几百像素置信度普遍在0.6以上球只有几十像素又经常被球员身体遮挡置信度经常只有0.3。统一用0.5的阈值会漏掉大量真实球框。我的做法是按类别ID单独设阈值用ONNX Runtime写推理逻辑import cv2, numpy as np, onnxruntime as ort sess ort.InferenceSession(best.onnx) inp sess.get_inputs()[0].name img cv2.imread(frame.jpg) img cv2.resize(img, (1024, 1024)) img img[:, :, ::-1].transpose(2, 0, 1)[None].astype(np.float32) / 255.0 pred sess.run(None, {inp: img})[0][0].T # 每行归一化的bbox置信度类别 for det in pred: conf, cls_id det[4], int(det[5]) thr 0.25 if cls_id 0 else 0.5 if conf thr: # 反算回原图坐标后进入NMS或直接按场景规则过滤 pass代码里pred的转置是为了方便逐条遍历。针对class_id0球放低阈值到0.25class_id1球员保持0.5。放低球阈值后会多出一些误检但网球视频分析中球的误检可以通过多帧轨迹平滑消除而单帧漏检却无法靠后处理补救。实际部署时建议在(1, 84, 8400)输出上接一个标准NMS再丢给跟踪器。5.3 处理视频时用跟踪结果补洞拿到每帧检测结果后我通常配合ByteTrack做视频跟踪因为球被球拍或球员身体短暂遮挡一两帧时检测器会短暂丢目标跟踪器可以靠前后帧的运动预测补上轨迹线。验证标准很简单跑一段2分钟的比赛视频把预测框和轨迹叠加输出看球的轨迹线是否连续球员是否频繁跳ID。如果检测阈值已经按类别分开调了轨迹仍然断层优先怀疑imgsz或帧率检查抽帧是不是每5帧才取一帧导致球的位移超过半个框宽这种情况下把推理帧率提高到每2帧一次比继续调阈值更见效。本文还有配套的精品资源点击获取
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻