ARTICLE DETAIL

资讯详情

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

盲道障碍物识别实战:3500张图像分割数据集与U-Net训练避坑指南

盲道障碍物识别实战:3500张图像分割数据集与U-Net训练避坑指南 简介这是一套面向盲道识别与障碍物检测的多类别图像分割数据集重点服务计算机视觉、智慧交通与辅助出行场景。数据准备阶段已完成标注与划分训练集约两百三十张、验证集约八十张全部采用图像目录与掩码目录组织每张原始图片都配有PNG格式的mask标签标签类别包括背景、盲道与障碍物下载后可直接送入常用分割框架训练省去清洗和转换格式的步骤。压缩包内共六百三十五个文件主体为三百余张JPG原图和对应PNG标签图另附类别说明文本以及可视化脚本文件整体体积约三十六点七二兆。其中可视化脚本可以随机取一张图片将原图、人工标注的GT图以及GT叠加原图后的效果并列展示并保存便于直观核对标注质量也适合快速生成模型效果对比图。目前已有二百六十三人查看学习整体数据量适合做分割模型的快速验证和课程设计也适合用于不同分割网络的效果对比与调优。1. 盲道、障碍物识别、图像分割数据集3500张“处理完”到底意味着什么做盲道、障碍物识别这件事最贵的从来不是模型而是标签。目标检测框住“盲道”容易可盲道被遮挡、断裂、被共享单车压住时矩形框根本表达不了“哪一段能用、哪一段断了”所以这类任务真正适合的是逐像素的图像分割。这个标题里的数据集约3500张图和标签且“已处理完可以直接训练”意味着标注、清洗、格式统一这些最耗人力的环节已经替你走完你把数据下下来跑分割训练脚本就能得到能识别盲道、行人、车辆、其他障碍物的多类别分割模型。适合谁的定位也很明确做无障碍设施巡检评估、视障辅助导航、园区机器人跟随或智慧城市路况分析的团队都会卡在“没有干净标签”这一步。3500张不算大但对分割任务来说只要类别分布合理、划分得当配合预训练编码器足够训出一个能上线的版本。这篇笔记不打算复述数据集作者的原实现而是按我拿到类似数据后的标准动作来讲先检查什么、怎么训练、参数怎么设、最容易在哪翻车。2. 拿到标签先别开训三步检查数据目录、掩码格式与类别真实分布“已处理完可以直接训练”是我最警惕的一句话。标签文件齐全不等于标签约定清楚至少有三件事必须在写训练脚本前确认否则后面所有指标都可能建立在错位的数据上。2.1 用一段脚本把数据集目录摸成“一一对应”的文件清单先看目录长什么样。常见做法是 images 和 masks 两个并列目录同名不同后缀也可能把掩码直接放在标签子目录里。命令行先探路DATAdataset find $DATA -maxdepth 2 -type f | sort | head -30再看图像和掩码是否一一对应from pathlib import Path data_dir Path(dataset) images sorted((data_dir / images).glob(*.jpg)) masks sorted((data_dir / masks).glob(*.png)) print(images:, len(images), masks:, len(masks)) assert len(images) len(masks), 图像和掩码数量不一致 stem2img {p.stem: p for p in images} stem2msk {p.stem: p for p in masks} common stem2img.keys() stem2msk.keys() print(匹配上的编号:, len(common)) missing set(stem2img) - set(stem2msk) if missing: print(缺掩码的样本:, list(missing)[:10])这段脚本的价值是把“文件层面是否干净”用数字确认掉。凡是缺掩码、多掩码、后缀乱的都要先修数据再谈训练。如果 stem 对不上常见原因是某些标注工具会给掩码加_mask后缀那就在构建 stem 映射时统一去掉后缀再比。文件名对齐这一步漏掉训练时 Dataset 里一个索引错位模型会拿 A 图的掩码去监督 B 图跑出来指标差还不明原因。2.2 掩码是单通道还是彩色标签图两种格式怎么统一多类别分割数据集的掩码常见两种存法一种是单通道灰度 PNG像素值直接等于类别 ID背景 0、盲道 1、行人 2这种最省事另一种是彩色 PNG每个类别用一种 RGB 颜色填充给人看方便但训练前必须转成单通道 ID。先确认是哪一种python - PY import cv2 import numpy as np m cv2.imread(dataset/masks/00001.png, cv2.IMREAD_UNCHANGED) print(shape:, m.shape, dtype:, m.dtype) print(unique values:, np.unique(m)) PY如果输出 shape 是(H, W)且 unique 是[0 1 2 3 4]单通道直接用。如果 shape 是(H, W, 3)按颜色映射表转import numpy as np import cv2 # 按你自己的标签说明调整这里只是常见约定 COLOR_MAP { 0: [0, 0, 0], # 背景 黑 1: [128, 0, 0], # 盲道 深红 2: [0, 128, 0], # 行人 绿 3: [0, 0, 128], # 车辆 蓝 4: [128, 128, 0], # 其他障碍 橄榄 } def color_mask_to_id(mask_bgr): mask_rgb cv2.cvtColor(mask_bgr, cv2.COLOR_BGR2RGB) out np.zeros(mask_rgb.shape[:2], dtypenp.uint8) for cls_id, rgb in COLOR_MAP.items(): out[(mask_rgb np.array(rgb)).all(axis-1)] cls_id return out要注意两个坑。第一颜色映射表里的 RGB 值必须和标注工具导出的一致有些工具存的是 BGR不转就全匹配不上得到一张全黑背景图。第二all(axis-1)要求颜色值精确匹配PNG 压缩不会改像素值但如果导图时开了抗锯齿盲道边缘会出现既不是纯红也不是纯黑的过渡色这些像素会被归到背景 0等于给背景类注入噪声。遇到这种情况先对彩色掩码做一次量化或最近邻重采样再转 ID。2.3 把标签叠到原图上看出真实覆盖质量这一步最容易跳过也最容易救命。只看 unique 值不会知道盲道标签是不是整体偏移了几个像素也不会知道障碍物类别是不是把整片树荫都标了进去。写个快速可视化脚本import cv2 import numpy as np from pathlib import Path out_dir Path(debug_overlay) out_dir.mkdir(exist_okTrue) # 直接用 2.1 里生成的列表 for i, (img_p, msk_p) in enumerate(zip(images, masks)): if i 5: break img cv2.imread(str(img_p), cv2.IMREAD_COLOR) mask cv2.imread(str(msk_p), cv2.IMREAD_GRAYSCALE) if mask is None: print(掩码不能以灰度读取:, msk_p) continue overlay img.copy() # BGR 颜色盲道黄、行人绿、车辆蓝、其他障碍橙 colors {1: (0, 255, 255), 2: (0, 255, 0), 3: (255, 0, 0), 4: (0, 128, 255)} for cid, col in colors.items(): overlay[mask cid] col blended cv2.addWeighted(img, 0.55, overlay, 0.45, 0) cv2.imwrite(str(out_dir / f{i:03d}.jpg), blended) print(保存, out_dir / f{i:03d}.jpg)叠加图能快速暴露三类问题标签整体偏移、类别混标、小目标漏标。尤其是盲道这种细长结构偏移 3 像素在单张图上不明显但训练时会让分割边界学成“两层皮”。如果叠出来发现大面积错标这个数据集就不能无脑直接训需要先做标签校正哪怕只校正明显的那部分。3. 用 U-Net 在 3500 张上训练多类别分割加载、损失、权重与完整训练循环数据检查完就到了真正动手训练的部分。这一章给的是我验证过的最小可跑方案U-Net 加预训练编码器配合正确的数据增强和类别权重。3.1 小数据集先选 U-Net 和预训练编码器盲道细长结构更适合逐像素分割U-Net 最早在医学图像分割里打出口碑它对细长、小目标、边界模糊的结构特别友好因为编码器逐步下采样提取语义解码器再逐步上采样恢复空间细节中间还有跳连接把浅层边缘信息直接传给解码器。盲道恰恰是这种结构宽度可能只有几十像素但长度跨越整张图用目标检测框不住用普通分类网络又丢失位置U-Net 的跳连接能保住“它在哪、边界在哪”这两件事。另一个原因是 3500 张对从零训练来说太小。分割网络参数量动辄几千万全靠这批数据学底层纹理不现实必须加载在 ImageNet 上预训练过的编码器权重让前几层直接复用通用边缘和纹理特征训练只负责把注意力拉到盲道和障碍物上。用现成的库能少写很多胶水代码import segmentation_models_pytorch as smp model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classes5, )encoder_name我优先选 resnet34 而不是 resnet503500 张数据下更深的编码器容易过拟合推理速度也更快。encoder_weightsimagenet是迁移学习的关键不要为了省下载时间而去掉。3.2 Dataset 加载与数据增强掩码必须最近邻缩放忽略索引设为 255分割任务的 Dataset 和分类任务最大的区别在于图像和掩码必须做完全相同的几何变换而掩码的插值方式必须固定为最近邻。很多人在这里用默认的双线性插值缩放掩码类别边界就被插出中间值比如 1 和 2 之间插出 1.4转成整数后边界乱跳盲道直接学成虚线。import torch from torch.utils.data import Dataset import albumentations as A from albumentations.pytorch import ToTensorV2 import cv2 class SegmentationDataset(Dataset): def __init__(self, img_paths, msk_paths, size(512, 512), augNone): self.img_paths img_paths self.msk_paths msk_paths self.aug aug or A.Compose([ A.RandomResizedCrop(heightsize[0], widthsize[1], scale(0.5, 1.0), p1.0), A.HorizontalFlip(p0.5), A.ColorJitter(brightness0.2, contrast0.2, saturation0.1, p0.5), A.Normalize(mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)), ToTensorV2(), ]) def __len__(self): return len(self.img_paths) def __getitem__(self, idx): img cv2.imread(str(self.img_paths[idx]), cv2.IMREAD_COLOR) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(str(self.msk_paths[idx]), cv2.IMREAD_GRAYSCALE) augmented self.aug(imageimg, maskmask) return augmented[image], augmented[mask].long()albumentations 的 mask 参数内部自动走最近邻把掩码当成 mask 传而不是当成 image 传就不会出现插值污染。RandomResizedCrop的scale(0.5, 1.0)是裁剪缩放范围太小会把盲道裁没对细长目标我一般保留这个范围同时加 HorizontalFlip不额外加旋转类增强因为盲道方向一旦旋转 30 度以上语义就变样了。Normalize用的 ImageNet 统计量因为编码器是在 ImageNet 上预训练的输入分布不一致会削弱迁移效果。这里额外提醒一点掩码不需要归一化只转 long 型。ignore_index我习惯在损失函数里设 255这个值在任何数据里都不该出现主要用于处理边缘遮挡区域也方便以后加带 ignore 的标注。3.3 训练主循环AdamW、多项式学习率、带权重的 CrossEntropy 与 mIoU 评定训练循环不要自己造轮子但每个部件的选择要说清理由。优化器用 AdamW初始学习率1e-4分割任务里学习率太大会让预训练权重被快速破坏出现前几轮 loss 猛降、后面再也升不回来的情况。学习率调度用 OneCycleLR它在小数据集上比固定步长衰减更容易收敛到底。import segmentation_models_pytorch as smp import torch import torch.nn as nn from torch.utils.data import DataLoader n_classes 5 epochs 60 batch_size 8 model smp.Unet( encoder_nameresnet34, encoder_weightsimagenet, in_channels3, classesn_classes, ).cuda() optimizer torch.optim.AdamW(model.parameters(), lr1e-4, weight_decay1e-4) total_steps len(train_loader) * epochs scheduler torch.optim.lr_scheduler.OneCycleLR( optimizer, max_lr2e-4, total_stepstotal_steps, pct_start0.1 ) criterion nn.CrossEntropyLoss(weightclass_weights, ignore_index255) for epoch in range(epochs): model.train() for step, (imgs, masks) in enumerate(train_loader): imgs, masks imgs.cuda(), masks.cuda() logits model(imgs) loss criterion(logits, masks) optimizer.zero_grad() loss.backward() optimizer.step() scheduler.step() if step % 20 0: print(fepoch {epoch} step {step} loss {loss.item():.4f}) miou validate(model, val_loader) print(fepoch {epoch} val_mIoU {miou:.4f})注意smp.Unet直接返回[B, 5, H, W]的 logits 张量不是字典这一点和 torchvision 的分割模型不同写训练循环时不用解包。验证函数我单独维护import numpy as np def validate(model, val_loader): model.eval() ious [] with torch.no_grad(): for imgs, masks in val_loader: pred model(imgs.cuda()).argmax(dim1).cpu() for c in range(n_classes): inter ((pred c) (masks c)).sum().float() union ((pred c) | (masks c)).sum().float() ious.append((inter / (union 1e-6)).item()) return float(np.mean(ious))这个 mIoU 是把每个类别单独算 IoU 再平均对类别不均衡比直接算整体像素准确率诚实得多。盲道占比可能只有 3%像素准确率会永远显示 95% 以上但 mIoU 才能暴露盲道到底学没学到。3.4 类别权重计算不让背景把盲道和障碍物“吞”掉3500 张数据里背景像素通常会占 70% 以上盲道可能只占 5%。如果直接算 CrossEntropyLoss模型学会“全部都预测背景”就能拿到很低 loss盲道和障碍物类别完全退化成零。所以要先统计全量训练集掩码的类别频率import numpy as np def compute_class_weights(mask_paths, n_classes5): counts np.zeros(n_classes, dtypenp.float64) for p in mask_paths: m cv2.imread(str(p), cv2.IMREAD_GRAYSCALE) counts np.bincount(m.ravel(), minlengthn_classes) freq counts / counts.sum() weights 1.0 / np.log(1.02 freq) weights np.clip(weights, 0.5, 10.0) return torch.tensor(weights, dtypetorch.float32)这个公式是中值频率平衡的简化版。np.log(1.02 freq)让高频类权重趋近但不会变成零背景权重会小于 1盲道和行人这类低频类权重被放大。.clip(0.5, 10.0)是经验值权重超过 10 会让模型对少数类过度敏感反而把大片背景误判成障碍物。如果是边界标注质量一般的数据集光加权重还不够。CrossEntropy 对模糊边界处的像素给出的是硬标签1 像素的标注偏移就会强迫模型学一个错误边界。我一般会在损失里加一点标签平滑等价于把硬标签扰动向软标签让边界像素的监督不那么绝对这对盲道这种细长目标边缘的稳定性帮助很明显。4. 从 U-Net 换到 DeepLabV3 或 SegFormer选型对比与训练配置不推倒重来先跑通 U-Net再考虑换模型这是最稳的路线。这一章解决的问题是当你觉得 U-Net 精度不够或者推理速度不达标想换成别的分割结构时哪些参数和代码要动、哪些可以原样保留。4.1 三个分割方案在同一数据上的取舍精度、显存、小数据集友好度先给一个经验对比按 512x512 输入、batch size 8 估消费级显卡为参照方案编码器参数量级训练显存估算小数据集友好度对盲道细长结构表现U-NetResNet34约 44M6-8 GB高边界清楚跳连接保留细节DeepLabV3ResNet50约 59M8-11 GB中高ASPP 感受野大但细线易被平滑SegFormerMiT-B0约 3.8M3-5 GB中高分层结构对小目标尚可速度不错如果你之前照 YOLOv8 训练自己的数据集那套流程走得很顺想换成 Mask2Former 这类 mask 级模型我建议在 3500 张规模上先冷静一下。Mask2Former 训练时对学习率和迭代轮数更敏感收敛慢数据集偏小时容易在 mask 分类头上过拟合调试成本明显高于传统语义分割模型。它适合的是类别极多、目标边界特别复杂的场景盲道 5 个类别用不上这么重的结构。4.2 切换模型的最小改动模型工厂与两个可插拔实现我不建议为每个模型写一套训练循环。把模型创建收口成一个工厂函数训练脚本只认model这个对象切换成本就只剩改一个字符串import segmentation_models_pytorch as smp def build_model(name, n_classes, encoderresnet34): if name unet: return smp.Unet( encoder_nameencoder, encoder_weightsimagenet, in_channels3, classesn_classes, ) if name deeplabv3plus: return smp.DeepLabV3Plus( encoder_nameresnet50, encoder_weightsimagenet, in_channels3, classesn_classes, ) if name segformer: from transformers import SegformerForSemanticSegmentation return SegformerForSemanticSegmentation.from_pretrained( nvidia/mit-b0, num_labelsn_classes, ignore_mismatched_sizesTrue, ) raise ValueError(funknown model: {name})DeepLabV3 和 U-Net 在 smp 库里的输出格式一致都是[B, C, H, W]训练循环完全不用改。SegFormer 从 HuggingFace 加载时要注意它的输出是一个SegformerOutput对象需要取.logits并且模型内部自带 resize 到输入尺寸的逻辑数据增强里的尺寸设置要保持一致。ignore_mismatched_sizesTrue一定要加预训练权重头是 150 类我们只标 5 类不忽略尺寸不匹配会直接报错。从 U-Net 换到 DeepLabV3 时我有一个实际经验学习率要调低到5e-5附近。DeepLabV3 的空洞卷积层感受野更大对预训练权重的扰动更敏感沿用1e-4会在前三轮出现 loss 震荡。SegFormer 则相反它是从零训练的 Transformer 结构初始学习率可以放到6e-5配合线性 warmup 效果更好。4.3 显存、Batch 与混合精度让 3500 张数据在消费级显卡上跑完3500 张图、60 个 epoch如果单卡显存只有 8GB深度模型很容易 OOM。优先降输入尺寸而不是降 batch从 512x512 降到 448x448显存占用大约降四分之一盲道这种大尺度结构受影响不大。batch size 保底 8因为 BatchNorm 在 batch 太小时统计量不稳分割结果会出现推理时颜色斑块。混合精度是省显存最直接的手段scaler torch.cuda.amp.GradScaler() for epoch in range(epochs): for imgs, masks in train_loader: imgs, masks imgs.cuda(), masks.cuda() with torch.autocast(device_typecuda, dtypetorch.float16): logits model(imgs) loss criterion(logits, masks) optimizer.zero_grad() scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()注意 masks 保持 long 型不进半精度只有模型前向和 loss 计算放进autocast。如果反向传播出现 NaN先检查是不是GradScaler没调用scaler.update()这是半精度训练最常见的训练环境事故。混合精度在 30 系以上显卡收益最大老卡如果出现 loss 不降直接关掉 AMP 退回 FP32这种小数据量任务慢一点也能跑完。换模型之后还有一个容易被忽略的点DeepLabV3 的输出 stride 是 16特征图分辨率比原图小上采样回原尺寸时边缘天然比 U-Net 糊一点。如果追求盲道边界像素级准确DeepLabV3 不一定比 U-Net 强别只看 mIoU 数字。5. 盲道分割训练踩过的 5 个坑现象、原因与解决办法这一章是血泪经验集中营。以下 5 个问题在盲道、障碍物这类多类别小数据集上几乎必现每条都按现象、原因、解决来写。5.1 标签缩放后盲道断成虚线插值方式错位现象训练几十轮后验证集盲道 mIoU 停在 0.5 左右上不去把预测结果放大看盲道边缘呈锯齿状细的地方直接断开。原因训练管线里用cv2.resize统一缩放图像和掩码图像用了双线性插值掩码也顺手用了同样的插值。类别 ID 是整数双线性插值会在边界插出 0.6、1.4 这种值取整后边界像素归属随机跳动盲道细线被切成虚线。解决掩码缩放一律interpolationcv2.INTER_NEAREST或者把掩码交给 albumentations 的 mask 参数处理它会自动选最近邻。全局搜索代码里所有cv2.resize(mask, ...)和 PIL 的resize确认没有漏网的默认插值。5.2 小目标障碍物类别 mIoU 一直为 0类别权重配置失效现象背景和盲道 mIoU 正常但“其他障碍物”这个类别的 mIoU 从头到尾是 0测试图里对应区域全部预测成背景。原因第一层怀疑权重没生效但更常见的是掩码里类别 ID 和训练脚本里的n_classes对不上。比如背景是 0某类实际像素值是 255统计类别频率时np.bincount的minlength数组超界compute_class_weights算出来全是 0 或 NaN权重一进 CrossEntropy 就废了。解决在数据检查阶段打印掩码的 unique 值确认类别 ID 连续且小于n_classes。同时把class_weights打印出来看一眼背景权重应小于 1稀有类权重大于 1如果全是 1 就是没加载成功。还有一类隐蔽情况是验证集里该类别样本数极少比如只有 2 张那就按场景分层划分训练集和验证集保证每个类别在验证集有足够样本。5.3 验证集 mIoU 不错但实景推理发飘训练与推理尺寸不一致现象训练时验证集 mIoU 有 0.78导出模型到视频里实测盲道定位整体向右偏远处盲道时有时无。原因训练时把图像统一Resize(512, 512)推理时直接喂原图 1920x1080。模型在全连接分类头不存在的情况下对尺寸不是完全无感的预训练编码器的下采样比例和最终上采样比例在极端尺寸下会改变有效感受野覆盖尤其盲道这种细长目标尺寸不一致会让浅层特征对齐错位。解决推理时套用训练时同一套预处理先 resize 到 512x512预测完再把 mask resize 回原图尺寸resize 掩码仍用最近邻。更稳的做法是训练时就固定一个推理友好尺寸比如(768, 1024)验证集和测试集都用它保证“训练看到什么推理就看什么”。5.4 盲道边界糊成一片但 loss 仍在降硬标签噪声问题现象训练后期 loss 缓慢下降但可视化结果显示盲道边缘与真实边界偏差 5-8 像素两侧各有一层“半透明”预测带。原因标注人员在画盲道边界时本身就有 1-3 像素误差这些噪声被硬标签 CrossEntropy 当成确定真值模型被迫在噪声边界上做尖锐决策学出来的边界就是糊的。loss 下降是因为模型把噪声边界也背下来了。解决给损失函数加标签平滑比如nn.CrossEntropyLoss(label_smoothing0.1)让边界像素的监督从 0/1 硬标签变成软化的分布或者对掩码边界做一次腐蚀把距离边界 2 像素的区域设成ignore_index255让模型只在确定区域学边界特征。后者对盲道效果更直接但要注意别把所有细盲道都腐蚀没了。5.5 数据集划分只打乱一次随机种子导致的跨实验对比失真现象同一个模型跑两次一次 mIoU 0.76一次 0.68只改了随机种子差异大到无法判断模型改进有没有用。原因3500 张数据量不大随机划分时某个类别可能整体偏向某一侧。比如按文件名排序切分前 3000 张全是晴天后 500 张全是傍晚验证集只在傍晚数据上评估指标当然大起大落。解决固定全局随机种子并用手动划分替代随机划分。先按场景、时间段或采集地点做分组再把组按比例分到训练集和验证集保证每组场景都同时出现在两边。代码里四个地方都要固定Python 的random.seed、NumPy 的np.random.seed、PyTorch 的torch.manual_seed、DataLoader 的generator。只有跨实验条件可复现后面调参才有意义。6. 把模型从“出图”变“交付”连通域后处理、逐类指标与报告页输出训练完成只是第一步真正验收时对方看的不是 mIoU 数字而是“盲道断没断、障碍物标没标全”。最后一章讲两个交付前必做的后处理动作以及怎么把指标变成能交代的产出。6.1 按类别做连通域过滤去掉孤立像素分割模型的原始输出里总会有一些零星噪点肉眼不影响判断但交付给巡检系统会被当成误报。按类别做连通域过滤是成本最低的去噪方式import cv2 import numpy as np def filter_small_regions(hard_mask, area_th50): out np.zeros_like(hard_mask) for cid in range(1, hard_mask.max() 1): mask_c (hard_mask cid).astype(np.uint8) num, labels, stats, _ cv2.connectedComponentsWithStats(mask_c, 8) keep np.zeros_like(mask_c) for i in range(1, num): if stats[i, cv2.CC_STAT_AREA] area_th: keep[labels i] 1 out[keep 1] cid return out注意类别不同阈值要分开设。盲道是细长结构本身连通域面积可能很小area_th设 50 都可能误删可以单独对盲道类设area_th20行人和车辆类目标较大area_th放到 100 没问题。这个函数只用在推理后处理不要用在训练标签上。6.2 逐类 mIoU 与混淆矩阵分清“谁吃掉了谁”总 mIoU 会掩盖单项问题。交付前至少打印一次逐类 IoUfrom sklearn.metrics import confusion_matrix # flat_gt、flat_pred 分别是验证集全部像素的类别数组 cm confusion_matrix(flat_gt, flat_pred, labelsrange(5)) iou_per_class cm.diagonal() / ( cm.sum(1) cm.sum(0) - cm.diagonal() 1e-6 ) print(per-class IoU:, iou_per_class)看混淆矩阵时要抓两件事一是背景列占比高说明目标被漏检二是盲道和行人互相吃掉说明两类外观相似或标签本身有位移需要回到数据检查那一步。6.3 输出带半透明掩码的报告页给验收方直接看最后把预测掩码叠回原图保留原图纹理输出成 PNG 报告页。这一步不需要额外技术但一定要做因为“看得见”的交付比任何指标都有说服力。推理前记得统一走一遍预处理掩码上采样回原尺寸时用最近邻别在这一步破坏边界。我自己的教训是第一次拿类似数据集交付时验证集 mIoU 做到 0.81 觉得没问题结果把视频丢给业务方对方第一眼就发现盲道在阴影处断成三段。查下来不是模型问题是推理时忘了和训练保持相同的尺寸与归一化参数。从那以后推理脚本和训练脚本共用同一个预处理函数谁都不许单独改。这类小数据集项目成败往往不在模型多先进而在这些边界细节有没有抠到底。希望这套流程能帮你在盲道和障碍物识别上少走一段弯路。本文还有配套的精品资源点击获取
返回列表