停车场车位识别实战:视觉方案、模型训练与部署避坑指南
停车场车位识别听起来好像是个挺成熟的活儿市面上方案一抓一大把但真正自己从头到尾做一遍你会发现坑多到怀疑人生。我最近刚好把一个基于视觉的停车场车位识别系统从零搭到了能稳定跑起来中间踩了不少雷也总结了一些在文档里翻不到的经验。这篇文章不整虚的就把我实际用到的技术选型、代码思路、训练细节和部署时那些血泪教训全部分享出来给正准备做类似项目或者想入坑计算机视觉落地场景的朋友一个参考。先交代一下背景。我做的这个项目不是那种只用一张俯视全景图做演示的Demo而是要对接一个真实的露天停车场摄像头装在立柱上视角斜着往下看一个机位大概覆盖八个到十二个车位。识别任务分两块第一车位上有没有车第二如果有车这车占的是哪个车位。听起来简单但实际做起来光线变化、车辆遮挡、视角畸变、地面标识不清任何一个因素都能让你的模型在测试集上表现优秀、在真实场景里原形毕露。1. 整体思路与方案选型1.1 先想清楚要解决什么问题很多新手一上来就想着用目标检测模型直接框出“空闲车位”和“占用车位”这其实是个误区。车位不是一个像人、车那样有明显边界的目标画框的话框多大、框在哪都很难定而且不同摄像头的视角差很多换个场景就要重新标数据、重新训练成本极高。我最终采用的是“两步走”方案先做车位区域的固定坐标映射再做每个车位上的占用状态分类。这个思路的核心在于停车场的物理车位位置是固定的摄像头装好之后不会动所以车位的坐标只需要标一次。之后真正需要模型处理的只是独立判断每个固定区域内是否停着车。这样做有几个好处模型任务从“检测任意位置的车位状态”简化成“分类固定位置的状态”难度直接降了一个量级换摄像头或者换车位布局时只需要重新标定区域不需要重新训练模型可以很方便地在不同车位上使用不同的判断规则比如有些车位被柱子遮挡了一部分单独处理即可1.2 主方案的选型对比在动手写代码之前我把市面上几种常见的做法都梳理了一遍也做了个小实验验证下面是我的评估结果。方案优点缺点我的结论纯目标检测YOLO系列直接框车位端到端、看起来直观车位边界定义模糊、标注难度大、跨场景泛化差不推荐传统图像处理边缘检测形态学速度快、无需训练对光照和阴影极其敏感稍微变天就失效可作辅助不建议做主识别深度学习的车位状态分类VGG/ResNet轻量化变体准确率高、泛化能力强需要自己标数据、需要训练环境推荐语义分割PSPNet/U-Net等能精细区分地面和车辆标注成本高、推理速度慢、有点大材小用不推荐除非有特殊需求我的最终选择是车位区域用多边形坐标预先标定状态识别用轻量级卷积神经网络分类器。每个车位区域从摄像头画面中按坐标抠出来经过透视矫正和缩放后送入一个二分类网络有车/无车。这套方案本质上把一个大问题拆成了两个小问题每个小问题都可以用最成熟的技术解决。坐标映射部分是纯几何操作稳定可靠状态识别部分是标准的图像分类任务数据好标模型也容易训练。2. 数据集的准备与标注细节2.1 数据采集的难点与应对数据是这套方案里最关键的环节模型能不能扛住真实场景完全取决于训练数据的质量。我一开始图省事只从下午两三点光线最好的时候拍了大概两千张图结果第一次现场测试时早上和傍晚的准确率惨不忍睹。后来我总结出几个必须要覆盖的变化维度光照变化晴天、阴天、雨天、清晨、正午、傍晚、夜间有路灯和无路灯车辆类型轿车、SUV、面包车、卡车不同颜色的车深色车和浅色车在视觉特征上差异很大遮挡情况相邻车位大型车遮挡、树木遮挡、行人经过停放姿态斜停、压线停、车头朝里和朝外采集方式我用了两种结合。第一种是每隔五分钟从摄像头自动抓帧跑上整整一周能覆盖大部分光照和天气情况。第二种是刻意挑选一些特殊场景手动拍摄比如雨天后地面反光、黄昏时分的强逆光等。2.2 标注策略坐标和状态分开做标注分成两步。首先在某一帧画面上把每个车位的边界用多边形标出来存成一份配置文件。这个动作只需要做一次但要注意几个容易犯的错多边形尽量贴合车位线的内沿但不要精确到线上因为车停进去之后轮胎会压住边线圈小了容易把车的一部分切掉相邻车位之间不要留空隙稍微重叠一点都没关系避免缝隙处的小块区域被漏掉如果车位有一部分长期被柱子或树遮挡就把遮挡部分在坐标里排除掉宁可少判不要误判状态标注用半自动方式。我先写了个脚本把每个车位区域扣出来拼成一张大图然后用一个简单的颜色阈值初筛比如地面和车辆的颜色差异筛完后再人工逐张确认。一千多张图画下来大概花了两个半天比纯手工从零标快很多。另外建议给每个车位单独建一个子目录按车位ID分好方便后面做数据均衡和问题追溯。我一开始把所有数据混在一起结果发现模型在个别车位上准确率特别低回头一查才发现那个车位的数据量偏少而且大部分是同一个方向的车头朝向。2.3 数据增强的必要参数数据增强在车位识别这个项目里不像在通用分类任务里那么随意有些操作是绝对不能用的比如随机裁剪。车位区域一旦裁掉了一部分车的特征就不完整了模型会学到错误的信息。我实际用的是这些水平翻转停车方向左右对称翻转后语义不变非常安全亮度/对比度扰动模拟不同时段的光照HSV颜色空间轻微扰动应对白平衡漂移高斯噪声增强鲁棒性但强度要小随机旋转控制在正负三度以内大了会改变车位语义随机擦除对我来说效果一般可能因为车辆外形比较整体擦掉一块反而让模型去依赖某些不该依赖的局部特征。3. 核心算法实现与训练调优3.1 车位坐标映射与透视矫正摄像头是斜着往下拍的车位区域是一个不规则的四边形直接送到网络里模型很难学到统一的空间特征而且相邻车位之间的透视变形差异也会干扰判断。我的做法是先用原图上的多边形坐标把区域抠出来再做透视矫正变成一个规整的矩形再缩放成分类网络的输入尺寸。透视矫正的关键是找到四边形的四个顶点。这里有一个细节多边形可能不止四个点因为车位线的边界可能被画成带圆弧的或带斜角的形状。我的处理方式是取多边形的最小外接矩形然后手工微调四个角点保证车辆在矫正后的画面里是正向的、完整的。矫正之后的分辨率我定的是 96x96。这个尺寸看着小但足够分辨一辆车是否占用了车位而且推理速度快。如果你用的摄像头分辨率是200万像素一个车位在原图里大约占200x300像素缩到96x96之后车牌之类的细节消失但车辆的轮廓、车顶、车窗这些特征足够保留了。透视矫正可以用OpenCV的 getPerspectiveTransform 和 warpPerspective 来做整个过程大概几十行代码就能跑通。3.2 分类网络的结构选择在模型结构上我试了三个方案ResNet-18、MobileNetV2和一个自己搭的四层卷积小网络。结果比较有意思ResNet-18准确率最高在小规模数据集上很稳但推理速度一般在CPU上单帧要30毫秒左右MobileNetV2准确率略低但速度快很多CPU上十毫秒以内自搭小网络在数据量不够大时表现不稳定容易过拟合我最终选了MobileNetV2并把最后的全连接层改成两个输出节点加softmax。实际测试下来准确率在白天场景能到99%以上夜间在路灯照明下也能维持到96%左右。如果你对速度没有那么敏感又希望模型更稳可以考虑ResNet-18泛化能力确实更好。训练过程中的几个关键参数优化器Adam初始学习率0.001损失函数交叉熵Batch Size32训练轮数60轮学习率衰减在40轮和50轮时降为原来的0.1倍还有一个很重要的点类别不均衡。停车位有时候全天都是满的或者全天都是空的导致两个类别的样本数差距悬殊。我的处理方式是给少数类加权重计算损失时乘以一个系数系数等于多数类样本数除以少数类样本数。如果某个车位长期全满或全空就需要多补充一些反例数据。3.3 后处理策略状态平滑与置信度过滤模型输出的是一堆每帧的0/1判断但直接用于系统层面会产生很多抖动。比如一辆车正在倒车入库这个过程中车位区域会连续切换几次有车/无车状态如果直接上报给收费系统就会产生无效的进出记录。我的解决办法是加一个时间维度的平滑逻辑。具体做法是维护每个车位的最近十帧判断结果只有连续五帧以上判定为同一状态才认为状态发生了切换。这个“五帧”的阈值需要根据摄像头的帧率来调整如果是10帧每秒五帧就是0.5秒足够过滤掉绝大多数的瞬时误判。另外分类器会输出一个置信度分数。当置信度介于0.4到0.6之间时我会标记为“不确定”不参与状态切换等待后续帧确认。这个操作在光线剧烈变化时比如云层突然遮住太阳特别有用能大幅减少误报。置信度的阈值不要设得太高否则状态切换会有明显延迟。我试过0.8的高阈值结果是车开走后大约十秒才更新空位状态系统体验很僵硬。最后调到0.6延迟和准确率达到了一个可接受的平衡。4. 一体化部署与性能优化4.1 从单张图片到实时视频流的架构最开始我写的是一个最简单的脚本传入一张图片输出车位的状态列表。但这个脚本没法直接用于现场环境因为现场摄像头出的是RTSP视频流还要考虑多路并发、状态缓存、异常恢复等问题。我把整体架构设计成了三个模块采集模块负责连接摄像头拉取RTSP流解码成帧识别模块对每一帧做车位抠图、矫正、分类推理业务模块维护每个车位的实时状态、处理状态切换事件、和上层收费系统对接采集模块和识别模块之间用队列解耦。采集线程按固定帧率抓图识别线程从队列里取图处理这样即使识别速度暂时变慢也不会丢帧或者阻塞采集。帧率不需要太高两到三帧每秒就足够了。车位状态本身是一个变化很慢的事件过高的帧率只会增加无谓的计算消耗。我用的是2帧每秒CPU占用率不到百分之十五。4.2 多线程与推理加速的细节OpenCV解码RTSP流本身是阻塞操作网络抖动时可能卡住整个程序。我的做法是把摄像头读取放到独立线程里并且给读取操作设置超时。如果连续三秒没有读到新帧就主动断开重连。这个重连逻辑是实测中必须有的因为现场的网络环境不可能一直稳定摄像头偶尔也会自动重启。推理部分我用了OpenVINO来加速MobileNetV2。原来的PyTorch模型转成OpenVINO的IR格式之后在CPU上的推理时间从十毫秒左右降到了五毫秒以内。这里有一个需要留意的地方OpenVINO对模型算子有限制转换前最好先确认一下模型里的算子都在支持列表里。我的MobileNetV2转换过程完全没有遇到问题ResNet-18也顺利转过去了。如果你的部署环境是NVIDIA显卡可以考虑用TensorRT或者直接使用PyTorch的CUDA推理。如果只有CPUOpenVINO基本是首选。4.3 性能基准到底能跑多快我在一台普通的工控机上做了完整测试配置是六核CPU、16GB内存、无独立显卡。单路摄像头、2帧每秒的情况下CPU占用约12%内存占用约800MB主要是OpenCV解码缓冲和队列堆积。换算一下这个配置撑四到六路摄像头问题不大。如果想要更高的帧率或者更低的延迟可以考虑把输入尺寸再缩小一点或者把抠图后的尺寸从96x96降到64x64。但64x64是个底线低于这个尺寸之后深色车和地面阴影的区别就变得非常模糊了。5. 常见问题与排查技巧实录5.1 夜间识别准确率骤降一开始我的夜间准确率只有70%仔细排查后发现主要问题在于训练数据里夜间样本太少了。后来我专门挑了几个夜晚每小时存一批图连续存了三天把夜间样本补到总数的百分之三十左右准确率才提上去。第二个影响因素是红外补光灯。有些摄像头的红外光在很近的距离会过曝车头部分的细节完全变成白花花的一团。这种情况下仅仅靠补数据是不够的更有效的办法是调整摄像头的红外亮度参数让车头的轮廓能够看清。5.2 停车位上长期停着一辆“假车”有没有遇到过车位明明是空的模型却一直报警说车还在。我把这个问题排查了很久最后发现是地面上的一摊油渍导致的。那摊油渍的形状和颜色恰好与俯视视角下的车顶非常相似模型被欺骗了。这种问题靠调阈值解决不了因为油渍的外观确实和车非常接近。我用了一个很笨但有效的方法在判断逻辑里加入一个时间维度。如果某个车位被判定为有车超过七天从未变化而且系统没有记录到对应的入场事件就自动标记为“疑点状态”由人工二次确认。这个规则虽然简单但在实际运营中解决了很多类似的问题。5.3 大雨天后摄像头镜头模糊露天摄像头的镜头遇到大雨加风沙很容易变得模糊。模糊状态下的画面分类器基本等于瞎猜准确率会直线下降。这是一个很随便的运维问题但也需要有一套自动检测机制。我用的是拉普拉斯方差来做清晰度检测当画面整体方差低于某个阈值时触发“雾天/脏镜头”告警通知运维人员清洁镜头。另外如果摄像头是自动对焦的注意不要把对焦模式设成连续自动对焦否则夜间会频繁拉风箱导致画面一直处于模糊状态。固定焦距是更适合停车场场景的选择。5.4 状态切换事件丢失业务侧偶尔会报车明明出场了系统还显示占用。问题不在于识别模型而在于状态平滑逻辑太保守了。前面提到了连续五帧才确认状态切换但如果期间有十帧的连续断帧比如网络闪断五帧的门槛就很难达到状态就一直不更新。我的修复办法是加一个超时兜底如果某个车位连续一段时间内画面内容发生了明显变化用帧间差异衡量就暂停平滑逻辑直接采用当前分类结果。同时把断帧情况和状态变化都写入运行日志方便事后回溯。6. 场景扩展与方案复盘6.1 这个方案还能用在哪些地方车位识别这个方案本质上是“固定区域状态分类”的通用架构换个场景也可以复用。比如室内停车场的车位指示灯引导系统逻辑差不多只是把识别的结果接到LED指示牌上高速服务区或者商场停车楼的多层车位统计每层装一个摄像头做汇总临时停车场、活动场地的车辆计数虽然有变化但坐标标定一次也能用如果想在现有基础上升级可以考虑加入车辆类型识别区分轿车/SUV/货车、车牌号关联配合车牌识别API甚至用目标跟踪来辅助判断长期占位和违停。6.2 整个项目的时间线复盘从零开始做的话大概的时间分配是这样的数据采集一周摄像头安装、位置调整、连续自动抓帧数据标注两天到三天车位坐标标定、状态数据半自动标注模型训练一天到两天搭建训练脚本、调参、验证系统部署三天到四天流媒体接入、多线程服务、平滑逻辑、对外接口现场调优至少一周观察真实运行状态、修正数据偏差、处理边缘情况这个时间线里面最容易被低估的是现场调优。实验室里跑得好好的装到现场就会冒出一堆意料之外的情况没有一个真实场景是完全干净的。要给现场调优预留足够的时间。6.3 关于这套方案的几个心得体会我自己做完这个项目最大的感受是算法模型的准确率只是项目成功的一部分甚至不是最重要的部分。真正决定系统能不能稳定运行的是数据质量、异常处理、运维便利性这些“看不见”的环节。另外一个很有价值的反思是不要一上来就追求高大上的模型也不要盲目相信端到端的解决方案。在固定场景下充分利用先验信息车位位置固定、相机位置固定做任务拆分往往比堆模型参数更有效也更可靠。如果你正在计划做类似的识别项目我建议你从最小的闭环开始。先标一两个车位训练一个简单模型跑通一版完整的识别流程然后再慢慢扩展到全部车位。这样每个环节的风险都会在早期暴露出来不会到最后一次性爆发。这个项目做完之后我最大的体会是方案设计阶段的取舍远比写代码和调模型对最终结果影响更大。希望这篇拆解能让你少走一些弯路。