news 2026/9/16 5:20:41

基于YOLO的网球运动分析:目标检测、关键点与测速实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于YOLO的网球运动分析:目标检测、关键点与测速实战

简介:这是一份基于YOLO算法实现的网球运动实时分析项目源码,适合计算机视觉学习者、体育数据分析爱好者及想落地目标检测与关键点识别流程的开发者。项目可完成球员与网球的检测、球场关键点提取,并进一步统计运动员速度、击球速度和击球次数,属于完整的多任务视觉分析方案。包体共1231个文件,约598MB,主要内容包括581个txt标注文件与578个jpg图像样本,用于模型训练;15个py脚本和3个ipynb笔记文件覆盖球检测、球场关键点标注及击球分析等阶段的训练与推理;另含avi/mp4示例视频、pt/pth模型权重及yaml配置,便于直接验证效果或继续微调。目前已有739人学习下载,适合希望结合实战数据集跑通检测、关键点回归与速度/次数统计的读者。

1. 从一局比赛录像到可视化数据表,YOLO网球分析做了什么

把一段网球比赛录像拖进程序,几分钟后得到球员跑动速度曲线、击球次数和落点分布,听起来像是专业转播团队才能做的分析。这个项目把整条链路压缩在了几个 notebook 里:先用 YOLO 实时检测球员和网球,再用一个轻量 CNN 回归球场关键点,最后通过坐标换算计算出速度、击球次数等指标。它不是那种只出个 demo 效果图的教学项目,而是连模型训练代码、标注样本和处理好的 avi 视频一起给出的可复现工程。适合想搞懂目标检测怎么和运动场景结合的人,也适合正在找 yolo 实战项目源码做毕设或技术预研的开发者。项目运行环境是 Win10 + Python 3.9.7,所有依赖都在常见深度学习栈内,不用特殊硬件也能跑通。

2. YOLO目标检测与网球训练:小目标、损失函数、标注顺序

2.1 为什么选YOLO而不是Faster R-CNN

网球场景里有两类目标:球员是中等尺寸目标,而网球在画面中常常只有十几到几十个像素,属于典型的小目标。Faster R-CNN 这类两阶段检测器精度上限高,但在单张 1080p 画面上的推理速度很难做到实时,尤其后期还要跑关键点网络和追踪,CPU 或普通 GPU 压力会非常大。YOLO 将分类和回归合并到一次前向传播中,v5 版本在 GTX 1060 上跑 640x640 输入能到 50 FPS 左右,给后面的关键点计算留出了充足预算。

项目中给出的tennis_ball_detector_training.ipynb和主分析文件ball_analysis.ipynb都沿用 YOLO 格式的训练流程。如果你打开过 YOLOv5 源码就会发现,它的数据组织方式非常直接:每张图对应一个同名 txt,每行写一个目标。球员和球可以放在同一个模型里输出两个类别,但实际经验是把它们分开训练更稳妥,因为球的尺度太小,和球员共享特征图时容易被训练噪声带偏。这个项目选择的是分开处理:球检测器专注小目标,球员检测器负责稳定的大目标框。

2.2 标注数据与数据集格式转换

打开项目里的tennis_court_keypoints_training.ipynb之前,建议先看懂球检测的训练数据。项目附带了几张 Roboflow 导出的红土场样本,文件名类似clay175_jpg.rf.fc05e01e37329c42850a077f7497c418.jpg,这种命名是 Roboflow 导出的典型格式,框信息通常在配套的_classes.txt_annotations.coco.json里。如果你自己从零标注,我一般推荐用 CVAT 或 LabelImg 直接导出 YOLO txt,省去转换环节。

如果你手上只有 COCO 格式的标注,则需要转换成 YOLO 路径。下面这段脚本能把 COCO 的 bbox 转成 YOLO 的归一化中心坐标:

