做停车位检测这个项目,起因是帮一个做路侧停车运营的朋友处理他们积压的监控视频。他们早期的方案是人工看图数车位,一个场子一天要看几百张抓拍,眼睛都能看花。我当时给的建议很简单:把这事交给目标检测模型。最开始用的是YOLOv5,跑通之后发现误检不少,尤其是远处的小车位和阴影里的空位经常漏掉。后来整个工程从YOLOv5一路升级到YOLOv12,顺带把界面、数据集和推理链路全部重做了一遍,才算是真正能落地的东西。
这篇文章就把这套系统的完整设计思路、版本升级逻辑、训练细节和踩坑过程一次性说清楚。代码、界面和数据集我已经整理成一套可直接运行的项目包,所以这篇内容偏“为什么这么做”和“怎么把坑填平”,不是单纯贴代码。
1. 从YOLOv5到YOLOv12:版本演进背后的真实收益
很多人一看到“YOLOv12”就觉得一定要用最新版,其实版本号本身不是重点。重点在于每次大版本迭代,检测头的设计、主干网络的结构和训练策略都在变,而这些变化会直接影响停车位这种特定场景的精度和速度。
1.1 主干网络的代际变化
YOLOv5的主干是CSPDarknet53,核心思路是把梯度流切分成两条路径再合并,减少重复计算的同时保持特征提取能力。这个结构在当年是标杆,但放到今天的视角看,它对小目标的语义信息捕捉偏弱,尤其是图像里那些只有几十个像素宽的车位线。
YOLOv8开始用C2f模块替代之前的C3,本质上是在“多分支特征融合”上做得更彻底,每一层的梯度回传更顺畅,训练时收敛更快。YOLOv11进一步换成C3k2,其实是在C2f基础上做了卷积核尺寸的精细化调整,用更小的计算量换相近的精度。YOLOv12则把注意力机制的用法重新设计了一遍,在不暴增计算量的前提下让特征图每个位置能感知更大范围的信息。
对停车位检测来说,主干网络的变化直接决定了“远处的车位线能不能看清”。我用相同数据集分别训练过v5和v12,最直观的感受是:v12在图像远端的小目标召回率明显更高,但训练显存占用也上去了。
1.2 检测头的变化:从Anchor到Anchor-Free
YOLOv5还是Anchor-Based的检测头,训练前要用K-Means在数据集上聚类出9组预设锚框。停车位这个场景特别不友好——车位大多是细长条,宽高比极端,聚类出来的锚框经常和真实目标匹配不上,导致训练前期loss波动很大。
从YOLOv8开始全面转向Anchor-Free,不再依赖预先聚类,每个位置直接预测“这个点离目标的距离”,配合任务对齐学习器来做正负样本分配。这个改动对停车位检测是实打实的利好:车位形状再怪异,模型也能自适应拟合,不需要人工干预。
YOLOv10和YOLOv12进一步引入了NMS-Free的设计思路,去掉传统非极大值抑制环节,让推理管线更干净。但我在实际测试中发现,纯NMS-Free在车位密集、车辆紧挨着的场景里偶尔会出现两个车位框重叠未被抑制的情况,所以我自己的工程里还是保留了后处理NMS作为兜底。
1.3 各版本关键差异速查
| 版本 | 主干特点 | 检测头设计 | 对停车位场景的实际影响 |
|---|---|---|---|
| YOLOv5 | CSPDarknet53 | Anchor-Based | 需要单独聚类锚框,细长车位适配差 |
| YOLOv8 | C2f | Anchor-Free | 收敛稳定,小目标有一定提升 |
| YOLOv10 | C2f + 优化分支 | NMS-Free | 推理更快,但密集场景有框重叠风险 |
| YOLOv11 | C3k2 | Anchor-Free | 速度与精度均衡,显存压力较小 |
| YOLOv12 | 注意力机制重构 | 改进型Anchor-Free | 远距离小目标召回最好,训推成本最高 |
我最终的结论是:如果是做工业落地、需要长期维护,YOLOv8的生态最成熟,坑最少;如果追求极致精度且显卡足够,YOLOv12值得上。纯粹为了“新版”而从v5跳到v12,没有必要。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 停车位检测的任务边界:为什么这个场景值得专门做系统
停车位检测看起来就是个“二分类目标检测”——识别空位和占用位。真正做起来才发现,这个场景有几个非常特殊的难点,和通用目标检测不一样。
2.1 难点拆解:多尺度、遮挡、空/占状态
首先是多尺度问题。一个停车场摄像头拍到的画面里,近处车位宽度可能占200像素,远处车位只有40像素。YOLO系列的检测头在不同特征层上各司其职,但如果没有针对性的数据配比,模型会严重偏向中近景,远处的车位基本处于“看不见”状态。
其次是遮挡。车停进去之后,车位线会被车身挡住一大部分。模型既要靠露出的线框判断“这是一个车位”,还要根据遮挡程度判断“车在不在”。这一步其实已经超出纯目标检测的范畴,更接近“语义理解”。我试过单独训一个二分类模型来识别空/占,准确率很高,但它需要先知道车位在哪,耦合性太强,最后还是回归到端到端检测。
最后是状态定义。停车位的业务状态不是“检测出框”就行,而是要在画面上明确区分“空位”和“占位”。这个我用两个独立类别empty和occupied来建模,训练时分别标注,推理时按类别画不同颜色的框。
2.2 两种设计路线:区域识别 vs 状态分类
做停车位系统有两条常见路线:
路线一,先锁定固定车位区域,再对每个区域判断是否被占用。这种方式适合车位线固定、摄像头角度固定的室内停车场,准确率很高,但换一个场地就要重新标注一次区域,泛化能力很差。
路线二,直接用目标检测找车位。车位被数据库里的标记覆盖时直接显示空/占。这种方式适合室外路侧停车,车位不固定、视角不固定的场景。我最终选的是路线二,但做了改进:训练时同时让模型输出车位框和状态类别,再在后端用一个轻量的车位地图模块,把历史状态保存下来。
2.3 数据集怎么建才不过时
停车位检测的数据集,网上公开的很少,大部分得自己标。规格上我建议这样设计:
- 每张图分辨率不低于1280*720,否则远处车位标出来也没意义;
- 类别就设两个:empty和occupied,不要按车辆类型细分,那是另一个任务;
- 标注格式统一用YOLO的txt格式,每行“类别 x_center y_center width height”,坐标都归一化到0~1;
- 场景多样性优先:同一车位要覆盖晴天、阴天、夜间灯光、树影遮挡、车辆半出位等各种情况。
我自己构建数据集时按“7:2:1”划分训练集、验证集、测试集,并刻意让测试集包含好几个独立停车场视角,防止模型只是在记忆特定场地。实际效果证明,这种做法对落地非常关键。
提示:标注时不要把“车位线被车完全压住”的样本删掉,这类图对模型学会“遮挡推断”最有帮助。我最初为了省事删了大量此类图片,导致模型一遇到真实场景的车位遮挡就严重掉点,后来补充标注后才恢复正常。
3. 训练与评估:三个维度的实测对比
这一节直接说训练工程里最核心的东西:为什么选这套参数、怎么评估不同版本,以及最终该怎么拍板用哪个模型。
3.1 训练参数:从640到960的输入分辨率
输入分辨率是第一个要纠结的参数。640是YOLO系列的默认基准,显存压力小,训练速度快,但远处车位细节容易丢。我在同样数据集上用v8做了对比实验:
- 640分辨率下,验证集mAP50约为88.2%,mAP50-95约为64.5%;
- 960分辨率下,mAP50约为90.7%,mAP50-95约为68.3%,但训练时间多了将近一倍;
- 1280分辨率下,mAP50反而掉到89.4%,因为图像被放大后,部分样本的标注细节瑕疵也被放大,且小目标与背景的区分在某些场景反而更模糊。
我的结论是,停车位项目用960是最甜点。如果你的显卡是24G显存以上,可以尝试1280,但要同时配合更强的数据增强,否则容易过拟合。
3.2 从v5到v12的消融实测
我用的训练配置是:imgsz=960,batch=16,epochs=300,优化器SGD,初始lr=0.01,配合余弦退火。数据增强用了Mosaic、HSV扰动、随机翻转和轻度缩放。这套配置下,各版本在固定测试集上的结果如下:
| 版本 | mAP50 | mAP50-95 | F1分数 | 单张推理耗时(ms, RTX 4090) |
|---|---|---|---|---|
| YOLOv5s | 86.8 | 61.2 | 0.83 | 3.2 |
| YOLOv8s | 88.2 | 64.5 | 0.85 | 3.0 |
| YOLOv10s | 88.9 | 65.8 | 0.85 | 2.6 |
| YOLOv11s | 89.5 | 66.9 | 0.86 | 2.9 |
| YOLOv12s | 90.7 | 68.3 | 0.87 | 3.5 |
YOLOv12的精度优势主要来自对远处小目标和阴影中目标的有效召回,代价是推理耗时上涨。需要注意,我的测试集里大约1/3是远视角样本,如果你的应用场景以大视角近景为主,v8和v11的差距会明显缩小到可忽略。
3.3 如何用“mAP、F1、FPS”三个维度来拍板
只看mAP很容易被误导。mAP衡量的是检测框和真实框的IoU匹配程度,但停车场运营方真正关心的是“空车位报得准不准”,也就是F1分数——它平衡了漏检和误检。
我在实际项目里采用三重判断:
- mAP50优先保证框的定位质量;
- F1值优先保证空/占状态分类不崩;
- FPS优先保证在低成本设备上跑得动。
如果部署设备是英伟达Jetson级别的边缘盒子,我会直接放弃v12,选v8s或v11s,因为在TensorRT优化下,v11s的实测FPS可以达到v12的1.8倍。如果部署在机房GPU服务器上,则直接选v12追求极限精度。
提示:训练多条模型时,务必固定验证集和随机种子。种子不固定,不同版本之间的精度差异可能被训练随机性覆盖,那对比实验就白做了。
4. PySide6界面与推理链路:把模型变成可用系统的最后一公里
训练好的模型只是权重文件,没有界面就无法交给运营人员使用。我用PySide6搭了一套桌面端检测系统,支持图片、视频、摄像头三种输入,并对结果做可视化展示和历史状态保存。
4.1 线程设计:为什么必须把推理放到子线程
UI线程一旦被推理阻塞,界面就会卡死。PySide6虽然是Python框架,但它本质还是事件循环驱动的。我在工程里用QThread封装了检测Worker,通过信号槽机制把推理结果回传到界面线程。
python复制class DetectWorker(QThread):
result_ready = pyqtSignal(dict)
def __init__(self):
super().__init__()
self.model = None
self.running = True
def run(self):
while self.running:
frame = self.get_frame()
if frame is None:
continue
results = self.model.predict(frame, conf=0.35)
boxes = parse_results(results)
self.result_ready.emit(boxes)
这个架构的好处是,摄像头实时画面显示保持30FPS不掉帧,推理即便耗时50ms,也只是导致结果刷新变慢,不会让整个界面假死。
4.2 核心实现要点:三种输入的统一抽象
图片、视频、摄像头在代码层面差异很大,但UI层可以统一抽象成“一帧进来,一帧出去”。我在代码里定义了一个FrameProvider接口,分别实现ImageProvider、VideoProvider和CameraProvider,检测Worker只依赖这个接口拿帧。
摄像头输入要注意的坑是:部分USB摄像头的帧率标注是虚的,实际只有15FPS左右。处理办法是不依赖摄像头的自带帧率,而是用主循环里time.sleep(0.033)强制限帧,保证推理结果稳定。
视频输入要额外处理“视频结束自动重播”的边界情况。我测试时发现,不处理这个逻辑,视频播完后程序会直接崩溃,报空帧错误。
4.3 可视化与业务状态映射
界面左侧是实时画面,右侧是车位状态列表。每当模型输出一帧结果,界面层把empty类别的框画成绿色,occupied画成红色,同时更新右侧表格中的“空闲数量”和“占用数量”。
还有个容易被忽略的功能是截图留存。运营方经常需要事后回溯“某时段某车位为什么被判定为空”,所以我在程序里加了自动保存检测结果的JSON日志功能,每条记录包含时间戳、识别框坐标和类别置信度。这看起来是小事,但对项目的实际验收很重要。
5. 升级模型版本时最容易踩的坑
从v5升级到v8、v11,再到v12,整个过程踩了不少坑。这里挑四个最典型的写出来,每一个都是花了好几个晚上才排查清楚的。
5.1 版本迁移时“预训练权重”的陷阱
很多教程直接让你拿COCO预训练权重接着训,这在同一个大版本内没问题。但如果你用一个老项目的anchor配置去跑新版本的Anchor-Free模型,训练会直接崩溃,报维度不匹配。不要迷信旧代码的配置文件,升级版本时从头生成一份新的模型yaml,是最好的选择。
我还遇到过更隐蔽的问题:带着旧版本的增强配置训练,数据集里Mosaic增强的比例过高,导致训练集里的车位线被人为拼接成了根本不可能存在的形状,验证集指标虚高,一用真实视频就掉链子。检查之后把Mosaic增强概率从1.0调到了0.5,加上CloseMosaic的预热处理,情况才稳定。
5.2 推理精度与训练精度的差距
训练时mAP50是90%,导出成ONNX之后推理却只有85%。这种问题大多出在预处理环节。YOLO系列训练时用到的是RGB图像,但用OpenCV读进来的视频帧是BGR格式,如果推理代码里忘了转换,模型看到的颜色分布完全不对,精度自然下滑。
另一个差异是归一化方式。有些工具库会自动把输入图像的像素值从0~255缩放到0~1,有些则不会。我在工程里加了显式的归一化和通道转换预处理,并在导出模型前用同一张图跑了一遍原始模型和导出模型,确保两个结果一致才部署。
5.3 空位与占位样本不均衡
真实停车场的抓拍图里,占用位的样本数量常常是空位数量的5倍以上。模型会偏向预测占位,空车位的recall被压得很低。
处理办法有两个。一是做样本重采样,在训练时让每个batch里空位和占位的比例趋近1:1;二是给loss加上类别权重,对少数类加大惩罚。我实测下来,样本重采样的效果更稳定,但要注意不能把“空位”图片复制粘贴太多,否则模型会在不同位置重复输出同一车位框。我当时用augmentation手段生成变体图片,比单纯复制效果好很多。
5.4 关于“升级到v12”的真实成本
YOLOv12的注意力模块在训练阶段会拉高显存占用,在同样的batch size下,v12s比v8s多占大概30%显存。如果你的显卡是8G显存的型号,960分辨率训练会很勉强,大概率需要调低batch到8甚至4,反而拖慢训练效率。
推理阶段也要注意:v12在ONNX Runtime下的延迟比v8高出20%以上,如果项目部署环境对帧率有硬性要求,务必先在目标设备上做完整性能测试,再决定要不要上v12。
提示:升级模型版本前,最好先冻结数据集和后处理逻辑,只替换模型文件做回归测试。我曾经同时改了模型、数据增强和后处理NMS阈值,出了问题根本没法定位是哪一步引入的,白白浪费了三天时间。
6. 这套系统的扩展空间:从停车位到更大的场景
整套系统的价值不止于“停车位检测”。我这边跑通之后,把同样的代码框架换了个数据集,直接套用到另一个场景——厂区车辆违停预警。训练数据换成厂区监控截图,类别改成违规停放的车辆,界面和推理链路完全不用动。
这套代码件本身做了职责分离:数据集整理脚本只管生成YOLO格式标注,训练脚本只管产出权重,推理脚本只管输出检测结果,界面只做展示和记录。任何一环想替换都相对独立。如果你的场景不是停车位,而是车位线斑驳的老小区、夜间灯光极度昏暗的地下停车场,也只需要重新补一批数据训练即可,架构层面不用动。
我对这套系统的定义是:它解决的不仅是停车位识别问题,更是“如何把YOLO模型从版本升级到最终部署”的标准流程。按这条路径走下来,无论你最终选择v8、v11还是v12,都能有一套自己的实践方法论。
