简介遥感影像道路分割数据集面向计算机视觉、遥感与深度学习方向的开发者。约4000张已处理好的图片及对应标签可直接用于训练多类别图像分割模型适合需要快速开展道路提取实验的研究者或学习者。数据已按训练集与验证集划分训练集约2800张验证集约1200张均包含images原图目录与masks标签目录标签中0为背景、255为道路另有classes文件说明类别信息。压缩包共2000个文件以png图像和jpg原图为主另含txt类别说明与py可视化脚本整体147.76MB结构清晰便于调用。配套脚本可随机抽取一张图将原始图片、GT图像及叠加蒙版效果保存到当前目录帮助直观核验数据质量。已有59人学习下载适合作为U-Net、SwinUNet等分割网络训练与改进的基准数据。1. 遥感影像道路分割数据集约4000张图和标签拿来就能训遥感影像道路分割是一项被低估的数据工程活。影像下载渠道很多真正花时间的不是网络结构而是把原始影像切成切片、筛掉云和阴影、再做 mask 标注——这套流程手工走一遍两三天就没了。这份遥感影像道路分割数据集已经把这一步省掉了约 4000 张图像和对应标签训练集 2800 张左右、验证集 1200 张左右PNG 格式已处理完毕拿来就能直接进 Unet 或类 Unet 结构的训练流程。标签只有两类语义0 是背景、255 是道路二值标注看起来简单但真正训练时坑都在预处理、loss 和验证指标上。适合刚入门语义分割、想跑通完整流程的人也适合手里有遥感项目但暂时没精力自建标注集的团队。2. 目录结构与标签语义0 和 255 之外开训前摸清底细数据集拿到手第一步不是找模型而是先把目录结构和标签含义彻底弄明白。很多人在这一步省了几分钟后面在 DataLoader 里翻车一晚上。2.1 目录划分train 2800 张、val 1200 张images 和 masks 靠同名配对整个数据集按训练集和验证集划分好目录结构是标准的图像分割组织方式RemoteRoadSeg/ ├── train/ │ ├── images/ # 约 2800 张jpg/png 原图 │ └── masks/ # 与 images 同名的 maskPNG 格式单通道 ├── val/ │ ├── images/ # 约 1200 张 │ └── masks/ └── classes.txt # 0 background, 255 roadimages 和 masks 靠文件名一一对应同名的两张图就是一对训练样本。mask 是单通道 PNG不是三通道彩色图这一点决定了后面读图的方式——用cv2.imread(path, cv2.IMREAD_UNCHANGED)而不是默认的彩色读法否则 mask 会被展开成三通道类别信息直接乱掉。val 放到 1200 张对分割任务来说是偏宽裕的划分。遥感影像场景差异大不同地区、不同光照下的道路视觉特征差别明显验证集多留一些样本反而能更真实地反映模型的泛化能力不至于出现训练集 acc 高、验证集抖动剧烈的情况。2.2 标签语义为什么标注是 255 而不是 1以及 255→1 的映射时机classes.txt 里只有两行对应两类像素值。这里有一个很多初学者会忽略的点为什么标注工具输出的是 255 而不是 1像素值语义训练时目标值0background0255road1原因很简单多数标注工具labelme 转 png、Photoshop 导出等在 8bit 位深下用 255 表示前景区域保存后视觉上显示为纯白色方便人工检查和修正。255 这个值是为可视化设计的不是给网络设计的。大多数分割框架期望 target 是从 0 开始的连续整数索引直接把 255 喂给 CrossEntropyLoss 会把它当成第 255 个类别直接越界报错或训练出诡异结果。所以在构建 Dataset 时必须把 mask 重映射到 0/1。常见做法是import cv2 import numpy as np mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED).astype(np.float32) mask mask / 255.0 # 0 - 0, 255 - 1这段代码只对二值场景有效。mask / 255.0隐含的假设是像素值只有 0 和 255 两种除法把一个区间映射到 [0, 1]。这没问题。但如果以后想在这份数据上扩展多类别比如加建筑、加水体把 255 之外的数值加进来这个除法就彻底失效了。届时要改成查表映射np.where(mask 255, 1, 0)这个点在后面第 5 章还会展开。2.3 开训前最后一道检查像素分布统计与文件名对齐拿到数据集直接写模型是高风险动作。我一般会先跑两段脚本把数据本身的健康度确认一遍再碰网络结构。第一段是像素分布统计看道路像素在整个数据集里到底占多少比例。这个数字直接决定 loss 函数的选择——如果道路占比只有百分之几那默认的 BCE loss 会训出全背景预测。import cv2 import numpy as np import glob import os mask_dir train/masks stats {} for mask_path in glob.glob(os.path.join(mask_dir, *.png)): mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) vals, counts np.unique(mask, return_countsTrue) for v, c in zip(vals, counts): stats[int(v)] stats.get(int(v), 0) int(c) total sum(stats.values()) for v in sorted(stats): print(fvalue {v}: {stats[v]} px, {stats[v] / total * 100:.2f}%)cv2.IMREAD_UNCHANGED强制按原始位深读入这是在检查这一类数据时最容易踩的第一个坑。统计结果如果是 0 占 95%、255 占 5%说明类别不平衡严重后面 loss 要用 dice 那一套如果 255 占比超过 30%说明标注偏粗需要先看可视化。第二段是文件名对齐检查。目录里图片可能是 .jpgmask 可能是 .png后缀不同但 basename 相同。如果 Dataset 里直接os.path.join(mask_dir, name)拼路径遇到后缀不一致就会 FileNotFoundError而且只在训练到一半才暴露。import os for split in [train, val]: images set(os.path.splitext(f)[0] for f in os.listdir(f{split}/images)) masks set(os.path.splitext(f)[0] for f in os.listdir(f{split}/masks)) missing images - masks orphan masks - images print(f{split}: missing mask {len(missing)}, orphan mask {len(orphan)})用os.path.splitext取纯文件名做集合运算一眼就能看出哪些图没有对应 mask。这一步跑完再进 DataLoader 就不会有“丑话说在后头”的尴尬。如果 missing 数不为 0大概率是某次预处理脚本漏文件了这种问题拖着不处理后面每个 epoch 都会随机报错一次。3. 自带可视化脚本随机抽图看清原图、GT、蒙板动手前先抽查一轮数据集里带了一个图像分割可视化脚本逻辑简单但非常实用。它的核心功能是随机抽一张图片把原始图像、GT 灰度图、GT 蒙在原图上的效果三个画面并排展示保存到当前目录。这一步能让你在十分钟内对整份数据的标注质量建立起直观判断。3.1 自带脚本做了什么随机拿一张三幅图一次看清脚本的内部流程不复杂随机选一个文件名读原图和同名 mask用 matplotlib 画出三个子图——左边是原始影像中间是 mask 的灰度显示右边是 mask 以半透明红色叠加在原图上的效果最后保存成图片文件。这样你不用一张张打开图片查看器翻目录一屏就能看出道路标注是否贴合影像中的真实道路走向。自带的脚本够用但它在批量检查面前还是有点单薄。我在它基础上改了一版加了命令行参数和批量抽查能力一次性随机抽 9 张图每张生成一张三子图拼版全部保存到指定输出目录。3.2 增强版可视化脚本参数化、批量抽查import argparse import random import os import cv2 import numpy as np import matplotlib matplotlib.use(Agg) # 服务器无显示环境必须用非交互后端 import matplotlib.pyplot as plt def visualize(image_path, mask_path, save_path, alpha0.4): img cv2.imread(image_path)[:, :, ::-1] # BGR 转 RGBmatplotlib 才显示正常 mask cv2.imread(mask_path, cv2.IMREAD_UNCHANGED) overlay img.copy() mask_bool mask 0 overlay[mask_bool] ( overlay[mask_bool] * (1 - alpha) np.array([255, 0, 0]) * alpha ).astype(np.uint8) fig, axes plt.subplots(1, 3, figsize(15, 5)) axes[0].imshow(img) axes[0].set_title(image) axes[1].imshow(mask, cmapgray) axes[1].set_title(GT) axes[2].imshow(overlay) axes[2].set_title(mask overlay) for ax in axes: ax.axis(off) plt.tight_layout() plt.savefig(save_path, dpi150, bbox_inchestight) plt.close() if __name__ __main__: ap argparse.ArgumentParser() ap.add_argument(--image_dir, defaulttrain/images) ap.add_argument(--mask_dir, defaulttrain/masks) ap.add_argument(--out_dir, defaultvisualize_out) ap.add_argument(--seed, typeint, default42) ap.add_argument(--num, typeint, default9) args ap.parse_args() names os.listdir(args.image_dir) random.seed(args.seed) random.shuffle(names) os.makedirs(args.out_dir, exist_okTrue) for i, name in enumerate(names[: args.num]): base os.path.splitext(name)[0] visualize( os.path.join(args.image_dir, name), os.path.join(args.mask_dir, base .png), os.path.join(args.out_dir, fcheck_{i}.png), ) print(fdone, {args.num} figures saved to {args.out_dir})几个参数说清楚alpha0.4是红色蒙板透明度数值越小原图越清晰建议 0.3 到 0.5 之间。mask 0的判断兼容 0/255 和 0/1 两种编码不会因为标签格式差异报错。matplotlib.use(Agg)是血泪经验——服务器上跑训练通常没有显示器不加这一行plt.savefig会报 no display 错误。--seed固定随机种子保证每次抽查的样本一致方便对比不同批次的标注质量。3.3 可视化检查时看什么四条标注质量线索生成图片后重点看四个地方。第一白色 mask 是否贴着道路边缘如果出现大面积锯齿或偏离路面中线说明标注精度有限模型学出来的边界也不会好。第二道路交叉口是否断裂很多标注在十字路口处会缺一块这会导致模型对路口区域预测不稳定。第三立交桥、高架阴影区域的 mask 是不是全黑阴影下的道路本就难辨认如果标注也没覆盖模型几乎不可能学出来。第四图像切片边缘有没有明显拼接缝大影像切小图时如果切在道路中间一张图里的道路会在边缘处被硬生生截断模型会学着“抄近道”而不是沿道路走向预测。可视化这一步做完数据集的底细基本清楚了。我拿任何新的分割数据集都会先跑一遍这个脚本再谈训练这个习惯帮我避掉很多坑——有的数据集一眼扫过去就能看到标注错位或者漏标一整片区域这些东西在训练曲线里是看不出来的。4. 直接训练避坑记从 DataLoader、Dice loss 到 mIoU 的五个坑这一章进入实操。数据本身干净但直接开训还是会踩到几个高频问题。这些问题不是网络结构不够好而是集中在数据装载、loss 设计和验证指标计算上。作者在后续文章里整理了 unet、swinUnet、transUnet 等改进网络不过在此之前先把底层的坑填平。4.1 DataLoader图像和 mask 同步 resizemask 只能用 NEAREST分割任务的 Dataset 核心是同步读取图像和 mask任何对图像做的尺寸变换、数据增强mask 都必须做完全相同的变换。最容易翻车的是 resize 插值方式。import torch import cv2 import os import numpy as np class RoadDataset(torch.utils.data.Dataset): def __init__(self, image_dir, mask_dir, size(512, 512)): self.names [f for f in os.listdir(image_dir) if f.lower().endswith((.jpg, .png))] self.image_dir image_dir self.mask_dir mask_dir self.size size def __getitem__(self, idx): name self.names[idx] base os.path.splitext(name)[0] img cv2.imread(os.path.join(self.image_dir, name)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) mask cv2.imread( os.path.join(self.mask_dir, base .png), cv2.IMREAD_UNCHANGED, ) img cv2.resize(img, self.size, interpolationcv2.INTER_LINEAR) mask cv2.resize( mask, self.size, interpolationcv2.INTER_NEAREST ) img img.astype(np.float32) / 255.0 mask mask.astype(np.float32) / 255.0 return ( torch.from_numpy(img).permute(2, 0, 1).float(), torch.from_numpy(mask).unsqueeze(0).float(), ) def __len__(self): return len(self.names)mask 的 resize 必须用cv2.INTER_NEAREST不能用双线性插值。原因很直接双线性插值会在 0 和 255 之间生成 127 这类中间值把二值 mask 变成三值甚至多值target 类别数凭空增加模型训练直接乱掉。图片本身是彩色影像像素值连续变化用默认的 LINEAR 没问题mask 是离散标签只能用最近邻。如果你熟悉 yolov8 训练自己的数据集的流程会发现那边要先把 polygon 坐标转成 txt 格式再转 mask而这份数据直接给 PNG mask省掉了最绕的一步。但反过来如果你以后要把这份数据转成 yolov8-seg 的格式就需要把 mask 边缘提取成 polygon 再保存那是另一个方向的转换这里不展开。4.2 损失函数道路像素占比个位数BCE Dice 更稳单类别道路分割的常规选择是 BCEWithLogitsLoss但遥感道路场景下正样本占比常常只有几个百分点。模型很容易收敛到“全图预测为背景”的局部最优BCE loss 看着在降实际输出全黑。这时候需要 Dice loss 兜底。def dice_loss(pred, target, smooth1.0): pred torch.sigmoid(pred) intersection (pred * target).sum(dim(2, 3)) union pred.sum(dim(2, 3)) target.sum(dim(2, 3)) dice (2 * intersection smooth) / (union smooth) return (1 - dice).mean()调用时和 BCE 加权组合bce torch.nn.BCEWithLogitsLoss() loss 0.5 * bce(pred, target) dice_loss(pred, target)Dice loss 对正负样本不平衡不敏感因为它直接优化的是预测区域和真实区域的交并比。smooth参数的取值是个玄学——有人用 1e-5有人用 1.0在极端不平衡场景下我一般固定 1.0数值稳定性更好避免 Dice 分母为 0。系数 0.5 是 BCE 和 Dice 的权重如果发现预测图边缘破碎可以加大 Dice 权重到 0.7如果发现训练初期 loss 波动大就把 BCE 权重提回去。4.3 验证指标mask 没转干净mIoU 全是幻觉训练集和验证集的 mask 都是 0/255 编码如果验证时直接拿原始 mask 和预测结果算 IoU255 会被当成第三个类别参与计算。单类别分割通常按两类分别算 IoU 再平均也就是 background 一类、road 一类。这个指标叫 mIoU但前提是 target 必须是 0/1。pred_mask (pred 0.5).float() # sigmoid 输出不是 argmax target target.float() # 已经归一化到 0/1 inter (pred_mask * target).sum().item() union pred_mask.sum().item() target.sum().item() - inter iou inter / (union 1e-6)注意 pred 要先过 sigmoid 再阈值化成二值 mask。分割单类别任务里不要对输出做 argmax——argmax 在单通道输出上永远返回同一个索引算出来的“acc”是假的。预测值大于 0.5 判为道路这是常规默认但阈值本身是超参数数据越不平衡越建议训练完看 sigmoid 输出的直方图再调0.35 有时比 0.5 靠谱得多。4.4 五条避坑记录现象、原因、解决以下五条是我在实际训练中踩过、也帮别人排查过的坑每条都按现象 → 原因 → 解决展开。坑一resize 后 mask 出现大量灰值。现象Dataset 里随手打印一个 mask 的np.unique发现除了 0 和 255 之外还有 127、64 等值训练 loss 异常。原因mask 用了和图像相同的cv2.INTER_LINEAR插值双线性算法在离散标签上生成了中间值。解决mask 的 resize 一律用cv2.INTER_NEAREST且先 resize 再转 float不要在浮点图上 resize。坑二模型输出全背景预测图全黑。现象训练 20 个 epochBCE loss 掉到 0.1 以下验证时预测图整张是黑的没有一个道路像素。原因道路像素占比不过 5%BCE 的最小值策略就是全预测为 0模型学会偷懒了。解决换成 BCE Dice 联合 lossDice 对正样本的惩罚力度足够大逼模型必须输出非零区域。坑三验证集 mIoU 数字高但可视化预测边缘跟 GT 完全对不上。现象mIoU 报到 0.85预测图放大看道路走向和 GT 差了半条街。原因验证脚本里没有把 255 归一化IoU 的计算里把 255 当成了一个真实存在的类别分母被无意义放大数字被污染了。解决验证集进入计算前统一执行mask / 255.0确认np.unique只输出 0 和 1。坑四16G 显存一上来就 OOM。现象batch_size 设 8一张 1024×1024 的原图直接进模型还没跑完第一个 step 显存就爆了。原因遥感原图尺寸比 ImageNet 大得多整图前向传播的显存占用和分辨率成正比。解决训练时随机 crop 到 512×512 或 384×384配合随机翻转做在线增强。推理时用滑窗拼接保持预测和训练的分辨率一致不要训练用小图、推理用大图效果会缩水。坑五训练到一半 FileNotFoundError文件名对不上。现象第 8 个 epoch 卡在某个 batch报某个 mask 文件不存在。原因images 目录里是 .jpgmasks 目录里是 .pngDataset 里直接用os.path.join(mask_dir, name)拼后缀后半段文件名匹配不上。解决在 2.3 节的文件名检查脚本里提前过滤Dataset 内部用os.path.splitext取纯文件名再做映射。5. 从二值道路到多类别分割合并技巧、验证习惯和后处理最后一章处理两件事如果任务从“只分割道路”变成“分割道路、建筑、水体等多类别”这份数据怎么快速改造以及训练完成后除了看指标还需要什么样的验证习惯和后处理技巧。5.1 多类别扩展查表映射与合并顺序标签只有 0 和 255不代表这份数据只能做二分类。目录结构和脚本都是按多类别预留的要做扩展核心是 mask 的重映射方式。二值场景用mask / 255.0一旦引入第二类前景这个除法就失效了。多类别时手动查表映射按类别优先级逐个覆盖merged np.zeros_like(road_mask) merged[road_mask 255] 1 # 道路 - 类别 1 merged[building_mask 255] 2 # 建筑 - 类别 2 merged[water_mask 255] 3 # 水体 - 类别 3合并顺序有讲究如果某个像素在 road_mask 和 building_mask 里同时是前景比如建筑物屋顶边缘紧贴道路后执行的merged[building_mask 255] 2会覆盖掉之前写的 1。实际项目中谁覆盖谁要根据语义优先级定道路穿过建筑阴影是常见冲突一般让道路优先。loss 也要从 BCE 切换成 CrossEntropyLoss模型输出通道数改成类别数 4含背景。5.2 验证习惯与后处理预测图要人眼过一遍连通域过滤去噪点训练完只看 mIoU 数字是会翻车的。我一般固定抽 10 张验证集图片把预测 mask 叠到原图上保存肉眼过一遍道路是否连续、路口是否断裂、阴影区域有没有误检。这一步能发现指标看不出来的系统性问题。后处理方面直接过滤掉孤立的小连通域是性价比最高的操作import cv2 import numpy as np pred (sigmoid_output 0.5).astype(np.uint8) pred cv2.morphologyEx(pred, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) n, labels, stats, _ cv2.connectedComponentsWithStats(pred) for i in range(1, n): if stats[i, cv2.CC_STAT_AREA] 200: pred[labels i] 0开运算先腐蚀再膨胀切断细小的错误连接面积过滤把小于 200 像素的孤立噪点直接清零。这个阈值对 512×512 的训练尺寸合适如果训练分辨率改成 1024孤立噪点的面积天然会变大阈值要跟着放大。这份数据集的下载入口就在原文发布页代码脚本都在压缩包里。拿到后建议先跑第 3 章的可视化脚本看一轮数据再按第 2 章做像素统计最后才动手写 DataLoader。那次把可视化步骤跳过去、直接进网络训练吃过的亏让我记住了数据集的检查顺序不能省。后来每次拿到新的分割数据我都强制走“像素统计 → 可视化抽查 → 文件名对齐”这套流程再没因为数据问题返工过。希望帮到你。本文还有配套的精品资源点击获取