# coco2yolo.py:将COCO标注转为YOLO训练格式 import json from pathlib import Path def coco2yolo(coco_json, out_dir): with open(coco_json) as f: data = json.load(f) cat_map = {} for cat in data['categories']: # 假设 categories 里 id=1 是网球,id=2 是球员 cat_map[cat['id']] = cat['id'] - 1 # 转为 0/1 for img in data['images']: w, h = img['width'], img['height'] lines = [] for ann in data['annotations']: if ann['image_id'] != img['id']: continue cls = cat_map[ann['category_id']] x, y, bw, bh = ann['bbox'] cx = (x + bw / 2) / w cy = (y + bh / 2) / h lines.append(f"{cls} {cx:.6f} {cy:.6f} {bw / w:.6f} {bh / h:.6f}") out_path = Path(out_dir) / (Path(img['file_name']).stem + '.txt') out_path.write_text('\n'.join(lines))

代码里把 COCO 的左上角宽高格式转成了 YOLO 需要的中心点加归一化宽高。注意cat_map的映射逻辑,如果你的类别 id 不是从 1 开始,需要先验证一下样本的category_id,否则训练时类别标签会错位。转换完成后,把data.yaml中的nc设为实际类别数,names设为['tennis_ball', 'player']或你自己的顺序。

2.3 训练YOLO模型的命令与损失函数影响

如果复用 YOLOv5 的训练入口,命令如下:

python train.py --img 640 --batch 16 --epochs 100 \ --data tennis.yaml --weights yolov5s.pt \ --cache --workers 4

参数的含义并不复杂:--img控制训练尺寸,640 是速度和精度的平衡点;--batch受显存限制,GTX 1660 上 16 比较稳妥,16G 显存可以开到 32;--epochs我一般先跑 100 看看收敛曲线,球检测器通常 60 epoch 左右就能收敛;--cache会把图片预加载到内存,避免每次 epoch 都读磁盘;--workers在 Windows 上建议不要超过 4,否则会遇到 dataloader 卡死的问题。

说到损失函数,YOLOv5 的损失由三部分构成:box 回归损失用的是 CIoU,置信度损失和分类损失都用 BCE With Logits。训练小目标时最影响漏检的是置信度损失,因为背景样本远远多于前景样本,大多数负样本的梯度会淹没球的回传。项目里如果出现“训练完球老是检不到”的情况,先检查是否用了默认的hyp.scratch.yaml,可以适当调低loss_ot相关的正负样本分配阈值。YOLOv8 之后引入了 TAL 动态标签分配,对小目标的容忍度比 v5 高,但项目源码基于 v5 结构,不做迁移的话调参方向还是集中在训练尺寸和类别权重上。

2.4 训练完成后的验证:conf-thres与iou-thres

训练完模型后,最容易被忽略的是推理阈值。YOLO 默认conf_thres=0.25,对于网球这种小目标,置信度往往只有 0.2 左右,直接导致球被过滤掉。项目实战里我通常会先跑一遍检测脚本,把每帧的原始置信度打印出来,看分布再定阈值:

python detect.py --weights runs/train/exp/weights/best.pt \ --source input_video.avi --conf-thres 0.15 --iou-thres 0.5

conf-thres降低到 0.15 后,球框会出现一定数量的误检,但后续的轨迹跟踪可以靠运动连续性滤掉离群点。iou-thres是 NMS 的阈值,0.5 对于紧凑的球目标足够了,设得太高会把同一个球拆成多个框。验证时不要只看 mAP,还要抽查几段视频,观察球是否在击球瞬间保持连续检测,这是后续统计击球次数的数据基础。

3. CNN提取球场关键点与透视变换

3.1 关键点回归:热图比全连接更适合球场定位

球场检测的目的是拿到场地边界和发球线位置。常见做法不是直接回归四个角点坐标,而是让 CNN 输出热图。每个关键点对应一个二维概率分布,网络在学习过程中会隐式编码空间上下文,比全连接层直接输出数值稳定得多。项目中的tennis_court_keypoints_training.ipynb就是这种思路:输入是一帧图像,输出是 K 个通道的热图,每个通道对应一个球场关键点。

如果用 PyTorch 自己复现一个轻量版,结构大致是这样:

# keypoint_model.py:轻量关键点热图回归网络 import torch.nn as nn class CourtKeypointNet(nn.Module): def __init__(self, num_keypoints=6): super().__init__() self.backbone = nn.Sequential( nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, 3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(64, 128, 3, padding=1), nn.ReLU(), ) self.head = nn.Conv2d(128, num_keypoints, 1) def forward(self, x): feat = self.backbone(x) # 输入 [B,3,H,W] -> [B,128,H/4,W/4] heatmaps = self.head(feat) # 输出 [B,num_keypoints,H/4,W/4] heatmaps = nn.functional.interpolate( heatmaps, size=x.shape[2:], mode='bilinear', align_corners=True) return heatmaps

head用的是 1x1 卷积,把特征图压缩成关键点通道数,最后用双线性插值把热图恢复到原图尺寸,这样计算 loss 时可以直接和标注的高斯热图对齐。训练时损失函数选 MSE 就可以,但要注意对热图做高斯模糊标注,而不是标一个硬点。硬点会让网络输出尖峰,在后续argmax找坐标时容易出现像素级抖动,高斯热图给出的响应更平滑。

3.2 从关键点到单应矩阵

拿到关键点之后,最核心的一步是把图像坐标系的点映射到标准场地坐标系。网球场的底线、边线分布是固定的,只要知道图像里至少四个对应点,就能用cv2.findHomography算出单应矩阵 H。

# homography.py:用关键点计算单应矩阵并校正坐标 import cv2 import numpy as np # 标准球场坐标系(单位:英尺) std_pts = np.array([ [0.0, 0.0], # 左下角 [36.0, 0.0], # 右下角 [36.0, 39.0], # 右上角 [0.0, 39.0] # 左上角 ], dtype=np.float32) # 图像中检测到的对应关键点(由热图argmax得到) img_pts = np.array([ [120, 450], [520, 448], [518, 160], [125, 162] ], dtype=np.float32) H, status = cv2.findHomography(img_pts, std_pts, cv2.RANSAC, 3.0)

RANSAC 的 3.0 是重投影误差阈值,单位是像素,这个值控制了内点筛选的严格程度。关键点定位的抖动如果在 2 像素以内,阈值给 3 不会误伤;如果模型训练得一般,抖动达到 5 像素以上,建议放大到 5.0,否则后续测速时每帧的坐标会突然跳变。算出 H 之后,任何图像坐标都可以用cv2.perspectiveTransform转到标准坐标系,从而计算真实距离。

3.3 标注自己的关键点训练集

项目里没有直接给出关键点标注的原始标注文件,只有几张 jpg 样例。如果你要复现并落地到其他球场,需要自己标注。工具上我用 labelme,导出 JSON 后转成热图。标注的关键点位置选择很有讲究:不要只标四个角点,最好把两条发球线和网带交点也标上,因为网球比赛大多数击球落在发球区附近,有这些内点约束,单应矩阵的求解会更稳。

# labelme_json2heatmap.py:把关键点坐标转成高斯热图 import numpy as np import cv2 def generate_heatmap(img_h, img_w, points, sigma=3): # points: [(x, y), ...] heatmaps = [] for (x, y) in points: hm = np.zeros((img_h, img_w), dtype=np.float32) if x < 0 or y < 0: heatmaps.append(hm) continue # 高斯核覆盖范围为3倍sigma d = int(3 * sigma) xx, yy = np.meshgrid(np.arange(max(0, x-d), min(img_w, x+d+1)), np.arange(max(0, y-d), min(img_h, y+d+1))) hm[yy, xx] = np.exp(-((xx - x)**2 + (yy - y)**2) / (2 * sigma**2)) heatmaps.append(hm) return np.stack(heatmaps, axis=0)

