简介:这是一份面向目标检测学习者的网球场景数据集,围绕网球场与运动员两类目标构建,可直接用于YOLO系列算法的训练与验证。数据集共1956张图像,已按训练与验证需求划分完毕,并附带data.yaml配置文件,兼容yolov5、yolov8、yolov9、yolov7、yolov10及yolo11等主流版本,适合课程设计、算法对比实验与模型微调等场景。压缩包内共2000个文件,以1707个xml标注文件和293个txt标注文件为主,分别对应VOC与YOLO两种标注格式,其中YOLO格式采用类别索引加归一化中心点与宽高的五元组表示,便于直接读取训练。资源包整体约87.8MB,目录结构清晰,标签文件与图像一一对应,省去自行标注与格式转换的环节。目前已有131人学习下载,读者可快速获得一份开箱即用的网球目标检测数据,用于验证模型精度、调试训练流程或开展迁移学习实验。
1. 网球数据集到手先别急着训练:1956 张图像带标签的 YOLO 落地判断
拿到一个名为「yolo算法-网球数据集-1956张图像带标签-网-运动员.zip」的压缩包,第一反应不该是解压后直接yolo train,而是先判断这批数据到底能不能撑起一个可用的检测模型。1956 张图像在目标检测里属于小体量,但如果标注质量高、场景聚焦(网球、球网、运动员三类目标),它完全够跑通一个 YOLO 网球检测的完整链路,甚至能做出可演示的 demo。问题在于,很多人卡在第一步:不知道标签是什么格式、类别怎么映射、训练集验证集怎么切、预训练权重选哪个。这篇笔记就按一线实操的顺序,把从解压到推理的每一步拆开讲,包括我踩过的坑和参数怎么调。适合手里已经有类似数据集、想快速跑通 YOLO 训练并部署的工程师,也适合想理解小数据集训练边界的同学。
2. 拆包先看结构:1956 张图像和标签到底长什么样
2.1 解压后的目录结构与文件格式判断
拿到压缩包后,先别用图形界面双击,用命令行解压并统计文件类型,这样能快速判断数据集的组织方式。常见做法是:
unzip yolo算法-网球数据集-1956张图像带标签-网-运动员.zip -d tennis_dataset cd tennis_dataset find . -maxdepth 2 -type d | head -20 find . -type f | sed 's/.*\.//' | sort | uniq -c | sort -rn第一行解压到指定目录,第二行进入目录,第三行列出前两层目录结构,第四行统计所有文件的扩展名分布。如果输出里.jpg或.png约 1956 个,.txt数量接近甚至等于图像数,那基本可以确定是 YOLO 格式的标注(每张图对应一个同名 txt)。如果出现.xml,那就是 VOC 格式,需要转换。如果只有图像没有 txt,那这个「带标签」可能指的是分类标签或分割掩码,需要进一步确认。
我一般还会抽查几个 txt 内容:
head -5 $(find . -name "*.txt" | head -1)YOLO 格式每行是class_id x_center y_center width height,数值都是归一化到 0~1 的浮点数。如果看到坐标是整数且范围在图像宽高内,那可能是 VOC 或 COCO 转过来的中间态,需要写脚本归一化。
2.2 类别映射与标签一致性检查
标题里提到「网-运动员」,说明至少有三类:网球、球网、运动员。但实际标签里的 class_id 可能从 0 开始,也可能从 1 开始,甚至有的数据集把「网球」和「球网」合并成一类。必须先把类别名和 id 的对应关系确定下来,否则训练出来的模型会把球网识别成网球。
常见做法是写一个统计脚本:
import os from collections import Counter label_dir = "tennis_dataset/labels" class_counter = Counter() for txt_file in os.listdir(label_dir): if not txt_file.endswith(".txt"): continue with open(os.path.join(label_dir, txt_file), "r") as f: for line in f: parts = line.strip().split() if len(parts) >= 5: class_counter[int(parts[0])] += 1 print("类别分布:", class_counter)这段代码遍历所有标签文件,统计每个 class_id 出现的次数。如果某个 id 的样本数极少(比如少于 50),那这个类别在 1956 张里可能只有几十个实例,训练时容易欠拟合。参数说明:label_dir要换成你实际的标签目录,通常是labels/或与images/平级。如果统计结果里只有两个 id,那说明「网球」和「球网」可能被合并了,需要根据实际业务决定是否拆分。
提示:如果类别数超过 3 个,但标题只提了三个目标,检查是否有「运动员」被细分为「发球方」「接球方」等子类,这种细粒度在小数据集上通常训不好,建议合并。
2.3 图像尺寸与标注框的合理性抽检
1956 张图像可能来自不同来源,尺寸不一致。YOLO 训练时默认会 resize 到统一尺寸(如 640×640),但如果原图长宽比差异太大,resize 后目标会变形。先统计图像尺寸分布:
python -c " from PIL import Image import os sizes = {} for img in os.listdir('tennis_dataset/images'): if img.endswith(('.jpg','.png')): with Image.open(os.path.join('tennis_dataset/images', img)) as im: sizes[im.size] = sizes.get(im.size, 0) + 1 for k,v in sorted(sizes.items(), key=lambda x:-x[1])[:10]: print(k, v) "如果前几种尺寸占比超过 80%,说明数据集相对统一,可以直接用默认的 letterbox 策略。如果尺寸非常分散,建议在训练配置里开启rect=True做矩形训练,减少 padding 带来的无效计算。
另外,抽几张图可视化标注框,确认没有框错、漏标。我习惯用下面这段脚本快速画框:
import cv2 import os img_path = "tennis_dataset/images/000001.jpg" label_path = "tennis_dataset/labels/000001.txt" img = cv2.imread(img_path) h, w = img.shape[:2] with open(label_path) as f: for line in f: cls, x, y, bw, bh = map(float, line.split()) x1 = int((x - bw/2) * w) y1 = int((y - bh/2) * h) x2 = int((x + bw/2) * w) y2 = int((y + bh/2) * h) cv2.rectangle(img, (x1,y1), (x2,y2), (0,255,0), 2) cv2.imwrite("check.jpg", img)跑完打开check.jpg,如果框的位置明显偏移,说明标签坐标系和图像坐标系不一致,可能是归一化时用错了宽高顺序。这个坑很常见,血泪经验是:YOLO 的x_center是相对宽度归一化,y_center是相对高度归一化,千万别搞反。
3. 用 YOLOv8 跑通训练:从环境到第一个 baseline
3.1 环境安装与预训练权重选择
YOLOv8 是目前小数据集上最容易出效果的版本之一,安装直接用 pip:
pip install ultralytics如果要用 GPU 训练,确认 CUDA 版本和 PyTorch 匹配。我一般会先跑一行命令验证环境:
yolo checks输出里会显示 CUDA 是否可用、版本号等信息。预训练权重选yolov8n.pt还是yolov8s.pt?1956 张图属于小数据,n 版本参数少、收敛快,适合先跑 baseline;s 版本精度略高但容易过拟合。我的习惯是先用 n 跑 50 epoch 看 mAP 趋势,如果验证集 mAP 还在涨,再换 s 微调。
权重文件不需要手动下载,ultralytics会在第一次训练时自动拉取。但要注意:如果网络环境不稳定,可能会卡在下载环节,可以提前把yolov8n.pt放到项目根目录,训练时指定本地路径。
3.2 数据集 YAML 配置与路径陷阱
YOLOv8 要求一个 YAML 文件描述数据集路径和类别名。常见写法:
path: /home/user/tennis_dataset train: images/train val: images/val names: 0: tennis_ball 1: net 2: player这里有几个容易翻车的点。第一,path必须是绝对路径,或者相对于运行训练命令时的当前目录。我遇到过在runs/目录下启动训练,结果path解析错误,报「No images found」。第二,train和val是相对于path的子路径,不是绝对路径。第三,names的 id 必须和标签里的 class_id 完全一致,顺序不能错。
如果数据集还没有划分 train/val,需要先切分。1956 张图按 8:2 切,验证集约 390 张。切分脚本:
import os, random, shutil random.seed(42) img_dir = "tennis_dataset/images" label_dir = "tennis_dataset/labels" train_img = "tennis_dataset/images/train" val_img = "tennis_dataset/images/val" train_lbl = "tennis_dataset/labels/train" val_lbl = "tennis_dataset/labels/val" for d in [train_img, val_img, train_lbl, val_lbl]: os.makedirs(d, exist_ok=True) imgs = [f for f in os.listdir(img_dir) if f.endswith(('.jpg','.png'))] random.shuffle(imgs) split = int(len(imgs) * 0.8) for i, img in enumerate(imgs): src_img = os.path.join(img_dir, img) src_lbl = os.path.join(label_dir, img.rsplit('.',1)[0] + '.txt') if i < split: shutil.copy(src_img, os.path.join(train_img, img)) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(train_lbl, os.path.basename(src_lbl))) else: shutil.copy(src_img, os.path.join(val_img, img)) if os.path.exists(src_lbl): shutil.copy(src_lbl, os.path.join(val_lbl, os.path.basename(src_lbl)))参数说明:random.seed(42)保证每次切分结果一致,方便复现。split控制训练集比例,小数据集可以调到 0.85 增加训练样本。注意标签文件要和图像同名,否则训练时会被当成无标签样本忽略。
3.3 训练命令与关键参数设置
启动训练:
yolo detect train \ data=tennis.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ lr0=0.01 \ patience=20 \ project=tennis_runs \ name=baseline逐项说明:data指向 YAML 文件;epochs=100对小数据集足够,配合patience=20早停防止过拟合;imgsz=640是默认输入尺寸,如果原图分辨率普遍低于 640,可以降到 416 加速训练;batch=16根据显存调整,8G 显存跑 n 版本可以到 32;lr0=0.01是初始学习率,小数据集建议比默认的 0.01 再小一点,比如 0.005,避免震荡。
训练过程中重点看mAP50和mAP50-95两个指标。如果mAP50在 30 epoch 后就平了,说明模型容量不够或数据太简单;如果mAP50波动很大,检查 batch size 是否太小或学习率太高。我一般会在训练结束后用 TensorBoard 看 loss 曲线:
tensorboard --logdir tennis_runs如果分类 loss 下降但定位 loss 不降,可能是标注框质量有问题,回到 2.3 节重新抽检。
4. 训练完别只看 mAP:推理、导出与踩坑排查
4.1 用验证集跑推理并可视化结果
训练结束后,权重保存在tennis_runs/baseline/weights/best.pt。先用验证集跑一遍推理,看实际效果:
yolo detect predict \ model=tennis_runs/baseline/weights/best.pt \ source=tennis_dataset/images/val \ save=True \ conf=0.25 \ iou=0.45conf=0.25是置信度阈值,低于这个值的框会被过滤;iou=0.45是 NMS 的 IoU 阈值,控制重叠框的合并。如果发现漏检多,把conf降到 0.1 试试;如果误检多,提高到 0.4。结果图默认保存在runs/detect/predict/下,打开几张看看网球是否被框住、球网是否被误判成背景。
我还会用 Python 脚本批量统计 TP/FP/FN,更客观地评估:
from ultralytics import YOLO model = YOLO("tennis_runs/baseline/weights/best.pt") metrics = model.val(data="tennis.yaml", split="val") print(metrics.box.map) # mAP50-95 print(metrics.box.map50) # mAP50 print(metrics.box.mp) # 平均精度 print(metrics.box.mr) # 平均召回如果mr明显低于mp,说明漏检比误检严重,优先检查小目标(网球)的标注是否完整。
4.2 导出 ONNX 与部署前的尺寸对齐
如果要把模型部署到边缘设备或 C++ 推理,通常导出 ONNX:
yolo export model=tennis_runs/baseline/weights/best.pt format=onnx imgsz=640导出后得到一个.onnx文件。注意:导出时的imgsz必须和训练时一致,否则推理结果会偏移。我遇到过训练用 640、导出用 416,结果框全乱的情况。另外,ONNX 推理时预处理要自己做 letterbox,保持和 YOLO 训练时相同的缩放和 padding 逻辑,否则精度掉点。
4.3 小数据集训练的四个典型翻车现场
现象一:训练 loss 正常下降,但验证集 mAP 始终为 0。原因:验证集路径配置错误,或者验证集标签文件缺失。YOLO 在验证时如果找不到标签,会跳过该图,导致 mAP 计算为空。 解决:检查val路径下是否有对应的labels目录,且每个图像都有同名 txt。用find val_images -type f | wc -l和find val_labels -type f | wc -l对比数量。
现象二:网球检测效果差,球网和运动员正常。原因:网球在图像中占比小,属于小目标,YOLOv8n 的 P3 特征图感受野有限。 解决:换用yolov8s.pt或更大模型,或者在训练时开启mosaic=1.0增强小目标样本。也可以把imgsz提高到 1280,但显存消耗会翻倍。
现象三:训练到 50 epoch 后 mAP 突然掉点。原因:过拟合。1956 张图对 n 版本来说仍然偏少,模型开始记住训练集噪声。 解决:加数据增强(hsv_h=0.015, hsv_s=0.7, hsv_v=0.4, degrees=10, translate=0.1),或者用早停patience=15。我一般还会冻结 backbone 前 10 层跑 20 epoch,再解冻全量微调。
现象四:推理时框的位置整体偏移。原因:标签归一化时用错了图像宽高,或者推理预处理没有做 letterbox。 解决:回到 2.3 节的可视化脚本,确认训练标签框位置正确。如果训练时正确、推理时偏移,检查推理代码的 resize 逻辑,确保和ultralytics的LetterBox一致。
注意:如果数据集里混入了非网球场景的负样本(比如空场地),不要直接删掉,保留 5% 作为背景负样本,能降低误检率。
5. 把 1956 张图用到极致:增强策略与迭代习惯
小数据集训练 YOLO,核心思路不是换更大的模型,而是让每一张图产生更多有效梯度。我习惯在 baseline 跑通后做三件事:第一,用albumentations做离线增强,把训练集扩到 5000 张左右,重点加随机裁剪、旋转和色彩抖动,模拟不同光照和视角;第二,用训练好的模型在验证集上跑推理,把置信度在 0.1~0.3 之间的框人工复核,确认是漏标还是误检,漏标的补上标签再训一轮;第三,尝试yolov8s和yolov8n的模型融合,用 WBF(加权框融合)代替 NMS,通常能涨 1~2 个点 mAP。
具体增强脚本可以这样写:
import albumentations as A import cv2, os transform = A.Compose([ A.HorizontalFlip(p=0.5), A.RandomBrightnessContrast(p=0.3), A.Rotate(limit=15, p=0.3), A.RandomCrop(width=512, height=512, p=0.3), ], bbox_params=A.BboxParams(format='yolo', label_fields=['class_labels'])) img = cv2.imread("tennis_dataset/images/train/000001.jpg") with open("tennis_dataset/labels/train/000001.txt") as f: bboxes = [] class_labels = [] for line in f: cls, x, y, w, h = map(float, line.split()) bboxes.append([x, y, w, h]) class_labels.append(int(cls)) aug = transform(image=img, bboxes=bboxes, class_labels=class_labels) cv2.imwrite("aug_000001.jpg", aug["image"]) with open("aug_000001.txt", "w") as f: for bbox, cls in zip(aug["bboxes"], aug["class_labels"]): f.write(f"{cls} {' '.join(map(str, bbox))}\n")参数说明:HorizontalFlip对网球场景安全,因为左右翻转不改变语义;Rotate限制在 15 度以内,避免球网倾斜过度;RandomCrop可能裁掉小目标,建议配合min_visibility=0.3过滤掉裁后面积过小的框。增强后的标签要重新归一化,albumentations的yolo格式会自动处理。
最后说一个我自己的习惯:每次训练完,不管 mAP 多高,都会把best.pt在验证集上跑一遍,挑出置信度最低的 10 张图,逐张看标注。十次里有八次能发现漏标或错标,补完再训一轮,mAP 通常还能再涨。小数据集没有捷径,标注质量就是上限。希望帮到你。
本文还有配套的精品资源,点击获取