ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

多模态遥感数据预处理实战:红外、可见光、高光谱与SAR的读取、配准与训练

多模态遥感数据预处理实战:红外、可见光、高光谱与SAR的读取、配准与训练 简介这份多模态遥感图像数据集面向计算机视觉、遥感与深度学习研究者汇集红外、可见光、高光谱以及SAR四种成像模态的真实场景图片可用于目标检测、图像融合、地物分类等实验与算法验证。压缩包共129个文件大小约72.49MB文件类型覆盖bmp、jpg、tif等常用图像格式并包含mat、m等数据文件以及enp、hdr等格式说明文档方便直接读取与二次处理。已有506人浏览学习适合入门到进阶的科研人员作为实验数据基础。数据集内图片来自真实拍摄场景图像清晰、模态对应明确能帮助使用者快速开展多源遥感数据对比分析同时附带docx、txt数据说明降低了数据理解与预处理门槛便于结合博客详情深入了解各文件用途。1. 拿到这个数据集压缩包先别急着解压一个 rar 包里同时塞下红外、可见光、高光谱和 SAR 四种模态这基本是遥感或临近空间感知方向的研究素材。你可能是要做红外与可见光图像融合也可能是拿高光谱做地物分类或者用 SAR 做变化检测——不管哪种第一反应别是双击解压然后拖进网络里跑。先花十分钟搞清楚数据说明文档里写的是什么传感器、什么波段范围、什么空间分辨率这决定了你后面所有预处理脚本怎么写也直接关系到模型能不能收敛。这个标题里最值钱的部分其实是“数据说明”四个字它通常包含了辐射定标系数、波长范围、坐标系和采集时间没有这些元信息图像矩阵对算法来说只是没有物理含义的数字。这篇文章就顺着这个压缩包的典型打开方式讲清楚四种数据各自要做什么处理、怎么读取、怎么统一到同一个训练框架里。2. 数据说明文档到底在说什么四种模态的差异决定了处理思路2.1 先从压缩包里的文件命名反推数据组织方式这类数据集压缩包解压后通常能看到类似scene_01_vis.tif、scene_01_ir.tif、scene_01_hsi.npy、scene_01_sar.png的命名结构也可能是一整个文件夹按日期或区域编号划分。我一般会先输入一条 bash 命令把目录树打出来看层级关系而不是用图形界面逐个翻。find . -type f | head -50 # 或者用 tree 命令看完整结构 tree -L 2 -d第一条命令列出所有文件路径检查命名规律第二条只看目录层级确认不同模态是否分文件夹存放。如果发现某个场景只有可见光没有 SAR 对应文件说明这个数据集存在缺失样本后续做多模态训练前要做配准和筛除不能直接按索引对齐。数据说明文档通常是README.txt或dataset_description.pdf里最优先找三个信息传感器名称、波长范围、空间分辨率。红外和可见光的分辨率差异很大比如可见光是 0.5 米、红外是 2 米那融合前必须做重采样高光谱如果是 400-1000nm 的可见光-近红外范围和水汽吸收波段无关辐射定标相对简单SAR 如果是单极化强度图那后向散射系数的范围通常是 0 到 1 的浮点值而不是 8bit 整数。2.2 数据说明中的关键参数逐项解读参数决定脚本怎么写所以我把数据说明里最常出现的字段和它们对下游任务的影响列出来。下面这张表是我自己的解读习惯也可以作为你写数据读取代码时的注释参考。参数字段常见取值对下游任务的影响Spatial Resolution0.5m / 1m / 10m决定是否需要重采样对齐融合任务要求同分辨率Radiometric Resolution8bit / 16bit / float32影响归一化方式和存储占用16bit 不能直接转 uint8Wavelength Range400-1000nm / 8-14μm确定光谱响应区间也决定了大气校正时要用哪些波段PolarizationVV / VH / HH / HVSAR 极化方式影响地物散射特征做分类时输入通道不同Projection / CRSUTM 50N / EPSG:32650多模态叠加需要统一坐标系否则像素对不齐Acquisition Date2021-08-15 等跨日期样本要做季节归一化SAR 也要考虑入射角差异这些参数如果你在数据说明里没找到可以看文件名是否有 DAT 缩写或者在解压后的 aux 目录里寻找元数据 JSON 或 XML。真正动手处理前建议先把所有影像读进来取一个样本场景打印阵列 shape 和 dtype用几行代码摸清数据底细。2.3 为什么红外、可见光、高光谱、SAR 不能直接拼在一起训练直接把这四种数据做通道拼接再丢给卷积网络是新手最常犯的错误原因有三个方面。第一是空间分辨率不一致不同传感器网格尺寸可能差 10 到 20 倍直接拼接会让网络误以为不同像素对应同一块地面第二是辐射尺度差异可见光反射率集中在 0 到 1红外亮温可能是 280K 到 320KSAR 后向散射的不同地物量级完全不同第三是几何畸变不同SAR 是斜距成像侧视导致叠掩和阴影光学影像近似中心投影不先做几何校正的话即使分辨率一致同名地物仍可能偏移好几个像素。所以数据说明文档的核心价值就在这里它告诉你每一层数据的物理量纲和采集几何你的深度学习模型是否有效其实从预处理的第一步就开始分层了。更具体的落地路径可以这样安排先按数据说明把各模态裁剪到同一空间范围再通过重采样统一分辨率最后分别做归一化而不是在一个脚本里笼统地min-max一把梭。下一章我会给出读取这套数据的最小代码闭环。3. 用 Python 读取并可视化四类图像数据的最小代码闭环3.1 环境准备与库选型rasterio、numpy、matplotlib 够不够处理这些遥感数据我常用的组合是rasterio、numpy、matplotlib再加一个osgeo.gdal作为备选。rasterio对 GeoTIFF 支持很好能直接读取地理坐标信息如果压缩包里给的是 ENVI 格式的.dat和.hdr则用spectral库更省事。安装命令如下pip install rasterio numpy matplotlib spectral opencv-python参数说明rasterio负责栅格读取spectral专用于高光谱数据ENVI 格式opencv-python在后面对 SAR 做滤波时用得上。建议在虚拟环境里装避免和系统 Python 包冲突。3.2 读取四种数据并统一到 numpy 数组下面这段代码的作用是把四种数据读成 numpy 数组同时打印元信息供你判断后续要不要做重采样或归一化。我用一个最小函数实现关键步骤都有注释。import rasterio import numpy as np from spectral.io import envi def load_vis_and_ir(vis_path, ir_path, sar_pathNone): 读取可见光和红外 GeoTIFF返回 (vis, ir) 的 numpy 数组。 如果 SAR 也是 GeoTIFF传入 sar_path 一并读取。 # 可见光通常是多波段R/G/B 或更多用 out_channels3 控制读前三个 with rasterio.open(vis_path) as src: vis src.read([1, 2, 3]) # shape: (3, H, W) vis_meta src.profile print(可见光 shape:, vis.shape, dtype:, vis.dtype) # 红外多半是单波段读取所有波段 with rasterio.open(ir_path) as src: ir src.read() # shape: (1, H, W) 或 (bands, H, W) print(红外 shape:, ir.shape, dtype:, ir.dtype) # SAR 图像常见 8bit 或 16bit 单通道直接读出来 if sar_path is not None: with rasterio.open(sar_path) as src: sar src.read(1) print(SAR shape:, sar.shape, dtype:, sar.dtype) else: sar None # 转成 float32方便后续计算避免 uint16 溢出 vis vis.astype(np.float32) ir ir.astype(np.float32) if sar is not None: sar sar.astype(np.float32) return vis, ir, sar, vis_meta # 调用示例路径替换成你自己的文件名 # vis, ir, sar, meta load_vis_and_ir(scene_01_vis.tif, scene_01_ir.tif, scene_01_sar.tif)这段代码的逻辑说明rasterio.open后src.read()返回的轴顺序是(波段, 高, 宽)这和opencv的(高, 宽, 通道)不一样做可视化时要用np.transpose调整到(高, 宽, 通道)。dtype检查很重要如果发现可见光是uint16而红外是float32归一化必须分开处理。高光谱用envi.open(hdr_path)读取返回对象可以直接用load_data()拿数组轴顺序是(行, 列, 波段)。3.3 高光谱数据读取与波段选择高光谱数据量大一个 100 波段以上的场景可能占几百 MB全部载入有可能内存溢出。我一般会先用envi.open读取元信息按需选取波段子集而不是一次性load_data。from spectral.io import envi import numpy as np def load_hsi_subset(hdr_path, band_indicesNone): 从 ENVI 格式的 .hdr 文件中读取高光谱数据可指定波段索引。 band_indices 为 None 时读取全部波段否则按索引切片。 img envi.open(hdr_path) # 传入 .hdr 文件路径 # 查看波长信息 wavelength img.metadata.get(wavelength, None) print(f波段数: {img.nbands}, 行: {img.nrows}, 列: {img.ncols}) if wavelength: print(f波长范围: {float(wavelength[0]):.1f} - {float(wavelength[-1]):.1f} nm) # 按需读取避免全量载入 if band_indices is None: data img.load() # 返回数组索引依赖 envipy 内部版本 else: data img.read_bands(band_indices) # 部分版本用这个 return np.array(data, dtypenp.float32) # hsi, wl load_hsi_subset(scene_01_hsi.hdr, band_indices[0, 10, 50]) # print(高光谱 shape:, hsi.shape, 波长:, wl)这里有个容易搞混的坑spectral库的load()返回的可能是Memmap或者自定义数组对象要用np.array()转成实际内存数组否则后续做reshape或输入网络会报错。read_bands在 0.21 之后的版本已改用read_bands更早版本用read_bands或load_data(bands(...))语法不同只用一种会报AttributeError。3.4 可视化检查如何在一张图里对比四种数据写完读取函数后第一步不是去做模型而是把四种数据可视化出来用肉眼确认空间范围和地物是否对得上。这里用matplotlib子图实现import matplotlib.pyplot as plt def visualize_four_modalities(vis, ir, hsi, sar, figsize(16, 4)): 并排展示可见光、红外、高光谱假彩色和 SAR 的灰度图。 fig, axes plt.subplots(1, 4, figsizefigsize) # 可见光如果是 3 波段直接转成 RGB多了就选 3 个代表性波段 vis_rgb np.transpose(vis, (1, 2, 0)) if vis.ndim 3 else vis # 值域拉伸到 0-1 便于显示注意这里只是可视化不是归一化 vis_rgb (vis_rgb - vis_rgb.min()) / (vis_rgb.max() - vis_rgb.min() 1e-8) axes[0].imshow(vis_rgb[:, :, :3]) axes[0].set_title(Visible) # 红外单通道显示为灰度 ir_2d ir[0] if ir.ndim 3 else ir axes[1].imshow(ir_2d, cmapinferno) axes[1].set_title(Infrared) # 高光谱假彩色合成波段区间自己选一般近红外-红-绿 if hsi is not None and hsi.shape[-1] 3: # 这里假设 hsi 是 (H, W, C)取中间三个波段 h hsi.shape[-1] // 2 hsi_rgb np.stack([hsi[:, :, h], hsi[:, :, h - 20], hsi[:, :, h - 40]], axis-1) hsi_rgb (hsi_rgb - hsi_rgb.min()) / (hsi_rgb.max() - hsi_rgb.min() 1e-8) axes[2].imshow(hsi_rgb) axes[2].set_title(HSI (FCC)) # SAR灰度显示注意做对数拉伸突出弱散射区域 sar_2d sar if sar.ndim 2 else sar[0] sar_display np.log1p(sar_2d - sar_2d.min() 1) # 避免负数取对数 axes[3].imshow(sar_display, cmapgray) axes[3].set_title(SAR) plt.tight_layout() plt.show()这段代码用的是 min-max 拉伸做显示不是用于训练的数据归一化。显示时拉伸的数值范围只是让眼睛看得清模型训练要的是相对的物理尺度一致性。肉眼检查时如果可见光和 SAR 的地物轮廓在同一像素位置严重错位说明几何校正没做后面要做配准处理。如果红外和高光谱的亮温或反射率看起来整体偏暗不要急着调对比度先检查原始数据里有没有填充值比如nan或0填充值需要掩膜处理而不是参与显示或计算。3.5 统一空间分辨率的实操用 rasterio.warp 重采样不同模态分辨率不一样必须统一到同一个网格上。常见做法是以分辨率最高的影像通常是可见光为基准用最近邻、双线性或三次卷积插值把其他模态重采样过来。我一般用rasterio.warp.reproject完成。from rasterio.warp import calculate_default_transform, reproject, Resampling def resample_to_target(src_path, ref_path, output_path): 将 src_path 影像重采样到 ref_path 影像的分辨率和范围。 ref_path 作为基准网格src 作为待配准影像。 with rasterio.open(ref_path) as ref: transform, width, height calculate_default_transform( ref.crs, ref.crs, ref.width, ref.height, *ref.bounds, resolutionref.res[0] ) kwargs ref.meta.copy() kwargs.update({ crs: ref.crs, transform: transform, width: width, height: height, count: 1 # 默认只重采样单波段 }) with rasterio.open(src_path) as src: with rasterio.open(output_path, w, **kwargs) as dst: for band in range(1, src.count 1): reproject( sourcerasterio.band(src, band), destinationrasterio.band(dst, band), src_transformsrc.transform, src_crssrc.crs, dst_transformtransform, dst_crsref.crs, resamplingResampling.bilinear ) print(f重采样完成: {src_path} - {output_path})参数说明calculate_default_transform的作用是根据参考影像的范围和分辨率算出新的仿射变换矩阵这样目标影像的像素网格和参考影像完全一致。Resampling.bilinear是双线性插值适合连续量纲的反射率和温度数据SAR 的后向散射可以换成Resampling.average做均值聚合能够抑制相干斑噪声。多光谱和高光谱做重采样时注意波段数量count要改成实际波段数循环逐波段写入不能省。4. 数据预处理进阶高光谱转反射率、SAR 去噪与多模态对齐4.1 高光谱如何转反射率辐射定标与大气校正的最小步骤拿到的高光谱数据可能是 DN 值数字量化值也可能是已经做过辐射定标的辐射亮度只有转换成反射率跨时间、跨传感器比较才有意义。“高光谱如何转反射率”是遥感领域被搜烂的问题但放在这个数据集的语境里关键是看数据说明里有没有提供定标系数。import numpy as np def dn_to_reflectance(dn_data, gain, offset, solar_zenith_degNone, sun_earth_distance_au1.0): 高光谱 DN 值转表观反射率。 gain 和 offset 来自数据说明文档或元数据形状可以是标量或数组。 solar_zenith_deg 是太阳天顶角如果数据说明里没有就设为 None。 # 第一步DN 转辐射亮度 radiance dn_data * gain offset # 第二步辐射亮度转表观反射率不需要大气参数 if solar_zenith_deg is not None: cos_sza np.cos(np.deg2rad(solar_zenith_deg)) esun np.ones_like(radiance) * 1.0 # 实际需要按波段查表这里占位 reflectance np.pi * radiance * sun_earth_distance_au**2 / (esun * cos_sza) else: # 没有太阳角度时只能做归一化不能叫物理反射率 reflectance radiance / radiance.max() print(警告: 缺少太阳天顶角输出的是归一化值而非真实反射率) return reflectance理解这段代码的关键是两层逻辑第一层是去除传感器本身的响应增益把 DN 值变成有物理单位的辐射亮度第二层是去掉太阳辐照度的影响把辐射亮度变成无量纲的反射率。数据说明里如果没有给太阳天顶角那只能在场景内部做归一化这时候你得到的数值不能跨景比较。真正的反射率还需要做大气校正常见做法是使用 6S 模型或 MODTRAN 查找表。如果数据集没有自带大气参数且不提供辅助文件你只能退而求其次或用SNAP软件的 Sen2Cor 功能做批量处理。处理高光谱 Transformer 模型输入时反射率数据和归一化光谱特征可以先并行走实际训练时优先用归一化版本。4.2 SAR 图像去噪为什么不能用高斯滤波SAR 图像有天然的多时相相干斑噪声看起来像撒了一层椒盐。针对这类特有问题常规的cv2.GaussianBlur会把边缘也抹掉在建筑边缘区域尤其明显导致后续分割或检测精度受损所以不建议直接套用。常见的做法是用增强 Lee 滤波或 Refined Lee 滤波。这里用opencv加scipy做轴对称滤波也可以用nansat这类专门库。import numpy as np from scipy.ndimage import uniform_filter def refined_lee_filter(sar_data, window_size7): 用增强 Lee 滤波抑制 SAR 相干斑噪声。 window_size 取值建议 5、7、9增大会更平滑但丢细节。 sar sar_data.astype(np.float64) # 计算局部均值和方差 mean uniform_filter(sar, sizewindow_size) mean_sq uniform_filter(sar**2, sizewindow_size) var mean_sq - mean**2 # 估算噪声方差用整个图像的 1/4 分位数近似 noise_var np.quantile(var, 0.25) # 权重系数噪声大的地方权重小 alpha np.maximum(var - noise_var, 0) / np.maximum(var, 1e-10) # 输出 局部均值 alpha * (原值 - 局部均值) filtered mean alpha * (sar - mean) return filtered.astype(np.float32)这段代码的核心思想是分而治之变化平缓的区域取局部均值边缘区域保留原值噪声尺度通过分位数估计自动调节。注意uniform_filter的size参数设置越大噪声压得越狠但边缘也会跟着糊。如果你的 SAR 数据是多时相的可以再做时间维度的多视处理效果通常比单张空间滤波更好。后续想接入深度学习模型做分割时建议把滤波后的 SAR 和原始 SAR 都保留——有些网络结构能自己学到去噪并且保留原始纹理对边缘检测是有效信息。4.3 多模态几何配准基于互信息的自动对齐方法几何配准是把红外和 SAR 对齐到可见光的空间坐标系。不同模态的成像几何差异导致同名点产生位移直接用rasterio重采样解决不了这个问题要用基于影像特征的配准。我一般用opencv的 ECC 算法或基于互信息的配准方法因为跨模态影像灰度差异大用 ORB/SIFT 提取特征点经常失败。import cv2 import numpy as np def align_image_to_reference(moving_img, ref_img, max_iterations100): 将 moving_img比如 SAR 或红外对齐到 ref_img可见光基准。 使用 ECC 算法适合跨模态灰度不一致的情况。 # 转成单通道 uint8ECC 要求输入是灰度 moving_gray cv2.normalize(moving_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) ref_gray cv2.normalize(ref_img, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8) # 初始变换平移 旋转用仿射变换 warp_matrix np.eye(2, 3, dtypenp.float32) criteria (cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, max_iterations, 1e-6) try: # MOTION_AFFINE 表示仿射变换平移、旋转、缩放、倾斜 cc, warp_matrix cv2.findTransformECC( ref_gray, moving_gray, warp_matrix, cv2.MOTION_AFFINE, criteria, None, 5 ) except cv2.error as e: print(ECC 配准失败检查输入尺寸是否一致:, e) return moving_img # 应用变换到原图 aligned cv2.warpAffine(moving_img, warp_matrix, (ref_img.shape[1], ref_img.shape[0]), flagscv2.INTER_LINEAR cv2.WARP_INVERSE_MAP) return aligned参数说明MOTION_AFFINE有 6 个自由度适合光学影像和 SAR 之间的小角度旋转和尺度变化如果是大幅度的投影差异需要先做粗校正再精配准。ECC 算法的默认迭代次数 100复杂地形区域可以加到 300但会变慢。如果配准后检查发现误差超过一个像素也可以换用基于互信息的配准思想通过最大化两幅影像的联合熵来求变换参数。配准完成后记得用掩膜把配准失败的区域例如 SAR 叠掩区标出来在训练时忽略这些区域避免给模型喂错误标签。5. 把这个数据集喂进深度学习框架组织方式与注意事项5.1 处理遥感大数据集时的训练集验证集切分原则这类数据集通常场景数不多但每个场景尺寸大。切分时不能随机把像素分配给训练和验证这样会造成严重的数据泄漏——同一景影像的相邻像素有大量冗余信息模型在验证集上虚高换成另一块区域就掉链子。正确的做法是“按场景切分”整个scene_01包含四种模态要么全进训练集要么全进验证集不能一个场景前 70% 训练后 30% 验证。import os import numpy as np import random def train_val_split_by_scene(file_list, val_ratio0.2, seed42): 按场景名称切分训练验证集返回两个列表。 file_list 是形如 [scene_01_vis.tif, scene_01_ir.tif, ...] 的完整文件路径列表。 # 提取场景编号假设文件名中都有 scene_XX 模式 scene_ids sorted(list(set([f.split(/)[-1].split(_)[1] for f in file_list]))) random.seed(seed) random.shuffle(scene_ids) val_count max(1, int(len(scene_ids) * val_ratio)) val_scenes set(scene_ids[:val_count]) train_files [f for f in file_list if f.split(/)[-1].split(_)[1] not in val_scenes] val_files [f for f in file_list if f.split(/)[-1].split(_)[1] in val_scenes] print(f训练场景数: {len(set([f.split(/)[-1].split(_)[1] for f in train_files]))}) print(f验证场景数: {len(val_scenes)}) return train_files, val_files这段代码提取scene_01的01作为场景 ID按场景 ID 去重切分。实际项目中你还需要考虑时间维度——同一个区域不同月份拍的两组影像如果都被分到训练集模型会学到时间上的记忆。多时相样本应该按采集日期进行嵌套切分比如单数日期训练、双数日期验证。5.2 多模态数据增强的坑旋转翻转要对所有模态同步做数据增强不能只对某一模态做否则模态之间的空间对应关系就被破坏了。比如可见光做了水平翻转红外也做了水平翻转但反向偏移了 5 个像素等于人为引入配准误差。我用一段代码说明正确的同步增强方式import numpy as np import random def sync_augment(vis, ir, hsi, sar, flip_prob0.5, rotate_deg90): 对四种模态做同步数据增强返回增强后的数据。 所有模态共用同一个随机种子保证空间变换完全一致。 # 统一随机状态 base_seed random.randint(0, 10000) # 水平翻转 if random.random() flip_prob: vi_ np.flip(vis, axis1) ir_ np.flip(ir, axis1) hsi_ np.flip(hsi, axis0) # 注意 hsi 是 (H,W,C) 或 (H,W) 看你自己的布局 sar_ np.flip(sar, axis0) else: vi_, ir_, hsi_, sar_ vis, ir, hsi, sar # 90 度旋转因为遥感影像一般不做任意角度旋转避免边界伪影 if random.random() 0.3: vi_ np.rot90(vi_, 1) ir_ np.rot90(ir_, 1) hsi_ np.rot90(hsi_, 1) sar_ np.rot90(sar_, 1) # 随机亮度扰动只对光学有效SAR 不能做亮度扰动 if random.random() 0.3: vis_gain random.uniform(0.8, 1.2) ir_gain random.uniform(0.9, 1.1) vi_ np.clip(vi_ * vis_gain, 0, 1) ir_ np.clip(ir_ * ir_gain, 0, 1) return vi_, ir_, hsi_, sar_这里关注的是不同数据类型的物理特性差异SAR 做亮度扰动会改变散射系数的含义一般不做高光谱的每个波段对亮度增益的响应不一样简单的乘法可能破坏光谱形状。如果你的模型输入是 3D 卷积光谱维空间维增强时可以考虑对光谱维做随机噪声注入模拟真实传感器的波段缺失。5.3 加载多模态数据的 PyTorch Dataset 写法写 Dataset 时要注意高光谱数据通常不能像可见光那样一次载入所有通道。我习惯把高光谱的波段先做个主成分降维把 100 个波段降到 3 到 5 个主成分再和红外、SAR 一起做通道拼接。这样网络输入小、训练快且不同模态的物理差异不会因为通道数悬殊而被稀释。import torch from torch.utils.data import Dataset import numpy as np from sklearn.decomposition import PCA class MultiModalDataset(Dataset): def __init__(self, vis_list, ir_list, hsi_list, sar_list, label_list, hsi_bands5): self.vis_list vis_list self.ir_list ir_list self.sar_list sar_list self.label_list label_list # 预先计算高光谱主成分模型避免每个样本都重复 fit self.pca PCA(n_componentshsi_bands) # 注意这里假设你已经把所有高光谱展平到了一个数组里 # 实际代码中应该在 __init__ 里做一次全量 fit def __len__(self): return len(self.vis_list) def __getitem__(self, idx): # 读取四个模态这里省略具体的 IO 代码 vis self.vis_list[idx] # 实际用 rasterio 读 ir self.ir_list[idx] hsi self.hsi_list[idx] # 预处理成 (H,W,C) 且已展平 sar self.sar_list[idx] label self.label_list[idx] # 高光谱 PCA 降维并归一化 h, w, c hsi.shape hsi_flat hsi.reshape(-1, c) hsi_pca self.pca.transform(hsi_flat).reshape(h, w, self.pca.n_components) # 归一化到 [0,1]按模态分别做 min-max不要全局做 vis_norm (vis - vis.min()) / (vis.max() - vis.min() 1e-8) ir_norm (ir - ir.min()) / (ir.max() - ir.min() 1e-8) sar_norm (sar - sar.min()) / (sar.max() - sar.min() 1e-8) # 通道拼接假设所有数据维度一致之前已重采样对齐 stacked np.concatenate([vis_norm, ir_norm, hsi_pca, sar_norm], axis-1) return torch.from_numpy(stacked).permute(2, 0, 1).float(), torch.tensor(label)这个Dataset的关键逻辑有三个一是高光谱降维放到__init__里用全量数据拟合 PCA不要在__getitem__中随机抽取样本拟合否则每个 batch 的降维方向都不同二是每个模态分开归一化避免量纲差异大的模态主导损失函数三是permute(2,0,1)把维度从(H,W,C)转成 PyTorch 期望的(C,H,W)。如果除了做分类还要做语义分割标签的读取方式和这里类似只是返回的标签也是二位数数组而不是标量。5.4 遥感模型训练时容易踩的坑边缘效应和类别不平衡模型在遥感大图上经常会出边缘效应——由于卷积的 padding 操作会引入边界伪影模型对图像边缘区域的预测明显变差。常见做法是在训练时做随机裁剪但裁剪时要留出一定的重叠区域。比如训练 patch 大小设为 256x256随机采样时让中心点在真实标注范围内避免裁剪窗口落到图像边界外。另外遥感数据的类别不平衡问题比自然图像严重得多比如水体、裸地的像素可能占整个图像的 80%其他地物占 20%直接用交叉熵损失会让模型把所有像素都预测成背景类。常见的做法是用加权交叉熵或 Dice Loss 与交叉熵组合还要注意按像素类别计算频率时要在 train split 上统计而不是在全体数据上统计否则验证集的类别分布会泄漏进训练过程。6. 数据集在训练后的验证技巧检查模型学到的模态特征是否合理模型训练完不是看一张准确率表就结束了要分别验证模型是否真的用上了每一种模态的信息还是退化成只用可见光、把红外和 SAR 当噪声处理了。一个简单有效的做法是模态消融实验在验证集上分别输入完整四模态、只输入可见光、只输入红外、只输入高光谱和只输入 SAR比较性能差距。如果加上红外没有带来任何指标提升你需要怀疑红外和可见光之间存在高度冗余模型并没有学到互补特征。import torch import numpy as np def modality_ablation(model, dataloader, device, modality_channelsNone): 模态消融测试验证模型对每种模态的依赖程度。 modality_channels 是一个字典例如 {vis: [0,1,2], ir: [3], hsi: [4,5,6,7,8], sar: [9]} 或根据你自己的拼接顺序定义从输入张量中切片。 model.eval() results {} # 首先测完整输入 full_preds, full_labels [], [] with torch.no_grad(): for batch in dataloader: x, y batch[0].to(device), batch[1].to(device) pred model(x) full_preds.append(pred.cpu().numpy()) full_labels.append(y.cpu().numpy()) full_preds np.concatenate(full_preds) full_labels np.concatenate(full_labels) # 假设是分类任务用准确率语义分割则换成 mIoU results[full] np.mean(full_preds.argmax(axis1) full_labels) # 逐个模态置零 for name, idx in modality_channels.items(): zero_preds [] with torch.no_grad(): for batch in dataloader: x, y batch[0].to(device), batch[1].to(device) x_zero x.clone() x_zero[:, idx, :, :] 0 # 置零指定通道 pred model(x_zero) zero_preds.append(pred.cpu().numpy()) zero_preds np.concatenate(zero_preds) results[fwithout_{name}] np.mean(zero_preds.argmax(axis1) full_labels) # 输出差异 for k, v in results.items(): print(f{k}: {v:.4f}) return results这段代码通过把输入张量的指定通道置零来模拟“模态缺失”。如果去掉某模态后指标显著下降说明模型确实依赖它如果反而上升说明该模态噪声大了考虑在预处理里替换消噪策略。需要注意的一点是置零测试只能反映模型在训练分布下的行为如果换一个传感器采集的数据集结论可能改变。验证后还要看模型的特征可视化。用 Grad-CAM 做热力图观察激活区域是否集中在有物理意义的轮廓上。红外模态的热力图应该集中在温度异常的工业设施或水体上SAR 应该集中在粗糙度差异大的建筑区或裸地。如果模型把注意力散落在随机位置上很可能是配准误差太大导致特征错位回到第 4 章的流程重新配准而不是继续调训练参数。另一个快速检查点是类别混淆矩阵的分布如果模型总是把某些地物混在一起且这些地物恰好是某一模态下纹理相似的类别可以考虑在输入权重分配上为那一模态增加权重。改动权重的方式不是简单地给通道乘一个系数而是将该模态的通道数增多做重复拼接或者引入一个注意力机制模块具体做法取决于你的网络结构。本文还有配套的精品资源点击获取
返回列表