news 2026/10/7 9:07:01

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
遥感影像道路分割数据集:从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 映射成 1:

mask = (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}, f"mask 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(f"Saved: check_show.png, image size {img.shape[1]}x{img.shape[0]}")

逻辑说明:glob负责按文件名前缀找到同名 mask,set(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, transform=None): 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(image=img, mask=mask) img = transformed["image"] mask = transformed["mask"] # 转成 tensor,mask 需要加通道维度并转 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__里先读原图和 mask,convert("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(p=0.5), A.VerticalFlip(p=0.2), A.RandomBrightnessContrast(p=0.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(weight=torch.tensor([1.0, 3.0]).cuda())

CrossEntropyLoss的weight参数会按类别进行加权。道路类别权重 3.0 的含义是:道路像素的分类错误对损失的贡献是背景像素的 3 倍,这能有效对抗背景主导问题。具体权重值建议先用脚本统计训练集里两类像素的真实比例,再设成背景与道路像素比的倒数或者对数变换后的值。

评估指标也要跟着换,准确率在这里是虚高的。道路分割的常规指标是 mIoU 和 F1,其中 mIoU 对类别不平衡更敏感。每一类的 IoU 公式是预测与真实交集除以并集,如果道路类 IoU 只有 0.4,就算背景 IoU 高达 0.99,mIoU 也只有 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_LINEAR,mask 用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, alpha=0.3, beta=0.7, smooth=1.0): prob = torch.softmax(logits, dim=1)[:, 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_classes=2): 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/1,GT 掩膜归一化为 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 的改进网络换上模型对比会更有说服力。从那以后,我每次拿到新分割数据集,都强制先跑一遍可视化脚本,再做一次类别分布统计,最后才碰训练代码。这几步小习惯帮我一次次避开了标签错位和判别指标失效的暗坑,也希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 9:06:22

ESP32上ModbusTCP分片缓存实战:从丢包到稳定通信

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 9:06:01

Hyperframes:紧凑帧设计与多路复用,解决高并发小包与队头阻塞

做网络传输优化的朋友&#xff0c;应该都有过这种体验&#xff1a;服务端并发一高&#xff0c;小包满天飞&#xff0c;每个包里装的数据没多少&#xff0c;头部开销倒是占了大头&#xff1b;抓包一看&#xff0c;成百上千个TCP小段在链路上排队&#xff0c;延迟蹭蹭往上走。我去…

作者头像 李华
网站建设 2026/10/7 9:05:57

分布式集群下的缓存感知路由:让多节点前缀树缓存命中率突破 85%

分布式集群下的缓存感知路由&#xff1a;让多节点前缀树缓存命中率突破 85%在大模型推理系统实现单机层面的前缀缓存&#xff08;Prefix Caching&#xff09;后&#xff0c;系统往往能在多轮对话与固定系统提示词场景下取得令人惊艳的首字延迟收益。在单卡单实例压测中&#xf…

作者头像 李华
网站建设 2026/10/7 9:05:54

微信pdf转word怎么弄?零下载超简单方法,新手也能一键搞定

日常办公、学习中&#xff0c;我们经常会在微信收到PDF文件。不管是工作合同、报表资料&#xff0c;还是学生作业、学习文档&#xff0c;PDF格式虽然方便传输、格式固定&#xff0c;但最大的短板就是无法直接编辑修改。很多人遇到需要修改PDF内容的情况&#xff0c;都会纠结&am…

作者头像 李华
网站建设 2026/10/7 9:04:44

Spyglass CDC/RDC验证目录深度解析与工程实践指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/7 9:04:43

MOS管五维测试法:告别万用表误判

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华