sigma 取 3 到 5 比较合适。sigma 越小,热图越尖锐,训练时网络越难收敛;sigma 太大,坐标定位的精度会下降。实际训练时可以对热图做随机旋转和平移增强,因为不同场地的拍摄角度差异很大。值得注意的是,热图输出分辨率如果比原图小,比如 1/4 或 1/8,那么到实际坐标映射时要把argmax的结果放大相应倍数,否则关键点位置会系统性偏移。

4. 从逐帧检测到指标计算:速度、击球次数与轨迹平滑

4.1 多目标跟踪:从检测框到轨迹IOU匹配

要计算球员速度,首先得区分哪几个框属于同一名球员。最简单的做法是基于 IoU 的贪心匹配,虽然不像 DeepSORT 那样带外观特征,但在网球这种运动员数量固定的场景中已经够用。核心逻辑是:上一帧的跟踪框和当前帧检测框计算 IoU,超过 0.3 就认为是同一个目标,否则开启新轨迹。

# tracker.py:基于IoU的轻量级目标追踪 class IoUTracker: def __init__(self, iou_threshold=0.3): self.tracks = {} # id -> [bbox] self.next_id = 0 self.iou_threshold = iou_threshold def update(self, detections): matched = {} used = set() for tid, prev_box in self.tracks.items(): best_iou, best_det = 0, None for i, det in enumerate(detections): if i in used: continue iou = self._iou(prev_box, det) if iou > best_iou: best_iou, best_det = iou, i if best_det is not None and best_iou >= self.iou_threshold: matched[tid] = detections[best_det] used.add(best_det) # 未匹配的检测框创建新轨迹 for i, det in enumerate(detections): if i not in used: self.tracks[self.next_id] = det matched[self.next_id] = det self.next_id += 1 # 更新轨迹 self.tracks = {tid: matched[tid] for tid in matched} return matched

这段跟踪代码里没有做卡尔曼滤波,所以轨迹没有预测能力。如果球飞行的过程中有遮挡,检测框消失后再出现,IoU 匹配会失败,轨迹会断成两条。此时要么引入卡尔曼做运动预测,要么使用 ByteTrack 这种基于低置信度框二次匹配的算法。对于球员速度测量,断轨迹影响不大,但对于击球次数,球轨迹断裂会直接丢计数,所以球跟踪必须额外做插值。

实际评估跟踪质量时,我还习惯计算多目标跟踪的中心点偏差和 ID Switch 次数。这个项目里不需要生成 MOTChallenge 标准结果,但我会在调参时统计同一轨迹的检测框数量是否连续,如果轨迹断裂过多,就调低 IoU 阈值或加入运动预测。

4.2 击球次数判定:球速突变与接近检测

击球本质上是个瞬态事件,可以从球轨迹的突变来识别。正手击球瞬间,球的方向会在 1-2 帧内发生明显反转,同时球和球员的距离极小。因此我常用两个条件联合判定:球在连续两帧的位移方向夹角大于 60 度,且球到任一球员腰部的距离小于 2 米。

# shot_count.py:基于轨迹突变和球员距离统计击球次数 import numpy as np def count_shots(ball_track, player_boxes): shots = 0 for i in range(1, len(ball_track) - 1): p0 = np.array(ball_track[i - 1]) p1 = np.array(ball_track[i]) p2 = np.array(ball_track[i + 1]) v1 = p1 - p0 v2 = p2 - p1 if np.linalg.norm(v1) == 0 or np.linalg.norm(v2) == 0: continue cos_angle = np.dot(v1, v2) / (np.linalg.norm(v1) * np.linalg.norm(v2)) angle = np.arccos(np.clip(cos_angle, -1, 1)) # 判定击球:方向变化超过60度,且离球员近 if angle > np.deg2rad(60): for box in player_boxes: player_center = ((box[0] + box[2]) / 2, (box[1] + box[3]) / 2) dist = np.linalg.norm(p1 - np.array(player_center)) if dist < 120: # 像素距离,根据画面尺寸调整 shots += 1 break return shots

