遥感数据增强秘籍:用GDAL给语义分割数据集扩容的3种高阶玩法

手里攒了一批遥感影像和对应的标注数据,准备大干一场训练个语义分割模型,结果跑了几轮发现模型总是过拟合,或者对某些角度、光照条件下的地物识别率惨不忍睹。这可能是许多从事遥感AI应用的朋友都踩过的坑。问题的根源往往不在于模型架构不够新,而在于我们喂给模型的数据“食谱”太过单一。遥感数据本身获取成本高、标注难度大,我们很难像处理自然图像那样轻易获得海量样本。这时候,数据增强就成了提升模型泛化能力、挖掘有限数据潜力的关键手段。

但数据增强不是简单的“开盲盒”。对于遥感语义分割任务,尤其是涉及多波段数据时,我们需要考虑几何变换时地理信息的保持、光谱通道处理的特殊性,以及增强操作对像素级标签的同步影响。盲目应用为自然图像设计的增强库,可能会破坏数据的内在一致性,甚至引入难以察觉的噪声。本文将抛开那些基础的翻转、旋转操作,深入三种基于GDAL的高阶数据增强策略。这些策略专为遥感数据设计,旨在不增加任何额外标注成本的前提下,系统性提升数据集的多样性和质量,特别适合用于Kaggle竞赛、科研项目或实际生产中的数据集优化。

1. 超越简单翻转:保持地理意义的几何增强策略

几何变换是数据增强中最直观的一类,但直接对遥感影像使用numpy.flipcv2.rotate可能会带来隐患。遥感影像附带着地理变换参数投影信息,这些是它的“身份证”。简单的数组操作会丢失这些信息,导致增强后的图像无法与真实世界坐标对应,在后续的推理或与其他地理数据叠加时出现问题。

1.1 基于GDAL的仿射变换与重投影感知增强

GDAL库不仅能读写地理空间数据,其底层的数据结构也为我们进行“地理感知”的增强提供了可能。核心思路是:在操作图像数据数组的同时,同步计算并更新其地理参考信息。

例如,进行一个非90度倍数的旋转。对于自然图像,这很简单。但对于遥感影像,旋转后图像四个角点的地理坐标都发生了变化,对应的仿射变换参数(geotransform)必须重新计算。

import numpy as np
from osgeo import gdal, osr
import math

def rotate_image_with_georef(src_path, dst_path, angle_deg):
    """
    旋转遥感图像并保持正确的地理参考。
    angle_deg: 逆时针旋转角度。
    """
    # 读取源数据
    src_ds = gdal.Open(src_path, gdal.GA_ReadOnly)
    gt = src_ds.GetGeoTransform()
    proj = src_ds.GetProjection()
    data = src_ds.ReadAsArray()
    src_ds = None

    # 假设data为 [bands, height, width]
    bands, height, width = data.shape
    angle_rad = math.radians(angle_deg)

    # 计算旋转后新图像的尺寸
    cos_a, sin_a = abs(math.cos(angle_rad)), abs(math.sin(angle_rad))
    new_width = int(height * sin_a + width * cos_a)
    new_height = int(height * cos_a + width * sin_a)

    # 使用OpenCV或scipy进行图像旋转 (这里以单波段为例,需循环处理多波段)
    # 注意:同时需要对标签进行完全相同的旋转操作
    from scipy import ndimage
    rotated_data = np.zeros((bands, new_height, new_width), dtype=data.dtype)
    for b in range(bands):
        rotated_data[b] = ndimage.rotate(data[b], angle_deg, reshape=True, order=1, mode='constant', cval=0)

    # **关键步骤:计算新的仿射变换参数**
    # 原左上角点坐标 (gt[0], gt[3])
    # 旋转中心设为原图中心
    center_x = gt[0] + gt[1] * width / 2 + gt[2] * height / 2
    center_y = gt[3] + gt[4] * width / 2 + gt[5] * height / 2

    # 新左上角相对于旋转中心的位置(在图像坐标系)
    new_ul_x = -new_width / 2 * gt[1] * math.cos(angle_rad) - new_height / 2 * gt[2] * math.sin(angle_rad)
    new_ul_y = -new_width / 2 * gt[4] * math.cos(angle_rad) - new_height / 2 * gt[5] * math.sin(angle_rad)

    new_gt = (
        center_x + new_ul_x,  # 新左上角X坐标
        gt[1] * math.cos(angle_rad) + gt[2] * math.sin(angle_rad),  # 像素宽度旋转
        gt[1] * -math.sin(angle_rad) + gt[2] * math.cos(angle_rad), # 旋转行
        center_y + new_ul_y,  # 新左上角Y坐标
        gt[4] * math.cos(angle_rad) + gt[5] * math.sin(angle_rad),  # 像素高度旋转
        gt[4] * -math.sin(angle_rad) + gt[5] * math.cos(angle_rad)  # 旋转行
    )

    # 写入新文件
    driver = gdal.GetDriverByName('GTiff')
    dst_ds = driver.Create(dst_path, new_width, new_height, bands, gdal.GDT_Float32)
    dst_ds.SetGeoTransform(new_gt)
    dst_ds.SetProjection(proj)
    for b in range(bands):
        dst_ds.GetRasterBand(b+1).WriteArray(rotated_data[b])
    dst_ds.FlushCache()
    dst_ds = None

