从球场标定到轨迹统计:羽毛球视频分析流水线拆解
本文目录
这是一篇公开开源项目的技术拆解,不是我的原创项目,也不代表我参与了仓库开发。内容依据公开仓库及本机源码快照做静态复核;本轮补充审阅固定在 24f2208929f84f441d7af3bdb1ba6bf6c109943f。没有安装模型权重、运行推理、测量准确率或复现性能数据。仓库自带 Apache-2.0 许可证;模型权重、训练数据和演示媒体的授权需要分别核对,不能由仓库代码许可证推断。
这类系统的难点并不是“调用一个检测模型”,而是把不同坐标系和不同时间尺度上的信号串起来:先从视频帧中定位球场,再把球员位置从像素投影到球场平面,随后追踪移动、识别回合边界,最后把结果写成视频和可继续分析的数据。
一、系统边界
项目面向本地比赛视频,提供命令行和 Gradio 浏览器界面。输入包括比赛视频、球场模板图和姿态/羽毛球检测模型;输出包含叠加标注的视频、逐帧 JSONL 检测记录,以及球员位置热力图和散点图。WebUI 的任务队列默认并发数为 1,适合本机依次处理较重的视频分析任务,不应直接等同于多用户在线推理服务。
从职责上可以拆成五层:
- 输入与预处理:读取视频帧、检查帧率与尺寸、加载模板图,并解析 CLI 或 WebUI 传入的模型与显示选项。
- 球场几何:自动找线并提出四个角点;自动检测失败时,由用户在模板图上手工点选四角。
- 检测与追踪:姿态模型找球员关键点,YOLO 模型找羽毛球候选,后续逻辑过滤不合理的位置并维护轨迹。
- 统计与可视化:按球场上下半区维护球员状态,计算移动距离和速度估计,叠加骨架、轨迹与统计信息。
- 结果交付:输出视频、元数据、逐帧结构化记录和位置图表;WebUI 还会尝试把视频转码为浏览器更容易播放的 H.264。
这条链路里最重要的依赖关系是:球场角点不是装饰性标注,而是后续球员分区、坐标换算和运动统计的基础。角点质量差时,即便人体关键点检测得很稳定,换算后的距离与速度也可能整体偏离。
二、四点标定为何决定统计质量
摄像机拍到的球场是透视图:远端球员在画面里显得更小,画面中的等长线段也未必等长。项目使用四个球场角点建立透视变换,把图像坐标映射到长 13.4 米、宽 6.1 米的平面坐标,再反向投影球场线用于视频叠加。
可把过程理解为:
- 在模板图上按固定顺序标出左上、右上、右下、左下四角。
- 将这四点与标准球场平面的四个对应点配对,求出单应矩阵。
- 把检测到的球员脚点投影到球场坐标,后续距离才具有“米”的近似单位。
- 反向变换标准球场上的边线、发球线和中线,让可视化叠回原视频。
自动检测部分不是简单地取画面最亮的四条线。源码会先提取横向/边线候选,再组合线段交点,按几何形状和参考球场线支持度打分,选择候选四边形;未找到可接受结果时返回失败,由界面提示手工标注。这种“自动建议 + 人工修正”的边界比假设所有球馆、光线和机位都相同更现实。
手工标注仍然是一个必须认真对待的校准步骤。拍摄机位、裁剪、模板尺寸或视频分辨率变化后,旧的四点不能机械复用;应重新确认四点顺序和预览叠线。标定输出要和原视频一起保存,最好记录模板图哈希、视频尺寸、角点以及模型版本,以便复算时知道结果从何而来。
三、球员与羽毛球是两条不同的检测链
球员姿态检测支持 RTMPose、RTMO 和 Ultralytics YOLO Pose 等模型族;不同模型和档位是在速度、显存与定位质量之间做取舍。检测器针对球场区域裁出姿态 ROI,减少无关背景上的推理工作,但羽毛球检测仍然以整帧为输入,再结合球场区域做候选过滤。
姿态模型提供的是关键点,不等于可靠的“球员身份”。项目把球场分为上半区和下半区,按检测点相对球场中线归入两个区域,并对同一半区的多个候选做选择。这适合固定机位、双方始终处于各自半场的分析场景;换边、交叉跑动、双打、遮挡或镜头切换时,不能把“上半区/下半区”误读成稳定的真实运动员身份。
羽毛球目标很小、运动快,运动模糊和背景线条都容易造成误检。检测结果还会通过 ROI 与离群点检查;未检出或被拒绝的点不会继续污染轨迹。这个做法能减少明显跳点,但它仍然是启发式筛选,不是基于多摄像机的精确三维轨迹重建。只凭单一普通视频,球速、击球点和遮挡期间的球路都应视为估计值。
四、回合分段与数据输出
系统通过连续若干帧是否仍像比赛球场视图来切换回合状态,而不是只看某一帧。进入稳定球场视图后启动新回合,连续离开球场视图达到阈值后结束当前回合并清理羽毛球轨迹。这能抑制单帧误判,但阈值与场景有关:暂停画面、广告、裁判入镜、摄像机轻微移动和场馆背景变化都可能影响分段结果。
JSONL 每行记录一帧的结构化数据,包含 schema 版本、原视频帧序号、时间秒数、实际参与检测的帧序号、球员记录和羽毛球图像坐标。它比只导出一张热力图更有价值:后续可以独立重画可视化、抽样检查误检,并把模型输出与人工标注做对比。
需要注意,检测帧序号与原始帧序号不是同一个概念。非球场帧会走提前返回路径;在当前实现中,这条路径没有把帧送入视频 writer。因此输出视频会省略一部分非球场画面。这样可能是为了压掉回合间空白,但也会改变成片时长;开启原音频时,必须实际检查音视频是否仍同步,不能只根据文件扩展名和转码成功来推断。
WebUI 会在有 FFmpeg 时把结果转为 H.264/AAC 并设置 fast-start;转码不可用或失败时回退到原始 OpenCV 视频文件。回退能保住分析结果,但原文件未必能被所有浏览器播放。部署或分享前应明确显示“浏览器可播版本是否生成”,并提供原始结果下载,不要把回退成功伪装成浏览器兼容成功。
五、从代码检查得到的可靠性提醒
下面是静态代码审阅发现的待验证风险,不是已通过实测复现的线上故障:
1. 速度统计可能压缩漏检时间
球员历史列表只在识别到有效球员位置时追加坐标。统计函数先过滤空位置,再把过滤后的列表下标差除以视频 FPS 当作时间跨度。若中间漏掉多帧,两个有效点在列表中仍可能只相差一个位置,计算出的时间会短于真实经过时间,速度因此可能偏高。
更稳妥的做法是让每个位置同时携带原始 frame_index 或 time_sec,速度分母直接使用两个检测点的真实时间差;再明确对低置信度点、身份切换和长间隔做拒算或插值策略。验证时应构造“连续帧、间歇漏检、长时间遮挡、重新出现”几组小型视频夹具,并与人工测量结果比较。
2. 输出视频是否应丢弃场外帧需要产品定义
处理循环遇到非球场视图会提前返回,写视频发生在后面的球场处理路径。结果是识别为非球场的帧不会进入输出视频。若目标是只导出有效回合,这可能是有意剪辑;若目标是保留完整原视频并叠加分析,则这是行为不一致。建议把模式显式化为“完整视频”与“仅比赛片段”,并按原始帧时间戳复核音频同步。
3. 依赖锁定应区分 CPU 与 GPU 环境
基础依赖同时列出 opencv-python 与 opencv-contrib-python,二者都提供 cv2 命名空间;在同一虚拟环境里并装,容易造成文件覆盖或依赖状态难以复现。可根据确实使用的扩展模块只保留一种发行包,并分别维护 CPU、CUDA/ONNX Runtime 的锁定方案。仓库 README 中的 GPU 安装示例是特定版本组合,不应脱离显卡驱动、CUDA 和 ONNX Runtime provider 状态直接照抄。
4. 推理结果要带上可复算的来源信息
建议把姿态模型名称/版本、羽毛球权重哈希、置信阈值、输入视频帧率与尺寸、模板图哈希、角点、软件提交号和参数写入元数据。仅保存坐标无法回答“这次为什么和上次不一样”,也不利于升级模型后做公平对比。
5. 处理异常时资源释放只在成功路径发生
process_video 打开 OpenCV 视频捕获器,创建视频 writer,并在处理开始后打开 JSONL 检测 writer;当前 _cleanup(cap) 位于主循环和进度回调之后的正常尾部。如果逐帧处理、进度回调或后续逻辑抛出异常,这条清理调用就不会执行,捕获器、视频 writer 或检测文件可能保持打开,输出目录也可能留下看起来像已完成的半成品。这里是源码控制流审阅所得的风险,没有通过故意注入异常做运行时复现。
改进时应把资源释放放进 try/finally 或上下文管理器,并分别保护各资源的 release/close,避免一个清理失败挡住其余清理;同时将产物先写入临时目录,只有视频合成、统计图和元数据都成功后才原子标记为完成。这样 UI 才能区分“分析完成”和“失败但留有部分文件”。
6. WebUI 输出目录名精度不足以保证并发唯一
WebUI 用输入视频文件名加 YYYYmmdd_HHMMSS 秒级时间戳生成输出目录,并允许目录已存在。Gradio 单实例串行队列可以降低碰撞机会,但多进程/多实例或其他并行调用者在同一秒处理同名文件时,仍可能写入同一个目录,造成元数据、检测记录或视频相互覆盖。文章没有通过并发运行复现该问题。
更稳妥的方式是加入随机唯一后缀,并用排他创建保证目录从创建时起就唯一;保留清理任务时,还应避免删除仍在处理中的目录,并记录无法清理的结果,而不是静默吞掉所有异常。
7. 帧数元数据为 0 会让收尾计算除零
process_video() 检查了 FPS 是否大于 0,但没有检查 CAP_PROP_FRAME_COUNT 是否为正数;若读取到的帧数是 0,video_duration 就会变成 0。收尾日志随后计算 processing_time / video_duration,会触发 ZeroDivisionError,而 _cleanup(cap) 位于这段日志之后,因此这条异常路径也绕过资源清理。该风险由静态控制流推导,尚未用零帧视频夹具复现。
应将“总帧数未知/为零”与“有效视频时长”分开处理:可在入口显式拒绝不可估时长的输入,或根据实际读取帧数计算时长并对零值作明确降级;无论何种处理,都应由 finally 保证视频捕获器、视频 writer 和 JSONL writer 关闭。
8. 已保存的球场标定文件会被当作 Python 表达式执行
BadmintonAnalysisSystem._setup_court_annotation() 在复用 court_annotations.txt 时,用 eval() 解析其中的角点和中线高度。正常流程会自己生成这个文件,但它是长期留在输出目录里的普通文本;如果文件被手工替换、从别处拷入或被其他流程写坏,下一次分析就可能执行文件内容,而不只是读取坐标。这是“可修改本地结果文件者”触发的代码执行风险,不等同于已经证实存在远程攻击入口。
应改用 JSON 或 ast.literal_eval(),并在构造透视变换前校验四个有限数值坐标、顺序、图像边界和四边形面积;无效状态应明确报错并要求重新标定,不能静默接受。
9. 坐标转换函数的 NumPy 输入契约不一致
CourtMapper.image_to_court() 声明接受 list、tuple 与 numpy.ndarray,但空值检查使用 not point。当调用者传入常见的多元素 NumPy 数组时,数组不能转换成单一布尔值,会抛出 “truth value is ambiguous” 错误。当前追踪路径通常传入列表,因此不能据此断言常规分析必然失败;这是公开函数参数契约和边界检查不一致的问题。
空值检查应显式判断 point is None,再按序列长度或数组 size 判断空输入;随后用列表和 NumPy 数组各补一组单元测试。
10. WebUI 的历史结果清理和元数据需要显式告知
每次开始 WebUI 分析前,_cleanup_old_outputs() 会把 outputs/webui_* 目录按修改时间排序,并删除保留数量之外的旧目录;删除异常被忽略,界面没有提示即将清理哪些结果。默认保留上限是 10 个,这属于结果保留策略,不应让用户误以为历史文件永久保存。应在界面标出保留数量、清理时机和结果下载方式,并确保并发任务不会删除仍在使用的目录。
此外,metadata.json 会写入输入视频和球场模板的路径。路径可能带有本机用户名或项目目录信息;分享分析结果前应允许移除或改为相对路径,并检查 JSONL、日志和视频是否包含其他不应公开的素材信息。
以上三项是对固定源码快照的静态补充,没有在原仓库改代码或运行 UI 验证。该结论只适用于所列 commit,不代表上游之后的版本仍保持相同行为。
六、怎样复现而不把演示当成验收
README 给出的基本流程是:建立隔离的 Python 虚拟环境,安装 CPU 基础依赖;如需 WebUI 再安装 Gradio 附加依赖;准备与代码版本相匹配的姿态模型和羽毛球权重;选择一段固定机位、球线清晰的视频与模板图;先完成自动或手工四点标定,再运行分析。
每次实验应固定输入视频、模板、依赖锁、模型文件和参数,并把原始帧号、角点以及输出目录一起归档。先用短视频确认解码、坐标映射、标注输出与统计逻辑,再扩大到整场视频。GPU 是否生效要分别检查 PyTorch CUDA 与 ONNX Runtime providers;仅仅安装了 GPU 包并不证明推理真的跑在 GPU 上。
当前文章没有执行该仓库的依赖安装、模型下载、CLI/WebUI 启动或硬件推理,也没有检查演示视频中的统计值。因此文中的性能和准确性结论均以代码行为与 README 声明为限,不是实测基准。运动分析可用于复盘与训练观察,不应被描述成专业裁判判罚、医疗建议或无误差测量设备。
总结
这个项目把视觉运动分析拆成可理解的步骤:四点球场标定提供统一坐标系,姿态检测与羽毛球检测分别处理不同目标,过滤和追踪生成时间序列,统计与可视化再把结果交给人检查。最值得借鉴的不是某个单独模型,而是“检测结果必须经过几何校准、时间对齐和人工校验”的系统观。
要把它从研究工具推向可靠产品,优先补齐真实时间戳速度计算、零帧数/未知时长的输入处理、完整视频与剪辑模式、音视频同步验收、异常清理、依赖隔离以及模型/输入元数据。每一项都能用小型可复现夹具验证,比单纯换更大的检测模型更能提升结果可信度。
项目源码
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。