资讯动态

盲道与障碍物分割数据集实战:从训练到推理落地

发布时间:2026/10/9 20:36:32 来源:尧图企业网站定制
简介面向盲道与障碍物识别的图像分割数据集已按背景、盲道、障碍物三个类别完成像素级标注并划分好训练集与验证集可直接用于多类别分割模型训练。压缩包共635个文件主体为316张jpg原图与317张png标签图另含类别说明txt与可视化脚本py整体大小36.72MB。包内目录按images和masks分别存放原图与标注训练集约230张、验证集约80张均提供对应mask并附带0背景、1盲道、2障碍物的类别索引文件省去数据清洗与标注环节。随包的可视化脚本可随机抽取一张图片将原始图、GT标签及GT蒙版叠加效果保存到当前目录无需额外配置即可快速核验数据质量也方便对比模型预测结果。目前已有268人学习下载适合需要构建盲道检测或障碍物识别baseline的开发者、以及想要快速评估分割模型效果的初学者使用。1. 盲道与障碍物分割数据集3500张图拿到手先做什么给智能辅助出行、低速巡检车或者无障碍设计做视觉感知最难的不是模型结构而是训练数据。盲道和障碍物识别属于典型的图像分割任务公开数据里要么是城市街景大而全要么是室内简单场景真正把盲道、路缘、常见障碍物单独定义成多类别的分割数据集非常少。这套资源包含约3500张图和对应标签标注已经过对齐和清理解压之后就能直接进训练流程。我实际跑下来最大的感受是省掉了标注清洗和格式转换这两段最磨人的环节。适合做盲杖辅助、轮椅导航、低速无人车避障的开发者也适合拿来做分割模型练手的同学。下面拆解从数据集到落地预测的关键环节。2. 数据集内部结构与标注规范从目录约定到类别权重拿到压缩包先别急着开训练花十分钟把目录结构和标签规范摸清楚能省掉后面一大串排查时间。盲道分割这类多类别图像分割任务最容易翻车的地方是标签读取方式不对比如彩色标注图被当成单通道掩码读类别数瞬间多出几十个。2.1 文件组织方式与掩码读取要点解压之后比较常见的目录约定是这样dataset/ ├── images/ # 原始RGB图像jpg或png格式 │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── masks/ # 语义分割掩码单通道PNG │ ├── 001.png │ ├── 002.png │ └── ... ├── train_ids.txt # 训练集图像编号一行一个 ├── val_ids.txt # 验证集图像编号一行一个 └── class_dict.csv # 类别ID与名称对照表这套数据我拿到时已经在“可训练”状态下了意味着图片和掩码文件一一对应、命名一致不需要再做VOC或COCO这种外部标注文件到分割掩码的转换。掩码文件的关键是保持单通道读取千万不要直接用cv2.imread的默认参数去读PNG否则三通道彩色图会让类别数从4变成几十甚至上百。读取掩码的标准姿势是这样的import cv2 # IMREAD_UNCHANGED 保留原始通道数单通道掩码读出来就是灰度图 mask cv2.imread(dataset/masks/001.png, cv2.IMREAD_UNCHANGED) # 掩码里的像素值就是类别ID直接用于训练 print(mask.shape) # (H, W)没有第三维 print(mask.dtype) # uint8类别数小于255时没问题注意训练时标签必须是torch.long类型PyTorch的交叉熵损失不接受uint8直接输入。再翻一下class_dict.csv。以我这个包为例类别定义大概是这样0为背景路面1为盲道砖区域2为障碍物立式路障、石墩、共享单车等3为普通人行道或路缘。多类别分割任务里类别定义直接影响训练策略比如障碍物这类小目标占比很低后边就要做类别权重处理。2.2 类别分布统计与输入尺寸选择盲道分割一个很典型的特征是类别极不平衡背景像素可能占85%以上盲道占10%不到障碍物往往只有1%到3%。如果不做分布统计就盲目训练模型很容易把所有像素都预测成背景mIoU看着还行实际一点用没有。建议先跑一遍像素占比统计import glob import numpy as np import cv2 mask_files glob.glob(dataset/masks/*.png) class_counts {} for mf in mask_files: mask cv2.imread(mf, cv2.IMREAD_UNCHANGED) unique, counts np.unique(mask, return_countsTrue) for cls_id, cnt in zip(unique, counts): class_counts[cls_id] class_counts.get(cls_id, 0) cnt total sum(class_counts.values()) for cls_id, cnt in sorted(class_counts.items()): print(fclass {cls_id}: {cnt / total * 100:.2f}%)这段代码的作用是把所有掩码文件逐张统计像素值分布并累加起来最后输出每个类别占全数据集像素的比例。统计完你会得到一份类似上面的分布结果后续损失函数的权重直接从这里取倒数或者做归一化。这批数据的图像分辨率并不统一常见做法是先统一到一个固定输入尺寸。分割模型对输入分辨率比较敏感尤其盲道是细长条结构缩得太小边界细节就丢了但盲目用大分辨率又会把训练时长拉得很长。我一般会先试512×512输入尺寸显存占用边界保留适用情况256×256最低盲道容易断快速验证pipeline是否跑通512×512中等边界基本清晰首选默认值兼顾速度与精度768×768较高细节保留好对障碍物小目标要求高时使用从3500张图这个数据量级来看512×512是性价比最高的选择。可以把图像统一resize到512×512掩码resize必须用最近邻插值原因下一节细说。3. 训练分割模型U-Net与DeepLabV3的选型逻辑和参数配置数据和标签都在手接下来就是模型选择。这一章把选型逻辑、损失函数、训练参数一次说清楚照着配基本能跑出像样的结果。3.1 模型选型为什么先跑U-Net再上DeepLabV33500张图属于中等偏小规模数据集第一原则是避免模型过大导致过拟合。现在比较火的Transformer分割模型比如SegFormer、Mask2Former在小数据集上收敛慢且需要较长的训练策略调优不适合作为起步方案。常见做法是先跑U-Net。U-Net结构相对轻量编码器下采样四次再对称上采样对细长结构有天然优势训练速度快显存占用低一张常见专业显卡就能带动。它的缺点是对全局上下文信息的建模偏弱遇到大块遮挡或光照剧烈变化时容易漏检。如果U-Net跑完精度不够下一步再切DeepLabV3加ResNet50编码器。DeepLabV3用空洞卷积在不降低特征图分辨率的情况下扩大感受野对道路上远景障碍物识别更稳。从实际项目看这类分割数据集用DeepLabV3-ResNet50基本能到合理上限再往上换大模型边际收益就很低了。我在跑这类任务时有个习惯先用U-Net把数据pipeline和损失函数调试通再换DeepLabV3做精度迭代。这样能快速区分问题是出在数据上还是模型容量上而不是一上来就开个大模型训练两小时发现掩码读取方式错了白白浪费算力。3.2 数据加载与损失函数类别不均衡怎么治数据加载部分除了常规的图像缩放归一化掩码的resize插值方式是个关键细节import torch from torch.utils.data import Dataset import cv2 import numpy as np class SegDataset(Dataset): def __init__(self, img_paths, mask_paths, size(512, 512)): self.img_paths img_paths self.mask_paths mask_paths self.size size def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img cv2.imread(self.img_paths[idx]) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(self.mask_paths[idx], cv2.IMREAD_UNCHANGED) img cv2.resize(img, self.size, interpolationcv2.INTER_LINEAR) # 掩码必须用最近邻线性插值会生成不存在的类别ID mask cv2.resize(mask, self.size, interpolationcv2.INTER_NEAREST) img img.astype(np.float32) / 255.0 # PyTorch交叉熵要求标签为long类型 return ( torch.from_numpy(img).permute(2, 0, 1), torch.from_numpy(mask).long(), )这里最关键的是掩码resize必须用INTER_NEAREST。如果用线性插值掩码边缘会混入类似127.5这种非整数像素值再转成整数类别时就会产生不存在的类别ID轻则训练loss异常重则直接报错。损失函数方面纯CrossEntropy在类别不平衡时对背景过拟合。我一般用加权CrossEntropy加Dice Loss的组合也就是让模型同时优化像素级分类准确率和区域级重叠度import torch import torch.nn as nn import torch.nn.functional as F class WeightedDiceLoss(nn.Module): def __init__(self, class_weights): super().__init__() # class_weights 是一个tensor长度等于类别数 self.class_weights class_weights def forward(self, logits, targets): probs torch.softmax(logits, dim1) # 转one-hot编码shape: (N, C, H, W) target_onehot F.one_hot( targets, num_classeslogits.size(1) ).permute(0, 3, 1, 2).float() smooth 1e-6 intersection (probs * target_onehot).sum(dim(2, 3)) union probs.sum(dim(2, 3)) target_onehot.sum(dim(2, 3)) dice (2 * intersection smooth) / (union smooth) weighted_dice (dice * self.class_weights.to(dice.device)).mean() return 1 - weighted_dice这个Dice Loss的计算逻辑是对每个类单独算Dice系数然后按类别权重加权平均。class_weights的取值可以参考2.2节的像素占比统计比如背景权重设为0.5盲道设为1.0障碍物设为2.0到3.0让模型更关注小目标。权重归一化后加总等于类别数即可。总损失我一般写成ce_loss dice_loss两者量级接近时不需要额外调平衡系数。某实验室的同学反馈过只加Dice不加CE训练初期会出现梯度波动大、收敛慢的情况加上CE后稳了很多。3.3 训练主循环与优化器参数一套能直接用的配置训练主循环不需要很花哨稳定比技巧重要。下面这段配置是我在类似数据集上调过多次的组合import torch import torchvision num_classes 4 model torchvision.models.segmentation.deeplabv3_resnet50( weightsDEFAULT ) # 替换分类头原头输出21类(COCO预训练) model.classifier[-1] nn.Conv2d(256, num_classes, kernel_size1) optimizer torch.optim.AdamW( model.parameters(), lr3e-4, weight_decay1e-4, ) scheduler torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr1e-3, epochs30, steps_per_epochlen(train_loader), ) # 混合精度训练显存不够时的首选方案 scaler torch.cuda.amp.GradScaler() for epoch in range(30): model.train() for images, masks in train_loader: images, masks images.cuda(), masks.cuda() optimizer.zero_grad() with torch.cuda.amp.autocast(): outputs model(images)[out] ce_loss nn.CrossEntropyLoss()(outputs, masks) dice_loss WeightedDiceLoss(class_weights)(outputs, masks) loss ce_loss dice_loss scaler.scale(loss).backward() scaler.step(optimizer) scaler.update() scheduler.step()lr3e-4是AdamW在分割任务里比较稳的起点如果换SGD可以提高到0.01到0.02但需要更长的warm-up。OneCycleLR的好处是先用小学习率热身再升到max_lr1e-3最后衰减到接近零30个epoch跑下来比固定学习率省心很多。weight_decay设到1e-4太大容易欠拟合太小起不到正则作用。掩码和模型输出都是512×512时batch size能到8左右。显存不够就把batch size减半配合梯度累积效果等同accum_steps 2 # batch_size4时等效batch_size8 loss loss / accum_steps loss.backward() if (step 1) % accum_steps 0: optimizer.step() optimizer.zero_grad()训练过程要记录每个epoch的验证集mIoU最低要求是做早停。3500张图、30个epoch、batch size为8的情况下单张专业显卡大概十几分钟一个epoch半天内能跑完完整实验迭代速度是够用的。4. 评估与推理落地mIoU、Dice和单张图预测训练完不等于结束分割模型的评估比分类模型要复杂一些既要看像素级准确率又要看区域级重叠度还要把预测结果可视化出来确认边界质量。4.1 评估指标怎么算mIoU和Dice的区别分割任务最常用的两个指标是mIoU和Dice。mIoU是预测区域与真实区域的交并比对边界误差不敏感Dice类似F1分数对小目标的波动更敏感。两者都建议记录尤其是障碍物这种小类别Dice掉了5个百分点比mIoU掉了5个百分点更值得警惕。def compute_metrics(pred, target, num_classes): pred: 模型argmax后的结果 (H, W) target: 真实掩码 (H, W) 返回每个类别的IoU和Dice以及mIoU ious [] dices [] for cls_id in range(num_classes): pred_mask (pred cls_id) target_mask (target cls_id) intersection (pred_mask target_mask).sum() union (pred_mask | target_mask).sum() if union 0: # 真实和预测都没有该类跳过或记为1.0 ious.append(float(nan)) else: ious.append(intersection / union) pred_sum pred_mask.sum() target_sum target_mask.sum() if pred_sum target_sum 0: dices.append(float(nan)) else: dices.append(2 * intersection / (pred_sum target_sum)) valid_ious [x for x in ious if not np.isnan(x)] miou np.mean(valid_ious) return ious, dices, miou评估时要把模型切到eval()模式并且在torch.no_grad()下跑不然BN层的均值和方差会被当前batch污染验证集指标忽高忽低。注意每个类别单独算IoU后再取平均得到mIoU而不是把所有像素混在一起算。盲道这种细长结构像素误差对IoU影响很大一条盲道宽约30像素如果预测结果偏移5像素IoU可能直接掉到0.6以下。所以单独看mIoU还不够要把每个类别的IoU单独打印出来重点盯障碍物类别的变化曲线。4.2 单张图推理与可视化输出验证集指标只能说明整体水平真正判断模型能不能用要把预测结果画出来叠在原图上肉眼看边界是否连续、盲道有没有断裂、障碍物框得准不准。import torch import cv2 import numpy as np def predict_single(model, image_path, device, size(512, 512)): model.eval() img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) orig_h, orig_w img.shape[:2] img_resized cv2.resize(img, size, interpolationcv2.INTER_LINEAR) # 归一化并转成 NCHW 格式 tensor torch.from_numpy( img_resized.astype(np.float32) / 255.0 ).permute(2, 0, 1).unsqueeze(0).to(device) with torch.no_grad(): logits model(tensor)[out] # (1, C, 512, 512) pred torch.argmax(logits, dim1).squeeze(0).cpu().numpy() # 恢复到原始图像尺寸最近邻保证类别ID不被插值破坏 pred cv2.resize( pred, (orig_w, orig_h), interpolationcv2.INTER_NEAREST ) return pred输出掩码之后用颜色映射生成可视化图背景保持深色盲道用黄色障碍物用红色。这样一眼能看出模型是不是把盲道识别成了普通路面或者把路缘石当成了障碍物。我一般在验证集上抽20到30张图把原图、预测掩码、叠加图横向拼在一起存成一张大图训练每5个epoch看一次。这种直观反馈比只看loss曲线有用得多很多时候loss在降但输出乱成一团就是有隐藏bug。4.3 验证集划分不要随意随机打乱训练集和验证集的划分方法直接影响你对模型真实水平的判断。这个数据集提供了train_ids.txt和val_ids.txt两个文件我建议先看一眼划分逻辑。如果这批图来自连续拍摄的视频帧随机划分会让同一段画面的相邻帧同时出现在训练集和验证集造成数据泄漏验证集指标虚高。判断方法很简单抽查十几张验证集图片看看有没有和训练集图像高度相似的同场景画面。如果有说明划分时按序列切分比较稳妥。常见做法是把同一来源或同一天拍摄的序列整体划分类似按摄像头ID分组训练和验证各自覆盖独立场景。这一步不要偷懒否则后期实拍泛化测试会让你措手不及。5. 常见问题与避坑训练盲道分割数据集踩过的几个坑这部分写几段实操中经常翻车的情况每一条都是先描述现象再讲原因最后给解决方式。如果你训练过程遇到类似问题直接对照排查。5.1 掩码边缘出现一圈噪音像素现象训练时loss一直在降但验证集预测结果里盲道边缘总有雪花一样的杂点放大看是一圈不存在的像素值。原因数据加载阶段掩码resize用了INTER_LINEAR或者默认的INTER_CUBIC插值产生小数后强制转long边缘类别被混成了其他类。解决把掩码的resize插值改成cv2.INTER_NEAREST同时确认数据增强里的旋转、翻转操作也要对掩码使用相同的最近邻策略。代码里搜索所有cv2.resize操作统一替换。5.2 验证集mIoU高达0.9实拍泛化一塌糊涂现象训练时验证集mIoU曲线非常漂亮接近0.9但把模型放到一个没用过的场景里测试盲道几乎全漏障碍物完全不识别。原因训练集和验证集划分时随机打乱了连续视频帧同一条路同一角度前后的画面同时出现在两边模型实际上记忆了场景背景没有学到盲道和障碍物的共性特征。解决看val_ids.txt和train_ids.txt里是否有同序列编号重新按拍摄序列划分比如连续编号每40帧取后8帧作为验证集训练集只用前32帧。划分后重新训练验证集指标会有所下降但这个数才反映真实水平。5.3 障碍物类别的IoU始终为0现象损失函数正常下降盲道类别IoU也到了0.7但障碍物类别的IoU一直是0预测结果里障碍物区域全是背景。原因类别极不平衡障碍物可能只占全数据集1%的像素模型把所有像素预测成背景就能把整体loss压得很低。解决在损失函数里给障碍物类加上权重上面写的WeightedDiceLoss里把障碍物权重设为背景的5到10倍。另一个有效做法是在数据加载时做裁剪增强随机裁剪512×512区域时保证障碍物区域有更高概率被包含进去。实现上可以先用模板匹配或简单阈值定位障碍物掩码的包围盒然后以包围盒为中心加偏移裁剪。5.4 训练时CUDA显存溢出现象batch size设成8跑两个step就报OOM。原因512×512输入配上DeepLabV3-ResNet50占用显存大约是256×256输入的4倍显卡显存不够。解决优先加混合精度训练只开自动混合精度通常能省30%显存。还不够就把batch size减半到4用梯度累积等效到8。再不够就换U-Net或者把输入尺寸降到384×384。最后一招是开梯度检查点PyTorch里可以用torch.utils.checkpoint包住编码器中间层用更多计算换更少显存但训练速度会明显变慢。5.5 标签里混入255像素导致loss爆炸现象训练过程中loss突然变成NaN或者交叉熵损失直接报错说目标类别超出范围。原因有些标注工具会把标注区域外的部分标成255表示“忽略区域”但没经过清理就进入了训练数据。处理时没有把255转换掉。解决在__getitem__里加一行mask[mask 255] 0把忽略区域归到背景类。也可以在全数据预处理阶段统计一下掩码的唯一值把不在类别字典里的像素值全部找出来单独处理。这个坑藏得很深因为大部分图正常只有少数几张图带255平时跑不出来一到某几个epoch数据顺序轮转时就突然炸。6. 进阶技巧把分割掩码变成可用的结构化输出模型训练收敛之后输出的是一张像素级分类图但实际避障系统需要的是“哪里有障碍物、面积多大、盲道在哪里、通向什么方向”。直接把掩码丢给决策模块使用会有两个问题一是掩码里有大量小噪点单个像素预测错就会产生一个假的障碍物二是盲道可能出现细小的断裂影响后续方向判断。所以跑推理之后我习惯再加一层后处理把mask变成更稳定的结构化信息。第一步做连通域过滤。对预测的障碍物类别用连通域分析把像素数少于阈值的散点全部去掉这是最简单的噪声抑制方式def filter_small_objects(mask, class_id, min_area50): 过滤掉mask中面积小于min_area的连通域 mask是单通道预测结果class_id为需要过滤的类别 class_mask (mask class_id).astype(np.uint8) num_labels, labels, stats, centroids cv2.connectedComponentsWithStats( class_mask, connectivity8 ) filtered np.zeros_like(class_mask) for label_id in range(1, num_labels): area stats[label_id, cv2.CC_STAT_AREA] if area min_area: filtered[labels label_id] 1 return filtered第二步对盲道做形态学闭运算填补短裂隙。闭运算对细长结构的裂缝修复效果明显但结构元尺寸要控制好3×3不够就试5×5太大会把背景和路缘粘连在一起。参数我一般会写在配置文件里方便在不同场景下快速切换测试。第三步是从处理后的盲道掩码提取骨架或最小外接矩形输出给上层做方向判断。骨架可以用cv2.ximgproc.thinning得到单像素宽的曲线后按列统计中心点坐标拟合成角度信息。对低速辅助设备来说知道盲道主轴方向和偏航角就够了不需要密集的像素级输出。经过这三步模型输出就变成了决策模块可以直接消费的结构化数据。某开发者把同样一组训练好的权重分别做“裸mask输出”和“后处理输出”对比测试后处理的误报率大概下降了四成盲道连续性也更稳定。从那以后我每次跑盲道分割项目都会强制走一遍“评估 → 连通域过滤 → 形态学修复 → 方向提取”这个流程。即使模型本身没变化这套后处理习惯也能救回不少现场效果。这个数据集我验证下来确实能直接开训、收敛稳定建议你下载后先把5.3节里的类别权重配置好再启动训练能少走不少弯路希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