像素距离 120 并不是通用值。如果摄像头是全景视角,整个场地宽度可能只有 600 像素,120 像素已经很短;如果是跟拍特写,球和球员的像素距离会更大。我在实际使用时会先计算球员高度和图片高度的比值,动态缩放这个距离阈值,比固定值鲁棒得多。另外,这个判定逻辑会把球员截击和发球都算进去,如果你的指标只统计回合内击球,还需要外加一个“球必须落在场地内”的条件。

4.3 速度计算与透视校正

原始像素坐标不能直接测速,必须先通过第 3 章算出的单应矩阵投影到场地坐标系。假设视频是 25 FPS,帧间间隔是 0.04 秒,那么球员某一时刻的速度可以近似为相邻两帧投影坐标的欧式距离除以时间间隔。

# speed.py:利用单应矩阵计算球员速度 def compute_speed(projected_track, fps=25): speeds = [] dt = 1.0 / fps for i in range(1, len(projected_track)): p0 = projected_track[i - 1] # 标准坐标系下的坐标 p1 = projected_track[i] dist = np.linalg.norm(p1 - p0) # 单位与标准坐标系标定时一致 speed = dist / dt # 每帧速度 speeds.append(speed) return speeds

注意dist的单位取决于std_pts定义时用的单位。如果用英尺,那么算出来的速度是英尺每秒,转成公里每小时要乘 1.09728。更合理的做法是把标准球场坐标直接定义成米,网球场长 23.77 米、宽 8.23 米,这样速度单位就是米每秒,观众更容易理解。实战中,我会额外用移动平均窗口对速度曲线平滑,窗口大小 5 帧,否则逐帧的检测噪声会带来明显的锯齿。速度曲线的峰值可以用于评估球员爆发力,短期均值则用来衡量跑动强度。

5. 把源码跑起来:环境配置、推理调参与iframe级坑

5.1 Win10 + Python 3.9.7 的环境搭建

项目源码是在 Win10 和 Python 3.9.7 下开发的,不建议用 Python 3.12 直接跑,因为部分深度依赖的预编译轮子在 Windows 上还不全。我用到了以下安装顺序,能省掉很多编译问题:

python -m venv tennis_env tennis_env\Scripts\activate pip install torch==1.10.0+cpu torchvision==0.11.0+cpu -f https://download.pytorch.org/whl/cpu/torch_stable.html pip install opencv-python==4.5.4.60 numpy pandas jupyter matplotlib tqdm

Windows 下最麻烦的依赖是pycocotools,直接 pip 安装会报 Microsoft Visual C++ 错误。常见做法是下载预编译的pycocotoolswheel,或者用pip install pycocotools-windows这个替代包。如果你使用的是 AMD 显卡,YOLOv5 默认的 CUDA 版本无法直接调用,可以安装torch-directml包,并把推理设备改名为dml。这会牺牲一部分速度,但至少能把项目跑通。

5.2 三个notebook的分工和运行顺序

拿到源码后不要急着从头跑到尾。tennis_ball_detector_training.ipynb负责球的检测训练,tennis_court_keypoints_training.ipynb负责球场关键点训练,ball_analysis.ipynb才是主流程推理和分析。如果你没有足够的训练数据,建议先直接用项目里训练好的权重跑ball_analysis.ipynb,确认整个管线的输入输出,再回头看训练代码。主流程里的输入是input_video.avi,输出是output_video.avi,中间会打印每一帧的检测结果。

ball_analysis.ipynb中,有一个窗口对检测结果做后验证。我一般会先提取前 100 帧测试运行速度,如果处理一帧花了超过 0.1 秒,就需要降低输入分辨率或把球检测和关键点检测分到两个进程。项目里没有给出独立的推理脚本,但你可以用nbconvert把 notebook 转成.py文件,再用argparse封装视频路径参数。

5.3 提高小目标检出率的实用技巧

