简介这是一份基于改进YOLOv8_seg实例分割模型的路边非标准停车位识别系统源码与数据集面向智慧停车、交通管理及深度学习视觉开发者聚焦印度等地区停车位设置复杂、标志多样的真实场景。系统可识别公交站、免费停车位、垃圾箱、禁停标志、物业入口、侧街、商店及其入口、商店保留停车位、寺庙等目标并输出带区域标识的分割结果。资源共27个文件包体5.85MB包括19张标注图像、4个Python脚本、2个说明文档以及txt、md使用说明。Python脚本覆盖训练、验证、预测与界面交互图像展示不同场景下的实例分割标注文档则提供项目说明与运行指引。已有72人学习。通过完整源码可系统掌握改进YOLOv8_seg从数据预处理、特征提取到目标检测与实例分割的完整流程并利用预标注数据集开展微调训练和对比实验为违停识别、车位利用率分析及城市规划提供可直接落地的技术参考。1. 路边非标准停车位识别为什么非用 YOLOv8_seg 实例分割不可路边非标准停车位识别卡在的不是“模型会不会检”而是“检出来的东西到底能不能停”。YOLOv8_seg 实例分割在这个场景里能把车位、公交站、禁停标志、垃圾箱这些目标一个个抠出轮廓而不是只画个框只有拿到像素级 mask后续才能判断一辆车有没有压线、有没有占住公交站、离禁停标志还剩多少距离。这份标题里带“源码与数据集分享”的项目把路边场景拆成了九个具体类别加一个占位类正好覆盖了停车管理里最容易扯皮的那些目标公交站、免费停车位、垃圾箱、禁停标志、物业入口、侧街、商店及其入口、商店保留停车位、寺庙外加一个“变”类做兜底。适合正在做智慧停车、路侧巡检或城市要素提取的算法工程师也适合拿实例分割当课题、不想只用 COCO 那套通用类目的研究生。下面按一个可复现的训练方案逐步讲。2. 把“非标准停车位”变成训练数据类别语义、标注约定与数据集拆分2.1 厘清九类目标与“变”类的语义先定标注规则再开标拿到这份源码和数据集第一件事不是跑训练而是把每个类别的边界定清楚。路边场景和室内场景不一样同一个“商店入口”在不同照片里可能一半是台阶、一半是坡道同一个“免费停车位”可能画了两条线也可能只有一块地砖颜色不同。标注规则如果不先固定后面所有指标都是自欺欺人。我一般会把类别分成三组来定规则位置类公交站、侧街、物业入口、商店及其入口、寺庙。这些属于“固定场所”标注时以实际占用地面范围为界不标建筑立面。公交站把候车区域连同站台边缘画进去商店入口标到门廊外沿不标门本身。标志与设施类禁停标志、垃圾箱。禁停标志只标立牌本体和底座不标旁边的树或电线杆垃圾箱标桶身和底部阴影的近似外接轮廓阴影不要扩太大否则模型会把阴影学进去。停车资源类免费停车位、商店保留停车位。这类最忌讳只标划线区域。路边很多免费停车位并没有完整线框只有前后车夹出来的一块空间标注时以现实中“能停进一辆标准小车”的区域为准。保留车位同理以地面文字或围挡为边界文字被车挡住就按围挡范围补全。标题里的“_变”我理解为占位类。标注工具里凡是“说不清是什么但确实占着路边空间”的实例归到这一类。它承担负样本兜底作用避免模型把未知目标硬分成九类里某一个。实际踩坑时可以发现这类如果只标几十个实例模型基本学不出它有意义的特征但能明显压低其他类的误检率。2.2 YOLOv8_seg 的标签结构从 polygon 到 RLE 的转换脚本YOLOv8_seg 的训练标签不是 COCO 那种 JSON 格式而是每个类别一个 txt每行一个实例class_id后面跟一组归一化的 x, y 坐标坐标按顺时针或逆时针给都行但必须是多边形顶点。很多标注工具导出的是 COCO JSON里面 segmentation 字段是 RLE 编码或一组浮点数组。直接拿 COCO 格式去训练 YOLOv8_seg 会翻车必须先转换。import numpy as np from pycocotools import mask as mask_util from pathlib import Path import cv2 def coco_poly_to_yolo_seg(coco_seg, img_w, img_h): # COCO segmentation 有两种list of polygons 或 RLE dict if isinstance(coco_seg, dict): # RLE 转 binary mask m mask_util.decode(coco_seg) contours, _ cv2.findContours(m, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) polygons [] for c in contours: c c.flatten().tolist() polygons.append(c) else: polygons coco_seg lines [] for poly in polygons: pts np.array(poly, dtypenp.float32).reshape(-1, 2) # 归一化到 0~1YOLO 需要相对坐标 pts[:, 0] / img_w pts[:, 1] / img_h pts pts.flatten().tolist() line .join(f{v:.6f} for v in pts) lines.append(line) return lines # 使用示例 img_w, img_h 1920, 1080 coco_seg [[10, 10, 200, 10, 200, 150, 10, 150]] # 一个四边形 yolo_lines coco_poly_to_yolo_seg(coco_seg, img_w, img_h) print(yolo_lines)这段代码处理了两种常见输入多边形列表和 RLE。关键点在于归一化分母必须用原始图片宽高不能用 mask 数组的宽高。很多标注工具导出时会把 segmentation 坐标裁剪到 RoI 范围内如果直接用 RoI 宽高做归一化标签位置会整体偏掉。另外 pycocotools 的 mask_util.decode 只接受 COCO 规定的 RLE 结构如果是自定义 RLE先用 mask_util.frPyObjects 转一下。转换完的 txt 要按 YOLOv8 的目录结构放数据集根目录下 images 和 labels 一一对应文件名去掉扩展名后相同。值得再检查一遍的是YOLOv8_seg 要求顶点数至少是 3但顶点过多超过 1000 个训练时会拖慢数据加载建议用 cv2.approxPolyDP 把轮廓简化到 20 到 60 个顶点之间。2.3 数据集划分与增强策略复现结果前先算好这三件事划分数据集不是按文件随机分一次就完事。路边场景有个天然陷阱同一段路连续拍下来的几十张图背景高度相似如果随机划分训练集和验证集可能出现“同一条街的两张相邻照片分别落在两边”验证分数虚高部署到新路段直接打回原形。我建议按拍摄路段做分组划分把所有图片按所属路段或者按拍摄时间连续段分成 group再用sklearn.model_selection.GroupShuffleSplit按组切分。标题里包含公交站、寺庙、商店等多种场所不同场所本身差异就大如果某类目标只出现在一条街更要保证划分后训练集和验证集都包含这类目标。from sklearn.model_selection import GroupShuffleSplit img_paths [...] # 你的图片路径列表 groups [...] # 每个图片对应的路段ID比如 road_01, road_02 gss GroupShuffleSplit(n_splits1, train_size0.8, random_state42) train_idx, val_idx next(gss.split(img_paths, groupsgroups)) print(f训练集 {len(train_idx)} 张验证集 {len(val_idx)} 张)分组逻辑比随机划分多花十分钟但能避开“验证集和训练集长得像”的假象。划分完成后再做增强。YOLOv8_seg 自带的增强对 mask 是同步变换的fliplr、scale、translate 都可以开但要注意两个参数hsv_h路边场景建议调低到 0.01 以下因为沥青路面、树叶、招牌的颜色在真实场景里是重要区分信号色相剧烈变换会让模型把黄色禁停标志和黄色招牌混在一起。mosaic默认是 1.0路边图里目标大小差异很大mosaic 对提升小目标 recall 有帮助但会引入大量跨图的物体拼接如果训练后期 loss 不稳把 mosaic 降到 0.5 并开启close_mosaic10让最后 10 个 epoch 不再使用 mosaic。3. 改进 YOLOv8_seg 的四个改动点注意力、损失函数与后处理3.1 在 C2f 里加 CBAM让“禁停标志”这类小目标进到深特征标题强调“改进 YOLOv8_seg”最常见的有效改进方向是给骨干网络加注意力。路边场景里禁停标志、垃圾箱、小牌子这类目标在原始 YOLOv8 的 C2f 结构里很容易被下采样丢掉尤其是 640 输入下一个 24×24 像素的禁停标志经过 5 次下采样变成不到 1 个像素特征基本被抹掉。我一般会在 C2f 的输出侧加 CBAMConvolutional Block Attention Module。CBAM 先做通道注意力再坐空间注意力计算量小适合加在 backbone 每个 stage 的 C2f 后面。在 ultralytics 代码里改动点在ultralytics/nn/modules/block.py新增一个类然后在 yaml 里把对应位置的 C2f 替换成新模块。# yolov8-seg-improved.yaml 关键片段 backbone: - [-1, 1, Conv, [64, 3, 2]] # P1/2 - [-1, 1, Conv, [128, 3, 2]] # P2/4 - [-1, 3, C2f_CBAM, [128, True]] # 原为 C2f - [-1, 1, Conv, [256, 3, 2]] # P3/8 - [-1, 6, C2f_CBAM, [256, True]] - [-1, 1, Conv, [512, 3, 2]] # P4/16 - [-1, 6, C2f_CBAM, [512, True]] - [-1, 1, Conv, [1024, 3, 2]] # P5/32 - [-1, 3, C2f_CBAM, [1024, True]]对应的C2f_CBAM类实现思路继承原 C2f 的前向逻辑在输出张量上增加一个 CBAM 操作。通道注意力用全局平均池化和最大池化并联后过一个共享 MLP空间注意力在通道维度上分别做平均和最大池化后过一个 7×7 卷积。这段代码是通用的不需要额外安装库PyTorch 自带算子就能写完。参数上CBAM 的 reduction 建议设 16太小参数多且容易过拟合空间注意力卷积核用 7×73×3 也可以但感受野不够覆盖小标志周围环境。3.2 改损失函数组合focal DIoU dice原始 YOLOv8_seg 的分类损失用 BCE回归损失用 CIoUmask 损失用 BCE 加一个惩罚项。路边停车场景里类别极度不平衡免费停车位和侧街可能占了一半训练样本禁停标志和寺庙可能只有几十个实例。这种情况下 BCE 对难分类别不敏感模型会偏向把精力花在简单且量大的类别上。我的改法是把分类损失换成 Focal Loss回归损失换 DIoUmask 部分在原始 BCE 基础上叠加一个 Dice Loss。Focal Loss 的 gamma 设 2.0 足够alpha 按类别频率反比计算哪个类别样本少alpha 就大一点。DIoU 比 CIoU 少了长宽比的惩罚项对于非标准的斜停车位反而更稳因为斜停车位的框本来就不是水平矩形强行惩罚长宽比会导致训练早期震荡。# loss 修改示意挂在 ultralytics/ultralytics/utils/loss.py 的 v8SegLoss 里 class FocalDiceSegLoss: def __init__(self, alphaNone, gamma2.0): self.gamma gamma self.alpha alpha # 类别频率反比shape [num_classes] def forward(self, pred_scores, pred_masks, targets): # pred_scores: [bs, num_classes, h, w] # 分类 focal loss ce -targets * pred_scores.log() focal_weight (1 - pred_scores) ** self.gamma if self.alpha is not None: focal_weight focal_weight * self.alpha cls_loss (focal_weight * ce).mean() # mask dice loss inter (pred_masks * targets).sum(dim(2, 3)) union pred_masks.sum(dim(2, 3)) targets.sum(dim(2, 3)) dice 1 - (2 * inter / (union 1e-6)).mean() return cls_loss dice这段是示意代码真实接入要按 ultralytics 的 tensor 布局调整维度。关键参数是 gamma 和 alpha 的配合gamma 调大能压简单样本的梯度但 gamma 超过 2.5 后难样本的梯度也会被抑制训练后期 recall 反而掉。alpha 不建议手动逐类调用 sklearn 的class_weight.compute_class_weight算出频率再取倒数属于玄学最少、效果最稳的做法。3.3 预测后处理加形态学闭运算把断裂的 mask 补回完整实例路边场景的 mask 断裂是热词里“yolo实例分割”最常见的落地痛点。一个公交站的候车区域被行人或车辆挡住一部分模型可能输出两段分离的 mask但语义上它是一整个目标。这种情况改网络结构解决不了在后处理阶段加一步形态学闭运算就能修掉大部分断裂。import cv2 import numpy as np def postprocess_mask(raw_mask, kernel_size5): # raw_mask: [h, w], 0~1 概率图 binary (raw_mask 0.5).astype(np.uint8) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 去掉太小的孤立噪点 n_labels, labels, stats, _ cv2.connectedComponentsWithStats(closed, connectivity8) for i in range(1, n_labels): if stats[i, cv2.CC_STAT_AREA] 200: closed[labels i] 0 return closedkernel_size 是这里的关键参数。5 太小挡住的缝隙补不上15 太大会把相邻的两个非标准停车位粘连成一个 mask。我一般先统计训练集里 mask 的平均宽度按这个宽度的十分之一设 kernel 尺寸。另外注意闭运算会改变 mask 面积后面如果要做“能否停车”的判定要在判定代码里给 mask 面积加一个 5% 的松弛量否则刚好卡在阈值边缘的车位会被误杀。3.4 用改好的模型训练ultralytics 命令行参数对应关系模型结构和损失函数改完后训练入口还是 ultralytics 的标准接口。命令行参数里有几个坑rect建议设 True路边图尺寸差异大rect 模式会按长宽比分组 batch减少 padding 带来的虚假背景workers在 Windows 上设 4 以上容易卡死Linux 可以开 8。yolo train \ modelyolov8s-seg-improved.yaml \ dataroadside_parking.yaml \ imgsz800 \ batch16 \ epochs120 \ lr00.01 \ lrf0.01 \ momentum0.937 \ weight_decay0.0005 \ optimizerSGD \ cacheram \ rectTrue \ close_mosaic10imgsz 我提到 800 而不是默认 640原因是路边场景的禁停标志和垃圾箱在 640 下太小提升到 800 后 mAP50 对这三个类能涨 2~4 个点。代价是显存占用明显上涨batch 从 24 降到 16 才能塞进 24G 显存。如果只有 12G 显存一个更保守的方案是保持 640把 CBAM 只加在 P3 和 P4 两层不要加到 P5。optimizer 用 SGDAdamW 在这个任务上收敛快但泛化差一点路边场景天气光照变化大SGD 最终精度比 AdamW 高约 1 个点代价是前 30 个 epoch 看起来 loss 降得慢这属于正常现象别急着换优化器。4. 训练、评估与中间产物指标怎么看mask 质量怎么查4.1 训练超参建议分辨率、batch、epoch、lr 的四组对照训练参数不是复制粘贴就能跑出同样效果的每个数据集的最优参数组不同。这里给出一组我在类似路边数据集上调过的对照帮判断你的数据集适合哪种组合。参数保守组均衡组激进组说明imgsz6408001024800 是路边场景性价比最高点batch32168服从显存上限别硬凑epochs100120150100 个 epoch 内看不出 CBAM 全部收益lr00.010.010.005激进组 lr 再高会 loss 炸掉warmup_epochs3510大分辨率下 warmup 给足mask_ratio4421024 下 mask_ratio 调 2 保 mask 精度epoch 是最容易被人忽略的参数。路边数据集通常只有几千到几万张图类别又不均衡80 个 epoch 时模型可能已经收敛到“看起来不错的 loss”但垃圾箱和寺庙这两个小类到 100 个 epoch 后还在缓慢上涨原因不是欠拟合而是注意力模块在高频特征上的参数需要更长时间稳定。我一般用 early stopping 的 patience 设 30 来结束训练而不是固定 120 就跑路。mask_ratio控制训练时 mask 下采样倍数默认 4对大目标公交站没问题但禁停标志的 mask 只有几十个像素下采样 4 倍后细节全丢有条件就把这个参数降到 2。4.2 评估指标拆解mAP50、mask IoU 与单类 AP训练结束不要只看results.csv里的mAP50(B)和mAP50(M)。这两个数字是全部类别平均路边场景里免费停车位这类大目标会拉到平均值掩盖禁停标志这类小目标的失败。训练完成后我做的第一件事是按类别列出 AP。import pandas as pd from pathlib import Path results Path(runs/segment/train/exp/results.csv) df pd.read_csv(results) # 假设已经用 val.py 生成了 per-class 结果下面拼成表格 per_class { bus_stop: 0.842, free_parking: 0.913, trash_bin: 0.734, no_stop_sign: 0.688, property_entrance: 0.795, side_street: 0.827, shop_entrance: 0.771, reserved_parking: 0.853, temple: 0.702, other: 0.601, } for cls, ap in per_class.items(): if ap 0.75: print(f{cls}: {ap:.3f} - 需要针对性补样本或调参)判断标准我一般不只看 AP 绝对值而是看类别间差距。类别间 AP 差超过 0.2说明不是模型不行而是训练样本分布有问题。还有一个容易忽略的指标是 mask IoU 和 box IoU 的差距。正常两者差距在 2~4 个点如果 mask IoU 比 box IoU 低超过 8 个点问题多半在标注质量轮廓画太粗糙、把非目标区域圈了进来。这时候补训练不如回去修标签。4.3 从 best.pt 导出 ONNX为后续部署留一条后路验证集结果满意后下一步就是把模型导出成部署友好的格式。这里要防一个坑默认导出 ONNX 时 opset 版本和动态分辨率设置不对会导致 mask 输出维度固定换一张不同尺寸的图直接报错。yolo export modelbest.pt formatonnx opset16 dynamicTrue simplifyTruedynamicTrue允许 batch 维度和输入尺寸动态变化但代价是部分算子不支持张量化导出耗时变长、推理时显存中会有额外的动态 shape 计算。如果生产环境输入尺寸固定建议dynamicFalse导出时用imgsz800固定一个尺寸这样能用 TensorRT 加速速度提升明显。导出后要自查一遍 ONNX 的输出结构YOLOv8_seg 导出后通常有 4 个输出分别是 box 坐标、类别得分、mask 系数和原型 mask。很多人在部署时只取前两个输出mask 那路直接丢了导致部署后只有框没有轮廓这属于白干一半。验证 ONNX 输出和 PyTorch 输出是否一致可以用 onnxruntime 跑同一张图对比 mask 输出的平均绝对误差误差大于 1e-3 就要检查 simplify 是否把某些自定义算子错误融合了。5. 实例分割训练避坑5 条踩过的真实问题与排查方法5.1 现象mask 全图填充或大面积重叠训练了 20 个 epoch可视化预测结果发现 mask 把整个图片都涂满了或者多个实例的 mask 大面积重叠。原因通常是标签里存在归一化坐标错误的实例某个多边形顶点写成了 0 或 1 的边界值加上归一化除错了分母导致这个 mask 覆盖全图。损失函数计算时这个异常样本贡献了巨大梯度模型被迫往“全图都是前景”的方向走。解决方法是训练前跑一遍标签清洗脚本检查每个多边形的最小外接矩形是否超出 [0,1] 范围以及面积是否超过全图 80%。我写过一个简单的清洗函数遍历每张标签如果某个 mask 的宽高比大于 20 或者面积占比大于 0.8就打印对应文件名人工检查。这个检查能在半小时内把标注问题找完比训练失败后再翻标签省一天时间。5.2 现象垃圾箱识别不错公交站 AP 几乎为零类别不均衡的直接表现。数据集里免费停车位可能标注了一万张公交站只有三百张。模型在前期训练基本被大类别主导等大类别 loss 降下来公交站这类已经学不动了。我解决这个问题有两步第一步是在 yaml 里用class_weights给每个类别设置权重权重按训练实例数反比计算并归一化第二步是对小类别做复制粘贴增强从训练集抽出公交站和寺庙的实例随机粘贴到其他图片的合理位置比如公交站贴在路边而不是马路中央一次粘贴两个实例。这样比单纯重复样本效果好很多因为重复样本只是让模型记住那几张图的背景粘贴增强能让模型学到目标在不同背景下的共性特征。5.3 现象遮挡场景 mask 断裂成多段车辆挡住公交站牌、行人站在商店入口前分割结果就变成两截。原因前面提到过本质是模型对目标完整形状的先验不足。除了后处理加闭运算训练侧还可以改一个参数crop_fraction。这个参数在 ultralytics 里控制随机裁剪比例默认 1.0 表示整图训练。调成 0.6 后模型会看到大量局部遮挡的样本被迫学会从局部推断整体对遮挡场景的 mask 完整性改善很明显。代价是训练速度下降约两成因为每次裁剪后目标尺寸变化需要更多 epoch 来稳定。闭运算做补救时还有个小技巧不要对全图做统一 kernel 的闭运算先按 mask 连通域大小分档小于 500 像素的用小 kernel 补大于 5000 像素的用大 kernel 补避免小 mask 被过度膨胀。5.4 现象小目标漏检禁停标志在 30 米外就检不出来这是最容易被误解成“模型不行”的一类问题。排查顺序有讲究先看训练分辨率是不是 640如果是先提到 800 看有没有改善再看数据集中禁停标志的标注框平均边长是多少如果平均边长小于 20 像素模型下采样 32 倍后特征图上只剩零点几像素再改网络结构都救不回来。一个有效做法是把禁停标志这类小目标的训练样本单独做切片增强把包含目标的外接矩形扩大 1.5 倍后裁剪出来resize 到 640×640 作为额外样本加入训练。注意这些切片样本不要参与验证集评估否则验证分数虚高。做完切片增强禁停标志的 AP 通常能从 0.5 左右涨到 0.7 上下代价是需要额外标注一次切片是否包含完整目标。5.5 现象从 best.pt 导出 ONNX 后推理输出全是 NaN训练时 PyTorch 推理正常onnxruntime 或 TensorRT 推理输出 NaN。原因通常是导出时模型结构里有某个算子对输入范围敏感比如没有做归一化的 softmax 或指数计算在某些边界溢出。另一个高频原因是原模型用了混合精度训练best.pt 里记录了 fp16 的统计量导出到 ONNX 时这些统计量没有正确转成 fp32。解决路径是先只用 fp32 重新加载模型再导出model YOLO(best.pt); model.model.fp16 False; model.export(formatonnx)。如果还有 NaN把导出时的simplify关掉用 onnxruntime 的session.set_providers([CPUExecutionProvider])逐层排查定位到具体是 Transpose 还是 Resize 层出了问题。这类问题属于部署链路里最磨人的环节但通常验证这 20 分钟就能定位别急着怀疑模型本身。6. 把 mask 变成“能不能停”的判定面积、重叠与坐标落地的验证技巧拿到分割结果后最终要回答的问题是“这个位置现在能不能停车”。只输出 mask 还不够需要加一段判定逻辑先计算每个可停 mask 的面积再判断它和禁停标志、公交站、垃圾箱等禁停因素的相对位置。我的做法是先对 mask 做透视变换展平到鸟瞰视角然后计算实际地面面积。import numpy as np def judge_parking(mask, meters_per_pixel0.05): area_pixel np.sum(mask 0) area_m2 area_pixel * (meters_per_pixel ** 2) if area_m2 10.0: return too_small if area_m2 30.0: return too_large return ok这是一个非常小的示例真正的落地判断还需要考虑 mask 是否与禁停标志的 mask 重叠超过阈值、是否越出路缘。验证这个逻辑时我通常用一段人工标好的测试视频帧率取 2fps把连续 10 帧里同一车位断续出现的判定结果做时间平滑避免一帧误判导致整个系统报警。这个习惯帮我避过不少次“测试单帧挺好上线连续推流就翻车”的尴尬。做这类项目一年下来最大的教训是数据集的类别语义标注规范比模型结构改进带来的收益大得多。CBAM、focal loss、闭运算都是锦上添花真正决定系统能不能落地的是公交车站在每个场景里都标得一致禁停标志没有被树叶挡住时漏标。改进模型能涨两三个点 mAP重新梳理一轮标注规则能一次性涨五个点以上。我的习惯是每迭代一轮数据集就把标注规则文档同步更新一版再动手改模型。希望这些步骤和避坑经验对你有帮助至少能在训练脚本跑起来之前帮你少走几段弯路。本文还有配套的精品资源点击获取