注意:上述旋转计算是一个简化示例,实际应用中需要更严谨地处理仿射变换中的旋转与错切分量。对于严格的几何一致性要求,建议使用gdal.Warp函数进行重采样和变换。

1.2 弹性形变与局部扭曲:模拟真实世界变形

对于某些地物,如河流、道路或植被边界,其在影像上的形状并非总是刚性的。模拟这种自然变形可以增强模型对不规则形状的鲁棒性。我们可以借鉴医学图像处理中的弹性形变方法,但将其应用于保持地理栅格结构。

一种实用的方法是使用薄板样条或基于控制网格的形变。这里给出一个基于规则网格和随机位移场的实现思路:

import numpy as np
from scipy.interpolate import RectBivariateSpline

def elastic_deform_image(image, label, alpha=10, sigma=3):
    """
    对图像和标签进行一致的弹性形变。
    image: 输入影像数组 [H, W, C] 或 [C, H, W]
    label: 对应标签数组 [H, W]
    alpha: 形变强度因子
    sigma: 高斯滤波标准差,控制形变的平滑程度
    """
    # 确保处理格式一致
    if image.ndim == 3 and image.shape[0] < image.shape[2]: # [C, H, W] -> [H, W, C]
        image = np.transpose(image, (1, 2, 0))
    h, w = image.shape[:2]
    
    # 生成随机位移场
    dx = np.random.uniform(-1, 1, (h, w)) * alpha
    dy = np.random.uniform(-1, 1, (h, w)) * alpha
    
    # 使用高斯滤波平滑位移场,使形变连续
    from scipy.ndimage import gaussian_filter
    dx = gaussian_filter(dx, sigma=sigma, mode='constant')
    dy = gaussian_filter(dy, sigma=sigma, mode='constant')
    
    # 创建采样网格
    x, y = np.meshgrid(np.arange(w), np.arange(h))
    indices = np.reshape(y+dy, (-1, 1)), np.reshape(x+dx, (-1, 1))
    
    # 对每个通道进行插值
    deformed_image = np.zeros_like(image)
    if image.ndim == 3:
        for c in range(image.shape[2]):
            deformed_image[..., c] = ndimage.map_coordinates(image[..., c], indices, order=1, mode='reflect').reshape(h, w)
    else:
        deformed_image = ndimage.map_coordinates(image, indices, order=1, mode='reflect').reshape(h, w)
    
    # 对标签进行最近邻插值,以保持类别完整性
    deformed_label = ndimage.map_coordinates(label, indices, order=0, mode='constant', cval=0).reshape(h, w).astype(label.dtype)
    
    return deformed_image, deformed_label

这种形变能有效模拟由于传感器视角、地形起伏或拼接误差造成的局部几何失真,尤其适用于高分辨率遥感影像。

2. 多波段遥感数据的通道级光谱增强

