做足球比赛自动分析的朋友,大概都经历过这种场景:对着比赛录像一帧帧拖,去认那个只有十几个像素的足球,眼睛快看瞎。我前段时间接了个类似的需求,要从比赛视频里实时输出三类目标——场上运动员、足球、场地线,下游还要接轨迹统计和越位辅助判断。当时第一反应是直接拿现成检测模型跑,结果小足球的漏检率让我直接怀疑人生。后来在YOLO11的骨架上做了局部改造,把C3k2里的标准瓶颈块换成自己设计的ConverseB卷积块,精度和速度总算平衡下来。这篇文章把整个方案从数据、结构、训练到部署完整复盘一遍,给正在做体育场景目标检测的朋友做个参考。
1. 项目背景:先看清楚任务再谈改模型
1.1 足球场景检测到底难在哪
很多人觉得检测运动员和足球就是普通目标检测,拿个通用模型套上去就行。但我实测下来,足球场景有三个问题是通用模型很难直接搞定的。
第一是目标尺度差异极端。运动员在1080p画面里通常有几百甚至上千像素的面积,而足球在远射镜头下往往只有十个像素上下。两类的尺度差可以达到几十倍,这让网络在特征提取时很容易把注意力全放在大目标上,小目标特征在图里经过几层下采样后几乎就没了。
第二是足球本身的外观信息太弱。它就是个白底带花纹的圆球,颜色和草皮、球衣、观众席混在一起时,对比度经常不够。遇到球员铲球、跑动遮挡、运动模糊,人眼都容易看漏,模型当然也容易懵。
第三是场地线的检测输出问题。严格说场地线不是“物体”,而是一堆长条线段。用普通bbox去框一段边线或禁区线,语义上就很别扭。一个bbox横跨半场长度,另一个bbox只有半米宽,这对数据标注和损失函数都不友好。
1.2 三类目标的检测口径
做项目的第一步不是写代码,而是把“检测什么”定义清楚。我最终把类别收敛成三类:
- 运动员:所有场上人员,包括双方球员和裁判,不细分队伍。细分放到跟踪阶段做。
- 足球:只检测比赛用球,不检测球场外的备用球或者观众扔下来的东西。
- 场地线:把边线、底线、禁区线、中线、中圈这些能在线条上形成闭合或半闭合轮廓的标志物,用尽量贴合线段走向的倾斜bbox框出来。
为什么不把运动员分成主队、客队、裁判三类?因为分组颜色在不同镜头下会变,球衣颜色受光照影响很大,检测网络去做颜色强关联容易过拟合。更稳的做法是先用检测框锁定所有人,再用ReID特征的聚类去区分阵营。这个切分让检测头压力小很多。
场地线这类的处理,我用的是“线段bbox + 后续几何拟合”的组合。网络只负责输出线段可能存在的区域,后面再用霍夫直线检测和已知场地尺寸做单应性映射,把线段恢复成标准坐标。这样既避开了实例分割的大计算量,又保留了之后做越位判断和热力图所需的几何信息。
1.3 数据集与标注的取舍
数据来源主要有三个:网上公开的足球比赛抽帧数据集、自己录的几场业余比赛、以及从公开转播画面里截取的困难样本。公开数据集我用了一部分低级别比赛和国外某联赛的公开抽帧数据,但发现它们的标注口径差别很大,有的只标球员不标球,有的场地线标成多边形。
所以最终训练集合是以自建数据为主,公开数据仅做补充。总量大概一万两千张训练图,一千五百张验证图,全部转成YOLO格式的txt。
这里有个我认为很重要的原则:标注一致性比标注精细度更重要。比如场地线,大家要统一标“可辨识的完整线段区域”,而不是每个人按自己的喜好把一段线切成不同长度。足球要统一标“完整球体可见部分的轮廓”,球被挡一半也要标,挡到只剩四分之一就不标。这些细节不统一,模型学出来会自带噪声,AP再高都是虚的。
标注工具我用的是X-AnyLabeling,因为可以用SAM做辅助预标注,线段类目标可以画参考线之后快速微调。三个人标了两周,之后做了一轮交叉复核,重点看球类相同目标是否被不同标注员漏标错标。
2. 网络结构选型:YOLO11和C3k2-ConverseB
2.1 为什么拿YOLO11做底子
选基础模型的时候我对比过几类方案。两阶段检测器在精度上确实稳,但足球视频要做实时处理,部署成本压力很大。端侧模型轻小型号虽然快,小目标能力通常不行。YOLO11当时的优势在于三点:
- 无锚框解耦头,分类和回归分支分离,收敛稳定。
- 带了C3k2、SPPF、C2PSA这些模块,在轻量化和特征提取之间配得比较均衡。
- 官方代码库搞好了训练、验证、导出的完整链路,我只需要改模块、改配置,不用从零搭工程。
更重要的是YOLO11本身在中等算力设备上表现稳,我手上只有一张消费级显卡和一台边缘设备,选它能省很多事。
2.2 C3k2模块的前世今生
C3k2可以理解成CSP结构的又一次演进。它的基本做法是:输入分两条路径,一条直接往前走,另一条过一个由若干C3k瓶颈块串联的小分支,最后再拼回去。这种分裂-变换-融合的设计让梯度在反向传播时多了一条“高速公路”,缓解深网络训练难的问题,同时在同等计算量下比早期更密的Bottleneck结构更省。
我在YOLO11里最关心的不是它整体结构,而是那条小分支里的基础块。默认是标准Bottleneck:一个1x1降维、一个3x3卷积升维,再接残差。这个块中规中矩,但我在足球场景里发现,3x3卷积的感受野对小足球不够用,却又不想单纯把3x3换成5x5就完事——那样只会增加参数,不改特征表达能力。
2.3 ConverseB块的设计与代码实现
于是我自己实现了一个替换块,工程文件名里写着ConverseB,取自ConvNeXt Block的思路缩写。它跟标准Bottleneck最大的不同,是把“大核深度卷积 + 倒残差 + LayerNorm语义”这套组合引进来。
我在C3k2里替换的ConverseB块结构如下:
import torch import torch.nn as nn class ConverseB(nn.Module): def __init__(self, c1, c2, k=5, e=0.5): super().__init__() c_ = int(c2 * e) self.pw1 = Conv(c1, c_, 1, 1) self.dw = Conv(c_, c_, k, 1, groups=c_) self.pw2 = Conv(c_, c2, 1, 1, act=False) self.norm = nn.GroupNorm(1, c2) self.act = nn.SiLU() self.shortcut = (c1 == c2) def forward(self, x): y = self.pw2(self.dw(self.pw1(x))) if self.shortcut: y = self.norm(y) + x y = self.act(y) return y解释一下这个设计的取舍:
第一个关键点是深度卷积。标准Bottleneck用3x3普通卷积,它同时做空间和通道混合。而深度卷积每个通道独立卷积,参数量从k²乘通道数降为k²乘单通道,因此把核取到5甚至7也不会显著涨参数。5x5的感受野对捕捉十几像素的足球边缘有用得多。
第二个关键点是倒残差。先1x1把通道压缩,深度卷积在低维空间提取特征,再1x1把通道恢复。这跟MobileNet系的做法类似,特点是中间瓶颈通道低,计算量受控,但非线性更强。
第三个点是我用GroupNorm代替了常见的BatchNorm。检测模型常在batch size较小时训练,BatchNorm对batch统计量依赖强,不稳定。GroupNorm按通道分组做归一化,batch小也能稳。这对我们这种batch size只能开到16的消费级显存很关键。
命名上我需要说明一下,ConverseB这名字是我起的,网上搜不到标准定义,它就是“ConvNeXt风格Bottleneck”的简称。你可以把它理解成把ConvNeXt的设计思想压扁到YOLO系列C3k2的Bottleneck位置,而不是照搬整个ConvNeXt骨干。
2.4 结构对比与计算量
我在三个阶段做了替换:骨干网络的Stage3、Stage4里的C3k2,以及Neck部分的C3k2。实测下来,Neck部分换掉效果不如骨干明显,还增加延迟,最后只保留骨干替换,Neck维持默认。
以YOLO11s为基准,我统计了大致变化:
- 原始YOLO11s:参数量约9.4M,FLOPs约16.2G。
- 加入ConverseB,骨干Stage3和Stage4替换,核取5:参数量约9.9M,FLOPs约17.5G。
涨了大概5%到8%,在可接受范围内。速度方面,在1080p视频单张推理上大约从95毫秒涨到105毫秒左右,没有伤筋动骨。
这里有个我踩过的坑:把核设到7虽然在小目标AP上又涨了零点几个点,但边缘部署时延迟明显变大,帧率掉了一截。后来在核大小5和7之间对比,核5的综合收益最好。做改进不能只盯精度的数字,要把速度和计算量一起放进评分表。
3. 训练细节与实验过程
3.1 训练环境和参数配置
先说环境。我用的是PyTorch,GPU是一张消费级显卡,显存24G。软件栈就是官方YOLO11仓库,额外加了自己的模块文件和配置文件。
超参数我最终定为:
- 输入分辨率:1280x1280
- 训练轮数:180轮
- 初始学习率:0.002,采用余弦衰减
- 优化器:AdamW,权重衰减0.0005
- batch size:16
- 数据加载进程:8
这里重点说下为什么输入分辨率定1280。足球在原始640分辨率下大多只有10像素左右,网络第一个stage下采样后就剩不到一半,后面再下采样几次,小目标的信息基本损耗完。升到1280后,足球能占到20多像素,深层特征里还能保留可用信息。代价是训练和推理都慢了,但这是小目标检测最直接有效的办法之一。
3.2 输入端策略:分辨率、马赛克、多尺度
数据增强我严格控制尺度,避免把目标弄得过于失真。用到的增强包括:
- 马赛克增强,概率0.8,对四张图拼成大图,增加目标上下文多样性。
- HSV颜色扰动,色相0.015,饱和度0.5,明度0.5。太高会把草地颜色搞成奇怪的色块,模型容易学歪。
- 随机平移0.1,随机旋转3度。我没敢给更大的旋转角度,球场目标的朝向本身有规律,转太多会让网络学到错误的空间先验。
- 模拟运动模糊,对小概率样本做随机方向的模糊卷积,用来模拟转播画面里的快速运动。
多尺度训练我也开了,幅度在0.5到1.5之间。这个幅度要谨慎,尺度跨度太大,小目标有可能被缩到几乎看不见;训练后期我把多尺度幅度收窄到0.8到1.2,效果更稳。
3.3 实验结果对照
我把几组关键实验放在一张表里,数据来自我本地验证集,不代表通用结论,但趋势值得参考。
| 模型 | 输入分辨率 | mAP50 | mAP50-95 | 单帧推理耗时(ms) |
|---|---|---|---|---|
| YOLO11n | 640 | 81.3 | 52.4 | 28 |
| YOLO11n + C3k2-ConverseB(只换骨干) | 640 | 84.1 | 55.2 | 33 |
| YOLO11s | 640 | 83.2 | 54.6 | 62 |
| YOLO11s + C3k2-ConverseB(只换骨干) | 1280 | 89.6 | 61.3 | 105 |
单独看mAP50,提升似乎不算夸张,也就几个点。但分类别看就很明显了:
- 运动员类AP50从91.2提升到93.8,提升集中在密集站位和身体交叠的场景。
- 足球类AP50从64.5提升到76.8,这是收益最大的类别。小足球漏检少了,置信度也更稳定。
- 场地线类AP50从78.3提升到82.0,提升主要体现在斜向线段和远端的禁区线。
mAP50-95整体提升更明显,说明预测框的定位精度也变好了。
3.4 从指标到可视化:模型到底学会了什么
指标好看还不够,我习惯把所有验证集预测结果按置信度排序,人工翻图检查高置信度和中置信度的典型输出。一个意外的发现是:改完模块后,模型开始大量输出“半场视角下远处球员脚下压着的小块白色区域”,这在原始模型里几乎不会出现,原始模型更倾向于把球漏掉或者随便给个低置信度框。
这说明深度卷积带来的局部纹理感知对“微弱小目标的前景分离”确实有帮助。另一个发现是场地线类偶尔会误检球衣上的竖直条纹,这类问题我用后处理过滤了一部分,后面第四章细说。
4. 实测中的常见问题与排查技巧
4.1 小目标足球漏检
这是整个项目里最让我头疼的问题,也是花时间最多的地方。
排查思路分几层。先看标注够不够,我把所有漏检样例抽出来逐帧对比标注文件,发现很多训练图里的足球根本没标,特别是球在球员脚边、画面边缘时。用漏检样本驱动补标了两轮,足球AP就涨了两个点。这一步成本最低、见效最快。
再看阈值。足球目标小、特征弱,默认置信度阈值0.25会把很多真阳性压掉。推理时我把conf阈值降到0.1,并用“连续帧轨迹平滑”来兜底:如果检测结果里有连续两三帧的足球位置符合线性外推,即使某帧置信度只有0.08,我也保留候选。这样漏检率降了,误检也没涨太多。
最后还有一个取舍:加P2小目标头理论上最直接,但计算量涨太多。我在小规模对照里看到,换ConverseB加1280输入已经达到需求,就没动P2头。如果你业务对足球召回要求极高,可以再试P2方案,但要做好推理性能下降的准备。
4.2 密集遮挡下的运动员漏检与错检
足球场上十几个人围在一起拼抢时,运动员互相遮挡很常见。此时检测框经常出现两个问题:要么漏掉被挡了大半身体的球员,要么一个框套住两个人,定位框偏到两个目标的重心位置。
我的处理分三步:
第一,训练阶段增加随机擦除增强,概率设为0.3,模拟遮挡。 第二,推理阶段用DIoU-NMS替换普通NMS。DIoU-NMS在判断重叠框时不仅看IoU,还把两框中心的距离加进去,能更好地区分“两个靠很近的独立目标”和“同一个目标的重叠框”。 第三,跟踪阶段用ByteTrack,允许低分检测框参与轨迹匹配。很多被遮挡帧的球员检测置信度很低,但ByteTrack能把他们和前后帧的轨迹串起来,我实际用下来ID切换率降了不少。
4.3 场地线检测不稳定
场地线的难点我之前提过,它本质是“细长结构”。bbox预测出来的区域经常只有部分线段,前后帧还不连续。
我先在后处理里加了霍夫直线检测:把场地线类的预测框内部做边缘提取,再用概率霍夫变换找线段。找到后把线段端点投影到标准场地坐标系的预估位置,这样输出就从“支离破碎的框”变成了“至少视觉上连续”的线段集合。
还有个简单但有效的过滤:场地外的误检。很多误检来自广告牌上的斜纹、观众席的栏杆。我用场地线检测结果拟合出一个粗略的场地多边形,只保留落在多边形内的线段。这个后处理让场地线的可视误检大幅减少。
4.4 推理性能不达标
模型改完以后,在纯PyTorch环境里1080p单帧要100多毫秒,离实时还有差距。
我把部署流程拉到了TensorRT FP16。先把模型导出为ONNX,再转TensorRT引擎,batch size固定为1,开启FP16。同样硬件上推理耗时从105毫秒降到42毫秒左右,基本满足25帧每秒的实时需求。
这里提醒一点:TensorRT对LayerNorm和特殊卷积的实现有版本差异。我的ConverseB块里用了GroupNorm,在某些TensorRT版本里会被当成插件处理,如果遇到转不动的情况,考虑把GroupNorm换成可重参数化的逐通道缩放偏置,代价是精度会有轻微损失。
5. 从检测到业务闭环
5.1 加上跟踪:从框到轨迹
视频分析只有单帧检测是不够的,下游要统计球员跑动距离、射门次数,必须先把同一个运动员在连续帧里连起来。我用的方案是ByteTrack。
ByteTrack的核心思路是把检测框分成高、低分两批,高分框先做匹配,低分框再在未匹配轨迹里做第二轮匹配。这在体育场景里非常好用,因为剧烈运动时部分运动员的检测置信度不稳定,低分框参与匹配能明显减少轨迹断裂。
训练完模型后,我用它配合ByteTrack在几场完整比赛视频上做了跑通测试。单路视频实时处理没问题,运动员ID保持情况在正常转播镜头下还可以接受,一个球员的轨迹能连续跟住几十秒,只在快速交叉跑动时偶尔发生ID互换。
5.2 场地坐标映射与战术数据
光有图像坐标没太大业务价值,战术统计要的是标准球场坐标系。这一步需要场地线和摄像机参数做单应性变换。
我的流程是:从场地线检测结果中提取至少四条不共线的线段,匹配它们在标准球场尺度的对应位置,解出单应矩阵H。有了H,运动员的二维图像坐标就可以投影到场地平面。
用这个投影做越位判断时有个关键技巧:不能取检测框中心做投影,而是取检测框底边中点。因为运动员脚部才是与场地平面接触的点,框中心会因为身体前倾、手臂摆动等上下浮动,直接投影中心会导致场地平面的位置偏差。这个小细节我踩过坑,一开始用框中心做越位判断,误差能到一两米,改成底边中点后稳定很多。
5.3 继续迭代的几条路线
如果这个项目继续推进,我不会再花大量时间魔改骨干网络,而是优先尝试三条路线:
第一条是半监督学习。转播画面里大量未标注视频只做了伪标签,置信度高的伪标签数据拿来做第二遍训练,对小目标召回往往有意外提升。
第二条是多帧融合。当前模型逐帧推理,没有利用时间信息。如果引入相邻帧的特征对齐,对球员快速转身和球被遮挡后的再出现会有帮助。
第三条是针对球检漏的专门化后处理。足球轨迹比一般目标更平滑,用卡尔曼滤波和轨迹外推把漏检点补上,比继续堆模型精度更划算。
6. 踩坑总结与一点个人体会
整个项目做下来,我的第一个体会是:不要一上来就魔改网络。我最初直接把ConvNeXt整套思想塞进YOLO11,结果模型又慢又难收敛。后来老老实实从数据清洗、标注复核、难例挖掘开始,简单改几个模块,效果反而立刻出来了。数据问题的优先级永远高于结构创新。
第二个体会是核大小和输入分辨率这类“土办法”往往比花哨模块更有效。1280输入加核5深度卷积,小足球AP涨了十几个点,而换更复杂的注意力模块只涨了一两个点,还把训练搞得不稳定。做工程,先吃透输入端的收益,再考虑结构端的求新。
最后再提一件事:C3k2-ConverseB这个组合对我这套足球数据有效,但不代表它能无脑平移到篮球、冰球或者别的体育场景。球的尺寸、场地线的形状、运动员的密集程度都不一样。真换场景时,先把原来的baseline跑起来,再拿我这套思路做AB实验,别直接照搬参数。调参这件事没有银弹,只有一步步试出来的结论才靠谱。