从卫星图到训练样本:一份给CV新人的DeepGlobe土地覆盖数据集预处理避坑指南
从卫星图到训练样本DeepGlobe土地覆盖数据集预处理实战手册第一次打开DeepGlobe数据集时我被那些看似相同却代表不同地貌的色块彻底搞晕了——明明在卫星图上清晰可见的农田在标签图中却显示为诡异的粉紫色。更崩溃的是当我兴冲冲把数据塞进模型后GPU内存直接爆满而导师只留下一句自己看文档。如果你也正在经历这种绝望别担心这份指南会带你绕过那些教科书不会写的坑。1. 解密DeepGlobe卫星数据背后的语言1.1 标签颜色的秘密语法DeepGlobe用三通道RGB图像存储标签但千万别被表象迷惑——那些鲜艳的颜色只是视觉辅助。实际训练时需要处理的是隐藏在颜色背后的像素值像素值类别视觉颜色实战陷阱0未知土地黑色常被误认为无效区域1城市建筑青蓝色不含道路需单独处理2农田黄色季节性变化导致标注偏差3牧场品红色易与森林混淆4森林绿色树冠密度阈值影响标注5水域蓝色小水体可能丢失6荒地白色与云层覆盖区域难区分# 查看真实标签值的正确方式 import cv2 label cv2.imread(label_001.tif, cv2.IMREAD_UNCHANGED) print(np.unique(label)) # 输出会是[0,1,2,3,4,5,6]而非RGB值1.2 数据集的隐藏剧情官方提供的1146张2448×2448图像看似规整但有几个暗坑农村地区偏见城市样本不足导致模型在城区表现差季节单一性所有图像采集于同一季节影响泛化能力边界模糊约5%的标注存在耕地与牧场过渡区争议提示用QGIS打开样本时记得关闭自动对比度拉伸否则标签会显示为全黑2. 预处理从原始数据到训练样本2.1 裁剪策略的GPU经济学面对2448×2448的巨幅图像你需要像裁缝一样谨慎下刀def smart_crop(img, size512, overlap64): 带重叠的智能裁剪避免重要特征被切断 :param overlap: 重叠像素防止地块边界信息丢失 crops [] h, w img.shape[:2] for y in range(0, h, size-overlap): for x in range(0, w, size-overlap): crop img[y:ysize, x:xsize] if crop.shape[0] crop.shape[1]: # 丢弃不完整区块 crops.append(crop) return crops显存紧张时的生存法则GTX 1060 (6GB)最大支持512×512 batch4RTX 2080 (11GB)可尝试1024×1024 batch2显存不足时减小batch size比降低分辨率更有效2.2 标签可视化给黑白数据上色原始标签看起来像黑屏试试这个颜色映射技巧def apply_colormap(grayscale_label): colormap { 0: [0,0,0], # 黑 1: [0,255,255], # 青 2: [255,255,0], # 黄 3: [255,0,255], # 品红 4: [0,255,0], # 绿 5: [0,0,255], # 蓝 6: [255,255,255]# 白 } rgb np.zeros((*grayscale_label.shape,3), dtypenp.uint8) for val in colormap: rgb[grayscale_labelval] colormap[val] return rgb3. 数据增强弥补样本缺陷的魔法3.1 针对遥感数据的特殊增强普通翻转旋转远远不够你需要这些地貌保持变换from albumentations import ( RandomSizedCrop, RandomBrightnessContrast, HueSaturationValue, RGBShift, ChannelShuffle, CLAHE ) train_transform A.Compose([ A.RandomRotate90(p0.5), A.RandomSizedCrop(min_max_height(256, 512), height512, width512, p0.5), A.RandomBrightnessContrast(brightness_limit0.2, contrast_limit0.2, p0.3), A.HueSaturationValue(hue_shift_limit10, sat_shift_limit20, val_shift_limit10, p0.3), A.CLAHE(p0.2), ], additional_targets{label: mask})3.2 类别不平衡的破解之道农田类别占40%而水域仅5%试试这些组合拳样本加权法在损失函数中给稀有类别更高权重class_weights torch.tensor([0.1, 0.2, 0.4, 0.15, 0.05, 0.05, 0.05]) criterion nn.CrossEntropyLoss(weightclass_weights)过采样技巧对稀有类别样本进行复制变换动态裁剪针对稀有类别区域提高裁剪概率4. 实战管道搭建从数据到模型4.1 高效数据加载方案避免成为IO瓶颈的PyTorch Dataset设计class DeepGlobeDataset(torch.utils.data.Dataset): def __init__(self, root, transformNone): self.image_dir os.path.join(root, images) self.label_dir os.path.join(root, labels) self.transform transform self.samples [f for f in os.listdir(self.image_dir) if f.endswith(.tif)] def __getitem__(self, idx): img_path os.path.join(self.image_dir, self.samples[idx]) label_path os.path.join(self.label_dir, self.samples[idx]) with rasterio.open(img_path) as src: image src.read().transpose(1,2,0) # CHW→HWC with rasterio.open(label_path) as src: label src.read()[0] # 单通道标签 if self.transform: augmented self.transform(imageimage, labellabel) image, label augmented[image], augmented[label] return image.float(), label.long()4.2 验证阶段的特别处理在验证时使用滑动窗口避免信息丢失def predict_large_image(model, img, window_size512, stride256): 大图预测的滑动窗口实现 :return: 概率图 (H,W,C) h,w img.shape[:2] probs np.zeros((h,w,N_CLASSES)) counts np.zeros((h,w)) for y in range(0, h, stride): for x in range(0, w, stride): window img[y:ywindow_size, x:xwindow_size] if window.shape[0] window_size or window.shape[1] window_size: continue with torch.no_grad(): pred model(window.unsqueeze(0).cuda()).softmax(dim1) probs[y:ywindow_size, x:xwindow_size] pred.squeeze().cpu().numpy().transpose(1,2,0) counts[y:ywindow_size, x:xwindow_size] 1 return probs / (counts[...,None] 1e-8)在完成第一个训练周期后我强烈建议用QGIS同时打开原图、预测结果和标签进行视觉比对——这往往比看指标更能发现问题。记得特别关注农田与牧场的交界处那里是大多数模型最先崩溃的地方。