球检测是整个分析流程最脆弱的部分,跑通之后值得花时间调优。一个立竿见影的方法是提高推理尺寸:把 YOLO 的--img从 640 提到 960,小目标的感受野会覆盖更多像素,漏检率明显下降。代价是推理时间大约翻一倍,但测试阶段可以接受。另一个技巧是 TTA,也就是测试时增强,把原图、缩放、翻转分别推理再综合置信度,能进一步提升召回。如果球在快速运动时出现运动模糊,可以尝试把视频先做逐帧去交错了。input_video.avi如果采集自电视转播,可能带隔行扫描,直接用会导致球和球拍边缘出现拖影,建议用ffmpeg -i input_video.avi -vf yadif output_progressive.avi预处理后再送入模型。

对于关键点网络,常见的坑是训练时热图尺寸和原图不一致导致坐标偏移。务必在tennis_court_keypoints_training.ipynb里检查热图标签的下采样比例,并在推理时把argmax坐标乘上对应的 stride。如果发现底线检测不稳定,可以把关键点数量从 4 调整为 6,增加发球线中点约束,单应矩阵的 RANSAC 迭代次数也相应提高。最终把ball_analysis.ipynbconf_thres设到 0.15、iou_thres设到 0.5,并用 RANSAC 阈值 3.0,这套组合在我重放视频时击球计数的准确率能到 85% 以上。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 5:19:58

APP开发工程师全链路解析:从技术选型到上架面试

1. 从需求评审到商店上架&#xff0c;一个APP开发工程师到底在忙什么我说个挺常见的现象&#xff1a;很多人以为APP开发工程师就是“写代码的”&#xff0c;每天对着Android Studio敲屏幕就完事了。等你真正干了这行才发现&#xff0c;写代码只是其中一小块&#xff0c;需求评审…

作者头像 李华
网站建设 2026/9/16 5:19:38

做网站应该注意些什么问题一文搞懂避坑指南

做网站应该注意些什么问题一文搞懂避坑指南 很多老板找我们建站,开口第一句就是:“我不懂代码,但我有个好想法,能做个网站吗?”这种心态太普遍了。其实, 自己不会代码想做网站…

作者头像 李华
网站建设 2026/9/16 5:19:05

Nsight Systems实战:一眼看穿CPU与GPU协同中的性能瓶颈

前几天帮同事排查一个“明明在调用GPU却慢得离谱”的程序&#xff0c;日志打点、print大法全用上了&#xff0c;折腾一天也没定位到根因。后来用 Nsight Systems 做了一次完整采集&#xff0c;时间线一展开&#xff0c;真相立刻浮出水面&#xff1a;几个 CUDA kernel 之间有大段…

作者头像 李华
网站建设 2026/9/16 5:17:43

基于SM8436与R7KA8D2KFLCAC的微小压力检测系统设计与实现

当初接手这个任务的时候&#xff0c;我其实有点低估了它。标题里写着“检测和监测微小的压力变化”&#xff0c;听起来就是把一颗压力传感器接上单片机&#xff0c;读数据&#xff0c;完事。真正开始调才发现&#xff0c;微小压力检测这条链路&#xff0c;从传感器选型、硬件布…

作者头像 李华
网站建设 2026/9/16 5:17:32

Node+Express+MySQL电影平台后端实战指南

简介&#xff1a;这是一套基于Node.js全栈技术实现的猫眼电影平台源码&#xff0c;面向Web开发初学者与全栈进阶学习者&#xff0c;帮助其掌握Express框架路由设计、MySQL数据建模、前后端交互及静态资源组织等核心能力。资源共37个文件&#xff0c;含28张PNG&#xff08;涵盖电…

作者头像 李华
网站建设 2026/9/16 5:16:51

提示工程架构师的核心挑战与多模态设计实践

1. 提示工程架构师的核心挑战与价值定位在AI应用爆发式增长的当下&#xff0c;提示工程架构师&#xff08;Prompt Engineering Architect&#xff09;已成为大模型落地关键岗位。这个角色不同于传统架构师&#xff0c;需要同时具备自然语言理解、系统设计思维和心理学洞察三重能…

作者头像 李华