简介:面向目标检测学习者与YOLO系列算法使用者,这份网球比赛场景数据集包含551张已标注图像,识别对象为球员和网球,可直接用于YOLOv5、YOLOv7、YOLOv8、YOLOv9、YOLOv10、YOLO11等模型的训练、验证与测试,适合作为目标识别实战练习或项目预训练数据。整个压缩包27.67MB,共1578个文件,包含551张jpg原图、513个txt标签、513个xml标签和1个data.yaml配置;txt采用YOLO格式,每行记录类别索引、目标框中心点坐标及宽高,且中心点与宽高均按图像宽高归一化,xml采用VOC格式,两类标签分别存放在独立文件夹中,便于不同框架直接读取。数据集已完成训练集与验证集划分,连同data.yaml配置一起提供,可省去手动整理图片、标注和划分数据集的时间,拿到后即可开始训练。目前已有138人学习,适合在真实网球比赛画面中验证检测效果,也可作为YOLO系列迁移学习、数据增强与调参训练的基础数据,帮助快速构建自定义目标检测项目。
1. 551张图的网球数据集,YOLO训练前先读懂这份约束
拿一份551张图像、带球和球员标注的YOLO数据集做目标检测,最常踩的坑反而不是模型跑不起来,而是数据量太小、目标尺度跨度太大。网球在画面里往往只占几十个像素,球员却占成百上千像素,同一个模型要同时适应这两种尺度,用默认配置跑很容易出现漏检小目标、对大目标过拟合。这篇文章把这个数据集从头拆一遍,从标签格式校验、训练集划分、YOLOv8训练参数到小目标调优,给出可直接抄的脚本和命令。适合刚拿到自定义数据、准备跑通第一个YOLO检测流程的开发者,也适合想了解小数据集如何做数据增强与防过拟合的工程师。
2. YOLO数据集解读:球与球员标签格式的完整拆解
2.1 目录结构与YOLO标签格式
使用任何YOLO数据集,第一步是确认目录排列和标注是否遵循YOLO规定。常见做法是images与labels同级,标注文件与被标注图像同名,仅扩展名不同。网球数据集的551张图像大概率按原始采集顺序混合存放,这个结构本身没问题,问题往往出在标签的归一化坐标上。
YOLO标签每行表示一个目标,格式为class x_center y_center width height。注意四个坐标值都是相对图像宽高的比例,不是像素值。class从0开始计数,按照标题中“球-球员”的顺序,大部分打包者会把class 0设为球(ball),class 1设为球员(player)。实际训练前以数据集里的classes.txt或name.yaml为准,不要凭经验猜。
| 字段 | 含义 | 取值范围 | 示例 |
|---|---|---|---|
| class | 类别序号 | 从0开始 | 0 表示球,1 表示球员 |
| x_center | 目标中心点横坐标/图像宽度 | 0~1 | 0.5287 |
| y_center | 目标中心点纵坐标/图像高度 | 0~1 | 0.3542 |
| width | 目标宽度/图像宽度 | 0~1 | 0.0813 |
| height | 目标高度/图像高度 | 0~1 | 0.1760 |
这种归一化格式的好处是,无论原始图像是1080p还是4K,标签都不需要按分辨率重新换算。坏处是如果原始图像被resize、crop或加黑边,标签会整体失效。很多人在训练时发现mAP突然降到接近0,排查到底,发现是图像预处理阶段把图缩放了但标签没有同步调整。
2.2 用脚本校验标签内容
拿到551张图像的标签后,我一般先写一个Python脚本把全部标签读一遍,检查是否有越界、空文件、类别数异常。这一步能在几分钟内暴露数据包最常见的低级错误,避免训练跑了半小时才发现问题。
import os from pathlib import Path labels_dir = Path("labels/train") # 换成实际路径 cls_counter = {} bad_files = [] for txt in labels_dir.glob("*.txt"): with open(txt, "r", encoding="utf-8") as f: lines = f.read().strip().splitlines() if len(lines) == 0: bad_files.append((txt.name, "empty file")) continue for line in lines: parts = line.split() if len(parts) != 5: bad_files.append((txt.name, f"field count error: {line}")) continue cls = int(parts[0]) x, y, w, h = map(float, parts[1:]) cls_counter[cls] = cls_counter.get(cls, 0) + 1 # 边界检查:中心点和宽高都应在合理范围 if not (0 <= x <= 1 and 0 <= y <= 1): bad_files.append((txt.name, f"center out of range: {line}")) if w <= 0 or h <= 0 or w > 1 or h > 1: bad_files.append((txt.name, f"size out of range: {line}")) print("类别统计:", cls_counter) print("异常文件数:", len(bad_files)) for name, reason in bad_files[:20]: print(name, reason)脚本逻辑分三块:空文件检测、字段数量检测、坐标区间检测。凡是中心点超出0到1范围的,都是无效标注;宽高出现0或负值,说明标注软件导出时出了问题。类别统计还能用于判断类别是否均衡——如果球标注数量比球员多出好几倍(球经常被重复围观),训练损失函数会偏向多数类,球员的召回率会被压下去。
手动抽查也不可省略。脚本只能验证数值合法性,验证不了语义是否正确。建议用OpenCV把标签画到图像上,生成一批带框的预览图,肉眼扫一遍,重点看球框是否包住完整球体、球员框是否包含整个身体还是只框了上半身。标注框的边框定义不统一,模型学到的特征就会不一致,这是导致最终mAP上不去的隐藏原因。
3. 训练前处理:数据划分与图像质量排查
3.1 数据划分需要防“兄弟帧泄漏”
551张图像规模不大,但若是从视频片段抽帧而来,相邻帧之间可能存在大量重复场景。如果随机按图像划分训练集和验证集,同一个球的连续帧可能同时出现在两边,验证集的指标会虚高,部署到真实比赛视频后性能大幅滑坡,也就是通常说的“兄弟帧泄漏”。
更稳妥的做法是按视频片段或时间段分区。假设原始551张图来自3段比赛录像,以文件名的前缀或目录为分组依据,让每个组整体落入训练集或验证集。
import random import shutil from pathlib import Path random.seed(42) def group_key(filename: str) -> str: # 假设文件命名格式为 match1_frame_001.jpg return filename.split("_frame_")[0] images = list(Path("images").glob("*.jpg")) video_groups = {} for img in images: key = group_key(img.stem) video_groups.setdefault(key, []).append(img) train_ratio = 0.8 train_imgs = [] val_imgs = [] for key, imgs in video_groups.items(): # 对每组内部再次打乱,避免取到连续帧 random.shuffle(imgs) split_idx = int(len(imgs) * train_ratio) train_imgs.extend(imgs[:split_idx]) val_imgs.extend(imgs[split_idx:]) print(f"train: {len(train_imgs)}, val: {len(val_imgs)}")代码按文件名中的match前缀把图像分组,再以组为单位按比例切分。这样同一段视频的画面不会同时出现在训练集和验证集里。分组数很少时,验证集可能只来自一个视频片段,这会引入分布偏差。理想情况是每段视频都切出一小部分进验证集,而不是某一段完全留作验证。
划分结果建议直接生成对应的labels目录。把图像和标签移动到images/train、images/val、labels/train、labels/val四个目录,YOLO训练时只需要一个data.yaml就能完成路径映射。
3.2 检查图像质量与原始分辨率
551张图像里混合了不同分辨率的情况非常普遍,可能有大特写,也有远镜头。YOLO训练时会把输入图像统一缩放到imgsz指定尺寸,不同来源图像缩放后的细节损失程度不同。我在训练前会统计所有图像的宽高比分布,宽高比差异太大的数据集,应优先考虑在训练时开启letterbox而不是直接拉伸。
import cv2 from pathlib import Path widths, heights = [], [] for img_path in Path("images/train").glob("*.jpg"): img = cv2.imread(str(img_path)) h, w = img.shape[:2] widths.append(w) heights.append(h) print(f"min: {min(widths)}x{min(heights)}") print(f"max: {max(widths)}x{max(heights)}")宽高比极端情况会影响anchors的选择。但YOLOv8已经改用Anchor-Free检测头,不需要手动配置anchor尺寸。对oul最简单的方式是保证所有图在letterbox后输入网络,让图片短边被等比例填充到640或1280。
小目标问题在网球数据上非常突出。如果原始图像是1920x1080,网球直径可能只有20像素左右,缩放到640后变成约7像素,很难被普通模型捕捉。针对这种情况,要么调高imgsz到1280,要么后续用SAHI做切片推理,这两个方案我放到第5章展开。
3.3 准备dataset.yaml与第一个基线训练
在完成数据划分后即可准备YOLOv8的配置文件。
# tennis.yaml path: /path/to/tennis_dataset train: images/train val: images/val names: 0: ball 1: playerpath指数据集根目录的绝对路径,train和val相对于根目录。names必须与标签里的class序号一致。运行训练:
yolo detect train data=tennis.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 patience=20这里的参数依次说明:model=yolov8n.pt指定使用nano规模的预训练权重,epochs设为100,imgsz为640,batch为16,patience=20表示连续20轮验证指标不提升则早停。551张图的数据量,先跑一遍nano可以快速确认数据组织是否正确,再判断是否需要换成更大的s或m模型。
4. 用YOLOv8在551张图像上跑通训练与评估
4.1 模型规模选择不是越大越好
针对这样的数据规模,模型选型首先考虑的不是精度上限,而是过拟合风险。YOLO提供的n、s、m、l、x五档规模,对应从nano到extra-large的参数量。551张图像往往只能支撑nano或s的容量,强行上x除了让训练时间翻几倍,更大概率是验证集mAP反而下降。
| 模型 | 参数量(约) | 推荐场景 | 显存占用 |
|---|---|---|---|
| yolov8n | 3.2M | 数据量小、边缘设备推理 | 约2-4GB |
| yolov8s | 11.2M | 小数据集+一定精度要求 | 约4-6GB |
| yolov8m | 25.9M | 数据量丰富、精度优先 | 约6-8GB |
迁移学习是让YOLOv8n在小数据上稳住的关键。yolov8n.pt自带在COCO上预训练好的特征提取能力,COCO包含person类别,所以球员检测可以继承大量通用特征,球则被当作近似圆形物体学习。训练时ULTRALYTICS默认冻结前几层特征,只微调高层语义和检测头,这会显著降低对数据量的要求。
我一般会把nano作为第一轮基线,记录其验证集指标,再根据结果决定是否需要增大模型规模。如果nano有过拟合迹象——训练损失持续下降而验证损失反弹——那么加大模型只会加剧问题,更应该做的是调整增强策略。
4.2 关键训练参数调整逻辑
跑YOLOv8训练时,以下几个参数对结果影响最明显。
yolo detect train data=tennis.yaml model=yolov8n.pt optimizer=AdamW lr0=0.001 lrf=0.01 warmup_epochs=3 box=7.5 cls=0.5 dfl=1.5 epochs=200 imgsz=640 batch=16 augment=Trueoptimizer=AdamW在目标检测任务上收敛比SGD稳定,尤其适合小数据集,学习率波动更平缓。lr0=0.001是初始学习率,lrf=0.01表示最终学习率衰减到初始值的百分之一。box、cls、dfl分别对应框回归损失、分类损失、分布焦点损失的权重系数,默认值在通用场景下表现均衡,不需要轻易改。
关于训练轮数,551张图像不需要死等150轮结束。设置了patience=10或patience=20后,模型通常在第60到80轮之间收敛,后续轮次只是反复震荡。用早停代替手动调epochs能节省大量时间。
需要注意batch size与学习率的联动关系。batch从32降到8时,梯度噪声变大,原学习率容易震荡,应该相应把lr0从0.001下调到0.0005。预算有限时优先保证batch不低于8,否则BatchNorm统计量在每轮内波动过大,迁移学习效果会被削弱。
4.3 解读验证集输出与PR曲线
训练结束后,跑一遍验证集拿到完整指标。
yolo detect val data=tennis.yaml model=runs/detect/train/weights/best.pt终端会输出mAP50、mAP50-95、precision、recall四组主要数据。网球场景重点关注mAP50,因为球目标较小,当IoU阈值从0.5提升到0.75时,预测框若存在几个像素的偏差就被判为负样本,mAP50-95数值会明显偏低,这并不代表模型完全不可用。
PR曲线在runs/detect/train目录下以PR_curve.png形式保存,它能直观展示precision和recall的权衡点。如果曲线在右下角塌陷,说明模型更保守,只输出高置信度预测,召回率不足;如果曲线整体靠近左下角走,则说明边界框定位质量差,得分上去后precision迅速下降。
混淆矩阵同样值得看。一个典型场景是球被误检为球员、球员缺失漏检。如果误检集中发生在远距离小目标区域,大概率不是类别特征学的不够,而是图像缩放后细节丢失,此时调整imgsz比调整损失函数权重要有效得多。
5. 提升小目标检测率的实用技巧
5.1 提高输入分辨率与切片推理
把imgsz从640提升到1280,对网球这类小目标带来的提升往往比换更大的模型更明显。训练1280成本高,但推理阶段可以单独用SAHI方案,推理时将原图切成多块640x640的切片图,对每个切片独立检测,再合并结果去除重复框。
python -m sahi predict --model_type yolov8 --model_path runs/detect/train/weights/best.pt \ --source test_video_frames/ --slice_width 640 --slice_height 640 \ --overlap_ratio 0.2 --confidence_threshold 0.25--slice_width和--slice_height控制切片大小,--overlap_ratio设为0.2是为了避免目标被切片边缘切断。confidence_threshold建议先用默认0.25观察效果,再根据PR曲线决定提高还是降低。
提示:切片推理本质牺牲推理速度换召回率,适合离线处理或对实时性要求不高的场景。生产环境若要求实时,优先考虑用1280的imgsz配合nano模型直接推理。
5.2 控制数据增强强度防止过拟合
数据增强是数据量不足时最有效的防过拟合手段。YOLOv8默认开启mosaic增强,但mosaic在目标极小、背景单一的场景下反而可能加剧训练难度。我把mosaic=0.5调低,同时保留hsv_h=0.015、hsv_s=0.7、hsv_v=0.4来模拟不同光线,让模型学到球在不同亮度下的共通特征。
判断增强强度是否合适,看训练集的loss下降曲线:如果loss下降非常缓慢,或验证指标出现周期性震荡,通常是增强过强导致模型学不到稳定规律,先关掉mosaic试一轮;如果训练损失快速过拟合而验证损失仍高,逐步提升增强强度。
一个被忽视的小技巧是用模型预测训练集本身,观察置信度分布。理想状态是训练集大部分样本置信度接近1。若某些训练帧置信度偏低,说明标注框存在边界不准确或漏标,需要回头修正数据。这个反向检测的数据质量反馈循环,对551张图的小数据集价值极大,能帮助你在这份数据上把mAP50跑进0.85以上。
本文还有配套的精品资源,点击获取