3. 遥感图像数据读取与元数据解析

拿到一景遥感数据,真正决定后续预处理能否做对的,往往不是图像本身,而是它“附带的那堆说明”。同一幅 Landsat 影像,GeoTIFF 里可能只存了 DN 值和少量标签,而 HDF 里则把每个波段、定标系数、观测几何、质量波段分门别类放在不同数据集里。如果读错了波段顺序、漏掉了定标增益、忽略了填充值,后面的辐射定标和大气校正就会整体偏移。因此本节的核心任务是:认识常见格式的组织方式,学会从头文件和元数据中提取真正参与计算的字段,并把这些字段正确映射到波段数据上。

一、四种常见格式的组织差异

GeoTIFF 是最通用的栅格格式,本质是 TIFF 加上地理标签。它的波段通常按“波段交错”方式存储,常见三种布局:BSQ(按波段分块)、BIL(按行交错)、BIP(按像元交错)。读取时若把 BIP 当成 BSQ 解释,图像会出现规律性条纹。GeoTIFF 的地理信息放在 GeoKey 标签中,包括投影坐标系、仿射变换六参数、NoData 值等,用 GDAL 可直接读取。

HDF(HDF4/HDF5)是分层数据格式,MODIS、VIIRS 等产品大量使用。它像一棵目录树,根下挂多个数据集(Dataset)和属性(Attribute)。例如 MOD09GA 中,sur_refl_b01 到 sur_refl_b07 是七个反射率波段,state_1km 是质量状态位,SensorZenith 是观测天顶角。HDF 的波段不是“第几个”,而是“叫什么名字”,必须按数据集名称索引。

ENVI 格式由一对文件组成:数据文件(如 .img 或 .dat)加头文件 .hdr。头文件是纯文本,字段清晰,例如 samples、lines、bands、data type、interleave、byte order、map info、wavelength。ENVI 的优点是元数据可读可改,缺点是头文件与数据文件必须同名同目录,缺一不可。

PDS(行星数据系统)格式常见于月球、火星探测数据,采用“标签 + 数据”结构,标签为 ASCII 键值对,描述数据对象的行列数、采样方式、缩放因子等。PDS4 进一步用 XML 描述,读取时需先解析标签再定位数据块。

二、头文件与元数据字段的读取

无论哪种格式,读取流程都可以归纳为“先读元数据,再读数据”。以 ENVI 为例,用 Python 读取头文件的关键字段:

import numpy as np

# 解析 ENVI .hdr 头文件
hdr = {}
with open('LC08.hdr', 'r') as f:
    for line in f:
        if '=' in line:
            k, v = line.split('=', 1)
            hdr[k.strip().lower()] = v.strip()

samples = int(hdr['samples'])      # 列数
lines   = int(hdr['lines'])        # 行数
bands   = int(hdr['bands'])        # 波段数
dtype   = hdr['data type']         # 数据类型编码
interleave = hdr['interleave']     # bsq / bil / bip

# 按 BSQ 读取:形状为 (bands, lines, samples)
data = np.fromfile('LC08.img', dtype=np.float32)
data = data.reshape(bands, lines, samples)
print(data.shape, data[0].mean())  # 查看第一波段均值

这里 data type 是编码值,ENVI 中 1 表示 8 位无符号、2 表示 16 位有符号、4 表示 32 位浮点,必须查表转换,不能直接当字节数用。byte order 为 0 表示小端、1 表示大端,读错会导致数值完全错乱。

GeoTIFF 用 GDAL 读取更省事:

from osgeo import gdal
ds = gdal.Open('LC08_B4.tif')
gt = ds.GetGeoTransform()   # (x0, dx, 0, y0, 0, dy)
proj = ds.GetProjection()   # WKT 投影字符串
band = ds.GetRasterBand(1)
nodata = band.GetNoDataValue()
arr = band.ReadAsArray()

其中 GetGeoTransform() 返回的六参数定义了像元到地理坐标的仿射关系:x = x0 + col*dx + row*0,y = y0 + col*0 + row*dy。注意 dy 通常为负值,表示行号增大时纬度减小,这是栅格坐标与地理坐标方向相反的约定,全文保持一致。

三、波段组织与辅助数据的使用

多光谱数据常按波长分组。以 Landsat 8 OLI 为例,波段 1 到 7 为可见光近红外,波段 8 为全色,波段 9 为卷云,波段 10、11 为热红外。读取时要确认波段索引与物理波段的对应关系,不能默认“第 1 个就是蓝光”。HDF 产品则通过数据集名称区分,如 MODIS 的 sur_refl_b03 对应蓝光。

辅助数据同样关键。质量波段(QA)用位标志记录云、云影、雪、水体等状态,使用时需按位解析:

qa = ds_qa.ReadAsArray()
cloud_bit = (qa >> 10) & 1   # 示例:第 10 位表示云
clear = cloud_bit == 0
print('晴空像元比例:', clear.mean())

观测几何数据(太阳天顶角、方位角、传感器天顶角)用于大气校正和 BRDF 归一化。定标系数(增益 gain、偏移 offset)用于把 DN 转为辐亮度:L = gain * DN + offset。这些字段通常存放在元数据属性中,读取后应单独保存为变量,避免与图像数组混淆。

遥感数据读取与元数据解析流程 格式识别 GeoTIFF(标签+栅格) HDF(分层数据集) ENVI(.img + .hdr) PDS(标签+数据) 元数据字段解析 samples / lines / bands data type / byte order interleave(BSQ/BIL/BIP) geo transform / projection 数据读取与映射 按 interleave 重塑数组形状 波段索引 ↔ 物理波段对应 NoData / 填充值处理 定标系数 gain / offset 提取 辅助数据与质量信息 质量波段 QA 按位解析云、云影、雪、水体 cloud = (qa >> 10) & 1 生成晴空掩膜 clear_mask 观测几何 太阳天顶角 / 方位角 传感器天顶角 / 方位角 用于大气校正与 BRDF 归一化 定标参数 radiance = gain × DN + offset 反射率 = (L - Lp) / (Esun × cosθ) 系数来自元数据属性字段

四、读取结果的检查与判断

数据读进来后,不要急着做校正,先做三项检查。第一,看形状:data.shape 是否等于 (bands, lines, samples),若维度顺序不对,说明 interleave 判断有误。第二,看数值范围:16 位无符号数据应在 0 到 65535 之间,若出现负数或极大值,多半是 byte order 或 data type 读错。第三,看 NoData:统计等于 NoData 值的像元比例,若比例异常高,可能是填充值设置错误。

以 Landsat 8 为例,读取后可用 data[3].mean() 查看红光波段均值,正常景应在数千量级;若接近 0 或超过 60000,需回头检查定标和数据类型。对于 HDF 产品,还要确认数据集名称是否与文档一致,例如 MOD09GA 的 sur_refl_b01 不能写成 sur_refl_b1。

读取时建议把元数据字段单独存为字典或结构体,与图像数组分离管理。这样在后续辐射定标、大气校正中,只需引用字段名即可,避免硬编码数值导致换一景数据就出错。
ENVI 头文件中的 data type 是编码值而非字节数,byte order 为 0 表示小端、1 表示大端。这两个字段读错,图像数值会完全错误,且不一定报错,必须通过数值范围检查发现。

掌握格式差异、字段含义和波段映射关系后,读取就不再是“打开文件”这么简单,而是为后续每一步预处理建立正确的数据基础。元数据解析得越准确,辐射定标、几何校正和大气校正的误差来源就越少。