1. 项目概述与核心需求解析
停车位检测这几年一直是计算机视觉落地场景里的“常青树”,原因很简单:它既有明确的应用价值(停车场管理、车位引导、反向寻车),又有足够的技术纵深(目标检测、状态分类、多摄像头协同、边缘部署全都沾边)。我最早接触这个方向是在一个智慧停车改造的模拟项目X里,客户要求用普通监控摄像头识别车位占用状态,准确率要超过95%,当时我第一反应就是:这不就是目标检测加一个状态分类的活儿么,直接用YOLO就能搞定。真做起来才发现,里面坑比想象中多得多。
先说清楚这个项目到底在做什么:基于YOLOv5/YOLOv8/YOLOv10构建一套智能停车位检测系统,输入是停车场监控画面,输出是每个车位的占用状态(空闲/占用),并能在低算力设备上实时运行。系统核心分三块:车位区域标定、车辆目标检测、车位状态判定。前两块是基础,最后一块才是真正决定系统好不好用的关键。
这三代YOLO怎么选?我的建议是别纠结太多。YOLOv5胜在生态成熟、部署资料多,遇到问题网上随便一搜就有答案;YOLOv8在准确率和速度上做了均衡,官方支持导出多种格式,适合快速验证;YOLOv10则把端到端检测做到了新高度,取消NMS后推理链路更干净,但部署时有些坑需要注意。如果项目周期紧、团队经验一般,直接用YOLOv8是最稳的选择。这篇文章我会把三代模型的差异、数据集构建、训练调参、后处理逻辑、部署优化全部过一遍,你照着做就能复现一套能跑的系统。
2. 方案设计:为什么停车位检测不能只靠“裸”目标检测
2.1 通用目标检测在停车场景下的局限性
很多人拿到这个项目,第一版思路是:训练一个检测模型,把“车”检测出来,然后判断车有没有压到车位线。听起来很顺,实际一跑就发现问题。
停车场监控通常是俯视角度,车辆之间遮挡严重,尤其是SUV和轿车并排停放时,车身部分被邻车挡住是常态。通用检测模型在这种场景下的漏检率会明显上升。更麻烦的是,你真正需要判断的并不是“画面里有没有车”,而是“某个车位是否被占用”。一辆车斜停在两个车位中间,或者车头超出车位线,这两种情况用通用检测结果是没法直接映射到车位状态的。
这就是为什么我必须把“车位区域标定”作为独立模块来做。车位是固定不动的物理空间,在摄像头画面里也是固定位置,所以可以在画面中预先画好ROI区域,检测任务从“全局找车”退化成“判断每个ROI内有没有车”,难度和稳定性都大幅优化。
2.2 YOLOv5/v8/v10三代模型的核心差异与选型逻辑
先说YOLOv5。它最大的价值不是性能天花板有多高,而是生态。从训练脚本到导出工具链,第三方封装多到数不清,即使是刚入门的新手也能在半天内跑通流程。CSPDarknet骨干网络加PANet颈部结构放到今天也不算落后,如果算力受限,nano版本在边缘设备上跑得非常舒服。
YOLOv8在结构上最大的变化是换掉了耦合检测头,改用Anchor-Free的解耦头,分类和回归各自独立输出。这个改动带来两个直接好处:收敛更稳定,小目标检测能力有所提升。训练时自带的一系列增强策略也省去了很多手动调参的时间。YOLOv8默认超参数对大多数场景都有不错的适应能力,属于“不管数据怎么样,先跑一版看看”的典型选择。
YOLOv10最核心的改进是去掉NMS,通过双标签分配策略把“一个目标对应多个预测框”的矛盾在训练阶段就解决了。推理时直接输出最终结果,少了一道后处理,延迟更低。但注意一点:它的端到端特性依赖特定训练方式,如果要拿预训练权重在自定义数据集上微调,流程跟v8不太一样,数据格式和超参适配都要重新来一遍。
三代模型的选型逻辑看场景:快速原型验证用v8,追求极致部署性能且算力紧张用v10,团队经验少或者需要大量资料参考用v5。如果是我个人做这个项目,直接锁定YOLOv8作为主力。
2.3 系统整体架构:检测与状态判定分离
我的方案是把系统拆成两个阶段。第一阶段是目标检测,用YOLO把画面中所有车辆框出来;第二阶段是车位状态判定,通过检测框和车位ROI的IOU关系来决定每个车位是“空”还是“占”。
分离的好处很多。第一,检测模型只负责“找车”,不需要理解“车位”这种抽象概念,训练数据也更纯粹。第二,车位状态判定逻辑可以独立迭代,比如想加入“停歪了”这种提示,不用重新训练模型,改规则就行。第三,如果摄像头角度变了,只需重新标定ROI区域,模型和判定逻辑都不用动。
实际工程里还有个细节:车位占用判定不只是“有没有车”,还要做时间维度的平滑。比如视频流里偶尔有一帧检测丢失或者误检,如果直接用单帧结果判断,屏幕上状态会频繁抖动。我在系统里加了一个轻量级帧序列投票机制,连续三帧中有两帧判定为占用,才真正把状态翻转。这个机制成本极低,却能把稳定率从95%左右推到99%以上。
3. 数据集构建:决定系统准确率上限的关键环节
3.1 数据采集策略与标注规范
停车位检测的数据集构建比很多人想象中更费时。我先说一个容易踩的坑:用公开数据集(比如通用车辆检测数据集)直接微调,效果往往不理想。原因也很简单——停车场监控的俯视角度、车位线特征、车辆遮挡程度,跟自然街景摄像头的视角差异太大,模型学到的特征对不上。
我实际用的采集方法是:找了两三个不同风格的地面停车场和地下停车场,每个场地在不同时段各录制半小时视频,覆盖白天、夜晚、阴天、雨天(条件不允许时就通过数据增强模拟)。采样时不要连续抽帧,每隔10到15帧抽一帧,这样既能控制数据量,又能保证画面多样性。
标注规范上,我统一只标“车辆”一个类别,不区分轿车、SUV、货车。原因在2.3里说过了:检测模型的任务是找车,不是认车。被严重遮挡的车只标露出面积超过30%的部分,如果一辆车只被另一个车挡住一个小角,还是得标出来,这样模型才能学会在拥挤场景下保持稳定。每张图的标注量至少保证5辆以上,不然模型学不到密集场景的特征表达。
3.2 数据增强组合与标注工具选择
停车位场景的增强策略跟通用目标检测不太一样。我试过一套固定的增强组合,实测效果不错,具体是:
- 色调、饱和度随机调整(幅度±25%),模拟不同光照
- 随机亮度和对比度扰动,让模型对反光和阴影不那么敏感
- 随机水平翻转,注意车位本身是不对称的,翻转后ROI也要同步翻转
- Mosaic增强(把4张图拼接成1张),对小目标检测帮助很大,但是停车场景的图片尺寸较大,Mosaic后目标会被缩小,所以最终mosaic概率我只设到0.5
标注工具我用的是LabelImg,老牌工具,导出Pascal VOC格式再转YOLO格式,流程很顺。标注矩形框时注意一个原则:框要紧贴车身实际轮廓,特别是车辆边缘跟车位线接近时,宁可不要贪多把框画大了。框如果比实际车大,会导致预测框定位偏移,直接影响后面ROI判定的IOU计算精度。
数据集规模方面,我最终用了约3500张标注图像。这个量级对于单类别检测来说基本够用,但如果摄像头角度和场地差异很大的场景,建议每个新场地补充500-800张标注数据做微调,否则跨场地泛化会出现明显掉点。
3.3 极度不均衡问题的处理:车位状态正负样本逻辑
训练检测模型时,你会发现自己标注的框里,几乎所有目标都是“车”,不存在“空车位”这个类别。车位是空的怎么处理?这就回到了车位ROI的概念。
我已经习惯的做法是:不在训练数据里标注“空车位”这一类别,而是让检测模型只负责检测“车”,然后通过“检测框与车位区域的IOU关系”来判断车位是否被占用。判断逻辑在4.3里会详细展开。这样设计最大的好处是,检测模型的样本分布天然均衡(因为只有“车”一个类别),车位空闲状态则完全由后处理规则决定,逻辑清晰、不容易出边界问题。
有个容易忽略的问题:灯光昏暗的地下停车场,如果车漆是深色且画面噪声大,模型漏检概率比其他场景高不少。我建议在数据增强里额外加入高斯噪声和运动模糊模拟,幅度适中,让模型适应低照度环境。
4. 训练过程与核心参数调优实录
4.1 训练参数配置与训练流程详解
模型选型确定后,训练参数是最直接决定效果的因素。我先给出一套我实测下来能快速达到稳定精度的基础配置,用YOLOv8m举例:
yolo detect train data=parking.yaml model=yolov8m.pt epochs=100 batch=16 imgsz=640 optimizer=AdamW lr0=0.001数据集配置文件parking.yaml的核心内容:
train: ./datasets/parking/images/train val: ./datasets/parking/images/val nc: 1 names: ['car']说几个参数选择的理由。输入分辨率用640640,是基于性价比的权衡。停车场监控原图通常是19201080,直接整图缩放会导致车辆目标过小;更好的做法是在预处理时按车位ROI区域做裁剪放大,让模型在高分辨率区域检测。但这样做会增加推理次数,如果只是做基础版先验证效果,直接在原图上做640推理就够了。
batch=16是最常见的显存平衡点,如果显卡只有8G显存,建议降到8;反之如果显存充足可以加到32,训练收敛速度会有提升。优化器我选AdamW,YOLOv8官方也支持SGD。个人感觉AdamW在自定义数据集的微调阶段比SGD更稳,尤其是学习率设置不合适时(AdamW对lr没那么敏感),但SGD的最终精度上限通常更高。统一建议:第一版用AdamW跑通,后期如果精度不足,再用SGD做精调。
4.2 训练过程中的损失曲线判读与调参技巧
训练时别只看mAP,损失曲线才是判断模型状态的第一道窗口。我至少要盯三条曲线:box_loss(框回归损失)、cls_loss(分类损失)、dfll_loss(分布焦点损失)。
box_loss如果在前30个epoch下降明显,后面趋于平缓,说明定位任务学得不错。cls_loss作为单类别检测,收敛速度本来就快,正常应该在20个epoch内降到很低。dfll_loss存在一定波动是正常的,它跟边框分布有关,后期小波动不用管,但如果长期不下降,说明模型对边界情况(严重遮挡、紧贴车位线的车)拟合不够,此时需要检查数据标注是否合理,而不是盲目加轮数。
一个我反复验证过的技巧:YOLO迁移学习时,骨干网络权重的学习率应比检测头低。YOLOv8 通过不同的层设置不同的lr参数可以实现;如果懒得配置,也可以先冻结骨干网络前30个epoch只训练检测头,之后再解冻全部微调。实测这个方法在标注数据量不足500张时特别管用,能显著减少过拟合。
4.3 车位状态判定的核心后处理算法逻辑
训练完模型,拿到的是“检测框”,不是“车位状态”。这一步的逻辑如果不设计好,前面的精度都会白费。
我的做法是:把每个车位定义为一个“检测框”,通常按照地面车位线的四个角点定位,在代码里用list结构保存,包含车位ID、ROI坐标(x1, y1, x2, y2)、车位类型等。然后对每一帧做如下处理:
def judge_parking_status(det_boxes, parking_rois, iou_threshold=0.15): """ det_boxes: 模型输出的车辆检测框列表, 格式为 [x1, y1, x2, y2, conf] parking_rois: 车位标注区域列表, 每个元素是单个车位的 [x1, y1, x2, y2] iou_threshold: 车辆与车位区域的最小重叠比例 返回: 每个车位的状态 (0=空闲, 1=占用) """ results = {} for pid, roi in parking_rois.items(): max_iou = 0 for box in det_boxes: iou = compute_iou(roi, box[:4]) if iou > max_iou: max_iou = iou results[pid] = 1 if max_iou >= iou_threshold else 0 return results这里的compute_iou是标准实现:交集面积除以并集面积。关键参数是iou_threshold,我最终使用的值是0.15,比很多人直觉上想的低很多。原因在于:一个检测框往往是车身主体,车头或车尾超出车位线很常见,如果阈值设到0.3以上,很多实际占用状态会被判成“空闲”。0.15这个值容忍了较大的车辆偏移和部分遮挡,在多个不同场地的测试里表现稳定。
如果追求更精细的判定,可以加入重心判断:检测框重心是否落在车位区域内。重心判断对“压线停车”场景尤其有效。我常用“检测框与车位框的交叠面积比例(IOA)加上重心判断”双规则,IOA侧重检测框是否切入车位区域,重心侧重于判断车辆是否“主要占据”这个车位,两个规则取并集判定占用,取交集判定占用。
4.4 可视化效果与验证标准
调试阶段必备的可视化输出格式如下,它能帮你快速定位问题:每个ROI区域画一个彩色边框(空闲绿色,占用红色),ROI顶部显示车位编号和置信度。车辆检测框用蓝色画。这样跑一段视频回放,一眼就能看出来哪里误判了。
我在验证集上最终得到的结果是:单类别车辆检测mAP@0.5达到0.96左右,车位状态判定的准确率约98.3%(在室内多为静态场景测试集上),帧率在Jetson设备上约28帧。这个准确率已经超过很多停车管理系统的招投标要求了。
5. 模型部署与性能优化实践
5.1 导出与推理优化:格式选择和关键技术点
训练好的模型要落地,必须做导出和推理优化。YOLOv8官方支持导出多种格式,但选哪个要看目标设备。
如果跑在带GPU的服务器上,直接用PyTorch模型或者ONNX就行,不需要额外优化;如果要跑在边缘设备(Jetson Nano、Jetson Orin这类设备),必须做TensorRT加速。在我实际测试中,同一模型用TensorRT FP16推理,相比ONNX Runtime有接近3倍的吞吐提升,在Jetson Orin上可以达到实时。
YOLOv10与YOLOv5/v8在导出上有个关键差异:由于去掉了NMS,导出后的模型输出维度会变得不太直观,而且部分第三方推理框架不支持这种输出形式。我自己遇到过一次问题:导出ONNX后,在之前写好的推理脚本里怎么都跑不通,查了好几个小时才找到原因是输出格式不符合预期。解决方案是导出时加一个后处理层,或者在模型外部实现非极大值抑制。对YOLOv8来说,导出流程是最顺的,官方给的代码基本可以直接用。
yolo export model=best.pt format=onnx dynamic=False imgsz=6405.2 边缘部署的算力估算与帧率实测
先说一个常被忽略的问题:停车监控摄像头通常是7x24小时运行的,推理设备放在现场,功耗和散热上限从一开始就要考虑。一台普通的x86迷你主机(带有入门级GPU)就可以跑YOLOv8n模型,帧率可以达到30帧以上;如果使用CPU跑同样的模型,帧率通常会跌到个位数。
我的建议是:如果只做“每隔几秒刷新一次状态”的车位引导屏,目标帧率设到5帧就够了,这种情况下CPU也能应付;如果需要实时监控视频流并且标注检测框,必须上GPU加速设备。
实测数据供参考:Jetson Orin Nano 8G运行YOLOv8s的TensorRT FP16推理,640输入分辨率,能够跑到35-40帧左右;运行YOLOv10s大约能到40-45帧。如果换成nano版本模型,帧率翻倍都不是问题,但检测精度会下降,具体用哪个型号需要你在准确率和速度上做个取舍。
5.3 多摄像头接入与RTSP流处理架构
真实停车场动辄十几个摄像头,部署时怎么管理这些视频流是个工程问题。我的方案是搭一个简单而稳定的流处理管线:每个摄像头对应一个独立进程/线程,内部循环做“拉流→解码→检测→车位判定→推结果”五个步骤,各线程之间不共享状态,车位状态结果统一推送到一个中心服务做持久化和业务展示。
多路流场景下最容易出的坑是RTSP连接不稳定,网络抖动会导致拉流线程卡死。我用了一个开关:拉流模块和检测模块中间加一个缓冲队列,拉流线程只负责把解码帧放入队列并设置队列最大长度;如果队列满了直接丢最老的帧,保证检测线程始终拿到的都是最新帧。这样即使某一路摄像头断线卡顿,也不会拖垮整个系统的其他路。
还要注意时区校准的问题。车位状态上报表单时,一定要以处理时间戳为准,因为各摄像头的录像时间经常有几分钟偏差(摄像头的内部时钟普遍不准),如果直接拿摄像头时间做统计,统计结果会乱七八糟。
6. 常见问题与排查技巧实录
6.1 漏检多、框不准:从标注和环境两个方向找原因
漏检率高是反馈最多的问题,通常逃不过几个原因。第一,训练数据里目标尺度单一,模型没有见过“车在画面角落被压缩得很小”这种场景,推理自然识别不出来。简单验证方法:把测试集的检测错误可视化,按目标尺寸做分类统计,如果小目标漏检占大头,优先在训练集中补充小目标样本,或者加大mmos的mosaic增强概率。
第二,车位线反光严重。某些停车场的地面是环氧地坪,灯一打反光特别强,车和地面根本分不开。我遇到过一次,排查了很久才发现不是模型问题,而是摄像头镜头上有一层雾。所以先检查图像质量,再做算法优化,顺序别反了。
第三,夜间场景。停车场夜间往往只有部分照明亮着,画面有大量暗部和强光对比,车辆轮廓很难分辨。这种问题的根治法是多收集几个夜间的真实数据,靠增强模拟出来的夜间效果总归是假的,泛化能力不足。
6.2 车辆检测正确但车位状态判定错误:规则和坐标体系问题
检测没问题,车位状态却判断错误,这种情况多半在ROI定义上。我总结了一个排查顺序,按概率从高到低排:
- ROI标定坐标是否与检测框坐标在同一坐标系下。数据预处理阶段做过缩放和裁剪的话,两种框的坐标尺度不同是常见的,排查时先打印输出检查每个坐标值是不是落到图像范围内了。
- ROI框是画在地面车位线上的,车身检测框是车身的外接矩形,两者天然有偏移。如果车位离摄像头远,俯视角度大,车身和车位线的空间错位会更严重,此时可以适当放宽iou_threshold。
- 相邻车位之间的干扰,特别是两辆车同时在小区域内停放时,状态判定的相互影响很难避免。我的处理是引入“优先分配”逻辑:当多个检测框重叠交叠时,先按置信度最高的检测框匹配车位,匹配完的车位标记为已完成,不参与后续匹配,避免一个车位被两个检测框同时盯上。
6.3 帧率不足与性能瓶颈定位方法
如果推理帧率达不到预期,先加一个时间统计装饰器,把每个阶段(取帧、预处理、推理、后处理、推流)的耗时打出来,直接看瓶颈在哪。大概率是预处理或后处理出问题,而不是模型推理本身。
预处理耗时多通常是因为每帧都做图像缩放甚至加马赛克增强,这些在训练时可以有、推理时绝对不能有,导出模型后要单独写推理管线,把批量归一化、尺度缩放这些操作融合进TensorRT里。后处理耗时多则多半是车位ROI数量很大(比如超过100个车位)且计算IOU是双重循环,复杂度是O(MxN),此时应该提前对检测框做粗过滤,或者引入空间索引缩小候选范围。
还有一个经常被忽略的小坑:代码里反复用numpy的坐标数组做切片、拷贝和变换,数据量大时这是很重的开销。尽量改用Tensor或原地操作;比如给车位上状态做统计更新时,不要在循环里反复分配新的列表对象,直接复用预先分配的数组。
6.4 系统长期运行的稳定性维护
停车系统是7x24小时运行的,长期稳定性跟算法精度同样重要。我的经验是至少做到以下三件事:
- 定期检查显存和内存占用。有些版本的推理框架在使用长视频流时存在内存泄漏,跑几天内存就爆了。方案是部署时设置一个定时重启策略,或者干脆用带垃圾回收机制的推理框架版本。
- 给视频流加自动重连逻辑。RTSP流断掉后不会自动恢复,所以代码里要做异常捕获并触发重连。实测下来,1秒重试1次基本能满足大多数场景的稳定性需求。
- 记录推理日志但不写太频繁。每秒写一条会把日志文件撑爆,我通常只在状态变化或者错误发生时写日志,正常状态最少隔10秒写一次心跳。
7. 写在最后的心得与踩坑复盘
这个项目如果让我重新做一遍,我大概率不会再从零标注3500张图开始。第一次做总觉得数据越多越好,实际上对于“检测车辆”这个单类别任务,3000多张标注图片就能达到不错的性能基线,更多的时间应该花在清洗误标注和设计稳健的后处理规则上。
具体来说,最先该做的是画好车位ROI并验证坐标体系没问题,这步如果出问题,训练完才发现就是灾难级返工。然后训练第一版模型,哪怕精差点也没关系,先把“检测→判定→可视化→推流”的完整链路跑通,后续的优化都是在一条能工作的流水线上迭代。我见过太多人先花三四周把数据标注得无比完美,结果部署时发现摄像头角度变了要重新标,前功尽弃。
还有一个个人很受用的经验:不要过早陷入模型的“最优解”比拼。YOLOv5跑到95%和YOLOv8跑到96%之间,在实际业务里的体验差别基本是零;但后处理规则的细节——比如车辆长时间压车位线时的判定策略——对用户体验和系统可用性的影响远比这1%的精度差异要大得多。
这套系统的扩展空间也很大。现在只做了单摄像头单画面的检测,后期可以扩展到多个摄像头拼接成全景车位地图、对接小程序反向寻车、加上车辆出入时间统计等功能。核心依旧是“检测质量+状态判定规则”这个地基,地基稳了,上层的业务逻辑怎么加都不会塌。