遥感影像道路分割数据集:从U-Net训练到mIoU评估全流程

发布时间:2026/10/7 9:07:17
遥感影像道路分割数据集:从U-Net训练到mIoU评估全流程 简介遥感影像道路分割数据集是为图像分割与遥感地物提取场景准备的标注数据包面向高校研究生、算法工程师及竞赛选手解决道路提取任务中数据收集和标注成本高的问题。数据总量约4000张已完成清洗和尺寸统一可直接投入UNet、SwinUNet、TransUNet等分割模型训练标签含义明确0表示背景、255表示道路二值化mask简洁易用。包体共2000个文件以797张jpg原始影像和1201张png标签为主另附类别说明txt与可视化脚本py以7z格式压缩整体约147.76MB。数据已按训练集约2800张、验证集约1200张划分省去自行切分步骤可视化脚本可随机抽取样本将原始图片、GT及GT叠加蒙版效果保存为图片便于快速核对标注质量。目前已有59人学习使用适合作为道路分割、多类别图像分割项目起步或算法对比的可靠数据基准。1. 遥感影像道路分割数据集为什么数据比网络更先卡住你第一次做遥感影像语义分割的人通常会把注意力放在网络结构上U-Net 还是 DeepLabV3 哪个更合适、要不要换 Transformer 主干。但真正卡住进度的往往是数据本身。这份约 4000 张的遥感影像道路分割数据集图片和标签已经对齐、划分成训练集和测试集标签固定为两类0 是背景255 是道路。拿到手不需要再做标注、筛选和格式转换直接套进分割网络就能开始训练。适合跑遥感影像语义分割实验、做道路提取方向的毕设以及想用 U-Net/SwinU-Net 验证改进效果的从业者。数据和标签都处理完毕剩下的事情就是怎么把 0/255 的标签正确喂进模型——这一步才是很多人翻车的地方。2. 拆包目录与标签语义0/255 掩膜的格式识别与划分确认2.1 目录里到底有什么images 与 masks 的对应关系这份数据集的目录结构很朴素核心就是 images 和 masks 两个目录文件按同名前缀对应。也就是说1494.jpg对应的掩膜就是1494.png或1494.jpg具体后缀要看压缩包里的实际文件但命名必须一致。下载后先别急着写训练脚本花一分钟用命令确认目录结构find . -maxdepth 2 -type d | sort常见结果是这样的./images ./images/train ./images/val ./masks ./masks/train ./masks/val或者另一种划分方式根目录下有 train 和 val 两个目录各自内部再放 images 和 masks。无论哪种训练脚本里都要根据实际层级调整 glob 路径。我一般会再补一条命令核对数量echo train images: $(ls ./images/train | wc -l) echo train masks: $(ls ./masks/train | wc -l) echo val images: $(ls ./images/val | wc -l) echo val masks: $(ls ./masks/val | wc -l)两两数量必须完全相等。如果出现数量不一致优先怀疑压缩包解压不完整或者有人手工删过个别坏图。数量对不上就直接影响训练集和验证集的划分这类问题早发现比训练到一半再排查省事得多。2.2 标签只有 0 和 255单通道掩膜为什么更适合直接训练打开任意一张 mask你会看到这是单通道灰度图只有两个像素值0 和 255。有人拿到后习惯性把它转成三通道 RGB 再训练这完全没必要还容易引入通道维度错误。分割网络的标签本质上是每个像素的类别编号二分类场景只需要二维矩阵H×W 就够了不需要 H×W×3。第一件要做的事就是验证 mask 的像素值分布import numpy as np from PIL import Image mask np.array(Image.open(./masks/train/1494.png).convert(L)) print(np.unique(mask)) # 应该输出 [0 255] print(mask.dtype, mask.shape) # uint8, (H, W)这里我用convert(L)强制读成单通道灰度防止某些 PNG 带 alpha 通道时被读成四通道。确认np.unique输出只有 0 和 255 后就可以在训练的数据集类里做归一化。0/255 这个标注习惯在遥感分割数据集里很常见因为直接用灰度图的极值做可视化方便但直接喂进交叉熵损失时255 和 0 会被当作两个相距很远的类别编号导致模型收敛变慢。所以读取后要立刻把 255 映射成 1mask (mask / 255.0).astype(np.uint8)这样标签就变成 0 和 1 两类语义值了。2.3 训练集与验证集的分工2800/1200 的划分逻辑原始摘要里写得很清楚训练集约 2800 张验证集约 1200 张合计约 4000 张。这个比例大约七三开对分割任务来说是合理配置。七成数据用于拟合三成数据用于评估泛化性能。遥感影像的特点是一张图里道路占比往往很小背景占比很大所以验证集不能只看整体准确率后面第 6 章会专门讲怎么用 mIoU 做靠谱评估。验证集单独划出来的另一个好处是你可以用它做早停Early Stopping训练时每个 epoch 结束跑一遍验证集当验证 mIoU 不再上升时就停止训练避免过拟合。很多开源数据集只给训练集不给验证集这份数据帮你省掉了自己切分的步骤。但要注意1200 张验证集来自某种划分逻辑可能和训练集存在同场景重复取样如果后续要提高评估可信度可以自己再按影像来源做一次二次划分。2.4 数据体检表拿到数据集后建议做的三件事检查项命令/方法合格标准目录结构find . -maxdepth 2 -type dimages 与 masks 一一对应标签数值Python 读 mask 后np.unique只包含 [0, 255]数量一致性统计 images 与 masks 文件数两两严格相等图片尺寸Image.open(...).size同一批尺寸一致或记录最大/最小这些检查做完数据集本身的底子就算摸清了。接下来进入实操环节先把可视化脚本跑通看数据和标签到底对不对得上。3. 先做数据体检可视化脚本、GT 对齐检查与 10 分钟排查3.1 跑通自带的场景对比脚本数据包里附带了一个图像分割可视化脚本作用是随机抽一张图把原始影像、GT 掩膜、GT 叠加在原图上的效果并列展示并保存到当前目录。这个脚本本质上是帮你确认两件事mask 和原图是否对齐、road 区域标得是否合理。运行方式一般是这样python visualize.py脚本会在当前目录生成一张类似check_show.png的对比图。如果你拿到的压缩包里脚本文件名不叫这个先ls看一眼再运行。跑通之后如果展示出来的掩膜和道路轮廓有错位马上停下来排查不要带着错位数据往下训练。3.2 自己写一个更直观的对比脚本自带脚本能满足基本需求但有时候我想同时看多张图的统计特征所以会另写一版用 PIL 实现三图横向拼接原图、GT 灰度图、红色蒙版叠加图。import numpy as np from PIL import Image import random, glob, os # 随机抽一张训练图像 img_path random.choice(sorted(glob.glob(./images/train/*.jpg))) base os.path.splitext(os.path.basename(img_path))[0] mask_path f./masks/train/{base}.png # 如果后缀不同改成实际后缀 img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) # 检查 mask 是否只有 0/255防止读到奇怪的灰度中间值 assert set(np.unique(mask)) {0, 255}, fmask has abnormal values: {np.unique(mask)} # 红色蒙版叠加道路区域混入红色 overlay img.copy() road mask 0 overlay[road] (overlay[road] * 0.4 np.array([255, 0, 0]) * 0.6).astype(np.uint8) # 横向拼接GT 灰度图转成三通道以便显示 mask_rgb np.stack([mask] * 3, axis-1) combined np.hstack([img, mask_rgb, overlay]) Image.fromarray(combined).save(check_show.png) print(fSaved: check_show.png, image size {img.shape[1]}x{img.shape[0]})逻辑说明glob负责按文件名前缀找到同名 maskset(np.unique(mask)) {0, 255}是安全断言一旦 mask 里出现 127 这类中间值说明预处理出了问题程序直接报错而不是带着脏数据往下跑。蒙版叠加用了overlay * 0.4 红色 * 0.6目的是让 GT 区域保持半透明效果既能看到红色标记又能隐约看到底下原始影像。拼接用np.hstack要求三张图高度一致否则横向拼接会报维度不匹配。3.3 看结果时重点盯三个位置第一看道路边缘是否贴合影像里的实际道路走向。遥感影像的道路通常是连续的浅灰色带状区域如果 GT 里出现大量孤立的零散白点说明标注噪声偏大。第二看道路宽度是否和影像比例吻合。有些标注会把人行道、建筑边界也圈进去这不一定错但你要心里有数。第三看影像与 mask 是否存在整体偏移比如道路在影像里居中而 mask 里的白线整体偏左这属于标注对齐问题需要联系数据提供方或自己做平移校正。一次随机抽一张还不够建议连续跑十次每次生成一张图快速浏览确认不同场景下对齐质量都稳定。4. 接入 Unet 训练管线DataSet、同步增强与损失函数怎么改4.1 读取阶段DataSet 的写法与三个关键点把这份数据接进 U-Net 图像分割训练管线第一步是写一个 PyTorch Dataset。最常见的错误是图片与 mask 的文件名配对方式不对所以我的数据类会把 images 目录和 masks 目录的文件名都读进来取交集配对而不是硬编码后缀。import os import glob import numpy as np import torch from torch.utils.data import Dataset from PIL import Image class RoadSegDataset(Dataset): def __init__(self, image_dir, mask_dir, transformNone): self.image_paths sorted(glob.glob(os.path.join(image_dir, *.jpg))) self.mask_dir mask_dir self.transform transform # 取文件名主干做配对兼容 png/jpg 后缀不一致的情况 self.mask_paths {} for ext in [png, jpg, jpeg, tif]: for p in glob.glob(os.path.join(mask_dir, f*.{ext})): self.mask_paths[os.path.splitext(os.path.basename(p))[0]] p self.items [] for img_p in self.image_paths: base os.path.splitext(os.path.basename(img_p))[0] if base in self.mask_paths: self.items.append((img_p, self.mask_paths[base])) assert len(self.items) 0, No paired samples found. Check image_dir and mask_dir. def __len__(self): return len(self.items) def __getitem__(self, idx): img_path, mask_path self.items[idx] img np.array(Image.open(img_path).convert(RGB)) mask np.array(Image.open(mask_path).convert(L)) # 255 - 1背景 0 保持不变 mask (mask / 255.0).astype(np.uint8) if self.transform is not None: # 图像和 mask 必须走同一个 transform transformed self.transform(imageimg, maskmask) img transformed[image] mask transformed[mask] # 转成 tensormask 需要加通道维度并转 long 类型 img_t torch.from_numpy(img).permute(2, 0, 1).float() / 255.0 mask_t torch.from_numpy(mask).unsqueeze(0).long() return img_t, mask_t逻辑说明__getitem__里先读原图和 maskconvert(RGB)保证输入是三通道convert(L)保证 mask 是单通道灰度。mask / 255.0这一步把 255 归一化成 1之后 mask 的像素值只可能是 0 或 1。permute(2, 0, 1)把 H×W×C 转成 C×H×W 以满足 PyTorch 的 NCHW 布局。unsqueeze(0)给 mask 补一个通道维度用于后续计算损失。参数说明mask_t用long()不用float()因为交叉熵损失的 target 要求是整型类别索引不是概率分布。如果后续要改用 Dice Loss则 mask 要转 float 且做 one-hot 编码。4.2 增强阶段图像与掩膜必须同步变换分割任务里最常见的翻车现场是数据增强只作用于图像而忘记作用于 mask导致模型训练时看到的是“错位标签”。解决方案是使用 Albumentations它在设计上强制 image 和 mask 走同一个变换管道随机参数共享。import albumentations as A from albumentations.pytorch import ToTensorV2 train_transform A.Compose([ A.RandomCrop(256, 256), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.2), A.RandomBrightnessContrast(p0.2), ]) val_transform A.Compose([ A.Resize(256, 256), ])参数说明RandomCrop固定裁剪尺寸遥感影像尺寸通常比较大直接整图送进网络显存不够裁剪到 256×256 是常规做法。HorizontalFlip和VerticalFlip对遥感影像都是合理的因为道路方向没有固定的上下语义不像自然图像里人不能倒立。RandomBrightnessContrast是对光照条件的模拟只作用于 image 不会作用于 mask这一项不会造成标签错位。注意如果你用了RandomCrop验证集就不要加翻转和亮度扰动否则验证指标会失真。验证集只做Resize保证所有验证图片尺寸一致。4.3 训练阶段损失函数与评估指标的选型依据二分类道路分割里背景占比往往极高一张图里道路像素可能只占 10% 到 20%。普通交叉熵在这个场景下会偏向预测背景因为把全部像素预测成背景已经有 80% 以上的准确率。常见做法是给交叉熵损失加类别权重给道路类更高的权重import torch.nn as nn # 二分类背景权重 1.0道路权重 3.0 # 按数据集统计道路像素占比调整比例约 1/0.3 到 1/0.5 之间 criterion nn.CrossEntropyLoss(weighttorch.tensor([1.0, 3.0]).cuda())CrossEntropyLoss的weight参数会按类别进行加权。道路类别权重 3.0 的含义是道路像素的分类错误对损失的贡献是背景像素的 3 倍这能有效对抗背景主导问题。具体权重值建议先用脚本统计训练集里两类像素的真实比例再设成背景与道路像素比的倒数或者对数变换后的值。评估指标也要跟着换准确率在这里是虚高的。道路分割的常规指标是 mIoU 和 F1其中 mIoU 对类别不平衡更敏感。每一类的 IoU 公式是预测与真实交集除以并集如果道路类 IoU 只有 0.4就算背景 IoU 高达 0.99mIoU 也只有 0.7 左右能真实反映模型对道路的提取能力。5. 避坑加载、Resize、增强、评估四个环节的翻车记录5.1 用 cv2 读 mask 读成了三通道现象训练代码报错Expected 3 channels, got 1或者模型输出通道数和 label 对不上。原因cv2.imread默认以 BGR 三通道读入图片mask 是单通道灰度图读进来后维度变成 H×W×3和分割模型输出 H×W 的标签矩阵对不上。解决读 mask 时显式指定单通道模式cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE)。如果要用 PIL就用Image.open(mask_path).convert(L)两条路都能保证 mask 是二维矩阵。建议在 DataSet 的__getitem__里加一行assert mask.ndim 2从源头拦截错误。5.2 Resize 时用了双线性插值标签出现 127 灰色现象训练过程中 mIoU 忽高忽低可视化预测结果时发现输出边缘有模糊的灰色带验证集 mIoU 始终上不去。原因image 和 mask 一起做了Resize(256, 256)但用了双线性插值。图像用双线性没问题mask 用双线性会把 0 和 255 之间插值出 127 这类中间值标签从一个二值分布变成了灰阶分布。解决图像和 mask 分开处理。图像用cv2.INTER_LINEARmask 用cv2.INTER_NEAREST。用 Albumentations 的话Resize默认就会自动对 mask 用最近邻插值所以用这个库能规避掉大半问题。如果你手写 transform这步要特别小心。5.3 翻转增强没同步验证时标签错位现象训练 loss 下降很快可视化验证集预测时发现预测的 road 区域明显偏离真实道路位置但单独看图像和 mask 都没问题。原因自己写增强时图像和 mask 分别做了随机水平翻转用了两次独立的随机调用导致二者翻转状态不一致。解决要么用 Albumentations 这类绑定 image 和 mask 的库要么手动固定随机种子import random from PIL import Image seed random.randint(0, 10000) img img.transpose(Image.FLIP_LEFT_RIGHT) random.seed(seed) # 必须保证同一张图同一次增强 mask mask.transpose(Image.FLIP_LEFT_RIGHT)先记录一副图像翻转前用到的随机种子再借它翻转 mask才能保证同步。用 Albumentations 就没这个问题它的 Compose 会确保 image 和 mask 共用同一组随机参数。5.4 验证集只看准确率99% 准确率却什么都提不出来现象验证集 accuracy 99%但生成的预测图里道路几乎全是黑的模型把整张图预测成背景。原因道路像素占比太少即使完全不预测道路准确率也能达到 90% 以上。准确率这个指标在类别严重不平衡时失真。解决改用 mIoU、F1、Recall 评估并单独打印道路类的 IoU 和 Recall。如果道路类 Recall 低于 0.5说明模型倾向于把道路判为背景这时应该调高道路类在损失函数里的权重或者采用 Tversky Loss 直接优化召回率import torch import torch.nn.functional as F def tversky_loss(logits, target, alpha0.3, beta0.7, smooth1.0): prob torch.softmax(logits, dim1)[:, 1, :, :] pred (prob 0.5).float() intersection (pred * target).sum(dim(1, 2)) fp (pred * (1 - target)).sum(dim(1, 2)) fn ((1 - pred) * target).sum(dim(1, 2)) return (intersection smooth) / (intersection alpha * fp beta * fn smooth)alpha 控制假阳性惩罚beta 控制假阴性惩罚道路提取时把 beta 调高模型会更尊重真实道路区域。6. 用 mIoU 验证一份可直接跑的指标脚本与多模型对比思路6.1 一份轻量级 mIoU 计算脚本所有训练结束后最终需要验证的不是 loss 多低而是 mIoU 多高。下面这段脚本可以直接用在验证集上import numpy as np def compute_miou(pred_mask, true_mask, num_classes2): ious [] for cls in range(num_classes): p (pred_mask cls) t (true_mask cls) intersection np.logical_and(p, t).sum() union np.logical_or(p, t).sum() if union 0: ious.append(np.nan) # 该类在 GT 中不出现时跳过 else: ious.append(intersection / (union 1e-6)) return np.nanmean(ious), ious使用方式是先把模型预测结果二值化为 0/1GT 掩膜归一化为 0/1再调用这个函数。返回的ious列表里第二个值是道路类的 IoU这个数字才是你真正要关心的道路提取质量。把这份数据分别跑通 U-Net、DeepLabV3、SwinU-Net 之后还能得到一组基线对比数据。我通常会把三组模型的 mIoU、参数量、推理耗时记录在同一张表里一方面验证改进网络的有效性另一方面也搞清楚什么样的模型规模匹配 4000 张图像的数据量。如果拿 Mask2Former 这类大网络直接跑小数据集很容易过拟合反而不如 U-Net 来得稳。6.2 进阶方向多类别扩展与硬样本分析这份数据集擅长道路分割标签是二分类但目录结构和加载逻辑完全可以复用到多类别扩展场景。比如你想把道路、建筑、水体做成多类只需要把 mask 从 0/255 改成 0、1、2、3 的语义编号后面的 DataSet 和损失函数都不用大改。另外一个值得做的进阶操作是硬样本分析在验证集合里找出道路 IoU 最低的十张图统计它们的共同特征。如果低 IoU 集中在立交桥、匝道区域说明模型对复杂道路结构的表达能力不足这时候再把 SwinU-Net 或 TransU-Net 的改进网络换上模型对比会更有说服力。从那以后我每次拿到新分割数据集都强制先跑一遍可视化脚本再做一次类别分布统计最后才碰训练代码。这几步小习惯帮我一次次避开了标签错位和判别指标失效的暗坑也希望帮到你。本文还有配套的精品资源点击获取