ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多类别土地覆盖分类数据集与遥感图像分类训练实战

多类别土地覆盖分类数据集与遥感图像分类训练实战 简介遥感影像解译是国土监测、农业普查和城市变迁分析等工程应用的基础而土地覆盖分类则是其中最重要的任务之一。深度学习和图像分类模型的训练高度依赖大规模、高质量的标注数据然而公开数据集常存在类别体系不统一、地物尺度差异大、标注质量参差不齐等问题。为降低数据准备门槛本文整理了一套约30,000张、涵盖建筑、道路、水体、森林、农田等8类土地利用类型的已标注图像分类数据集并系统梳理了从数据源选型、类别体系设计、标注质量控制到基于PyTorch的分类模型训练、YOLO检测格式转换以及类别不均衡、标注噪声清洗等工程实践中的关键技巧。无论是使用深度学习框架训练基线模型还是将分类结果扩展至目标检测、语义分割等下游任务该数据集与配套方案都能提供可靠的数据底座助力遥感场景下的模型快速落地与跨区域泛化。 很多朋友在接触遥感影像、土地覆盖分类这类项目时第一步就卡在数据上要自己标注几十万张图根本标不完要用公开数据集又要面对各种格式不统一、类别定义合不拢、质量参差不齐的问题。我自己前前后后被数据集折腾过好几轮所以这次整理这份约30,000张、已经标注好的多类别土地使用类型图像分类数据集时干脆把从数据整理、标注规范到模型训练踩过的所有坑一起记录下来。这份数据覆盖建筑、水体、道路、森林、农田、裸地等常见土地利用类型每一张都经过了标注校验可以直接用于图像分类模型的训练和评估也可以转成目标检测格式做进一步扩展。无论你是刚入手遥感图像分类的研究生还是在做国土监测、农业普查、城市变迁分析的应用开发者这篇文章都值得你花十分钟看完。1. 项目背景与整体设计思路1.1 为什么需要多类别土地覆盖分类数据集土地覆盖分类是遥感图像解译中最基础也最常见的研究方向之一。它的目标是把一张航拍图、无人机影像或卫星影像上的每个区域归入预定义的类别比如建筑、道路、水体、耕地、林地、草地、裸地等。看到图像分类这几个字很多人第一反应是拿ImageNet或CIFAR-10直接开训练但实际操作过就会发现自然影像分类和遥感土地覆盖分类完全是两回事。遥感影像的拍摄角度以俯视为主目标物体的尺度、朝向、光谱特征和自然照片差异很大。同样一块农田在不同季节、不同湿度条件下的颜色和纹理完全不同同样一栋建筑在阳光直射和阴天条件下屋顶材质和阴影范围也会让模型判断困难。再加上不同来源的影像分辨率差异从0.05米的无人机航片到10米的卫星影像一个在自然图像上表现很好的分类模型直接迁移到土地覆盖场景往往会出现严重的性能下降。所以在我看来一个高质量的、涵盖多种土地使用类型的已标注数据集是整个项目中最关键的资产。它直接决定了模型能不能在真实业务场景中跑得起来也决定了你后续做精细分类或目标检测时能不能有可靠的底座。1.2 数据源选型与采集方案在做这个数据集之前我花了不少时间考虑数据源。市面上的公开遥感数据源很多比如Sentinel-2、Landsat这些光学卫星影像或者USGS、Google Earth Engine上可以批量拉取的历史影像。但直接把公开影像拿来做分类训练有几个很现实的问题第一卫星影像的幅宽大动辄几百公里但单景影像上的目标数量有限直接整幅喂给分类网络的话类别覆盖不充分。第二不同地区和不同时相的影像存在很大的光谱差异如果不做裁剪、筛选和类别均衡模型学到的会是一些表面特征换一个地理位置就失效了。第三公开影像的地物标注需要依赖已有的土地覆盖产品比如ESA WorldCover或GlobeLand30这些产品的类别定义往往比较宽泛和具体业务需求不一致。我最终采用的方案是混合采集一部分来自高分辨率航拍和无人机影像确保建筑、道路这类小而密的类别有足够的细节一部分来自公开的卫星影像产品补充农田、森林、草地这类大范围类别。采集完成后统一按512×512像素进行裁剪再经过人工初筛去掉云雾遮挡严重、边界不清晰和质量过差的样本。这样合成的数据集既覆盖了不同尺度的地物又保留了不同传感器带来的丰富特征模型在真实业务中泛化起来会舒服很多。2. 标注规范与类别体系设计2.1 类别体系与含义界定标注工作在很多人眼里似乎只是画框或者打标签但事实上最考验功力的往往是类别体系的设计。类别分得太粗模型无法区分关键地物分得太细标注成本成倍增加类别间边界模糊反而会让分类器学得很困惑。这个数据集采用的是8个类别的平衡设计兼顾了业务常用需求和实际标注的可行性建筑含屋顶、大型建筑、农房道路含高速公路、城市道路、乡村道路水体含河流、湖泊、水库、池塘森林含密林、疏林、人工林农田含水田、旱地、大棚草地含天然草地、城市绿地裸地含沙地、裸土、施工空地其他含阴影、云雾遮挡、无法归类的复杂区域这里有一个很容易踩的坑如果项目需要你可以把建筑再拆成高层建筑和低矮建筑把道路拆成硬化路面和土路但一定要先问自己下游任务真的需要这种粒度吗拆得太细时标注员之间的判定标准很容易不一致——比如两三层的小楼算高层还是低矮这种模糊地带会变成标注噪声的主要来源比类别少带来的问题更恼人。2.2 标注流程与质量控制标注流程上我采用了初标交叉复核抽检验收三级机制。每个影像块先由标注员按照操作规范进行初标然后由第二名标注员独立复核发现不一致的样本全部返回修正最后由我参与抽检确保每批数据的合格率在98%以上。这个流程不能省因为分类数据的标注不像检测框那样有明确的边界框尺寸可以度量它高度依赖不同人的主观判断。一个具体的例子同样一块有稀疏树木的草地标注员A可能会因为树冠覆盖面积不足30%而标为草地标注员B却可能因为明显存在树木而标为森林。如果没有交叉复核这种样本就是模型训练的噪声源。我习惯用Python写一个标注统计脚本统计每个影像块被不同标注员标记的类别差异一旦发现某个类别的分歧率超过阈值就立刻定位到具体的标注指南盲区及时补充规则说明。标注工具方面如果你的数据格式是按块分类而不是画矢量边界直接用LabelMe、OpenLabeler这类工具就够了它们支持快速打标签和导出JSON/CSV批量操作起来比在QGIS里逐个画多边形高效得多。3. 从数据集到模型训练的完整实操流程3.1 数据集的目录组织与核心文件说明拿到数据集之后第一步不是急着训练而是把目录结构和文件内容梳理清楚。这份数据集的目录组织方式参考了PyTorch和TensorFlow常见的数据加载规范也兼容YOLO生态的自定义数据集配置方式landcover_dataset/ ├── images/ # 存放所有原始图像统一为JPG格式 │ ├── 000001.jpg │ ├── 000002.jpg │ └── ... ├── labels/ # 存放与图像一一对应的类别标签 │ ├── 000001.txt │ ├── 000002.txt │ └── ... ├── train.txt # 训练集图像路径与标签映射 ├── val.txt # 验证集图像路径与标签映射 ├── test.txt # 测试集图像路径与标签映射 ├── classes.txt # 类别名称每行一个 └── README.md # 说明文件、类别定义、使用许可对于图像分类任务每张图对应的labels/000001.txt文件里只存一个类别ID从0开始一行搞定3classes.txt的内容则是building road water forest farmland grassland bareland other有了这个结构后续加载代码非常简单。如果你用的是PyTorch直接用torchvision.datasets.ImageFolder就可以只要把图片按类别文件夹分好就行。如果你更习惯用文本映射的方式classMap {line.strip(): idx for idx, line in enumerate(open(classes.txt))}也足够。3.2 数据集的标准化检查与清洗拿到数据后强烈建议先跑一遍完整性检查。我在整理数据时写过一个500行的预处理脚本主要检查这四类问题第一图像通道和尺寸是否统一。数据集的图像统一为512×512、三通道JPG但因为来源不统一偶尔会混入灰度图或尺寸异常的图片。这类图片在训练时会直接报错或者导致Batch维度不一致必须在加载前过滤掉。第二标注文件是否与图像一一对应。偶尔会出现图像存在但没有对应标注文件的情况这类样本在训练时会被当成无标签样本跳过不报错但会浪费批次空间而且你很难察觉。我习惯把数量比对一下确保每张图都有且仅有一个标签ID。第三标签ID是否越界。如果classes.txt共8类ID为0~7但标注文件里出现了8就会导致embedding层或CrossEntropyLoss计算时越界报错。这类问题在人工标注后期修修补补时很容易出现一个简单的唯一值检查就能发现。第四类别概率分布是否均衡。这是我特别关注的一步。我会用collections.Counter统计某个类别在训练集里的数量占比如果某个类别占比过低比如低于5%模型在小样本类别上的表现大概率会崩。均衡性检查可以直接决定后面的采样策略。3.3 基于PyTorch的分类模型训练示例这个数据集的核心用途就是图像分类所以我把一套可以直接跑通的训练流程贴出来方便你做基准测试。这里以ResNet-18为例因为它在小规模数据集里表现稳定训练速度快同时不会像太深的大模型那样容易过拟合。import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchvision import transforms, models from PIL import Image import os class LandCoverDataset(Dataset): def __init__(self, img_dir, label_dir, file_list, transformNone): self.img_dir img_dir self.label_dir label_dir self.transform transform with open(file_list, r) as f: lines [l.strip() for l in f.readlines() if l.strip()] self.samples [] for line in lines: parts line.split() img_name parts[0] label_path os.path.join(label_dir, img_name.replace(.jpg, .txt)) # 若存在独立标签文件则读取否则直接读行内第二个字段 if os.path.exists(label_path): with open(label_path, r) as lf: label int(lf.read().strip()) else: label int(parts[1]) self.samples.append((os.path.join(img_dir, img_name), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label train_transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(15), transforms.ColorJitter(brightness0.2, contrast0.2, saturation0.2), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) val_transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) train_dataset LandCoverDataset(images, labels, train.txt, transformtrain_transform) val_dataset LandCoverDataset(images, labels, val.txt, transformval_transform) train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) val_loader DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4) model models.resnet18(pretrainedTrue) num_classes 8 model.fc nn.Linear(model.fc.in_features, num_classes) device torch.device(cuda if torch.cuda.is_available() else cpu) model model.to(device) criterion nn.CrossEntropyLoss() optimizer optim.AdamW(model.parameters(), lr1e-4) scheduler optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max30) def train_one_epoch(model, loader, criterion, optimizer): model.train() running_loss 0.0 correct 0 total 0 for images, labels in loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() epoch_loss running_loss / total epoch_acc correct / total return epoch_loss, epoch_acc for epoch in range(30): train_loss, train_acc train_one_epoch(model, train_loader, criterion, optimizer) scheduler.step() print(fEpoch {epoch1:02d} | Loss: {train_loss:.4f} | Acc: {train_acc:.4f})一个在实践中有用的细节我故意没有在验证集上用复杂的数据增强只用Resize和Normalize这样评估出来的指标更加真实。如果用RandomCrop做验证增强会高估模型的实际泛化能力打印出的准确率看着漂亮部署到真实影像上就露馅。3.4 基于YOLO的检测格式转换这份数据集的标签本身是分类格式但在很多业务场景下下游任务会变成目标检测——比如在土地分类基础上进一步检测具体建筑物、车辆、农业大棚等。这时候不需要重新标注可以转成YOLO格式省掉重复成本。转换的关键是把图像级标签变成目标框标注。这块没有统一做法但一个很实用的思路是如果你有额外的检测标注数据可以直接把分类数据作为负样本补充到YOLO训练集里让模型学会区分有目标和无目标如果你确实需要从分类图里生成检测框可以借助分割或连通域分析把同类像素聚合成候选框。一个更简单的做法是直接把分类标签转成YOLO的类别文件把整张图当作一个目标来处理生成如下的标注文件3 0.5 0.5 0.98 0.98其中3是类别ID0.5 0.5是目标中心点的归一化坐标0.98 0.98是归一化宽高。这种做法本质上是用一个框覆盖整张图在YOLO训练中可以作为弱监督初始化的起点。不过说实话如果是真正的检测任务我更推荐用专门的检测数据集来做分类数据集的主要价值还是在分类场景。4. 常见问题与排查技巧实录4.1 类别不均衡问题的应对30,000张数据听着不少划分到8个类之后难免有类别多、有的类别少。在实际使用中如果直接用原始分布去训练模型大概率会把占比最高的那个类别当作万能答案导致准确率虚高但实际泛化极差。我常用的方案有三个一是重采样。训练时让采样器按类别数量反比调整每个batch中各类别出现的概率。这个在PyTorch里实现比较简单用WeightedRandomSampler即可。二是损失函数调整。用带权重的CrossEntropyLoss给样本量小的类别更高的损失权重强迫模型重视它们。权重可以直接用1/类别频次做了归一化之后效果通常不错。三是数据增强补偿。对样本量较少的类别额外应用更强的增强策略比如随机旋转、透视变换、色彩抖动等扩大其有效样本量。这里要特别提醒的是不要为了均衡而均衡。如果你的业务场景本身就是建筑占了70%裸地只占3%那么强行把裸地权重拉高可能会让模型在真实分布下表现变差。要根据实际业务场景决定训练集的类别分布而不是只盯着学术指标。4.2 标注噪声对模型的影响及清洗方法标注噪声是任何数据集都无法完全避免的。分类标注中的噪声通常有两种一种是标注员主观判断导致的模糊类别错标比如前文提到的草地与森林的边界另一种是数据本身质量问题导致的明显错标比如一张图里出现了一半水体一半农田标成单一类别就很容易引起歧义。我的处理方式是先看模型在训练集上的混淆矩阵。如果模型在草地和森林之间混淆严重先不要急着调模型结构而是回到数据层面把这两类的样本拉出来逐一检查。很多看似是模型不够强的问题实际上是因为训练标签里存在系统性偏差。我曾经遇到过某批标注数据把水田大量标成水体因为水稻田在特定季节确实看起来像水塘如果不看原始影像调结构调损失函数都是白费功夫。另一个实用的清洗方法是用半监督的思路先用现有数据训练一个模型把训练集里预测置信度低比如softmax得分低于0.5的样本全部挑出来人工复核。这个方法可以很快定位到潜在的错标样本。实测下来一次复核往往能清理掉2%到5%的噪声样本模型准确率提升比换模型结构还明显。4.3 训练时图像尺寸与增强策略的选择数据集的原始图像是512×512但直接以这个尺寸训练显存压力会很大大部分人应该会resize到224×224或256×256。Resize会导致一些细节信息丢失尤其是道路、建筑这类边缘信息比较重要的类别。我的习惯是先用224×224跑通收敛最后再用256×256微调几轮实测能高出1%到2%的准确率。增强策略上有两个问题值得特别注意。第一不要对遥感影像使用会扭曲地物形态的增强操作。比如在自然图像里常用的RandomResizedCrop会把图像随机裁剪再拉伸导致道路弯折、建筑变形看着自然但在土地分类场景里会让模型学到错误的几何信息。一个稳妥的方案是把RandomResizedCrop的scale下限调高一点或者干脆用固定尺寸中心裁剪加随机翻转。第二ColorJitter这种颜色增强需要谨慎使用。遥感影像在不同时相、不同光照条件下确实存在色彩变化适当的颜色增强有助于模型抵抗光照差异但如果增强幅度过大会造成水体变红、农田变紫这类明显超出真实范围的伪样本。我的经验是把brightness、contrast、saturation的调节幅度控制在0.2以内效果更为可控。4.4 数据集的存储格式与加载效率优化一个30,000张图片的数据集全部是512×512的JPG总大小大概在6到10GB。如果每次训练都从磁盘逐个读取JPG数据加载会成为瓶颈尤其是设置了num_workers后CPU解压开销依然很明显。实测下来最快的方案是先把所有图像打包成LMDB或WebDataset格式或者直接转成HDF5。读入内存后配合DataLoader的prefetch_factor参数训练速度能提升30%以上。HDF5的做法简单直接import h5py import numpy as np from PIL import Image import os dataset_path landcover_dataset.h5 file_list [l.strip().split()[0] for l in open(train.txt) if l.strip()] with h5py.File(dataset_path, w) as f: images f.create_dataset(images, shape(len(file_list), 512, 512, 3), dtypenp.uint8) labels f.create_dataset(labels, shape(len(file_list),), dtypenp.int64) for idx, img_name in enumerate(file_list): img Image.open(os.path.join(images, img_name)).convert(RGB) images[idx] np.array(img) labels[idx] int(open(os.path.join(labels, img_name.replace(.jpg, .txt))).read()) print(done)这个方案的好处是训练时一次性把图像数据读入内存后面的每个Epoch都不必再访问磁盘可以大幅压缩I/O时间。坏处是内存占用比较大如果机器是16GB内存30,000张512×512的3通道图像全压缩进内存也接近极限。因此数据量太大时建议分块加载或者使用HDF5的chunk和compression选项。实际使用中我不太建议一开始就在数据加载上过度优化先把训练流程跑通、确认模型结构没问题之后再根据训练速度决定是否切换存储格式。不要为了加载优化花掉比训练本身还多的时间。4.5 跨区域泛化与数据集的扩展思路一个常见的使用误区是训练集和验证集都来自相同区域模型表现很好但一部署到新的城市或新的地形区性能就开始明显下降。这是因为不同区域的地物形态、建筑布局、植被覆盖特征有很大差异模型学到的是训练区域的特征而不是普适的土地覆盖规律。如果业务场景真的需要跨区域泛化我有两个建议第一不要在同一个地理区域内同时划分训练集和验证集而应该按区域划分。比如用A市的数据训练B市的数据验证这样评估出来的指标才更有说服力。如果数据来源包含多个城市或地区建议在README里说明每个样本的地理来源方便用户做区域独立划分。第二这个数据集可以作为预训练数据然后再在目标区域的小样本数据上微调。遥感影像分割模型通常都需要大规模预训练数据做底座用这30,000张图先在分类任务上预训练一个Encoder然后在目标检测或语义分割任务中用目标区域的少量标注数据微调效果通常比直接从头训练好很多。从扩展性角度看数据集后续可以做的事情还很多。比如增加时间序列信息同一区域不同月份的多时相影像向土地利用动态变化检测方向扩展也可以把类别的标注细粒度提高加入建筑层数、道路等级等属性信息还可以引入多光谱影像把RGB三通道扩展到红边、近红外等波段这对于植被相关类别的分类会有明显提升。我个人在实际操作中最推荐的一个扩展方向是把这套分类数据转换成弱监督语义分割的初始种子区域配合SAMSegment Anything Model做自动精化可以快速得到像素级标注数据把数据集的适用范围从图像分类延伸到语义分割。这个思路在很多遥感业务项目里都验证过比完全从零标注要省力得多。最后再分享一个小技巧无论你是用这份数据集训练分类模型还是准备扩展到检测或分割方向都建议先从训练集里随机挑200张图出来人工逐张看一眼对数据分布建立直观感受。数据质量摸清楚了后续一切模型调优都是水到渠成的事情数据本身有问题再强的模型也救不回来。本文还有配套的精品资源点击获取
返回列表