RGB图像的数据增强可以直接套用色彩抖动、对比度调整。但多光谱或高光谱遥感影像的每个通道都有明确的物理意义(如蓝、绿、红、近红外),盲目调整可能破坏不同地物光谱曲线的相对关系,导致模型学习到虚假特征。

2.1 基于统计特性的波段间关系保持增强

核心原则是:增强操作应保持波段间的协方差结构或特定指数(如NDVI)的相对稳定性。一种安全的方法是进行主成分分析变换,在PCA空间对主要成分进行轻微扰动,再变换回来。

import numpy as np
from sklearn.decomposition import PCA

def pca_spectral_augmentation(image_stack, noise_factor=0.01):
    """
    在PCA空间对多光谱图像进行噪声注入增强。
    image_stack: 形状为 [height, width, bands] 的多光谱图像
    noise_factor: 注入噪声的强度(相对于主成分标准差)
    """
    h, w, b = image_stack.shape
    # 将图像展平为 [pixels, bands]
    pixels = image_stack.reshape(-1, b)
    
    # 执行PCA
    pca = PCA()
    pca.fit(pixels)
    components = pca.transform(pixels) # 转换到PCA空间
    
    # 对每个主成分添加高斯噪声,噪声强度与该成分的标准差成正比
    for i in range(b):
        std = np.std(components[:, i])
        noise = np.random.randn(len(components)) * std * noise_factor
        components[:, i] += noise
    
    # 逆变换回原始光谱空间
    augmented_pixels = pca.inverse_transform(components)
    augmented_image = augmented_pixels.reshape(h, w, b)
    
    # 确保数据范围合理(例如,反射率在0-1或0-10000之间)
    augmented_image = np.clip(augmented_image, 0, 10000) # 假设数据为反射率*10000
    return augmented_image

这种方法能生成光谱特征略有不同但物理意义保持合理的新样本,特别适合用于应对不同大气条件或季节变化引起的光谱漂移。

2.2 针对特定地物类别的选择性光谱混合

在农田监测或土地分类中,我们可以模拟混合像元。例如,在植被和裸土的边界,可以按比例混合两者的光谱特征。

def spectral_mixing_augmentation(image, label, class_a, class_b, mix_ratio_range=(0.2, 0.8)):
    """
    模拟两类地物边界的光谱混合。
    image, label: 影像和标签
    class_a, class_b: 需要混合的两个类别标签值
    mix_ratio_range: 混合比例范围
    """
    # 找到两类区域的掩膜
    mask_a = (label == class_a)
    mask_b = (label == class_b)
    
    # 计算两类区域的光谱均值
    mean_spectrum_a = np.mean(image[mask_a], axis=0)
    mean_spectrum_b = np.mean(image[mask_b], axis=0)
    
    # 找到两类相邻的边界像素(简化处理:膨胀后的交集)
    from scipy.ndimage import binary_dilation
    dilated_a = binary_dilation(mask_a, structure=np.ones((3,3)))
    dilated_b = binary_dilation(mask_b, structure=np.ones((3,3)))
    boundary_mask = dilated_a & dilated_b
    
    if not np.any(boundary_mask):
        return image, label # 无相邻边界,直接返回
    
    # 对每个边界像素,随机生成混合比例并合成新光谱
    mixed_image = image.copy()
    boundary_indices = np.where(boundary_mask)
    for y, x in zip(boundary_indices[0], boundary_indices[1]):
        ratio = np.random.uniform(*mix_ratio_range)
        mixed_spectrum = ratio * mean_spectrum_a + (1 - ratio) * mean_spectrum_b
        mixed_image[y, x] = mixed_spectrum
        # 可选:修改标签为新的“混合类别”或保持原标签之一
        # label[y, x] = new_class_id 
    
    return mixed_image, label

3. 融合空间上下文的语义一致性增强

前两种方法主要关注像素本身或局部窗口。对于语义分割,充分利用图像全局或区域的语义上下文进行增强,能产生更符合现实逻辑的样本。

3.1 基于类频率的复制-粘贴增强

这种方法特别适用于处理类别不平衡问题。例如,数据集中“建筑物”样本很少,我们可以从其他图像中裁剪出建筑物实例,以符合透视和光照条件的方式“粘贴”到新图像的合适位置(如非水体、非森林的平坦区域)。

import random
from skimage.measure import find_contours
import cv2

def copy_paste_augmentation(background_img, background_label, instance_img, instance_label, target_class):
    """
    将实例图像中的目标物体粘贴到背景图像上。
    """
    # 1. 从实例图像中提取目标物体的精确掩膜
    instance_mask = (instance_label == target_class)
    if not np.any(instance_mask):
        return background_img, background_label
    
    # 2. 找到物体的最小外接矩形
    coords = np.column_stack(np.where(instance_mask))
    y_min, x_min = coords.min(axis=0)
    y_max, x_max = coords.max(axis=0)
    instance_patch = instance_img[y_min:y_max+1, x_min:x_max+1]
    mask_patch = instance_mask[y_min:y_max+1, x_min:x_max+1]
    
    # 3. 在背景图像中寻找合适的粘贴位置(非目标类别区域,且大小合适)
    bg_h, bg_w = background_label.shape
    patch_h, patch_w = instance_patch.shape[:2]
    
    max_attempts = 50
    for _ in range(max_attempts):
        # 随机选择粘贴位置
        paste_y = random.randint(0, bg_h - patch_h)
        paste_x = random.randint(0, bg_w - patch_w)
        
        # 检查该位置是否“合理”(例如,不覆盖重要地物,如道路中心、水体)
        target_region = background_label[paste_y:paste_y+patch_h, paste_x:paste_x+patch_w]
        # 简单规则:允许覆盖的背景类别(如草地、裸土)
        allowed_classes = [0, 1] # 假设0是背景,1是草地
        if np.all(np.isin(target_region, allowed_classes)):
            # 4. 进行融合粘贴(简单覆盖或Alpha混合)
            for c in range(background_img.shape[2] if background_img.ndim==3 else 1):
                if background_img.ndim == 3:
                    channel = background_img[paste_y:paste_y+patch_h, paste_x:paste_x+patch_w, c]
                    channel[mask_patch] = instance_patch[..., c][mask_patch]
                else:
                    background_img[paste_y:paste_y+patch_h, paste_x:paste_x+patch_w][mask_patch] = instance_patch[mask_patch]
            
            # 5. 更新标签
            background_label[paste_y:paste_y+patch_h, paste_x:paste_x+patch_w][mask_patch] = target_class
            break # 成功粘贴一次后退出
    
    return background_img, background_label

提示:为了更逼真,粘贴前可以对实例块进行轻微的亮度、对比度调整,以匹配背景图像的光照条件,并使用羽化技术平滑边缘。

3.2 模拟不同传感器与成像条件的融合增强

如果我们拥有同一区域不同分辨率、不同时相或来自不同传感器(如Sentinel-2和Landsat-8)的数据,可以模拟数据融合过程作为增强。这能提升模型对多源数据的适应性。

一种方法是分辨率退化与重建:将高分辨率图像通过高斯金字塔下采样再上采样,模拟低分辨率传感器的成像效果,同时将对应的标签进行最近邻下采样以保证对齐。

def simulate_cross_sensor_augmentation(hr_image, hr_label, scale_factors=[0.75, 0.5]):
    """
    模拟多分辨率传感器成像。
    hr_image: 高分辨率影像 [H, W, C]
    hr_label: 高分辨率标签
    scale_factors: 模拟的降尺度比例列表
    """
    augmented_pairs = []
    h, w = hr_label.shape
    
    for scale in scale_factors:
        new_h, new_w = int(h * scale), int(w * scale)
        
        # 使用GDAL或OpenCV进行重采样
        # 影像使用双线性或立方卷积插值(保持光谱连续性)
        lr_image = cv2.resize(hr_image, (new_w, new_h), interpolation=cv2.INTER_CUBIC)
        # 标签必须使用最近邻插值,防止产生无效的类别值
        lr_label = cv2.resize(hr_label, (new_w, new_h), interpolation=cv2.INTER_NEAREST)
        
        # 再将低分辨率数据上采样回原始尺寸,模拟“软”标签或低分辨率输入
        up_image = cv2.resize(lr_image, (w, h), interpolation=cv2.INTER_CUBIC)
        up_label = cv2.resize(lr_label, (w, h), interpolation=cv2.INTER_NEAREST)
        
        augmented_pairs.append((up_image, up_label))
    
    return augmented_pairs

