ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

YOLOv8+Streamlit实战:球员与足球检测跟踪全流程

YOLOv8+Streamlit实战:球员与足球检测跟踪全流程 简介这是一套面向计算机视觉初学者与进阶开发者的足球场景分析实战资源围绕YOLOv8与Streamlit构建可完成球员、裁判与足球的检测跟踪并进一步实现球队颜色预测、球场关键点识别以及战术地图上的位置估计。资源包共67个文件约380.97MB包含29张jpg与15张png图像素材、5段mp4演示视频、4个yaml配置、4个pt模型权重、2个csv数据表、2个py脚本及ipynb笔记本等覆盖从数据配置、模型推理到可视化页面的完整链路。Streamlit主界面设有使用说明、团队颜色、模型超参数与检测三个Tab页便于快速上手调试。目前已有421人学习下载适合希望掌握目标检测与体育视频分析的中高级读者参考可借助现成权重与演示视频复现检测效果并在此基础上扩展战术分析与位置估计功能。1. 从一段球场录像说起YOLOv8Streamlit 到底能解决什么手里有一段比赛录像想快速知道每个球员跑了多少、球在哪些帧里出现、谁在什么时候靠近球——这件事如果纯靠人眼盯一场 90 分钟的比赛够看一整天。基于 YOLOv8Streamlit 实现球员和足球检测与跟踪本质就是把「检测」和「跟踪」两件事串成一条流水线YOLOv8 负责在每一帧里框出球员和足球跟踪算法负责给这些框分配稳定 IDStreamlit 负责把结果变成一个能上传视频、点一下就能看标注结果的网页。它解决的不是「训练一个 SOTA 模型」的问题而是「让一个检测跟踪 demo 在半天内跑起来、还能给别人演示」的问题。适合谁适合手里有比赛录像、想快速做战术分析原型的体育方向开发者也适合刚学完 YOLOv8 想找一个完整落地项目练手的人。下面按「环境怎么搭 → 检测怎么跑 → 跟踪怎么接 → 界面怎么做 → 坑在哪」的顺序讲透。2. 环境与数据把 YOLOv8 和 Streamlit 装进同一个 Python 环境2.1 为什么选 YOLOv8 而不是 v5 或 v11YOLOv8 在 Ultralytics 体系里是工程化最成熟的一代ultralytics一个包同时提供训练、验证、推理和导出API 统一文档里model.predict()和model.track()直接可用跟踪不需要再单独接 DeepSORT 的复杂配置。相比 YOLOv5v8 的 anchor-free 头对小目标足球在远景镜头里可能只有十几个像素更友好相比 YOLOv11v8 的社区示例和预训练权重更全遇到问题更容易搜到答案。我一般会直接装ultralytics它会把 PyTorch、OpenCV 等依赖一起拉下来。2.2 一条命令装好环境并验证# 建议 Python 3.9~3.113.12 部分依赖还没跟上 python -m venv venv source venv/bin/activate # Windows 用 venv\Scripts\activate pip install ultralytics streamlit opencv-python pandas # 验证 YOLOv8 能加载预训练权重 yolo predict modelyolov8n.pt sourcehttps://ultralytics.com/images/bus.jpg这段命令做了三件事建虚拟环境隔离依赖、装核心包、用官方示例图跑一次推理。yolov8n.pt是 nano 版本第一次运行会自动下载权重。如果卡在下载可以手动把权重放到当前目录再指定model./yolov8n.pt。CPU 版本也能跑只是帧率会低gtx1660ti这类显卡跑 nano 模型实时性足够。2.3 数据从哪来、怎么标球员和足球检测的数据集常见做法是两条路一是用公开的足球比赛数据集如 SoccerNet 系列做预标注再人工修正二是自己截帧用labelme标注后转 YOLO 格式。YOLO 格式每张图对应一个.txt每行是类别 x_center y_center width height坐标都归一化到 0~1。转换脚本如下import json, os from pathlib import Path # labelme 的 json 转 YOLO txtclasses 顺序要和 data.yaml 一致 classes [player, ball, referee] def convert(json_dir, out_dir): for jf in Path(json_dir).glob(*.json): data json.loads(jf.read_text()) h, w data[imageHeight], data[imageWidth] lines [] for shape in data[shapes]: label shape[label] if label not in classes: continue (x1, y1), (x2, y2) shape[points] # 归一化并转成中心点宽高 cx (x1 x2) / 2 / w cy (y1 y2) / 2 / h bw abs(x2 - x1) / w bh abs(y2 - y1) / h lines.append(f{classes.index(label)} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) (Path(out_dir) / (jf.stem .txt)).write_text(\n.join(lines))逻辑说明labelme 存的是左上右下两个点YOLO 要的是中心点和宽高所以先算中心再除以图像宽高归一化。参数上classes列表的顺序决定了 txt 里的类别索引必须和训练时data.yaml的names完全一致否则模型会把球员学成足球。标完记得按 8:1:1 划分 train/val/test足球样本少的话要做过采样不然模型对足球的召回会很低。3. 检测与跟踪用 model.track 一次拿到框和 ID3.1 检测和跟踪为什么要分开理解检测是「这一帧里有什么」跟踪是「这一帧的框和上一帧的哪个框是同一个人」。YOLOv8 只做检测跟踪靠的是 Ultralytics 内置的 ByteTrack 或 BoT-SORT。model.track()把两步封装在一起返回的每个框多了一个id字段。理解这个分工很重要如果球员被遮挡几帧后 ID 变了那是跟踪器的问题不是检测器的问题调参方向完全不同。3.2 最小可运行的检测跟踪脚本from ultralytics import YOLO import cv2 # 加载自己训练的权重没有就用 yolov8n.pt 先跑通流程 model YOLO(runs/detect/train/weights/best.pt) cap cv2.VideoCapture(match.mp4) writer None while cap.isOpened(): ret, frame cap.read() if not ret: break # persistTrue 让跟踪器在帧间保持状态这是 ID 稳定的关键 results model.track(frame, persistTrue, trackerbytetrack.yaml, conf0.3, iou0.5, verboseFalse) annotated results[0].plot() # 直接把框、ID、类别画在图上 if writer is None: h, w annotated.shape[:2] writer cv2.VideoWriter(out.mp4, cv2.VideoWriter_fourcc(*mp4v), 25, (w, h)) writer.write(annotated) cap.release() writer.release()逻辑说明persistTrue是整段代码里最容易漏的参数不设它跟踪器每帧都重置ID 会从头开始跳。tracker可选bytetrack.yaml或botsort.yaml前者快、后者对遮挡更稳。conf0.3是置信度阈值足球小目标可以降到 0.2 提高召回代价是误检变多。results[0].plot()返回的是画好框的 numpy 数组直接喂给VideoWriter就能出视频。3.3 关键参数怎么调参数作用建议值调整方向conf检测置信度阈值0.25~0.35漏检多就降误检多就升iouNMS 重叠阈值0.5球员密集时降到 0.4 减少框合并imgsz推理分辨率640足球太小可升到 960显存换精度tracker跟踪器选择bytetrack遮挡严重换 botsortpersist帧间保持状态True必须为 True否则 ID 乱跳imgsz是最影响足球检测的参数。远景镜头里足球可能只占 20 个像素640 分辨率下特征几乎消失升到 960 或 1280 能明显改善但推理速度会成倍下降。我的习惯是先固定 640 跑通全流程再针对足球单独看召回不够再升分辨率。4. Streamlit 界面把检测跟踪包成一个能演示的网页4.1 为什么用 Streamlit 而不是 FlaskStreamlit 的核心优势是「不用写前端」。上传文件、显示视频、放进度条、调滑块都是几行 Python。对于检测跟踪这种「上传视频 → 跑模型 → 看结果」的线性流程Streamlit 的脚本式写法比 Flask 的模板路由省一半代码。代价是它不适合做复杂交互和长期运行的服务但对 demo 和内部演示完全够用。4.2 上传视频并展示结果的完整页面import streamlit as st import tempfile, cv2 from ultralytics import YOLO st.title(球员与足球检测跟踪) model YOLO(runs/detect/train/weights/best.pt) uploaded st.file_uploader(上传比赛视频, type[mp4, avi, mov]) conf st.slider(置信度阈值, 0.1, 0.9, 0.3, 0.05) if uploaded and st.button(开始检测): # Streamlit 上传的是内存文件先落盘给 cv2 读 tfile tempfile.NamedTemporaryFile(deleteFalse, suffix.mp4) tfile.write(uploaded.read()) cap cv2.VideoCapture(tfile.name) stframe st.empty() while cap.isOpened(): ret, frame cap.read() if not ret: break results model.track(frame, persistTrue, confconf, verboseFalse) # plot 返回 BGRStreamlit 要 RGB stframe.image(cv2.cvtColor(results[0].plot(), cv2.COLOR_BGR2RGB)) cap.release()逻辑说明file_uploader拿到的是内存对象OpenCV 读不了必须先写临时文件。st.empty()创建一个占位符循环里不断替换它就实现了视频播放效果。颜色转换那一步是血泪经验忘了转整个画面会偏蓝。conf滑块让演示时能实时调阈值比改代码重启直观得多。4.3 让页面不卡死的两个处理上面这段代码在长视频上会一直占着主线程页面转圈。常见做法是把帧率降下来比如每 2 帧处理 1 帧或者用st.progress显示进度。另一个坑是web_view加载 Streamlit 白屏通常是端口没对或者跨域本地跑用streamlit run app.py默认 8501 端口浏览器直接开localhost:8501就行。如果要在 PyCharm 里跑配置运行脚本为streamlit run app.py而不是直接运行 py 文件。5. 避坑与排查五个真实翻车现场5.1 球员 ID 每帧都在变现象视频里同一个球员的 ID 从 1 跳到 5 再跳到 12。原因model.track()没加persistTrue跟踪器每帧重新初始化。解决加上persistTrue并确认是在循环外加载模型、循环内调用 track。5.2 足球几乎检测不到现象球员框很准足球基本没框。原因足球像素太少640 分辨率下特征丢失且训练集里足球样本远少于球员。解决推理imgsz升到 960训练时对足球类别过采样conf降到 0.2。5.3 显存爆了跑一半中断现象跑几分钟报 CUDA out of memory。原因imgsz调太高或 batch 太大视频帧累积没释放。解决降imgsz到 640循环里不要保存所有 results处理完一帧就丢。5.4 Streamlit 上传大视频直接崩现象上传几百 MB 视频页面无响应。原因Streamlit 默认上传上限 200MB且一次性读进内存。解决在.streamlit/config.toml里设maxUploadSize或先本地压缩视频再上传。5.5 自己训练的模型类别对不上现象检测出来的框类别全是错的。原因data.yaml的names顺序和标注时的classes不一致。解决训练前打印data.yaml核对标注脚本里的classes列表必须和它逐字一致。6. 进阶把跟踪结果变成可分析的战术数据跑通检测跟踪只是第一步真正有价值的是把 ID 和坐标变成数据。results[0].boxes里有id、xyxy、cls遍历一遍就能导出成 CSV再用 pandas 算每个球员的跑动距离、触球次数。下面这段把跟踪结果落成结构化数据import pandas as pd records [] frame_id 0 while cap.isOpened(): ret, frame cap.read() if not ret: break results model.track(frame, persistTrue, verboseFalse) boxes results[0].boxes if boxes.id is not None: for box, tid, cls in zip(boxes.xyxy, boxes.id, boxes.cls): x1, y1, x2, y2 box.tolist() records.append({ frame: frame_id, track_id: int(tid), cls: int(cls), # 用框底边中点近似脚的位置算跑动更准 foot_x: (x1 x2) / 2, foot_y: y2, }) frame_id 1 df pd.DataFrame(records) df.to_csv(tracks.csv, indexFalse) # 按 ID 分组算轨迹长度需要先做像素到米的标定 print(df.groupby(track_id).size().sort_values(ascendingFalse).head())逻辑说明用框底边中点而不是中心点代表球员位置因为脚的位置才反映真实移动。track_id分组后可以算每个 ID 出现的帧数出现帧数特别少的 ID 多半是误检可以过滤掉。像素到实际距离的换算需要场地标定常见做法是用球场中圈或边线做参照这一步没有标定就只能算相对跑动。验证跟踪质量有个简单办法把tracks.csv里某个 ID 的坐标按帧画成折线正常轨迹应该连续平滑如果出现大跳变说明 ID 切换了。我一般会先看出现帧数前 10 的 ID它们的轨迹是否合理基本能判断跟踪器在这段视频上的表现。另一个技巧是导出带 ID 的视频后用播放器逐帧看关键片段比看数字直观。最后说个习惯每次换视频或换权重先跑 100 帧看 ID 稳不稳、足球召回够不够再决定要不要跑全片。全片跑完才发现 ID 乱跳那才是真的后悔药没处买。这套流程从环境到界面大概半天能跑通难的是数据标注和参数微调那部分只能靠具体场景磨。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进