遥感道路图像分割:二值数据集与U-Net训练实践
简介这份遥感道路分割数据集面向图像分割方向的学习者与算法研究者适用于训练和验证二值分割模型类别严格区分为背景与道路并配有classes文件供对照。数据划分清晰训练集约4300张含images图片与masks模板目录测试集约1800张结构一致可直接用于模型精度评估整体约6000张覆盖大分辨率下的实际遥感场景。随包另附一个Python可视化脚本可随机抽取一张影像将原始图片、GT分割图以及GT在原图上的蒙板效果并排展示方便快速核验标签质量与分割效果。资源包共2000个文件以png标签图和原图为主辅以1个txt类别说明与1个py脚本压缩包大小359.11MB已有104人学习下载适合作为道路提取、遥感地物分割等任务的训练数据或算法验证基准。1. 遥感道路图像分割数据集六千张大图拿来就能训做遥感图像分割的人都有个共同的痛开源数据集一堆但要么是航空影像带一堆无关类别要么分辨率低到道路成一坨色块。这份图像分割数据集是专门为「大分辨率遥感影像下的道路提取」准备的2值分割、约6000张图和对应标签。训练集4300张左右、测试集1800张左右类别就两个背景和道路。整个数据从目录组织到标签格式都规整附带的可视化脚本能让你在十分钟内确认GT有没有翻车而不是等训练完才发现标签错了。适合正在跑U-Net、SegFormer、Swin-UNet这类语义分割模型的同学也适合给遥感影像标注、yolov8或mmrotate训练流程做前期的数据准备。一句话这是一个不需要你花大量时间清洗就能直接进训练管线的高质量二值分割数据集。2. 数据集结构与2值分割标签先看清目录和classes再动手拿到数据集的第一步不是急着写训练脚本而是先把目录结构和标签映射搞清楚。遥感图像分割的数据集如果一开始没对齐后面每个环节都是错的。2.1 目录结构与数据文件命名images与masks如何对应这套数据的组织方式和主流遥感分割数据集一致dataset/ ├── train/ │ ├── images/ # 4300张左右遥感原图 │ └── masks/ # 与images一一对应的道路标签 ├── test/ │ ├── images/ # 1800张左右遥感原图 │ └── masks/ # 与images一一对应的道路标签 └── classes.txt # 类别定义文件images下放的原始遥感影像masks下放的是同名同前缀的标签图。比如原图叫509171.png对应标签就是509171_mask.png。我在拆这套数据时发现一个细节不带_mask后缀的图片是原始影像带后缀的是GT这个命名规则在写数据加载器时可以直接用字符串替换来配对不需要额外维护一张映射表。images和masks在数量上一一对应文件名前缀相同。我在第一次检查数据时习惯写个小脚本确认两边文件数量一致、前缀集合一致避免后面训练时出现FileNotFoundError或者更隐蔽的「图A配了标签B」问题。常见的做法是取前缀集合做差集一行代码就能验证import os img_dir train/images mask_dir train/masks img_prefix {f.rsplit(_mask, 1)[0] if f.endswith(_mask.png) else f[:-4] for f in os.listdir(img_dir)} mask_prefix {f[:-9] for f in os.listdir(mask_dir) if f.endswith(_mask.png)} print(仅在 images 中出现, img_prefix - mask_prefix) print(仅在 masks 中出现, mask_prefix - img_prefix)这段代码的意义在于如果你发现两边前缀集合不一致说明数据拷贝时丢过文件这时候应该先去补齐再训练而不是让 PyTorch 在训练中途报错。我在拆包时跑了一遍两边前缀完全一致说明这份资源的整理没有偷懒。2.2 classes文件与2值分割的标签语义classes.txt定义的是两个类别背景、道路。这是标准的2值分割语义分割任务。所谓2值分割就是每个像素只有两个归属属于道路或不属于道路。但存储方式上有两种常见写法标签索引为 0 和 10表示背景1表示道路训练时直接用CrossEntropyLoss模型输出两个通道用argmax取类别。掩码值为 0 和 2550表示背景255表示道路可视化时直接显示黑白图训练时需要先做归一化或阈值化把255变成1。我的经验是读取mask后先用np.unique看一遍像素值分布再决定怎么喂给模型。这个动作看起来多余实际上能帮你避开「标签值全为0/255、模型却在用0/1做交叉熵」这种陷阱import numpy as np from PIL import Image mask np.array(Image.open(train/masks/509171_mask.png)) print(mask 像素值, np.unique(mask)) print(mask 尺寸, mask.shape)我在实际拆包时看到的就是0/255两种取值所以处理起来很简单读入后mask (mask 0).astype(np.uint8)即可把标签统一到0/1语义。注意这里不要用mask // 255来代替(mask 0)因为如果标签图里存在个别的中间灰度值比如标注工具的羽化边缘整数除法会把它们错误归零直接造成部分道路变背景。2.3 自带可视化脚本先跑一遍确认GT没翻车这套资源带的可视化脚本是它的加分项。脚本的作用是随机抽一张图把原始图片、GT标签、GT蒙板叠加在原图上的效果这三样并排展示并保存到当前目录。python visualize.py脚本跑完后会在当前目录生成包含三张对比图的输出文件。我自己的习惯是先连续跑三次确认每次抽到的样本都是「原图有清晰道路、GT标注与道路吻合」这比只看一张图要可靠得多。如果运行时报缺依赖常见做法是补齐Pillow、matplotlib、numpy这几个基础库pip install pillow matplotlib numpy这个脚本虽然简单但它解决了遥感数据集一个很实际的问题你没法靠肉眼一整天盯几百张图片只能靠这种抽检机制来快速判断GT质量。如果抽到的样本里道路边缘标注是齐整的再往下做切图、训练就有底气了。3. 大分辨率图像的预处理管线滑窗切图与标签对齐遥感影像的典型问题就是「一张图顶十张普通照片」直接扔进网络要么被强制缩放到面目全非要么把显存撑爆。这一章讲我拆这套数据时用到的预处理方案。3.1 为什么大分辨率不能整图进网络显存限制与感受野的取舍遥感大图的宽高经常到几千像素而主流的语义分割模型输入尺寸通常不超过1024×1024。如果直接把整张图缩放到512×512两三米宽的道路可能只剩两三个像素模型学到的不是「道路」而是「一条虚线」。保持原始分辨率做滑窗切图是通行解法。滑窗切图有几个关键参数需要权衡参数推荐值说明patch尺寸512×512 或 1024×1024取决于显存和模型U-Net用512比较稳SegFormer可尝试1024重叠率10%-20%避免道路恰好被切在patch边缘导致断裂过滤阈值前景占比 5%全是背景的patch不参与训练减少无效计算切图大小直接决定后续训练batch size和模型感受野。我用512×512配合U-Net能放到比较大batch用1024×1024时显存吃紧就把batch降到4。3.2 滑窗切图代码带重叠率与样本过滤下面这份脚本是我处理这套数据时的完整切图实现同时处理原图和mask保证两者切出来的区域完全对齐import cv2 import numpy as np import os from tqdm import tqdm def sliding_crop(image, mask, patch_size512, overlap64, min_foreground0.05): 滑窗切图返回原图patch和mask patch的列表。 min_foreground: mask中道路像素占比低于该阈值的patch被丢弃。 h, w image.shape[:2] stride patch_size - overlap patches_img, patches_mask [], [] for y in range(0, h - patch_size 1, stride): for x in range(0, w - patch_size 1, stride): img_patch image[y:ypatch_size, x:xpatch_size] mask_patch mask[y:ypatch_size, x:xpatch_size] # 过滤纯背景道路占比过低的patch直接跳过 road_ratio np.mean(mask_patch 0) if road_ratio min_foreground: continue patches_img.append(img_patch) patches_mask.append(mask_patch) return patches_img, patches_mask image cv2.imread(train/images/509171.png) mask cv2.imread(train/masks/509171_mask.png, cv2.IMREAD_GRAYSCALE) img_patches, mask_patches sliding_crop(image, mask, patch_size512, overlap64) for i, (ip, mp) in enumerate(zip(img_patches, mask_patches)): cv2.imwrite(ftrain_patches/images/509171_{i}.png, ip) cv2.imwrite(ftrain_patches/masks/509171_{i}_mask.png, mp)这段代码有三个关键点需要说明。第一stride patch_size - overlap保证相邻patch有重叠区域如果道路正好跨在两个patch的边界上重叠能保证道路至少在一个patch内是完整的。第二min_foreground过滤掉全是背景的patch遥感图里背景占比通常很高不过滤的话训练样本中背景占绝对主导模型会学成「把所有像素都预测为背景」——准确率看着很高但mIoU拉胯。第三mask用IMREAD_GRAYSCALE读取这样mask_patch 0的操作得到的是二值掩码。如果原图尺寸不是patch的整数倍边缘会有遗留区域没被切到。稳妥的做法是先把图padding到能被stride整除的尺寸padding区域在mask上补0即可。我在实际处理时还会把左边缘和上边缘的patch加进来避免漏掉图像边界上的道路。3.3 切完图之后的目录组织与文件命名切图会生成大量小文件目录结构建议保持原数据风格train_patches/ ├── images/ # 所有训练patch └── masks/ # 所有对应mask patch这个结构直接对接PyTorch的Dataset类加载时只需遍历两个目录拼接文件名即可。文件命名我建议带上原始文件名前缀和patch坐标比如509171_0_0.png、509171_0_512.png这样万一某个patch有问题可以追溯到原图的哪个区域排查效率高很多。4. 从数据集到训练Pipeline加载、增强、损失与miou评估数据准备好了下一步就是把它接进训练脚本。这一章直接给出可用的PyTorch实现覆盖加载、增强、损失函数和评估指标四个环节。4.1 继承torch.utils.data.Dataset训练集与测试集两个入口这份数据集的训练集和测试集目录结构完全一致写一个带split参数的Dataset类就能同时服务两个集合import os import cv2 import numpy as np import torch from torch.utils.data import Dataset class RoadSegDataset(Dataset): 遥感道路2值分割数据集加载器 def __init__(self, data_root, splittrain, img_size512, augmentFalse): self.img_dir os.path.join(data_root, split, images) self.mask_dir os.path.join(data_root, split, masks) self.img_size img_size self.augment augment self.img_names [f for f in os.listdir(self.img_dir) if f.endswith(.png)] self.img_names.sort() def __len__(self): return len(self.img_names) def __getitem__(self, idx): name self.img_names[idx] img_path os.path.join(self.img_dir, name) mask_path os.path.join(self.mask_dir, name.replace(.png, _mask.png)) img cv2.imread(img_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread(mask_path, cv2.IMREAD_GRAYSCALE) # 统一到0/1标签语义 mask (mask 0).astype(np.float32) # 缩放到训练尺寸 img cv2.resize(img, (self.img_size, self.img_size), interpolationcv2.INTER_LINEAR) mask cv2.resize(mask, (self.img_size, self.img_size), interpolationcv2.INTER_NEAREST) # HWC - CHW归一化到[0,1] img torch.from_numpy(img.transpose(2, 0, 1)).float() / 255.0 mask torch.from_numpy(mask).unsqueeze(0) return img, mask这段代码有几个细节值得注意。mask的resize必须用INTER_NEAREST不能用线性插值否则二值标签会被插值成0.3、0.7这样的中间值后续loss计算时这些像素的梯度会非常奇怪。图像用INTER_LINEAR没问题。标签统一到0/1之后再转成float配合后面要讲的BCE loss直接使用。注意mask在resize前是0/255我通过(mask 0)把它映射到0/1这个操作在行数上就完成了。unsqueeze(0)把mask从[H, W]变成[1, H, W]让它和模型的单通道输出直接做loss计算。4.2 遥感场景的增强策略翻转旋转与亮度扰动遥感影像和自然图像不同没有「上下颠倒」的概念所以水平翻转、垂直翻转、随机旋转90度都是安全且有效的增强手段。我给这份数据集常用的增强配置如下import random def augment_sample(img, mask): # 随机水平翻转 if random.random() 0.5: img cv2.flip(img, 1) mask cv2.flip(mask, 1) # 随机垂直翻转 if random.random() 0.5: img cv2.flip(img, 0) mask cv2.flip(mask, 0) # 随机旋转90度的倍数 k random.randint(0, 3) if k 0: img np.rot90(img, k).copy() mask np.rot90(mask, k).copy() # 随机亮度扰动遥感图阴影变化模拟 if random.random() 0.5: factor random.uniform(0.8, 1.2) img np.clip(img * factor, 0, 255).astype(np.uint8) return img, mask旋转90度的倍数而不是任意角度是因为任意角度旋转会引入插值让道路边缘模糊这对分割任务的标签是有害的。亮度扰动模拟同一地区不同时相的光照差异能提升模型对阴影的鲁棒性。这套增强是在切图之后、进入网络之前做的增强后img和mask必须做完全相同的变换这个同步性是增强代码最常见的bug来源。4.3 损失函数与评估指标交叉熵、Dice与miou/pa2值分割的loss选择上我在这个场景下通常用BCEWithLogitsLoss和DiceLoss的组合。道路在整张图中占比通常只有20%-30%单独用交叉熵会让模型偏向预测背景。DiceLoss直接优化区域重叠度对正负样本不均衡相对不敏感。import torch.nn as nn class DiceLoss(nn.Module): 二值分割Dice损失输入logits和0/1标签 def __init__(self, smooth1.0): super().__init__() self.smooth smooth def forward(self, logits, targets): probs torch.sigmoid(logits) # 将单通道输出与单通道标签展平 probs probs.view(probs.size(0), -1) targets targets.view(targets.size(0), -1) intersection (probs * targets).sum(dim1) dice (2.0 * intersection self.smooth) / (probs.sum(dim1) targets.sum(dim1) self.smooth) return 1.0 - dice.mean()bce nn.BCEWithLogitsLoss()和dice DiceLoss()两者相加作为总loss权重一般取total_loss bce dice即可不需要手动调权。这两个loss一个逐像素推得稳一个从区域尺度拉高重叠度组合起来对细长道路结构的效果明显好于单独使用其中任意一个。评估指标上不能只看accuracy。如果道路占20%、背景占80%模型全预测背景也有80%的准确率但这毫无意义。必须看mIoU平均交并比和PA像素精度def compute_iou(pred_mask, gt_mask): 计算单张图的IoUpred_mask和gt_mask都是0/1二值数组 intersection np.logical_and(pred_mask, gt_mask).sum() union np.logical_or(pred_mask, gt_mask).sum() if union 0: return 1.0 # 两者都是背景视为完全一致 return intersection / union评估时用pred_mask (torch.sigmoid(logits) 0.5).cpu().numpy()把模型输出转成二值图再和GT比较。mIoU是衡量道路分割效果最核心的指标建议每个epoch结束后在测试集上完整计算一次。4.4 从可视化脚本到完整训练脚本U-Net改造接入示例如果你要用U-Net跑这套数据只需把模型输出通道数设为1最后接sigmoid即可import torch.nn as nn class UNetHead(nn.Module): U-Net最后输出改为单通道适配2值分割 def __init__(self, in_channels): super().__init__() self.conv nn.Conv2d(in_channels, 1, kernel_size3, padding1) def forward(self, x): return self.conv(x)训练循环里份几个固定步骤取一个batch的img和masklogits model(img)计算loss反向传播。每轮迭代后对logits做sigmoid再按0.5阈值转成预测二值图累计计算mIoU。推理阶段可以用滑窗预测拼接还原到原图尺寸测试集评估建议保持patch评估不要硬拼整图省去拼接边缘的人工处理。5. 避坑指南文件匹配、标签映射与大图拼接的常见翻车点这份数据集整体质量比较高但我在处理过程中还是踩到了一些典型的坑。下面按「现象 → 原因 → 解决」的格式逐一记录给后面用的人当参考。5.1 mask直接做resize导致边缘多了一圈灰色过渡带现象训练时loss正常下降但预测出来的道路边缘有一圈灰色模糊带阈值处理之后边缘表现锯齿状。原因我说过一次INTER_NEAREST但还是想强调这个坑非常隐蔽。很多人习惯把img和mask写在同一行resize代码里直接用默认的线性插值。对img来说线性插值是合理的但mask是0/255二值图线性插值会在道路边缘产生127这种中间值这些值经过阈值化后表现为边缘抖动。解决mask的resize永远用cv2.INTER_NEAREST或者不resize而是用前面章节的滑窗切图保持原始分辨率。我后来把切图流程固定为先切图再增强再归一化不再对mask做任何形式的缩放。5.2 训练集和测试集混淆导致的假高精度现象我在测试集上跑出了95%以上的mIoU换到新图上一验证立刻掉到70%。原因训练和测试的图片来自同一批场景的不同位置存在一定程度的空间相关性。这是在遥感数据集里非常常见的现象同一条道路的相邻区域可能一张在训练集、一张在测试集模型相当于开卷考试。解决评估指标只作参考真正能说明问题的是你拿一份完全独立的数据来跑推理。我一般会在本地额外标注20张左右与数据集场景无关的遥感图作为验证集用这些图来测试模型的泛化能力。这份数据集本身的分train/test是合理的但不要因测试集数字好看就放松警惕。5.3 大图推理时的显存溢出与patch割裂现象部署推理阶段直接拿一张几千像素的大图跑前向传播显存报错CUDA out of memory换用切图推理后道路在patch接缝处出现断线。原因整图推理超出了显存容量切图推理时没有做重叠区域patch边缘的道路被切断。解决推理时复用滑窗切图逻辑带10%-15%重叠率。拼接时重叠区域有两种处理方式取两侧概率的平均值或者比较两侧预测置信度取较高者。我通常采用前者能在保持道路连续性的同时避免边界生硬。后处理再配合下节要讲的形态学闭运算能进一步修复接缝处的断裂。5.4 前景占比过滤阈值设太高导致细道路样本被丢光现象切图后统计样本数量发现道路patch比预想少了很多小路上模型效果特别差。原因min_foreground设为0.110%占比时那些道路只有三五个像素宽、占整个patch不到10%的样本全被过滤掉了。遥感图里的粮食小路宽度远小于主干道10%的阈值把它们都丢掉了。解决这个数据集的场景下我把阈值调低到0.03-0.05宁可保留一些低前景占比的样本也要确保路网结构的完整覆盖。数据量够大时这些样本对模型的细道路分割能力贡献显著。6. 进阶验证多图抽样可视化与细长道路修复技巧数据集上手跑通之后有两个小技巧能明显提升你对这套数据的掌控力一是把自带的单图可视化脚本扩展成多图抽样对比二是对预测结果做形态学后处理来修复细长道路的断裂。先说多图抽样。自带的脚本每次随机抽一张但随机抽一张时容易抽到主干道、样本整体偏乐观。我一般会改成抽9张以3x3网格的方式拼成一整张对比图每行画面里包含原图、GT和预测结果一眼就能看出模型在哪类场景下翻车。这个改造只需用matplotlib的subplot功能抽出9个索引后循环绘制即可。如果9张图里有一半以上在细路上都有断线说明模型对窄道路的敏感度不够优先回头调数据增强或降低前景占比过滤阈值而不是盲目加深网络。细长道路断裂是遥感道路分割最经典的翻车场景。道路的语义特征和背景的区分度强但几何上非常细U型结构经过几次下采样后窄路很可能在feature map上只剩一个像素。我的后处理做法是两步走先用形态学闭运算把邻近的断线接上再用连通域过滤去除散落的噪声块import cv2 import numpy as np def postprocess(pred_mask_bin): 预测二值图后处理闭运算接路连通域去噪 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) closed cv2.morphologyEx(pred_mask_bin, cv2.MORPH_CLOSE, kernel) num_labels, labels, stats, _ cv2.connectedComponentsWithStats(closed, connectivity8) cleaned np.zeros_like(closed) for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if area 50: # 小于50像素的连通域视为噪声 cleaned[labels i] 255 return cleaned闭运算对道路断线很有效但它也会把两段很靠近但实际不相连的道路粘在一起。area 50的阈值不要设太高因为遥感道路即使是一段很短的路段连通域面积也可能远超这个值设太高的阈值可能把模型预测出的真实短路段误删。我通常先跑一组不同面积阈值看可视化结果再选定一个稳定的下界。另外针对细道路分割我还有一个训练侧的习惯把前景占比过滤阈值从0.05再砍低一档到0.02-0.03。这个改动会让训练集中包括很多道路极少的patch但每次epoch多看到的细小道路碎片反而帮助模型逐步积累起「抓住窄路」的能力。从那以后我每次拿到新的遥感分割数据集都会强制走一遍这套流程先跑自带可视化脚本看GT质量再写脚本检查文件匹配切图时保持原图分辨率不缩放mask训练时用Dice配合交叉熵最后用多图抽样确认细路效果。这套数据集的底子不错按上述流程一步步来基本能省掉大部分调数据的冤枉时间。希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →