简介这份荷兰耕地语义分割遥感影像数据集面向遥感图像处理、计算机视觉与农业监测方向的研究者和开发者用于训练和验证耕地识别与像素级分割模型。资源包共2000个文件以xml标注文件为主要类型对应每张遥感影像的像素级类别标签与地理边界信息压缩包约786.99MB规模适中便于本地加载与批量处理。数据集将多光谱遥感图像、语义标签和矢量边界信息结合可支撑模型训练与验证、农作物生长与病虫害监测、土地覆盖变化分析以及非法占用耕地检测等任务帮助读者快速搭建语义分割实验流程并评估算法效果。目前已有1607人学习下载适合具备一定深度学习基础、希望获取真实标注数据开展耕地分割实践的用户参考使用。1. 荷兰耕地语义分割遥感影像数据集从标注完成到模型可用的最短路径拿到一份「已经标注完成」的耕地遥感数据集很多人的第一反应是直接开训。但真实情况往往是打开一看标签是灰度 PNG类别值 0/1/2/3 混着 255影像有 4 个波段但只有 3 个能可视化坐标系还是某个欧洲投影。荷兰耕地语义分割遥感影像数据集这类资源核心价值不在于「有标注」而在于标注体系是否和你的任务对齐、空间分辨率是否支撑地块边界、以及能不能在半天内跑通一个 baseline。这篇笔记面向已经拿到或准备使用该数据集的算法工程师、遥感应用开发者以及做农业监测方向的学生。我会按「先搞清楚数据长什么样再决定怎么切、怎么训、怎么验」的顺序把中间每一步的命令、参数和翻车点都摊开讲。如果你手里正好有这样一份数据跟着走一遍至少能省掉两天的试错。2. 先读懂标注体系耕地语义分割的类别定义与栅格结构2.1 为什么「已标注」不等于「可直接训练」遥感语义分割数据集的标注完成度通常只说明像素级标签已经生成不代表标签规范统一。荷兰耕地场景下常见标注来源包括地块边界矢量转栅格、人工勾绘、以及基于作物类型图的再分类。这三种来源产出的标签在类别数、边界精度、忽略区域处理上差异很大。我一般会先做三件事统计标签唯一值、检查影像与标签的尺寸和地理范围是否一致、确认是否存在 255 或 0 作为 ignore index。如果标签里出现 255而你的损失函数没设 ignore_index训练时就会把忽略区域当成一个真实类别去学mIoU 直接崩掉。另一个容易被忽略的点是类别不平衡。耕地数据里单一作物类型可能占 70% 以上像素道路、沟渠、建筑等非耕地类占比很小。如果直接算交叉熵模型会倾向于全预测成多数类。常见做法是先用类别频率算权重或者改用 Dice CE 的组合损失。下面这段脚本用来快速摸清标签分布输出每个类别的像素占比和唯一值列表。import numpy as np from PIL import Image from collections import Counter # 读取单张标签统计唯一值和像素占比 mask np.array(Image.open(label_001.png)) unique, counts np.unique(mask, return_countsTrue) total mask.size print(唯一值:, unique) for u, c in zip(unique, counts): print(f类别 {u}: {c/total:.4f} ({c} 像素)) # 检查是否存在 255 忽略值 if 255 in unique: print(警告存在 255需在损失函数中设置 ignore_index255)逻辑说明np.unique返回排序后的唯一值和对应计数return_countsTrue是必须的。参数上如果标签是 RGB 三通道伪彩色需要先转成单通道索引图否则唯一值会是颜色组合而不是类别 ID。判断标准很简单类别数应该等于你预期的类别数加可能的忽略值如果多出来说明标注体系里有未合并的细类。2.2 影像波段与标签对齐的检查方法荷兰耕地遥感影像常见波段组合是 RGB NIR部分数据会带红边波段。标签通常是单通道 8 位 PNG 或 TIFF。对齐检查不能只看文件数量要逐对验证尺寸、地理变换和投影。我习惯用 rasterio 读元数据一次性打印影像和标签的 shape、CRS、transform肉眼比对。如果 transform 有微小偏移说明标签在转栅格时没有严格对齐这种数据训出来的模型边界会糊。import rasterio with rasterio.open(image_001.tif) as src_img, \ rasterio.open(label_001.tif) as src_lbl: print(影像尺寸:, src_img.shape, 波段数:, src_img.count) print(标签尺寸:, src_lbl.shape, 波段数:, src_lbl.count) print(影像 CRS:, src_img.crs) print(标签 CRS:, src_lbl.crs) print(影像 transform:, src_img.transform) print(标签 transform:, src_lbl.transform) # 尺寸不一致时直接报错避免后续训练出玄学问题 assert src_img.shape[1:] src_lbl.shape[1:], 宽高不匹配参数说明src_img.shape返回 (波段, 高, 宽)所以比较时用[1:]。transform是仿射变换六参数前两个是左上角坐标后四个是像素分辨率与旋转。如果两者 transform 的像素分辨率不同说明标签被重采样过边界精度会损失。这种情况要么重新生成标签要么在训练时把标签最近邻重采样到影像分辨率但后者会引入锯齿。2.3 类别映射表的建立与统一不同来源的标签类别 ID 含义可能完全不同。有的数据集 1 代表耕地、2 代表草地有的反过来。在动手写 Dataset 之前必须固定一张类别映射表写进配置文件后续所有脚本都引用它。我一般用 JSON 存字段包括 id、name、colorcolor 用于可视化验证。映射表一旦确定就不要在训练中途改否则 checkpoint 的类别头会对不上。类别 ID类别名可视化颜色说明0背景/非耕地黑包含建筑、道路、水体1耕地绿主要作物种植区2草地浅绿牧草或休耕255忽略红云、阴影、边界模糊区这张表的作用是让数据加载、损失计算、指标评估三处引用同一份定义。如果忽略值不是 255 而是 0那背景类就要另选一个 ID否则背景和忽略会混淆。检查方法是随机抽 20 张标签用映射表的 color 上色后拼图看如果出现大面积红色说明忽略区域过多需要评估是否值得保留这些样本。3. 把数据集切成可训练的样本滑窗、增强与 DataLoader 配置3.1 大图滑窗裁剪的参数怎么定遥感影像单张动辄几千乘几千像素直接整图训练显存扛不住。标准做法是滑窗裁剪成固定大小 patch。这里有两个关键参数窗口大小和重叠度。窗口大小一般取 256 或 512取决于地块平均尺寸和显存。荷兰耕地地块相对规整256 通常够用如果地块很大且边界细长512 能保留更多上下文。重叠度我一般设 0.25即步长 窗口大小 × 0.75这样边缘样本不会因为裁剪丢失上下文。import numpy as np def sliding_window_crop(image, label, patch_size256, overlap0.25): 滑窗裁剪返回 patch 列表 stride int(patch_size * (1 - overlap)) h, w image.shape[:2] patches [] 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] lbl_patch label[y:ypatch_size, x:xpatch_size] # 跳过全忽略或全背景的 patch减少无效样本 if len(np.unique(lbl_patch)) 1: continue patches.append((img_patch, lbl_patch)) return patches逻辑说明stride由重叠度反推overlap0.25 意味着相邻 patch 有 25% 区域重叠。跳过单一类别 patch 是为了避免大量无信息样本拖慢训练但要注意如果某个小类别只出现在边缘跳过策略可能把它滤掉。参数上patch_size 必须是 2 的幂次方便于下采样对齐overlap 不建议超过 0.5否则样本冗余严重一个 epoch 时间翻倍。3.2 遥感场景下的增强策略与踩坑通用增强里随机旋转、翻转、颜色抖动都能用但遥感有两点特殊一是地理方向有意义垂直翻转可能把南北向的田块纹理破坏不过语义分割任务对方向不敏感翻转通常安全二是颜色抖动幅度要控制因为作物光谱特征和颜色强相关过度抖动会让模型学到错误的颜色先验。我一般用 albumentations配置如下。import albumentations as A train_transform A.Compose([ A.RandomRotate90(p0.5), A.HorizontalFlip(p0.5), A.VerticalFlip(p0.5), A.RandomBrightnessContrast(brightness_limit0.1, contrast_limit0.1, p0.3), A.GaussNoise(var_limit(5.0, 20.0), p0.2), ]) val_transform A.Compose([]) # 验证集不做增强参数说明RandomRotate90只做 90 度倍数旋转不引入插值模糊比任意角度旋转更安全。brightness_limit和contrast_limit设 0.1 是保守值超过 0.2 容易让耕地和草地的光谱混淆。GaussNoise的方差上限 20 是经验值再高会掩盖真实纹理。注意增强只对训练集做验证集必须保持原始分布否则指标不可比。3.3 Dataset 与 DataLoader 的工程写法Dataset 类要处理三件事读图、应用增强、返回 tensor。遥感影像常是 16 位需要归一化到 0-1 或按均值和标准差标准化。标签要转成 long 类型忽略值保持 255 不变交给损失函数处理。DataLoader 的 num_workers 在 Linux 下设 4 到 8Windows 下设 0 避免多进程报错。import torch from torch.utils.data import Dataset, DataLoader import rasterio import numpy as np class FarmlandDataset(Dataset): def __init__(self, samples, transformNone, mean(0.485, 0.456, 0.406), std(0.229, 0.224, 0.225)): self.samples samples # [(img_path, lbl_path), ...] self.transform transform self.mean np.array(mean).reshape(3, 1, 1) self.std np.array(std).reshape(3, 1, 1) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, lbl_path self.samples[idx] with rasterio.open(img_path) as src: image src.read([1, 2, 3]).astype(np.float32) # 取 RGB 三波段 with rasterio.open(lbl_path) as src: label src.read(1).astype(np.int64) image image / 255.0 image (image - self.mean) / self.std image np.transpose(image, (1, 2, 0)) # HWC 给 albumentations if self.transform: augmented self.transform(imageimage, masklabel) image, label augmented[image], augmented[mask] image np.transpose(image, (2, 0, 1)) # 回到 CHW return torch.from_numpy(image).float(), torch.from_numpy(label).long()逻辑说明src.read([1,2,3])只取前三个波段如果数据是 4 波段且 NIR 有用需要改成[1,2,3,4]并调整 mean/std 长度。归一化用 ImageNet 统计量是迁移学习的常见做法如果从零训练建议用数据集自身统计量。np.transpose两次转换是因为 albumentations 要求 HWC而 PyTorch 要求 CHW。忽略值 255 在转 long 后仍然是 255不会溢出。4. 训练配置与损失函数耕地分割的类别不平衡怎么破4.1 模型选型U-Net、DeepLabV3 还是 SegFormer耕地语义分割的边界比较规整但类别间光谱差异小尤其是耕地和草地。U-Net 在中小数据集上表现稳定参数量可控适合快速验证。DeepLabV3 的空洞卷积能扩大感受野对大面积地块更友好但训练更吃显存。SegFormer 基于 Transformer在遥感分割上近期表现不错但需要更多数据才能发挥优势。我的建议是如果数据集覆盖区域有限、样本量在几千 patch 级别先用 U-Net ResNet34 编码器跑 baseline如果地块面积大且边界复杂再上 DeepLabV3。不要一上来就堆大模型耕地分割的瓶颈往往在标签质量而不是模型容量。4.2 损失函数组合与 ignore_index 设置交叉熵在类别不平衡时会偏向多数类Dice Loss 对前景敏感但训练不稳定。常见做法是 CE Dice 加权求和权重各 0.5。关键是 ignore_index 必须设成标签里的忽略值否则忽略区域会参与梯度计算。下面是一个可直接用的组合损失。import torch import torch.nn as nn import torch.nn.functional as F class CombinedLoss(nn.Module): def __init__(self, ignore_index255, ce_weight0.5, dice_weight0.5): super().__init__() self.ignore_index ignore_index self.ce_weight ce_weight self.dice_weight dice_weight self.ce nn.CrossEntropyLoss(ignore_indexignore_index) def forward(self, pred, target): ce_loss self.ce(pred, target) # Dice 计算时屏蔽忽略区域 pred_soft F.softmax(pred, dim1) valid_mask (target ! self.ignore_index).unsqueeze(1) target_onehot F.one_hot(target.clamp(0, pred.shape[1]-1), pred.shape[1]) target_onehot target_onehot.permute(0, 3, 1, 2).float() target_onehot target_onehot * valid_mask pred_soft pred_soft * valid_mask intersection (pred_soft * target_onehot).sum(dim(0, 2, 3)) union pred_soft.sum(dim(0, 2, 3)) target_onehot.sum(dim(0, 2, 3)) dice_loss 1 - (2 * intersection 1e-6) / (union 1e-6) dice_loss dice_loss.mean() return self.ce_weight * ce_loss self.dice_weight * dice_loss参数说明ignore_index255让 CE 跳过忽略像素。Dice 部分用valid_mask把忽略区域置零避免它们影响交集和并集。target.clamp(0, pred.shape[1]-1)是防止 255 在 one_hot 时越界因为 clamp 后 255 会变成类别数减一但随后被 valid_mask 置零不影响结果。1e-6是平滑项防止除零。如果类别数很少且极不平衡可以把 dice_weight 提到 0.7。4.3 学习率、优化器与训练轮次的实操参数优化器用 AdamW学习率 1e-4权重衰减 1e-4。如果 batch size 能开到 16 以上学习率可以到 3e-4。调度器用 CosineAnnealingLRT_max 设为总 epoch 数。训练轮次上U-Net 在几千 patch 的数据集上通常 50 到 80 epoch 收敛早停耐心值设 10。验证指标用 mIoU但要注意忽略类不参与计算。下面是一个训练循环的核心片段。from torch.optim import AdamW from torch.optim.lr_scheduler import CosineAnnealingLR model model.cuda() optimizer AdamW(model.parameters(), lr1e-4, weight_decay1e-4) scheduler CosineAnnealingLR(optimizer, T_max60) criterion CombinedLoss(ignore_index255).cuda() for epoch in range(60): model.train() for img, lbl in train_loader: img, lbl img.cuda(), lbl.cuda() optimizer.zero_grad() out model(img) loss criterion(out, lbl) loss.backward() optimizer.step() scheduler.step() # 验证逻辑略重点看 mIoU 是否在 10 epoch 内上升逻辑说明AdamW的 weight_decay 是解耦的比 Adam 更稳。CosineAnnealingLR让学习率平滑下降到接近零避免后期震荡。如果 10 epoch 内 mIoU 不升反降优先检查标签对齐和 ignore_index而不是调学习率。5. 避坑与排查耕地分割训练中最容易翻车的五件事5.1 现象mIoU 卡在 0.3 不动损失也不降原因通常是标签类别 ID 和模型输出通道数不匹配。比如标签里有 0、1、2、255模型输出 3 通道但 255 被当成第 256 类CE 直接报错或静默忽略。解决方法是打印标签唯一值确认最大有效类别 ID 小于模型输出通道数且 ignore_index 设置正确。5.2 现象验证集 mIoU 远高于训练集这听起来反直觉但在遥感里常见。原因是验证集 patch 可能集中在单一作物区域类别分布比训练集简单。解决方法是按地理区域划分训练验证集而不是随机划分 patch避免同一块地的相邻 patch 分到两边造成信息泄漏。5.3 现象预测结果边界呈锯齿状原因是标签在转栅格时用了最近邻重采样或者滑窗裁剪时窗口没有对齐到像素网格。解决方法是检查标签生成流程确保矢量和栅格分辨率一致裁剪时 stride 取整数避免半像素偏移。5.4 现象训练到一半 loss 突然变 NaN常见于 Dice Loss 的除零或学习率过大。检查 Dice 平滑项是否加够学习率是否超过 1e-3。另外如果影像有 16 位且未归一化输入值过大会导致梯度爆炸。解决方法是先做 min-max 归一化或按数据集统计量标准化。5.5 现象推理时整图预测出现网格状拼接痕迹滑窗推理时重叠区域没有做加权融合直接覆盖导致边界不一致。解决方法是在推理阶段对重叠区域做高斯加权或简单平均确保每个像素被多个窗口覆盖时取平均预测。6. 从 baseline 到可用模型验证策略与一个提点技巧训练完一个 baseline 后不要只看 mIoU 一个数。耕地分割的实际可用性取决于边界精度和漏检率。我一般会做两件事一是按地块实例评估把预测结果转成矢量和真实地块边界算 IoU 分布看有多少地块的 IoU 低于 0.5二是可视化错误样本重点看耕地和草地的混淆区域。如果混淆集中在某几个光谱相近的类别可以考虑在损失里给这些类别加权重或者引入 NDVI 作为额外输入波段。一个具体提点技巧是测试时增强TTA。对同一张图做水平翻转、垂直翻转、90 度旋转分别推理后把概率图平均再取 argmax。这个操作不需要重新训练通常能涨 1 到 2 个点 mIoU代价是推理时间翻几倍。对于耕地这种边界规整的任务TTA 对边界平滑效果明显。代码上就是在推理循环里加几个变换累加 softmax 输出。def predict_with_tta(model, image): TTA 推理image 为 CHW tensor model.eval() probs torch.zeros(1, num_classes, *image.shape[1:]).cuda() with torch.no_grad(): # 原始 probs F.softmax(model(image.unsqueeze(0).cuda()), dim1) # 水平翻转 probs F.softmax(model(torch.flip(image, [2]).unsqueeze(0).cuda()), dim1).flip([2]) # 垂直翻转 probs F.softmax(model(torch.flip(image, [1]).unsqueeze(0).cuda()), dim1).flip([1]) return probs.argmax(dim1)参数说明torch.flip的维度参数 [2] 是宽[1] 是高翻转后推理再翻回来才能对齐。累加后取 argmax 等价于平均概率。TTA 的收益在类别边界上最明显如果数据集本身边界模糊收益会打折。最后说一个我自己的习惯每次拿到新数据集先花半小时写一个可视化脚本把影像、标签、预测叠在一起看。这个习惯帮我省掉了无数次「指标好看但结果不能用」的后悔药。耕地分割的落地模型只占三成数据理解和验证策略占七成。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?