
1. 这个项目的起点一次让人抓狂的停车1.1 痛点分析空车位到底在哪相信每个开车的人都经历过这种场景周末商场地下车库指示灯显示还有空位结果绕了三层发现全是占位神器或者视线盲区里的通道空位。更气人的是好不容易看到前方有个空位刚要开过去对向一台车一打灯抢先一步进去了。停车场智能空车位检测系统听起来高大上本质要解决的就一个事把车位状态实时可视化。用户进停车场之前就能看到哪层哪个区域有几个空位进去之后也能跟着引导快速停好减少绕圈、减少拥堵、减少剐蹭。技术上拆解就是两个部分前端的信息展示用户看到的东西和后端的状态判定机器怎么知道车位空没空。我在做这个模拟项目X之前其实也调研过市面上现有的方案。超声波雷达方案成本高每个车位一个探测头施工布线麻烦地磁感应方案对环境依赖大金属遮挡、车辆缓行都容易误判视频视觉方案是当前最主流的一台相机就能覆盖一片区域配合深度学习算法能做到实时判定。我最终选的就是摄像头深度学习这条路线。1.2 目标与范围界定不做贪大求全做任何项目第一步都是划定边界。这不是行业内卷而是避免自己把自己拖死的必修课。我的目标定得很明确做一个能在本地运行的桌面级演示系统通过摄像头或视频文件实时检测画面中的车辆结合预先划定的车位区域判断每个车位的占用状态并通过PyQt5界面给用户直观展示空余数量、车位位置和俯视图。不做的部分同样重要不做后台管理系统、不做车牌识别、不做计费联动、不做云端推理。这些以后都能扩展但第一版如果全部塞进来大概率哪个都做不精。最终交付物就是一个可运行的桌面应用加一套训练好的检测模型可以在任意一台带摄像头的电脑上跑起来。提示先做小闭环而不是大全套。一个能完整跑通检测-判定-展示的demo比一个接口都没调通的宏大系统有价值一百倍。2. 技术选型为什么是YOLOv8 PyQt52.1 模型选型YOLOv8凭什么目标检测模型的选择基本是在精度、速度、易用性三个维度上做权衡。我在评估这个项目时候选方案有四类传统图像处理背景差分、边缘检测、两阶段检测器Faster R-CNN系、单阶段检测器YOLO系、SSD系、以及最新的Transformer系检测器DETR。传统图像处理在固定机位、光线稳定的室内场景确实能跑比如背景差分法只要背景模型建得好空车位检测准确率不低。但问题在于它对环境变化太敏感灯光变换、车辆阴影、行人走过都会带来大量误报而且每个摄像头点位都要单独调参维护成本很高。Faster R-CNN这类两阶段检测器精度确实高但推理速度压不下来在实时视频流场景下显得吃力。DETR虽然思路新颖但对小数据集并不友好训练收敛慢工程化资料也相对少。综合下来YOLO系列是平衡得最好的单阶段结构保证实时性在公开数据集上经过充分验证工程生态成熟而且Ultralytics官方把训练、验证、导出、部署的Pipeline都封装好了适合快速落地。我选的是YOLOv8n的轻量版本。选择推理依据很直接停车场检测是固定场景下的静态相机检测不需要超大模型去拟合复杂特征nano版本参数量才3.2M左右在CPU上也能跑到十几帧GPU上轻松上百帧对硬件要求低也方便后续部署。2.2 上位机框架选型PyQt5的优势服务端推理再厉害没有可视化界面就没法给用户用。这部分有两条路线Web前端Flask/FastAPI 浏览器和桌面GUI应用。Web方案的好处是跨平台、部署灵活适合做远程监控但演示项目要的是开箱即用还需要调用本地摄像头桌面应用更直接。桌面GUI框架有过几个候选Tkinter太简陋、wxPython生态偏老、C# WPF又不是Python生态。PyQt5是我在Python生态里用下来最顺手的控件丰富、样式可定制、QSS写出来跟CSS差不多的手感信号槽机制天然适合处理摄像头帧更新这种高频异步事件。而且Qt官方文档和社区资料都很全遇到问题基本都能查到解决方案。我这套系统的运行逻辑是主线程跑Qt事件循环工作线程跑视频采集和模型推理通过信号/槽机制把结果帧传回主线程刷新UI。这样设计的好处是界面永远不卡哪怕模型推理慢一点用户操作响应也是流畅的。2.3 整体架构与数据流整个系统从数据流的角度看非常清晰视频源本地摄像头或视频文件→ 逐帧读取帧数据送入YOLOv8模型 → 输出车辆检测框类别、置信度、坐标检测结果传入车位判定模块 → 与预划定的车位区域做IoU/中心点匹配判定结果叠加到原始帧上 → 在界面上绘制车位状态绿色空、红色占统计信息总数、已占、空闲、占用率→ 更新到界面面板这个架构的妙处在于模块解耦非常彻底。检测模型只负责看到车不关心车位怎么划分车位判定模块只做几何运算不关心模型内部细节UI层只负责展示。任何一个模块单独替换都不影响其他模块比如想让检测更准就换更大的模型想让页面好看就重画UI各管各的。3. 数据集准备车辆检测的基石3.1 数据从哪里来模型效果的上限是由数据集决定的这句话我是在踩了无数坑之后才真正理解的。一开始我觉得YOLOv8在COCO上训练过COCO里有car这个类别直接拿预训练权重跑就行。但真实停车场场景和COCO的日常街景差异很大高空俯拍视角、密集停放、光线变化明显直接推理的效果只能说勉强能用误检漏检都不少。我的做法是收集了三类数据公开数据集从多个开源停车场数据集里筛选出俯拍视角的图片大约几百张作为基础底料自己拍摄用手机在几个不同停车场拍了多段视频覆盖白天、黄昏、夜间、晴天、雨天网络视频截帧找一些商场地下车库的监控视角视频按每秒一帧截取最终整理下来大约有1200多张有效图片。说实话这个数量级不算大但因为是固定场景检测车辆外观在俯拍视角下相对一致配合数据增强完全够用了。3.2 标注规范与常见错误标注用的是LabelImg工具标注类别就一个vehicle。这里有个细节值得单独说一下我一开始把类别设成car后面发现有些画面里会出现较大的厢式货车、面包车这些车在实际车位占用判定时必须算进去。单一类别vehicle反而能避免召回率下降的问题。标注的原则我总结了三条车辆目标必须完整包含在框内宁大勿小露出半个车身的也要标被严重遮挡导致无法判断具体轮廓的车可以不标但被挡了一半且轮廓清晰的车必须标远处的车位如果有车但很小也要标出来这能训练模型对小目标的感知能力这条宁大勿小的标注原则是我在标注工作中最想强调的。我在做某图像处理Demo时有过一次惨痛教训标注时为了追求框贴合目标好多车只框了车身的一半结果训练出来的模型遇到车头朝向镜头时就频繁丢失检测。后来统一改大框重训效果立刻上了一个台阶。3.3 数据增强与划分策略数据增强我用的是Ultralytics自带的增强参数没有另外写离线增强。关键参数和我的配置如下参数配置值作用说明hsv_h0.015色相扰动模拟不同光照色调hsv_s0.7饱和度扰动hsv_v0.4明度扰动这个对夜间场景很关键mosaic1.0马赛克拼接四张图拼一张增强小目标感知fliplr0.5水平翻转停车场景左右对称天然适用degrees10轻微旋转补偿手持拍摄时的角度偏差数据划分保持训练集、验证集、测试集的比例为8:1:1并且划分时注意把同一个停车场的多张图片尽量分进同一个集合避免数据泄露——如果训练集和验证集里出现了同一辆车的不同角度照片模型指标会被虚高失去参考价值。4. 模型训练与调优4.1 训练配置与参数选择训练用的是Ultralytics YOLOv8框架训练脚本非常简单照着官方文档改动一下数据文件路径就行。数据配置文件data.yaml是这个样子# data.yaml path: ./dataset train: images/train val: images/val test: images/test names: 0: vehicle训练命令我做了几个关键的参数定制yolo train modelyolov8n.pt data./data.yaml epochs100 imgsz640 batch16 device0 workers4 optimizerAdamW lr00.001 patience20这里解释几个参数的选择理由epochs100我的数据集不算大100轮足够收敛配合patience20做早停实际训练在60多轮就停了。早停是个好习惯比硬跑完固定轮数省时间还不容易过拟合。imgsz640YOLOv8默认的输入尺寸。停车场场景里远处的车很小理论上加大到1280能提升小目标检测率但推理速度和显存占用会大幅上升。我实测过640和960的差异对停车位这种相对固定的检测场景640已经完全够用没必要牺牲速度。optimizerAdamW我用自己的数据微调时更喜欢AdamW而不是默认的SGD。SGD需要更精细的学习率调节AdamW对新手更友好收敛更稳定。后期如果追求极致精度再换回SGD也不迟。lr00.001预训练权重微调的通用学习率。如果从头训练可以提高到0.01但如果加载了COCO预训练权重还用0.01很容易破坏已经学好的特征0.001是比较安全的起点。4.2 训练过程观察与结果评估训练过程中我并不会只盯着loss曲线而是三个指标联合看train/box_loss、metrics/precision、metrics/recall。loss下降说明模型在学习precision和recall分别反映误检率和漏检率。停车场场景中我更在意recall——漏检一辆车会导致某个车位被误判为空这比多检一辆车把一个空位旁边误判成车更容易惹恼用户。训练完成后统计了几个核心指标指标数值Precision0.921Recall0.896mAP500.938mAP50-950.674单帧推理时间RTX 3060约5ms单帧推理时间纯CPU约60-80msmAP50-95只有0.67左右看起来数字不漂亮但这对单类别且场景固定的任务来说完全是可接受的水平。mAP50-95计算的是多个IoU阈值下的平均精度对目标框的精确贴合程度要求高而停车位占用判定只需要知道这个区域是否有车框稍微大几像素根本不影响判断结果。4.3 从评估到迭代我踩过的数据坑第一版模型训练完我在实际视频里测试发现一个特别头疼的现象画面里只有半辆车。车位靠柱子那侧停着一辆车车身有一半被柱子挡住了模型检测出来了框没问题但有些车位里停的是电动车、三轮车模型直接无视了。电动车和三轮车要不要算作占用车位从停车场管理的角度当然算但从数据集标注角度来看当时我的标注规范里根本没考虑这两类目标。解决方式有两种一是给这两类单独建类别二是把它们并入vehicle类别一起标注。我的选择是并入vehicle因为它们在车位判定里起的作用和汽车完全一样没必要区分得更细。重新补充标注了几十张含电动车三轮车的图片后recall提升明显。注意训练和实际使用场景之间的分布偏移是最大的性能杀手。如果部署环境里有大量摩托车、购物推车或者不同季节的树木遮挡数据集里没有对应的样本模型效果就会打折扣。提前想清楚目标环境比盲目堆积数据更有价值。5. PyQt5上位机开发从模型到可用产品5.1 UI布局设计思路界面设计的原则是少而清晰。停车场管理场景的屏幕不会太大值班人员扫一眼就要知道当前状况。我的主界面分了三个区域左侧视频显示区占最大面积实时显示带检测框和车位状态标记的画面右侧信息面板区显示总车位、空闲车位、已占车位、占用率以及每个车位的状态列表底部控制栏打开摄像头/视频、切换演示视频、启动/停止检测、导出报表按钮用QSS调整整体风格深色背景、高对比配色。车位状态用颜色区分是最直观的绿色空心框表示空车位红色实心框表示占用。5.2 多线程推理不卡UI的关键这是很多新手做PyQt5应用最容易翻车的地方。如果直接在UI线程里做视频处理和模型推理摄像头每来一帧就推理一次推理期间界面会完全卡住窗口拖不动、按钮点不了用户体感特别差。我的解决方案是双重线程隔离class DetectionWorker(QThread): frame_ready pyqtSignal(QImage) stats_updated pyqtSignal(dict) def __init__(self, model_path, video_source): super().__init__() self.model YOLO(model_path) self.cap cv2.VideoCapture(video_source) self.running True def run(self): while self.running: ret, frame self.cap.read() if not ret: break results self.model(frame) annotated_frame self.draw_results(frame, results) rgb_image cv2.cvtColor(annotated_frame, cv2.COLOR_BGR2RGB) h, w, ch rgb_image.shape qt_image QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qt_image)工作线程只负责读帧、推理、画框然后把处理完的QImage通过信号发回主线程主线程的槽函数只管把图片显示到QLabel上。QImage在跨线程传输时不会出现指针失效问题这也是PyQt写视频应用的舒适区。5.3 车位占用判定逻辑核心算法细节车位判定的核心思路是区域匹配而不是目标跟踪。每个车位预先在画面中划定一个矩形区域保存为一组坐标。模型检测出所有车辆框后计算每个车位区域与哪个车辆框重叠度最高。这里有一个关键参数IoUIntersection over Union。如果车位区域和某个车辆框的IoU超过阈值就判定该车位被占用。那阈值取多少合适我试过0.3、0.5、0.7几个档位最后定为0.45。为什么要这个值如果阈值太高车停得稍微歪一点或车身有一部分探出车位线IoU就低于阈值车明明在里面却被判为空阈值太低旁边车辆的检测框只要擦到车位边缘空车位就会被误报为占用。0.45是一个在多次实验后平衡了这两类误差的折中值。除了IoU之外还有两种情况需要特殊处理场景一车位里停的是紧贴车位线的车框完全包含车位区域。这种情况下IoU很高判定没问题。场景二两个相邻车位都停了车其中一个车的检测框也压到了另一个车位的角落里。如果只用IoU判定有可能出现一个空车位被旁边车辆的框污染。我在判定逻辑里加了一个条件优先选择IoU最大的那个车辆框且明确要求车位中心点必须在车辆框内部。def is_occupied(parking_spot, vehicle_boxes, iou_threshold0.45): max_iou 0.0 best_box None for box in vehicle_boxes: iou compute_iou(parking_spot, box) if iou max_iou: max_iou iou best_box box if max_iou iou_threshold: return False, None # 中心点校验空车位不能只被框的边缘蹭到 spot_center center(parking_spot) if not point_in_box(spot_center, best_box): return False, None return True, best_box5.4 功能扩展从能用向好用迈进核心判定做完之后我又补了几个提升体验的功能。车位配置持久化车位坐标如果每次启动都要手动调肯定会被骂。我把车位配置保存成JSON文件每次启动自动加载界面里只要拖动鼠标框选车位区域就能把新配置写回文件。这个功能实际写起来就是记录鼠标拖动的起点终点然后序列化保存逻辑不复杂但实用性极高。状态稳定性过滤连续帧判定结果抖动是最影响信任度的问题——前一秒显示空后一秒变占用再过一秒又变回空用户看到会以为系统坏了。我加了一个简单的状态机连续5帧判定为占用才真正切换到占用状态连续5帧判定为空才切回空。这牺牲了一点实时性但换来了肉眼可见的稳定性。导出统计报表每隔一段时间记录各车位的状态变化生成一个简单的文本报表对后续分析停车高峰时段、每车位周转率都有价值。我用的就是Python标准库的csv模块没有引入额外的数据库。注意状态过滤虽然提升了稳定性但阈值过大会带来展示延迟。如果停车场的实际需求是车一停进去立刻显示占用5帧可能还行如果要求秒级更新可以把帧数调小或者改成时间阈值例如200ms内连续判定一致才算稳定。6. 部署与运行性能优化6.1 模型导出不只是格式转换开发阶段用PyTorch的pt格式推理没问题但实际部署时我导出了ONNX格式推理速度提升很明显。导出命令非常简单from ultralytics import YOLO model YOLO(runs/detect/train/weights/best.pt) model.export(formatonnx, imgsz640, opset12, simplifyTrue)ONNX推理我选的是ONNX Runtime的CPU版本这样即使部署机器没有NVIDIA显卡也能跑。实测在CPU上ONNX比PyTorch直接推理快20%-30%因为ONNX Runtime对CPU算子做了很多优化比如多线程调度和算子融合。如果是GPU环境还可以进一步导出TensorRT引擎。TensorRT的优化幅度更大但转换过程复杂一些需要先导出ONNX再构建engine。我这个演示项目没有强上TensorRT毕竟部署机器不确定有没有GPU一份ONNX模型兼容性最好。6.2 减少资源占用的几个实操技巧限制推理频率视频是30FPS的但车位状态根本不需要每帧判断。我加了帧采样逻辑每3帧跑一次检测中间两帧直接用上一次的检测结果画出状态图。界面看起来依然是流畅的但CPU占用和功耗大幅下降。这是投入产出比最高的一项优化。图像压缩把摄像头原始帧等比缩小到1280像素宽度再送进模型。1280×720的输入和1920×1080的输入检测精度几乎没差别但推理时间显著下降。这个技巧在工程上非常常见本质就是模型输入尺寸决定了它能看到多少细节。推理线程优先级的设置在Qt里把工作线程的优先级设为QThread.HighPriority可以避免系统在CPU满负载时给推理线程分不到资源减少偶发的推理延迟。6.3 实测数据与资源占用我把整个系统跑在了一台中端配置的电脑上8核CPU 16GB内存无独立显卡摄像头源是USB外接摄像头。实测结果如下项目数据CPU占用约35%-45%内存占用约1.2GB检测延迟约80-100ms/帧界面响应流畅无卡顿连续运行8小时无崩溃这个资源占用对于演示系统来说完全够看。如果正式部署到树莓派或Jetson这类嵌入式设备建议走TensorRT导入加更低分辨率的输入仍然有优化空间。7. 常见问题排查与避坑实录7.1 训练阶段的高频问题问题1loss曲线剧烈震荡像心电图一样一上来就遇到这个别慌大概率是学习率太高或者batch太小。我的经验是检查两个地方一是lr0是否调到0.002以下二是batch size是否至少8以上。两个参数都调低之后震荡基本消失。问题2训练过程中mAP一直是0这个问题通常是因为数据标注文件没有正确关联到图片。检查数据集目录里是否存在空的txt标注文件YOLO格式的标注文件如果格式错误坐标越界、类别索引超出names范围Ultralytics会在日志里提示但不会直接报错很容易被忽略。验证方法是写个简单脚本读取一张图片和它的txt把框画出来看一眼是不是对应得上。问题3过拟合严重训练集指标很好验证集一塌糊涂我的应对方案是加权组合增大数据增强强度、加入dropout、降低训练轮数配合早停。停车场场景数据量有限的情况下过拟合几乎是必然现象关键在于控制住不要让它太早出现。7.2 部署阶段的高频问题问题4PyQt5界面黑屏不显示画面多半是QImage的数据格式和图像数据的步长对不上。我踩过的具体问题是cv2.cvtColor之后图像还是BGR顺序如果直接转QImage会颜色错乱。更隐蔽的是OpenCV处理后的Mat内存可能在主线程show之前被回收。正确做法是转成QImage时用rgb_image.copy()做一次浅拷贝确保数据生命周期和QImage一致。问题5视频流能跑但界面偶发崩溃这个和Qt的内存管理关系很大。如果每次新建QImage然后用完不销毁内存会缓慢增长运行几小时之后就崩了。我的解决方法是只在帧尺寸不变的情况下复用同一个QImage缓冲区而不是每帧新建。问题6车位的坐标在不同分辨率下漂移调试的时候用的1080p屏幕部署到一台1366×768的笔记本车位区域全部错位了。这是典型的坐标系统没做归一化。正确做法是车位配置里存归一化坐标0到1之间加载时再乘上当前画面的实际尺寸。一开始就做好这个设计后面能省一堆时间和兼容性问题。7.3 独家经验这些坑踩一次就够了上面这些问题其实都是查得到答案的问题下面这两点才是真正的经验门槛第一车位判定阈值不要在训练阶段调优要在部署现场调优。训练指标的IoU和现场判定的IoU完全是两回事。训练是在数据标注的框上做的现场是要在真实画面光照下做几何判断。我最初在办公室里调好参数拿到现场光线更暗的摄像头下面误判率直接从2%飙到15%。后来在系统里加了一个调参模式现场打开调参界面实时看检测效果几分钟就能定位到最优阈值。第二把相机固定住一切就成功了一半。深度学习和传统视觉最大的区别是它不需要背景建模但如果相机抖动导致画面漂移检测框和车位坐标之间的对应关系就会破坏。我的系统在部署时有一个硬性要求摄像头必须使用固定支架不能手持、不能放在车顶或台面上。这个看起来是硬件问题实际影响的是整个软件系统的稳定性。多次实测表明相机轻微位移不超过20像素时判定基本不受影响超过50像素误判率就开始恶化。提示把系统的软硬边界想清楚比多调几个参数重要得多。判定逻辑解决的是画面里看到的到底是什么的问题而对看不到的部分没有任何办法。摄像头被遮挡、逆光、夜间光线不足这些属于部署层面的约束应该在需求阶段就跟客户对齐而不是等技术方案完成了再发现。8. 后续可以怎么玩从Demo到产品的几条路径8.1 模型层面精度还有多少提升空间当前版本的mAP50有0.938对于车位检测已经够用。如果想让系统更稳更准方向有三条第一条是引入车位级分类网络。检测是整个画面里的车辆判定是车位区域中间其实隔了一步几何映射。可以换成先把每个车位区域裁剪出来单独用一个二分类网络判断空/占。这样判定逻辑更直接也能避免相邻车辆越界导致的IoU误判。第二条是引入序列信息。当前是单帧判定如果加入时序信息比如连续N帧的状态轨迹可以把临时遮挡、行人路过带来的瞬时干扰过滤得更干净。用LSTM或者简单的状态概率模型都能做。第三条是模型蒸馏。用大模型精标数据蒸馏出小模型可以在推理速度和精度之间找到更好的平衡点。不过对于这个体量的项目收益有限性价比不高。8.2 产品层面从看得见到用得上现在做的系统是人盯着看真正的产品化方向无外乎接入停车场管理系统空位引导屏、APP预约、微信小程序实时查询增加统计报表停车场运营方最需要的是利用率曲线、峰值时段、车位周转率这些数据在现有系统里都有原始记录只是没有加工成报表多路摄像头统一管理一个画面一个模型实例是最笨的做法更合理的是每路摄像头共享模型、按区域独立判定用中控页面展示整个停车场的地图热力图我个人最推荐的扩展是低位摄像头方案——不是每个车位装一个球机而是用一台能覆盖10-20个车位的广角相机配合车位框自动标定算法。这样部署成本能压到最低也是目前比较受认可的落地方向。8.3 最后几句实在话断断续续做这个项目前后花了两三周时间。最大的感慨是目标检测模型本身在2025年已经足够成熟真正的难点从来不在怎么训练一个模型而在怎么把模型可靠地集成到产品里还得一直稳定运行。YOLOv8和PyQt5的组合给我的体感是训练门槛极低工程坑极多。这也是我写下这篇长文的原因——希望后来者少走一点弯路把精力花在真正有价值的地方现场调研、数据处理、交互设计。如果哪天你把这个系统部署到某个真实停车场里我猜你收获最大的不是那几个技术指标而是第一次看到真实用户因为车还没停进车位屏幕上已经显示了占用而多看了你一眼的那种踏实感。那个时刻代码里所有的坑都值了。