
简介该资源为基于Python的SSD空停车位识别演示项目源码面向深度学习入门者、目标检测学习者及智能停车场系统开发者帮助理解SSD算法在真实场景中的落地流程。压缩包共78个文件约282KB以61个Python源码文件为主体覆盖数据预处理、模型训练、推理评估与客户端-服务器部署等模块另含5个XML配置、2个txt说明及yaml、jpg等辅助文件用于网络结构定义、参数配置与使用指引。项目围绕SSD目标检测展开包含backbone、box_head、detector等建模组件以及anchors、nms、box_utils等检测工具并给出demo、server、client等脚本展示实时查看停车位检测结果的简单架构。目前已有314人学习适合通过阅读源码掌握数据集标注、模型训练调参与系统部署的完整思路也可作为智能交通与智慧城市场景的实践参考。1. 从一张俯拍图说起SSD 空停车位识别到底在做什么地下车库的摄像头架在通道上方画面里一排车位有的停着车有的空着。人眼一眼能分辨但要让程序自动数出「还剩几个空位」就没那么简单了——车位线被柱子遮挡、相邻车位只隔一条白线、光照忽明忽暗、车头压线、地锁立起来……这些都是真实场景里天天遇到的麻烦。基于 Python 的 SSD-Demo 空停车位识别要解决的就是这件事用 SSD 目标检测算法把画面里的「空车位」和「占用车位」分别框出来输出可用的车位状态。这个方向适合三类人一是做智慧停车、园区管理的开发者需要一套能跑通、能改的检测基线二是学目标检测的学生和转行者想找一个比猫狗分类更贴近业务的练手项目三是已有摄像头资源、想低成本验证算法可行性的工程师。SSDSingle Shot MultiBox Detector的特点是单阶段、速度快、对小目标有一定容忍度配合 Python 生态里的 PyTorch 或 PaddlePaddle能在普通显卡甚至 CPU 上跑出可接受的帧率。源码层面这类 Demo 通常包含数据集标注、模型定义、训练脚本、推理脚本四块理解清楚这四块你就能把它改成自己的车位识别系统。2. SSD 检测空车位的原理与选型为什么不用分类网络硬套2.1 空车位识别本质是检测问题不是分类问题很多人第一反应是「裁出每个车位的小图丢进分类网络判断空/满」。这个思路在固定机位、车位框位置不变时能work但一旦摄像头有轻微位移、车位被遮挡、或者要换一个停车场整套坐标就得重标维护成本极高。更麻烦的是分类网络只能告诉你「这张小图里有没有车」它不知道车停歪了、压了两个车位、或者车位里堆了杂物。SSD 这类目标检测网络直接输出边界框和类别把「空车位」当成一个类别去学。它的好处是车位不需要预先切好网络自己在整张图里找车位被部分遮挡时只要特征还在仍有机会检出换场景时重新标注一批数据微调即可不用改代码结构。从工程角度看这是更可持续的方案。SSD 的核心设计有三点值得记住。第一多尺度特征图预测在 6 个不同分辨率的特征图上分别设置默认框prior box浅层特征图负责小目标深层负责大目标。第二默认框机制每个特征图位置预设多种长宽比的框网络只预测相对这些框的偏移量收敛更快。第三硬负样本挖掘正负样本极度不均衡时按置信度损失排序挑最难分的负样本参与训练避免模型被大量背景带偏。2.2 主干网络与输入尺寸怎么选SSD 常见主干是 VGG16 或 ResNet也有轻量化的 MobileNet 版本。车位识别场景里我一般推荐先用 MobileNet-SSD 跑通原因是车位目标通常不大、场景相对固定轻量主干足够而且推理快方便部署到边缘设备。如果检出率不达标再换 VGG16-SSD 或加 FPN 结构。输入尺寸直接决定小目标检出能力。SSD300 对小目标偏弱SSD512 明显更好但显存和耗时上升。车位在俯拍图里往往只占几十个像素建议从 512 起步。下面是一个基于 PyTorch 的 SSD 配置片段展示输入尺寸和默认框设置的关键参数# ssd_config.py # 输入尺寸车位目标小选 512 而非 300 IMAGE_SIZE 512 # 默认框配置每个特征图上的尺度与长宽比 # 车位多为横向矩形长宽比里保留 2 和 3 PRIOR_BOX_CONFIG { feature_maps: [64, 32, 16, 8, 4, 2, 1], # 7 层特征图 min_sizes: [35.84, 76.8, 153.6, 230.4, 307.2, 384.0, 460.8], max_sizes: [76.8, 153.6, 230.4, 307.2, 384.0, 460.8, 537.6], aspect_ratios: [[2], [2, 3], [2, 3], [2, 3], [2], [2], [2]], variance: [0.1, 0.2], } # 类别0 背景1 空车位2 占用车位 NUM_CLASSES 3这段配置里feature_maps是各层特征图的空间尺寸min_sizes和max_sizes控制默认框的绝对像素大小aspect_ratios决定框的形状。车位是扁长矩形所以长宽比保留 2 和 3 就够了不需要 1:1 之外的太多比例。variance是偏移量缩放系数一般保持默认。改这些参数时最直接的验证方式是看训练初期正样本匹配数量——如果匹配数太少说明默认框和真实车位框差距太大需要调min_sizes或aspect_ratios。2.3 数据集标注空车位和占用车位要分开标标注质量决定上限。空车位识别里我建议把「空车位」和「占用车位」都标出来而不是只标空车位。原因有两个一是网络能学到两者的边界差异减少把地面反光误判成空位二是推理时可以直接输出两类框方便统计总数。标注格式用 VOC 的 XML 或 YOLO 的 txt 都行关键是类别名统一。下面是一个把 VOC 标注转成 SSD 训练所需格式的脚本片段# voc_to_ssd.py import os import xml.etree.ElementTree as ET # 类别映射背景固定为 0 CLASS_MAP {empty: 1, occupied: 2} def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) w int(size.find(width).text) h int(size.find(height).text) boxes [] for obj in root.iter(object): name obj.find(name).text if name not in CLASS_MAP: continue bbox obj.find(bndbox) xmin float(bbox.find(xmin).text) / w ymin float(bbox.find(ymin).text) / h xmax float(bbox.find(xmax).text) / w ymax float(bbox.find(ymax).text) / h boxes.append([CLASS_MAP[name], xmin, ymin, xmax, ymax]) return boxes # 输出为每行class_id xmin ymin xmax ymax归一化 def convert(voc_dir, out_txt): with open(out_txt, w) as f: for xml_file in os.listdir(voc_dir): if not xml_file.endswith(.xml): continue boxes parse_voc(os.path.join(voc_dir, xml_file)) for b in boxes: f.write( .join(str(x) for x in b) \n)转换逻辑很直接读 XML取宽高做归一化按类别映射成整数。注意归一化用的是整图宽高不是框自身宽高这是 SSD 训练输入的要求。如果标注里有difficult标记建议先跳过避免难样本干扰初期训练。3. 从零跑通 SSD-Demo环境、训练、推理三步走3.1 环境搭建与依赖版本锁定Python 环境建议 3.8 到 3.10太新的版本有时和旧版 PyTorch 冲突。显卡驱动、CUDA、PyTorch 三者版本要对应这是最常见的翻车点。我一般用 conda 建独立环境避免污染系统 Python。# 创建环境 conda create -n ssd_parking python3.9 -y conda activate ssd_parking # 安装 PyTorch以 CUDA 11.8 为例具体按显卡驱动选 pip install torch2.0.1 torchvision0.15.2 --index-url https://download.pytorch.org/whl/cu118 # 其余依赖 pip install opencv-python numpy matplotlib tqdm pillow装完后务必验证 CUDA 是否可用import torch print(torch.__version__) print(torch.cuda.is_available()) # 应为 True print(torch.cuda.get_device_name(0))如果is_available()返回 False先别急着改代码九成是驱动或 CUDA 版本不匹配。用nvidia-smi看驱动支持的 CUDA 上限再回来看 PyTorch 版本。这一步没搞定后面训练会直接报错或静默退回 CPU速度差几十倍。3.2 训练脚本的关键参数与启动命令训练脚本通常包含数据加载、模型构建、损失计算、优化器、学习率调度五部分。SSD 的损失由分类损失和定位损失加权组成权重比一般设 1:1 到 1:2。下面是一个训练循环的核心片段# train.py import torch from torch.utils.data import DataLoader from ssd_model import SSD512 from dataset import ParkingDataset from multibox_loss import MultiBoxLoss device torch.device(cuda if torch.cuda.is_available() else cpu) # 数据集训练集和验证集按 8:2 划分 train_set ParkingDataset(rootdata/train, image_size512, augmentTrue) val_set ParkingDataset(rootdata/val, image_size512, augmentFalse) train_loader DataLoader(train_set, batch_size8, shuffleTrue, num_workers4) val_loader DataLoader(val_set, batch_size8, shuffleFalse, num_workers4) model SSD512(num_classes3).to(device) criterion MultiBoxLoss(num_classes3, overlap_thresh0.5, neg_pos_ratio3) optimizer torch.optim.SGD(model.parameters(), lr1e-3, momentum0.9, weight_decay5e-4) scheduler torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones[80, 120], gamma0.1) for epoch in range(150): model.train() for images, targets in train_loader: images images.to(device) targets [t.to(device) for t in targets] optimizer.zero_grad() out model(images) loss_l, loss_c criterion(out, targets) loss loss_l loss_c loss.backward() optimizer.step() scheduler.step() # 每 10 轮验证一次 if epoch % 10 0: model.eval() # 验证逻辑略重点看 mAP 和空车位召回率 torch.save(model.state_dict(), fweights/ssd_epoch_{epoch}.pth)参数说明batch_size8是 512 输入下的保守值显存 8G 以上可以试 16overlap_thresh0.5是正样本匹配的 IoU 阈值低于它算负样本neg_pos_ratio3表示负样本最多是正样本的 3 倍这是硬负样本挖掘的常用比例学习率用 SGD 从 1e-3 起步80 和 120 轮各降一次。启动命令就是python train.py但建议加日志重定向方便回看。训练时重点盯两个指标总 loss 是否稳定下降以及验证集上空车位的召回率。如果 loss 降但召回不涨多半是正样本匹配太少回去调默认框配置。3.3 推理与可视化把框画回原图推理脚本要做三件事预处理输入、前向计算、NMS 后处理并画框。下面是一个可复用的推理片段# inference.py import cv2 import torch import numpy as np from ssd_model import SSD512 device torch.device(cuda if torch.cuda.is_available() else cpu) model SSD512(num_classes3).to(device) model.load_state_dict(torch.load(weights/ssd_final.pth, map_locationdevice)) model.eval() CLASSES [background, empty, occupied] COLORS [(0, 0, 0), (0, 255, 0), (0, 0, 255)] # 空位绿占用红 def preprocess(img, size512): img_resized cv2.resize(img, (size, size)) img_rgb cv2.cvtColor(img_resized, cv2.COLOR_BGR2RGB) tensor torch.from_numpy(img_rgb).float().permute(2, 0, 1) / 255.0 mean torch.tensor([0.485, 0.456, 0.406]).view(3, 1, 1) std torch.tensor([0.229, 0.224, 0.225]).view(3, 1, 1) return (tensor - mean) / std def detect(img_path, conf_thresh0.4, nms_thresh0.45): img cv2.imread(img_path) h, w img.shape[:2] tensor preprocess(img).unsqueeze(0).to(device) with torch.no_grad(): detections model(tensor) # detections 需经 NMS此处省略具体实现 # 画框时把归一化坐标乘回原图宽高 for det in detections: cls_id, score, xmin, ymin, xmax, ymax det if score conf_thresh: continue x1, y1 int(xmin * w), int(ymin * h) x2, y2 int(xmax * w), int(ymax * h) cv2.rectangle(img, (x1, y1), (x2, y2), COLORS[int(cls_id)], 2) cv2.putText(img, f{CLASSES[int(cls_id)]} {score:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, COLORS[int(cls_id)], 1) cv2.imwrite(result.jpg, img) detect(test_parking.jpg)conf_thresh0.4是置信度阈值低于它的框直接丢nms_thresh0.45控制重叠框合并车位密集时可以调到 0.3 到 0.4避免相邻车位被误合并。画框时坐标要乘回原图宽高因为训练时做了归一化。这一步如果忘了框会全部挤在左上角是新手最常见的翻车现场。4. 空车位识别的避坑与排查那些让模型「看起来能跑」的陷阱4.1 现象训练 loss 正常下降但推理时一个框都不出原因通常有三个一是推理时的预处理和训练不一致比如训练用了 ImageNet 均值方差推理忘了减二是类别数对不上训练时num_classes3推理加载的模型却是 2三是 NMS 阈值设得太低把所有框都滤掉了。解决方法是先打印原始输出看有没有置信度大于 0.1 的框再逐步排查预处理和类别映射。4.2 现象空车位和占用车位频繁互换这是标注不一致导致的。同一个车位上午标成 empty下午标成 occupied网络学到的特征就混乱了。解决方法是标注时统一标准车位内没有车头、车尾、车轮可见就算 empty只要压线或部分进入就算 occupied。另外地锁、锥桶这类物体建议单独标一类或直接忽略不要混进空车位。4.3 现象相邻车位被合并成一个大框SSD 的默认框长宽比如果偏向正方形横向相邻的两个车位容易被一个框覆盖。解决方法是把aspect_ratios里的 2 和 3 保留同时把 NMS 阈值从 0.45 降到 0.35 左右让重叠框更容易被分开。如果还不行就在后处理里加一条规则框的宽高比超过 3 的拆成两个。4.4 现象夜间或逆光场景检出率骤降这是数据分布问题不是模型问题。训练集里如果全是白天图夜间必然翻车。解决办法是采集夜间样本重新微调或者在预处理里加自适应直方图均衡化CLAHE。我一般会在数据增强里加随机亮度、对比度扰动让模型对光照变化更鲁棒。4.5 现象换一个停车场模型完全失效车位线样式、地面材质、摄像头角度都变了模型没见过。这时候不要重训先拿新场景的几十张图做微调冻结主干只训检测头学习率降到 1e-4通常几十轮就能恢复。如果差异太大就重新标注一批数据从头训。5. 把 SSD-Demo 用起来从单帧检测到视频流统计的进阶技巧跑通单张图只是起点真正有价值的是把它接到视频流上实时统计空车位数量。这里有一个我常用的技巧不要每帧都跑检测而是隔帧检测加跟踪补偿。车位是静止的车进出是低频事件每 5 帧检测一次中间帧用上一帧的结果能省下大量算力。# video_pipeline.py import cv2 import time cap cv2.VideoCapture(parking_lot.mp4) frame_id 0 last_result None DETECT_INTERVAL 5 # 每 5 帧检测一次 while True: ret, frame cap.read() if not ret: break if frame_id % DETECT_INTERVAL 0: last_result detect_and_count(frame) # 返回空车位数量 # 在画面上叠加最近一次统计结果 cv2.putText(frame, fEmpty: {last_result}, (30, 50), cv2.FONT_HERSHEY_SIMPLEX, 1.2, (0, 255, 0), 2) cv2.imshow(parking, frame) if cv2.waitKey(1) 0xFF ord(q): break frame_id 1 cap.release() cv2.destroyAllWindows()这个管道的参数DETECT_INTERVAL要根据实际帧率和算力调。30fps 的视频间隔 5 帧就是每秒检测 6 次对车位状态来说足够。如果画面里车流很快就降到 3。另一个实用技巧是给每个车位分配固定 ID。方法是在第一次检测后把框的中心点存下来后续帧用最近邻匹配。这样统计时不会因为框的轻微抖动而重复计数。代码上可以用一个简单的字典维护{车位ID: 最近中心点}每帧更新。验证模型好不好别只看 mAP。空车位识别更该看两个业务指标空车位漏检率把空位判成占用和误检率把占用判成空位。漏检率高用户到了发现没位体验差误检率高系统显示有位实际没有更糟。我一般要求漏检率低于 5%误检率低于 3%达不到就回去补数据。最后说一个血泪教训别在训练集上验证。我见过太多人把训练图直接拿来测框画得漂漂亮亮一上真实视频就崩。验证集必须和训练集在时间、天气、摄像头上都有区分度否则你看到的只是模型在背答案。这个习惯我坚持了很多年希望帮到你。本文还有配套的精品资源点击获取