4. 构建自动化增强流水线与策略选择

掌握了多种增强方法后,我们需要一个系统化的策略来应用它们,而不是随机堆叠。过度增强或不当的组合可能会损害原始数据的代表性。

4.1 设计参数化与可配置的增强流水线

我们可以构建一个配置驱动的增强流水线,针对不同的数据集特性(如城乡场景、农林监测)启用不同的增强模块和强度。

import yaml
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class AugmentationConfig:
    """数据增强流水线配置"""
    enable_geometric: bool = True
    rotation_angles: List[int] = (-15, -10, -5, 5, 10, 15) # 度
    enable_elastic: bool = True
    elastic_alpha_range: tuple = (5, 15)
    enable_spectral_pca: bool = False # 多光谱数据启用
    pca_noise_factor: float = 0.02
    enable_copy_paste: bool = True
    copy_paste_classes: List[int] = None # 指定需要增强的稀有类别
    enable_cross_sensor: bool = False
    scale_factors: List[float] = (0.8, 0.6)
    
class RemoteSensingAugmentationPipeline:
    def __init__(self, config: AugmentationConfig):
        self.config = config
        self.augmentations = []
        self._build_pipeline()
    
    def _build_pipeline(self):
        if self.config.enable_geometric:
            self.augmentations.append(self._apply_geometric)
        if self.config.enable_elastic:
            self.augmentations.append(self._apply_elastic)
        # ... 其他增强方法的添加
    
    def _apply_geometric(self, image, label):
        # 随机选择一种几何变换
        aug_type = random.choice(['rotate', 'flip', 'scale'])
        if aug_type == 'rotate':
            angle = random.choice(self.config.rotation_angles)
            return rotate_image_with_georef(image, label, angle) # 需适配函数接口
        # ... 其他几何变换
        return image, label
    
    def __call__(self, image, label):
        # 随机选择1-3种增强方法顺序应用
        num_augs = random.randint(1, min(3, len(self.augmentations)))
        selected_augs = random.sample(self.augmentations, num_augs)
        
        aug_image, aug_label = image.copy(), label.copy()
        for aug_func in selected_augs:
            aug_image, aug_label = aug_func(aug_image, aug_label)
        
        return aug_image, aug_label

# 使用示例
config = AugmentationConfig(
    enable_copy_paste=True,
    copy_paste_classes=[3, 5], # 假设3是“桥梁”,5是“油罐”,都是稀有类别
    enable_spectral_pca=True
)
pipeline = RemoteSensingAugmentationPipeline(config)

4.2 增强策略与模型训练的协同

数据增强不是独立的预处理步骤,而应与训练过程动态结合。在线增强可以在每个epoch为模型提供略有不同的样本变体,有效防止记忆。同时,我们可以根据模型在验证集上的表现,动态调整增强策略的强度。

例如,如果模型在某一类别上持续表现不佳,可以在后续训练中针对该类别的样本提高copy_paste的概率或增加其光谱扰动的强度。这种“反馈式增强”需要将增强流水线与训练循环深度集成。

另一个高级技巧是课程学习:在训练初期使用较弱的增强(如仅水平翻转),让模型快速学习基本特征;随着训练进行,逐步引入更复杂的增强(如弹性形变、光谱混合),迫使模型学习更鲁棒、更本质的特征。

在实际项目中,我通常会为每个数据集维护一个增强配置日志,记录哪些策略对验证集指标提升最明显。例如,在处理山区土地覆盖分类时,发现弹性形变和模拟阴影的增强能显著提升模型在陡坡区域的识别率;而在处理城市建筑物提取时,copy-paste增强和模拟不同传感器分辨率的策略效果更好。这种经验性的记录对于快速适配新项目至关重要。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