ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

盲道与障碍物图像分割数据集实战:从数据自检到模型训练全指南

盲道与障碍物图像分割数据集实战:从数据自检到模型训练全指南 简介面向盲道与障碍物识别场景的多类别图像分割数据集适合计算机视觉初学者、智能交通研究者以及无障碍出行项目开发者用于语义分割模型的训练、验证与算法对比。数据已经划分好训练集和验证集训练集约两百三十组图像与掩码验证集约八十组标签包含背景、盲道、障碍物三类彩色原图与掩码图一一对应送入常见分割网络即可直接训练省去额外标注和预处理工作。压缩包共六百三十五个文件大小约三十六点七二兆字节主体为三百一十六张原始图像和三百一十七张掩码图像另附类别说明文档和可视化脚本脚本能随机抽取样本把原图、真实标注图、标注叠加效果并排展示并保存便于核查标签质量、生成论文或汇报配图。作者还整理了多种主流及改进分割网络参考适合在此基础上做消融实验或构建更强基线已有两百六十三人学习下载可用于教学演示、算法验证和竞赛起步。1. 盲道与障碍物图像分割数据集3500张标注图能省下两周标注时间吗在盲道与障碍物识别项目里最让人头疼的不是模型选择而是数据。要做过街辅助、无障碍导航或者智慧巡检本质上都是图像分割任务第一步就得让模型分出“哪里是盲道、哪里有障碍物”可一张带多边形标注的盲道图人工画线要3到5分钟3500张图意味着不加校验就要花掉一个多星期。这份“盲道、障碍物识别、图像分割数据集”就是冲着这个痛点来的约3500张图像和对应的分割标签多类别标注经过整理可直接送入图像分割训练流程。适合做辅助出行感知、城市公共设施检测、车载视觉避障的从业者也适合第一次接触图像分割想找个干净数据集跑通U-Net的入门者。它帮你跳过“清洗-标注-格式转换”三步脏活把精力放到训练和调参上。2. 数据集结构拆解标签格式、类别划分与训练前的验证清单拿到压缩包先别急着解压后直接跑命令行。我见过太多人把数据集扔进训练脚本结果跑一半因为掩码格式不对、类别ID不连续、图像尺寸不匹配而翻车。花10分钟做一次结构自检后面所有事都顺。2.1 目录结构与标注格式模型要读的到底是什么这类图像分割数据集的常规组织方式是images原图和masks标签两个目录图片和掩码同名不同后缀。掩码通常是PNG格式的单通道图像0表示背景1开始计每个前景类别。例如你打开掩码像素值会看到数组里只有0、1、2这样的整数而不是“红色、绿色、蓝色”的RGB颜色图。import os from PIL import Image import numpy as np img_dir images mask_dir masks for name in os.listdir(img_dir)[:5]: mask_name name.replace(.jpg, .png) if name.endswith(.jpg) else name img_path os.path.join(img_dir, name) mask_path os.path.join(mask_dir, mask_name) img Image.open(img_path).convert(RGB) mask Image.open(mask_path) img_arr np.array(img) mask_arr np.array(mask) print(f{name}: image {img_arr.shape}, mask {mask_arr.shape}, mask unique values: {np.unique(mask_arr)[:20]})这段脚本的逻辑很直白取前5张图打开对应掩码打印图像尺寸、掩码尺寸和掩码中出现的像素值。如果你的数据集里掩码命名的后缀是bmp或者直接用jpg把replace(.jpg, .png)改成对应后缀即可。需要关注的输出有两个。第一mask unique values应该是一串连续的整数且从0开始比如[0, 1, 2]如果有[0, 2, 5]这种断档说明标注时把某些类别从255或别的值开始编码了训练前要重映射。第二mask.shape如果等于(H, W, 3)即三通道说明掩码被存成了彩色图常见于某些标注工具导出的“视觉效果图”这时候需要先转成单通道索引图。这部分我在第4章避坑指南里会详细展开这里只提醒你确认格式。2.2 类别分布与样本质量多类别分割的“不均衡”从哪来多类别分割的“多类别”在盲道场景里通常指背景、盲道、障碍物三类有的版本还会把建筑物、行道树、路缘石拆得更细。拿到数据后建议先做一张类别像素占比统计表我一般会写一个小程序把所有掩码的像素值直方图累加起来。import numpy as np from glob import glob from PIL import Image hist np.zeros(10, dtypenp.int64) for mask_path in glob(masks/*.png): m np.array(Image.open(mask_path), dtypenp.uint8) values, counts np.unique(m, return_countsTrue) hist[values] counts pixel_ratio hist[:3] / hist.sum() print(背景/盲道/障碍物像素占比:, pixel_ratio)hist数组的索引就是类别IDvalues是掩码中出现过的像素值counts是对应像素数量。把每个掩码的统计累加之后除以总像素数就得到每类占比。如果你发现某类在hist里始终为0说明该类别在当前数据集中没有真实样本训练时要考虑补数据或者去掉这个类别。类别标签ID像素占比范围常见值标注特征背景065%85%路面、人行道、建筑、天空盲道15%12%条状凸起或块状砖纹细长障碍物28%20%石墩、栏杆、路障、自行车、车辆这张表的价值在于让你预见训练时的问题背景占比太高模型天然的偏向是把所有像素都预测成背景因此盲道这种瘦长结构一旦被预测错就会显著拉低召回。障碍物如果以小型物体为主比如20米外的锥桶那么它的像素占比会远低于表格中“常见值”的下限。这就是多类别分割不均衡的来源跟分类任务里的样本数不完全是同一个概念——这里看的是像素面积占比不是图像张数。统计完成之后还要抽样看样本质量。重点看三类图夜间或阴影下的盲道、被落叶或非机动车遮挡的障碍物、逆光拍摄的场景。如果一份数据里光照极端场景不足训练出来的模型在白天晴天指标很高一旦傍晚光线变化就会“打回原形”。这不算标注错误是数据覆盖度问题需要在训练时用数据增强去弥补。2.3 训练前必做的五步自检在进入训练之前按顺序做下面五步任何一步不满足都先处理再跑模型。第一步检查图片和掩码数量完全一致。用len(os.listdir(images))和len(os.listdir(masks))对比数量不一致说明有漏标或多余文件删除或补齐。这里要注意隐藏文件比如MacOS自带的.DS_Store它不影响数量但会在后续读取时报错。第二步检查掩码是否为单通道且索引连续。用前面提供的脚本遍历所有掩码收集全部出现的像素值确认最大值等于类别总数减1。如果发现掩码里存在没有出现在列表里的中间值比如只有0和2而没有1需要查阅数据集说明文档看1是否被统一归并掉了。如果数据集文档没写那就自己决定是否合并类别并把合并规则记下来训练时保持一致。第三步确认图像尺寸是否统一或者至少能被缩放。分割模型通常要求批量训练时尺寸一致你可以resize到640x640或随机裁剪但别天真认为模型能接收任意尺寸——推理时也需要同样预处理。看一下图像的宽高比范围如果有些图是1920x1080有些是720x1280直接resize到正方形会拉伸变形最好先做中心裁剪或者letterbox填充。第四步验证掩码与图像内容对齐。随机挑20张图把掩码以半透明方式覆盖到原图上肉眼确认盲道线条是否贴合图像中的盲道纹理障碍物是否框住了真实物体。这一步是“玄学”但最有效很多数据集标注框有偏移模型训练能过拟合但换一张图就露馅。具体实现可以用PIL的Image.blend或者直接存成彩色叠加图快速浏览。第五步检查是否有全0掩码没有前景。如果训练集里存在全0掩码相当于这张图只有背景会干扰损失计算。要么删除要么在数据读取逻辑中跳过。我一般会在Dataset类里加一句if mask.sum() 0: continue但训练时不建议把所有全背景图都剔除否则模型会失去对“纯路面”场景的判别能力保留少量反而能帮助抑制误报。做完五步这份数据集才算真正“可以直接训练”。再多说一句不要因为数据标题写着“已处理完”就跳过自检每个数据集在拷贝、解压、传输过程中都可能出现意外比如文件名被截断、掩码颜色空间被转换花10分钟换一次安心是值得的。3. 直接可训的分割模型选型从U-Net到YOLOv8-seg的实践路径数据集就绪之后选模型是第二步。不少新手一上来就纠结“哪个模型最强”其实在盲道和障碍物这类固定场景里稳定、可调试、参数不迷糊比刷榜更重要。下面两条路线我都在类似任务上跑过U-Net系适合快速验证和小样本场景YOLOv8-seg适合后续要接实时检测系统的情况。3.1 为什么分割任务优先选U-Net系列参数量与精度权衡U-Net的结构是编码器-解码器加跳跃连接它能在小数据集上得到不错的分割结果原因是跳跃连接把浅层空间细节直接送到解码器弥补了下采样带来的边缘丢失。盲道是细长条形结构边缘连续性很重要U-Net对这类几何特征比纯Transformer更友好。如果你的显卡显存只有8G用ResNet34作为编码器的U-Net输入512x512batch size 8训练时显存占用大约在6G左右完全跑得动。如果只做语义分割而不需要区分“这一个障碍物”和“那一个障碍物”U-Net的输出层只需要按类别数设置通道数比如3类就是3个通道然后对每个像素取softmax的argmax。训练损失函数一般用交叉熵或Dice损失的组合不涉及复杂的匹配逻辑调试起来直观。YOLOv8-seg则属于实例分割路线它把一个实例一个实例地分割出来输出每个目标的mask轮廓这对“障碍物有多个、要统计位置”的场景更直接。但它的训练需要将掩码转为多边形坐标并配合检测框数据预处理比语义分割复杂并且对密集的盲道条状区域实例分割反而不如语义分割整体预测干净。下表是我在同类任务上的对比维度U-Net语义分割YOLOv8-seg实例分割输出形式逐像素类别每个目标的边界框掩码小样本适应性好3000张即可需要更充足的多样化数据推理速度中需额外优化快适合实时边缘细节较好较粗取决于mask分辨率训练复杂度低读PNG即可需要txt坐标转换这个对比不是定论但给了一个选型框架你只关心“哪里是盲道、哪里有障碍物”U-Net足够你还想统计“每个障碍物在哪个位置、大小多少”用YOLOv8-seg。3.2 用YOLOv8-seg跑多类别分割配置与训练命令如果决定走YOLOv8-seg路线先把PNG掩码转换成YOLO用的txt文件。每一行格式是class_id x1 y1 x2 y2 ...即类别ID和归一化后的多边形顶点坐标。常见转换做法是先提取掩码中每个连通域的轮廓再用Douglas-Peucker算法简化轮廓点。下面是一个可用的转换片段基于OpenCV实现import cv2 import numpy as np def mask_to_yolo_seg(mask_path, output_txt_path, img_w, img_h): mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) classes np.unique(mask) lines [] for cls in classes: if cls 0: continue binary np.zeros_like(mask) binary[mask cls] 255 contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: epsilon 0.0001 * cv2.arcLength(cnt, True) approx cv2.approxPolyDP(cnt, epsilon, True) pts approx.reshape(-1, 2).astype(np.float32) pts[:, 0] / img_w pts[:, 1] / img_h pts pts.flatten().tolist() line f{cls - 1} .join(f{p:.6f} for p in pts) lines.append(line) with open(output_txt_path, w) as f: f.write(\n.join(lines))参数含义mask_path是单通道掩码文件img_w和img_h是原图宽高用来归一化顶点坐标。注意代码里cls - 1是因为掩码中0是背景、1是盲道而YOLO的类别ID从0开始所以盲道对应0障碍物对应1。这里使用cv2.RETR_EXTERNAL只提取最外层轮廓避免内部孔洞产生重复多边形epsilon是轮廓简化阈值太小会保留过多点太大会丢失拐角按轮廓周长的万分之一是个经验值。有了txt标签之后写一个data.yamlpath: ./dataset train: images/train val: images/val names: 0: blind_road 1: obstacle然后执行训练命令yolo segment train datadataset/obstacle.yaml modelyolov8n-seg.pt epochs150 imgsz640 batch8 device0yolov8n-seg.pt是预训练权重n表示nano版本体积最小、最快适合做基线。如果你显存允许换成s或m体积的版本能明显提高小目标召回。imgsz640是输入尺寸盲道纹理细节多imgsz建议不要低于512否则条状凸起会被压没。batch8在8G显存上大约能跑如果遇到CUDA out of memory先降batch到4而不是下调imgsz因为下调imgsz对细节损失更直接。训练完成后用yolo segment val评估要注意看每个类别的mAP50和mAP50-95尤其盲道类别的AP如果比障碍物低很多说明盲道结构或者标注边缘有问题回到第4章排查。3.3 损失函数与类别权重让“障碍物”小目标被学到U-Net路线中常见的组合是DiceLoss加上交叉熵损失权重比大概1:1。DiceLoss处理类别不均衡很有效它直接优化预测区域和真实区域的重叠度对背景像素占优的情况不像纯交叉熵那样敏感。如果你发现盲道的Dice指标只有0.5左右而背景的Dice达到0.98这就是不均衡的典型表现。这时要给盲道类别更高的权重。在PyTorch中实现加权交叉熵很容易import torch.nn as nn class_weights torch.tensor([0.5, 2.0, 1.5]) # 背景、盲道、障碍物 criterion nn.CrossEntropyLoss(weightclass_weights)class_weights的取值不是固定公式常见做法是取各类像素占比的倒数再归一化。比如背景占比0.75盲道0.1障碍物0.15那么权重可以是[1/0.75, 1/1.0, 1/0.15]的缩放版本我习惯在0.5到3之间调。权重调得太大会导致训练初期损失震荡所以我会先用均匀权重跑10个epoch看基线再叠加类别权重而不是一开始就猛拉。优化器上U-Net用AdamW配学习率1e-3比较稳每20个epoch做一次学习率衰减到原来的0.5。如果你发现loss在0.2附近反复横跳可以把学习率降到3e-4同时增大batch size。分割任务的学习率调整比分类更敏感因为每个像素都在反传梯度信号非常密集。YOLOv8-seg的话模型内置了分类损失和分割损失的多项组合一般不需要手动设类别权重。它会根据数据自动计算cls损失的正样本比例。不过你仍然可以在data.yaml里用weights:字段手动指定每个类别的损失权重当障碍物漏检严重时把这个类别设高一些。注意YOLO的类别权重与交叉熵权重不同它加在损失项上数值过大可能导致过拟合某个类一般不超过2。4. 避坑指南训练盲道分割模型常见的五个坑这章写的是我实际跑分割数据集时踩过的坑每条都是先看到现象、再找到原因、最后用解决步骤修掉的按顺序看能帮你省下至少一个星期的无效训练。4.1 坑一掩码被当RGB图读Loss一直不降现象训练刚开始Loss就停在很高的水平比如交叉熵损失0.9左右训练了20个epoch也没降下来验证集mIoU只有0.1。原因数据加载代码里用了Image.open(mask_path).convert(RGB)把本来单通道的掩码变成了三通道而模型输出通道数是类别数比如3两者维度不匹配是最直接的报错如果代码里做了np.array后只取前3个通道则不会报错但模型学到的是“R、G、B三个通道的伪颜色”和类别之间的错误映射所以训练也能跑但Loss永远不下降。解决读取掩码时用convert(L)转成灰度或者np.array(Image.open(...))后直接使用因为PNG掩码本身就只有一个通道。在PyTorch的Dataset类里写mask Image.open(mask_path) mask np.array(mask, dtypenp.int64)如果的确拿到了三通道彩色标注图需要先量化映射成单通道索引。常见做法是建立一个颜色到类别ID的字典color_to_class {(255,0,0): 1, (0,255,0): 2, (0,0,0): 0} mask_rgb np.array(Image.open(mask_path))[:, :, :3] idx np.zeros((mask_rgb.shape[0], mask_rgb.shape[1]), dtypenp.uint8) for color, cls in color_to_class.items(): idx[(mask_rgb np.array(color)).all(axis-1)] cls这段代码把每个颜色像素替换成类别ID生成单通道索引掩码。注意字典里的颜色必须和原图完全一致如果PNG导出的图有抗锯齿边缘像素会是中间色需要在标注工具里关闭抗锯齿或者做最近邻映射。4.2 坑二盲道与路面边缘混淆mIoU虚高现象预测结果里盲道边缘比真实标注宽出一圈视觉上好像没问题但计算mIoU时盲道类的交并比很高个别场景甚至超过0.8可是实际巡检时明显看到模型把普通砖缝、路缘石边缘也当成了盲道。原因标注时对盲道边缘的像素归属没有统一规则。盲道砖块和普通路面在边缘处颜色接近标注员A把凸起部分全标为盲道标注员B只标了凸起内侧导致同一场景下边缘像素的“真实值”不一致。模型为了降低loss会把边缘预测成概率更高的类别形成膨胀或收缩。解决标注规范里明确“盲道包含凸起条纹的全部表面积但不包含砖缝间隙”并在后处理中做形态学腐蚀。训练阶段可用下面的后处理脚本修正预测结果import cv2 import numpy as np def clean_mask(pred_mask, min_area50): kernel np.ones((3,3), np.uint8) pred_mask cv2.morphologyEx(pred_mask, cv2.MORPH_OPEN, kernel, iterations1) num, labels, stats, _ cv2.connectedComponentsWithStats(pred_mask, connectivity8) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] min_area: pred_mask[labels i] 0 return pred_maskMORPH_OPEN先腐蚀后膨胀能去掉盲道周围细小的噪声点同时保留条状主体。min_area50表示像素面积小于50的连通域直接清掉适用于障碍物远距离出现的场景。注意不要在训练前就把真实掩码腐蚀否则模型学的“标注本身”就不再是原始数据了这属于后处理技巧。4.3 坑三验证集划分泄露指标虚高但实测翻车现象训练完用验证集评估mIoU达到0.85心里很高兴部署到新拍摄的实景图后预测效果特别差mIoU可能只有0.4。原因划分训练集和验证集时随手用train_test_split默认写法数据集如果是按顺序拍摄的前面都是同一环境、后面是另一环境那么训练和验证重复了场景内容。模型实际上记住了场景而不是学会“分割”。解决使用分组划分按图像所属场景或拍摄时间分组。常见做法是从文件名提取场景前缀或者用GroupShuffleSplitfrom sklearn.model_selection import GroupShuffleSplit groups [fname.split(_)[0] for fname in img_files] splitter GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, val_idx next(splitter.split(img_files, groupsgroups))groups列表里每个元素代表一个场景ID比如scene01、scene02GroupShuffleSplit保证整个场景要么进训练要么进验证不会出现同一场景的相似帧同时出现在两边。这个坑是分割任务里最容易被忽视的尤其是数据量不大时指标虚高会给你一个“数据质量很好”的错觉。4.4 坑四类别不平衡模型全程输出背景现象训练过程loss在缓慢下降但打开预测图几乎所有像素都被标为背景盲道和障碍物完全没有输出只有偶尔几个碎点。原因背景像素占比超过70%模型发现“全预测背景”也能得到很低的loss因此梯度鼓励它忽视少数类。尤其在用交叉熵时背景类别对损失的贡献大模型干脆躺平。解决前面说过加权交叉熵这里再补一个操作计算掩码中各类像素占比然后动态调整损失权重。我常用的一种方式是每50个iteration重新计算一次当前batch的前景-背景比例并给前景更高的权重。也可以用Focal Loss它主动降低简单背景样本的权重让模型聚焦难分的前景像素。在U-Net训练里将CrossEntropyLoss替换为DiceLoss FocalLoss组合效果往往比单纯加权更稳。import torch.nn.functional as F def focal_loss(logits, targets, gamma2.0, alpha0.25): ce F.cross_entropy(logits, targets, reductionnone) pt torch.exp(-ce) loss alpha * (1 - pt) ** gamma * ce return loss.mean()alpha0.25是前景的正样本权重gamma2.0控制简单样本的衰减速度。如果你发现盲道细长条总是断开可以适当增大gamma到2.5让模型更关注边缘像素如果障碍物漏检严重把alpha提高到0.4。4.5 坑五数据增强时图像和掩码没同步变换现象训练时用RandomCrop或RandomFlip图像正常但掩码和原图内容错位预测时边缘对不上mIoU在验证集上波动极大。原因增强代码里对图像做了随机翻转或裁剪但掩码沿用了原来的内容没有做相同变换。常见于把图像和掩码分开加载后再分别调用不同torchvision变换的情况。解决用Albumentations库的Compose它能保证同时作用于image和mask。以下是一个适合盲道分割的增强管线import albumentations as A transform A.Compose([ A.RandomCrop(512, 512), A.HorizontalFlip(p0.5), A.RandomBrightnessContrast(p0.3), A.Rotate(limit10, p0.4, border_mode0, mask_value0), ]) augmented transform(imageimg, maskmask) img_tensor augmented[image] mask_tensor augmented[mask]注意mask_value0很重要旋转超出边界时空白区域会被填为0背景否则会填入随机值干扰训练。border_mode0表示边框填充方式0对应BORDER_CONSTANT配合mask_value使用才能保持两类之间不互相污染。还有一个细节RandomCrop(512,512)对原图和掩码都裁同一位置这是Albumentations自动完成的千万别改成自己分两次裁剪。5. 进阶验证技巧先用10%样本快速判断数据能不能训数据准备完毕、模型选型完成先别急着把3500张全扔进去。我的习惯是抽10%的样本大约350张图跑10个epoch观察三个指标训练loss曲线是否平滑下降、验证mIoU是否在5个epoch内脱离随机水平、盲道类别的Dice是否在逐步上升。这一步能在30分钟内暴露大部分数据问题比完整训练后返工效率高得多。具体操作是写一个快速训练脚本把数据加载的batch size调大比如32减少验证频次每2个epoch打印一次指标。下面是一个基于PyTorch的简化片段# 快速验证脚本用10%数据跑10个epoch from torch.utils.data import Subset import random indices list(range(len(dataset))) subset_idx random.sample(indices, int(len(dataset) * 0.1)) train_set Subset(dataset, subset_idx) train_loader DataLoader(train_set, batch_size8, shuffleTrue)这段代码用Subset随机抽10%的样本不修改原数据适合临时验证。如果你发现10个epoch后背景Dice接近0.99但盲道Dice还在0.3徘徊说明数据里盲道的像素占比极低或者标注边缘不统一。这时再回去看第2章的统计表和4.2节的边缘问题。另一个实用技巧是输出一张预测可视化对比图把原图、真实掩码、预测掩码拼成三栏用颜色区分类别。我习惯把盲道涂成黄色、障碍物涂成红色。肉眼看图比数字更直接——数字只会告诉你mIoU多少图片能告诉你错在哪里。这个习惯救过我一次有一次盲道Dice从0.41涨到0.65指标看着正常可视化图上一看模型把盲道旁边的石砖路全标成了盲道完全不能用。从那以后我每次跑完小样本验证都强制保存三栏对比图先看图再决定要不要进入完整训练。希望帮到你。本文还有配套的精品资源点击获取
返回列表