☰
国产卫星定标系数与光谱响应函数解析:从DN到反射率
2026/9/26 9:31:04 网站建设 项目流程

简介:国产卫星定标系数与光谱响应函数资料包,面向卫星遥感数据处理、GIS与地球观测应用场景,聚焦高分(GF)和资源(ZY)系列卫星,为开发者和科研人员提供辐射定标、大气校正及地表参数反演所需的关键参数。压缩包共93个文件,包含txt、json、doc、xls、xlsx、pdf等类型,其中txt和json文件对应各传感器光谱响应函数及定标数据,表格文件便于批量查阅和对比,doc/pdf收录历年国产陆地观测卫星绝对辐射定标系数文档,整体大小仅1.85MB。内容丰富且结构清晰,覆盖GF-1/2/4/6、ZY-02C/ZY-3、HJ-1A/B等型号,时间跨度从2011年到2019年,可配合6S辐射传输模型使用,有效提升遥感数据处理精度。目前已有2949人学习,适合遥感算法研究、生态环境监测及卫星数据应用开发等场景。

1. 拿到国产卫星定标系数和光谱响应函数.zip,先别急着解压就跑

做定量遥感的人第一次打开这个 zip 时,通常想的是赶紧把定标系数读出来算辐亮度。但真正让项目翻车的往往不是系数本身,而是参数表里的单位、公式版本和光谱归一化方式。定标系数解决的是 DN 值到物理辐亮度的换算,光谱响应函数(SRF)解决的是波段等效计算和传感器间光谱匹配,两个文件配合才能算出可靠的反射率产品。无论你是做地表参数反演、大气校正还是交叉定标,拿到包后先花十分钟搞清楚数据组织方式和有效期,比直接跑通一条命令更有价值。

2. 先搞懂这两类参数在矫正什么:从 DN 到辐亮度的链路和波段等效计算

2.1 绝对辐射定标:为什么 Gain 和 Offset 不能照搬别的卫星

定标系数是传感器把入瞳辐亮度转换成 DN 计数的桥梁。绝大多数国产卫星 L1 级产品给出的是线性定标关系:

L = Gain × DN + Offset

其中 L 为表观辐亮度,DN 为影像像元值。看起来很简单,但每个数据集的写法差异很大。有的给Gain和Offset,有的给Scale和Bias,还有的给C0和C1,甚至给的是辐亮度到 DN 的反向公式,用的时候要先把公式倒过来。单位也经常不一致,常见的有W/(m²·sr·μm)和mW/(m²·sr·μm),两者相差 1000 倍,如果不统一,算出的反射率不是零点几而是几百。

我拿到参数表后第一件事是整理一份单位对照,把包里的参数全部归一到国际单位制。下面是一个典型的核对表格,整理完再进代码:

参数名常见写法单位使用说明
辐射增益Gain / Scale / C1W/(m²·sr·μm)/DN乘以 DN 得到辐亮度
辐射偏移Offset / Bias / C0W/(m²·sr·μm)加到乘积结果上
中心波长Center_WLnm 或 μm用于波段标记,不参与加权计算
波段宽度FWHMnm辅助判断 SRF 覆盖范围
定标日期Cal_Dateyyyymmdd确认参数有效期

另外要注意,部分 CMOS 探测器或宽幅相机的定标关系不是线性的,包里可能给出的是二次多项式:

L = C0 + C1 × DN + C2 × DN²

这种公式常见于探测器响应存在压缩或非线性段的情况。如果文档里写了二次项,就别拿线性公式硬套,高亮目标(雪、云、盐碱地)计算值的偏差会比暗目标明显得多。我见过有人用线性公式处理 16 位影像的雪像元,反射率超过 1.6,后来核对发现包里的 C2 项根本没有进代码。

2.2 光谱响应函数:不是一条曲线那么简单

光谱响应函数描述的是传感器每个波段对不同波长能量的响应权重,横轴是波长,纵轴是归一化响应值。它解决两类问题:一是把地物光谱仪测到的高光谱数据换算到卫星波段,二是把不同传感器的波段做光谱匹配。计算公式是典型的加权平均:

ρ_band = ∫ ρ(λ) × SRF(λ) dλ / ∫ SRF(λ) dλ

关键在于分母的处理。很多 SRF 文件出厂时是按峰值归一化的,响应最大值等于 1,但这不是严格的归一化。如果做传感器间的等效计算,要让响应函数在波段范围内的积分等于有效带宽,也就是面积归一化。两者差多少取决于波段形状,矩形波段差距小,高斯型波段差距能到 5% 以上,对反演 Leaf Area Index 这类敏感参数影响很大。

判断包里 SRF 用的是哪种归一化,直接看文件末尾或说明文档中是否写了normalized to peak或area normalized。都不写的话,把整条曲线加起来看和带宽的关系,基本能猜出来。这一步很关键,因为后面做大气校正或模型模拟时,很多辐射传输模型要求特定格式的响应文件,归一化方式错了模型不会报错,只会给你偏小的等效反射率。

2.3 典型的 zip 包文件清单与命名规律

国产卫星的定标包没有统一标准,但组织方式大同小异。常见做法是按传感器分目录,目录下是每个波段的定标系数文本和光谱响应数据,附带一个 README 或 XML 描述文件。拿到包先不要散落解压到桌面,列一下文件结构:

文件类型常见扩展名内容说明
定标系数表.csv / .txt / .dat每波段一行,包含 Gain、Offset、有效期
光谱响应函数.csv / .txt / .srf每波段一列波长和响应值,或分文件存放
波段参数描述.xml / .json / .md中心波长、带宽、定标日期、公式版本
使用说明README / ReadMe公式约定、单位、更新记录

文件名通常有规律可循,比如WFV1_B1_Calibration.csv中的 WFV1 是传感器标识,B1 是波段号。先unzip -l看一眼清单再解压,能避免解压出一堆B1.txt之后分不清属于哪个载荷。把文件清单和 README 过一遍,对包内的公式版本、单位和有效期有数,才开始写解析脚本,这是省时间的做法,不是洁癖。

3. 用 Python 把 zip 里的参数吃到内存:解压、解析与定标计算

3.1 解压 zip 的三个前置动作:伪加密识别、GBK 文件名转码、目录规划

zip 文件看似普通,但遥感数据包经常有幺蛾子。先检查文件头是不是PK\x03\x04,有的下载渠道会把 zip 包做一层传输编码,拿到手是 base64 文本或者扩展名对不上的文件。还有一类叫 zip 伪加密,文件头标记了加密标志但其实没有加密内容,Windows 自带解压会直接要求输密码,而 7-Zip 有时能直接打开。下面是检查文件头和伪加密位的代码:

from zipfile import ZipFile import base64 import os def inspect_zip(path): with open(path, 'rb') as f: header = f.read(4) if header == b'PK\x03\x04': print("正常 ZIP 文件头") else: content = open(path, 'rb').read() try: decoded = base64.b64decode(content) if decoded[:4] == b'PK\x03\x04': print("检测到 base64 包裹的 ZIP,尝试还原...") with open(path.replace('.zip', '_decoded.zip'), 'wb') as f: f.write(decoded) except Exception as e: print("不是标准 ZIP,也不是 base64 包裹,注意核对下载来源") with ZipFile(path if header == b'PK\x03\x04' else path.replace('.zip', '_decoded.zip')) as zf: for info in zf.infolist(): flag_encrypted = info.flag_bits & 0x1 if flag_encrypted and not info.flag_bits & 0x2: print(f"伪加密嫌疑: {info.filename}")

代码先读文件前四个字节判断真实格式,再遍历 zip 内的文件条目检查通用标志位。伪加密的特征是第 0 位(加密标志)置 1,但第 1 位(压缩标志)为 0,这种文件用 Python 的 ZipFile 有时能直接读出内容,用 WinRAR 解压却要密码。参数说明:flag_bits & 0x1是加密位,& 0x2是压缩方式位,两者组合起来能区分真加密和伪加密。这段脚本不修改原始文件,只输出诊断信息,解压动作留给自己决定。

3.2 解析定标系数表:读 CSV 并把 Gain/Offset 组织成字典

定标系数表通常是 CSV 或空格分隔的文本,带注释行和表头。解析时要注意分隔符可能是逗号、制表符或连续空格,还有文件编码可能是 GBK 而不是 UTF-8,在 Linux 上直接用open()读会出现乱码。下面写一个兼容分隔符和编码的解析函数:

import csv import chardet def load_calibration(path): # 先检测编码,避免 GBK 文件被当作 UTF-8 读取 with open(path, 'rb') as f: raw = f.read(4096) encoding = chardet.detect(raw)['encoding'] or 'utf-8' print(f"检测到编码: {encoding}") calib = {} with open(path, 'r', encoding=encoding) as f: reader = csv.reader(f, delimiter=None) for row in reader: row = [r.strip() for r in row if r.strip()] if not row or row[0].startswith('#'): continue # 期望行格式: band, gain, offset, cal_date try: band, gain, offset = row[0], float(row[1]), float(row[2]) calib[band] = {'gain': gain, 'offset': offset} except ValueError as e: print(f"跳过无法解析的行: {row}, 原因: {e}") return calib # 使用示例 cal = load_calibration('WFV1_B1_Calibration.csv') print(cal)

delimiter=None让 csv 模块自动识别逗号、Tab 或空格分隔,减少手工处理。编码检测用chardet,它在中文遥感数据文件上准确率不错。参数说明:函数把波段名作为字典键,方便后续按波段索引;注释行统一跳过。如果文件里 Gain 和 Offset 的单位不是国际单位制,在这个函数里顺手除 1000 换算掉,后续计算就不用处处提心吊胆。

3.3 从 DN 到表观反射率:一条命令算完整链路

有了定标系数,下一步是把原始 DN 影像换算成表观反射率(TOA Reflectance)。辐亮度到反射率的公式是:

ρ_TOA = π × L × d² / (ESUN × cos(θ_s))

其中 d 是日地距离因子(天文单位),θ_s 是太阳天顶角,ESUN 是波段平均太阳辐照度。ESUN 一般由太阳光谱曲线与 SRF 加权积分得到,有的定标包里直接给了每个波段的 ESUN 值。下面是从辐亮度到反射率的批处理:

import numpy as np from osgeo import gdal def dn_to_reflectance(dn_path, calib_band, esun, sza_deg, d_au): ds = gdal.Open(dn_path) dn = ds.ReadAsArray().astype(np.float32) gain = calib_band['gain'] offset = calib_band['offset'] radiance = dn * gain + offset # 太阳天顶角转弧度,计算余弦 cos_theta = np.cos(np.deg2rad(sza_deg)) # 日地距离因子 d_au 由成像日期决定,可查表或天文公式计算 reflectance = (np.pi * radiance * d_au**2) / (esun * cos_theta) return np.clip(reflectance, 0, None) # 示例参数,ESUN 以 W/(m²·sr·μm) 为单位 rho = dn_to_reflectance('scene_B1.tif', cal['B1'], esun=1823.5, sza_deg=42.3, d_au=0.983)

参数说明:sza_deg从影像头文件或元数据里读,d_au可以按儒略日近似计算,1 月初约 0.983,7 月初约 1.016。np.clip把负值清零,避免大气校正前出现物理上不可能的结果。如果算出来的反射率普遍大于 1,不要急着怀疑代码,先回头检查定标系数的单位和 ESUN 是否对应同一套单位制。这一步是定性遥感的第一道坎,错了后面所有产品都不可信。

4. 把光谱响应函数重采样成波段模拟器:插值、归一化与模型输入

4.1 先做数据体检:波长单位、采样间隔、响应范围

SRF 文件的格式比定标系数更乱。有的波长列用纳米,有的用微米;有的曲线等间隔采样,有的在带肩处加密采样;有的响应范围正好覆盖半高宽附近,有的拖尾到很远。直接把原始曲线喂给插值函数,很可能得到错误结果。先写一个体检脚本,把每个波段的波长范围和采样点数打出来:

import pandas as pd def inspect_srf(path): # 读取 SRF 文件,假设列格式为: wavelength, response df = pd.read_csv(path, comment='#', sep=None, engine='python') wl = df.iloc[:, 0].values resp = df.iloc[:, 1].values # 判断波长单位,若最大值小于 100 则视为微米,转成纳米 if wl.max() < 100: wl = wl * 1000 print("波长已从微米转换为纳米") sr = pd.Series(resp, index=wl).sort_index() print(f"波长范围: {sr.index.min():.1f} - {sr.index.max():.1f} nm") print(f"采样点数: {len(sr)}") print(f"响应峰值: {sr.max():.4f}") print(f"响应积分: {sr.sum():.4f} (采样间隔 {sr.index.to_series().diff().median():.3f} nm)") return sr srf = inspect_srf('WFV1_B2_SRF.csv')

参数说明:seq=None会触发自动分隔识别,对 CSV 够用。判断波长单位用的是经验规则,纳米制波长一般到 2500,微米制一般不超过 30。响应积分值除以采样间隔可以估算有效带宽,如果响应最大值明显大于 1,说明文件可能不是归一化曲线,而是原始量子效率,需要先除以最大值。这一步把后续插值、归一化的数据质量问题提前暴露掉。

4.2 重采样到 1nm 网格并做归一化

不同来源的 SRF 采样间隔不同,做波段模拟前统一到 1nm 网格最省事。1nm 网格量级适中,既能保留曲线形状,又不会让积分计算太慢。重采样用线性插值即可,不需要高阶拟合,因为 SRF 本身是测量曲线,高频抖动多半是噪声。归一化要区分场合:峰值归一化用于展示曲线形状,面积归一化用于辐射计算。下面代码同时输出两种结果:

import numpy as np def resample_srf(srf, wl_grid=np.arange(400, 2501, 1)): # srf 是 pandas Series, index 为波长,值为响应 # 去掉重复波长并排序,避免插值报错 srf = srf[~srf.index.duplicated()].sort_index() # 线性插值到标准网格,超出原范围的填 0 resp_interp = np.interp(wl_grid, srf.index.values, srf.values, left=0, right=0) # 峰值归一化: 除以最大值 resp_peak_norm = resp_interp / resp_interp.max() # 面积归一化: 使曲线积分等于 1,即等效带宽为 1nm resp_area_norm = resp_interp / np.sum(resp_interp) return wl_grid, resp_peak_norm, resp_area_norm wl, srf_peak, srf_area = resample_srf(srf) print(f"峰值归一化积分: {srf_peak.sum():.2f} nm") print(f"面积归一化积分: {srf_area.sum():.6f} nm^-1")

参数说明:np.interp的left=0, right=0参数让插值范围外补零,避免外推带来的虚假响应。峰值归一化适合做曲线对比图,面积归一化适合做波段等效计算。积分结果可以自己验证:峰值归一化后曲线求和约等于有效带宽,面积归一化后求和等于 1。如果两个结果差异超过 5%,说明曲线形状偏离矩形较多,波段模拟时必须用面积归一化。

4.3 用 SRF 计算波段等效值:把高光谱数据折到传感器波段

有了归一化后的 SRF,就可以把野外地物光谱、机载高光谱影像或辐射传输模型输出换算到传感器波段。原理就是前面公式的离散实现,关键是保证波长网格对齐。这里写一个通用函数:

def band_equivalent(wl_spec, spec_values, wl_srf, srf_resp): """ 将高光谱数据与 SRF 卷积,得到传感器波段等效值 wl_spec: 高光谱数据的波长数组 (nm) spec_values: 对应波长的光谱值 wl_srf: 重采样后的 SRF 波长 srf_resp: 面积归一化的 SRF 响应 """ # 将高光谱数据插值到 SRF 网格,保证逐点相乘 spec_interp = np.interp(wl_srf, wl_spec, spec_values, left=np.nan, right=np.nan) valid = ~np.isnan(spec_interp) numerator = np.sum(spec_interp[valid] * srf_resp[valid]) denominator = np.sum(srf_resp[valid]) return numerator / denominator # 示例:假设野外实测光谱 reflectance 是 1nm 间隔数组 band_ref = band_equivalent(np.arange(400, 901, 1), field_spec, wl, srf_area) print(f"波段等效反射率: {band_ref:.4f}")

参数说明:函数把高光谱数据插值到 SRF 的波长网格再做加权平均,保证两条曲线逐点对应。valid掩码处理高光谱范围不覆盖 SRF 的情况,避免乘积出现 NaN。面积归一化后的 SRF 在这里做分母归一,等价于公式里的积分归一。这个函数可以用在多个场景:验证光谱指数、模拟不同传感器的波段差异、把地面光谱仪数据与卫星影像对比。唯一的坑是波长单位必须一致,传参前统一转成纳米。

4.4 给 6S/FLAASH 生成 SRF 响应数据文件

大气校正模型,比如 6S、MODTRAN 和 FLAASH,都要求输入传感器的光谱响应函数。各家格式略有不同,但共同点是接受两列的波长-响应文本。生成这类文件其实就是格式化输出,注意波长单位与模型要求一致。下面是输出 6S 响应的典型写法:

def write_srf_for_model(wl, srf_resp, output_path, unit='nm'): if unit == 'um': wl_out = wl / 1000.0 else: wl_out = wl with open(output_path, 'w') as f: f.write("* Wave_Length Response\n") for w, r in zip(wl_out, srf_resp): f.write(f"{w:.4f} {r:.6f}\n") print(f"SRF 文件写入: {output_path}, 共 {len(wl)} 行") write_srf_for_model(wl, srf_area, 'WFV1_B2_6S.srf', unit='nm')

参数说明:unit='nm'和unit='um'控制是否做微米换算,6S 的太阳光谱文件通常以微米为单位,FLAASH 则偏好纳米。输出格式不做过多修饰,保持空格分隔的纯文本。写完后人工抽查头尾几行,确认波长从低到高排列,响应值都在 0 到 1 之间。文件生成后可以先跑一次 6S 的测试模式,看看输出透过率曲线是否平滑,如果出现振荡,多半是 SRF 尾部有毛刺,需要对原始曲线做平滑或截断处理。

5. 用定标包最容易翻车的 5 个坑:现象、原因与排查办法

5.1 zip 伪加密:双击能打开但解压就报错,要求输密码

现象:在 Windows 上双击 zip 文件能看到里面的文件列表,但右键解压时提示需要密码,或者用 WinRAR 打开后文件带星号锁。用 7-Zip 有时却能正常解压,行为表现不一致。

原因:这类文件多半是 zip 伪加密,即文件头的通用位标志里加密位被置 1,但数据区并没有真正加密。产生原因可能是数据打包工具写错了标志位,也可能是人为设置防止直接预览。Python 的 zipfile 模块对伪加密容忍度较高,能直接读出内容。

解决:优先用 7-Zip 尝试解压,它默认忽略部分伪加密标志。如果 7-Zip 也不行,用前面代码里的flag_bits检查脚本确认加密标志和压缩标志的组合,确认是伪加密后用 PythonZipFile直接extractall(),一步到位。记住不要到处找密码,方向基本是错的。

5.2 Linux 下中文文件名乱码与解压路径失控

现象:在 Ubuntu 上用unzip解压后,文件名变成�之类的乱码,进入目录后找不到对应文件。更糟的情况是解压出带绝对路径的条目,文件散落到/tmp或其他目录。

原因:zip 里的文件名是 GBK 编码,而 Linux 的unzip默认按 UTF-8 解码,两者冲突。部分打包工具还把路径写成了绝对路径,比如/home/user/data/xxx,解压时直接按照绝对路径落地,脱离了当前目录。

解决:检查 zip 内容用什么编码,zipinfo -v看不了编码,但可以用 Python 打印info.filename.encode('cp437').decode('gbk')验证。解压时用unzip -O gbk指定编码,如果系统unzip版本不支持-O参数,写个 Python 脚本遍历infolist(),先转码再提取,同时过滤掉以/开头的条目,把路径重新映射到当前目录。这一步能同时解决乱码和路径失控两个问题。

5.3 把二次多项式定标公式当线性用,高亮区反射率失真

现象:同一景影像上,暗像元反演结果正常,但云和雪像元表观反射率超过 1.2,且与相邻传感器同期观测偏差明显。检查代码,Gain 和 Offset 代入了线性公式,一切看起来都对。

原因:定标系数包更新后采用了非线性定标模型,文档里写的是 L = C0 + C1 × DN + C2 × DN²,其中 C2 是负值,在大信号区压缩辐亮度。代码只取了 C0 和 C1,相当于忽略了 C2 × DN² 项,DN 越大偏差越大,1 万以上的 DN 值可差 10% 以上。

解决:解析定标表时不要硬编码两个字段,先读 README 确认公式形式。如果是多项式,把 C2 加进去,并让解析函数返回公式类型字段。另一个自检手段是画 DN-辐亮度散点图,如果中高 DN 段出现明显曲率,说明还有二次项没进代码。不要觉得多项式少见就不写,近两年新发射的传感器不少采用这种模型。

5.4 SRF 归一化方式用错,模拟反射率整体偏低

现象:用国产卫星 SRF 模拟 MODIS 波段反射率,结果比 MODIS 官方波段反射率系统性低 3%~6%,又不是明显的云或气溶胶影响,找不出原因。

原因:包里 SRF 是峰值归一化,最大值等于 1,但直接用它做波段等效计算,相当于默认把曲线面积积分当成 1。实际带宽是 60nm,峰值归一化后的曲线积分也是 60nm,但正确的面积归一化要把每个响应值除以积分宽度。两种归一化下,对平坦光谱差别不大,对植被这类陡坡光谱差别明显。

解决:在重采样函数里同时输出峰值归一化和面积归一化两种版本。做波段模拟、辐射传输、交叉定标时全部用面积归一化版本;只有画曲线对比图时用峰值归一化。代码里加一段断言,检查np.sum(srf_area) == 1,不满足就报错提醒。

5.5 用过期的定标系数处理跨年影像,把增益漂移当成地表变化

现象:同一地区 2022 年和 2023 年的同一传感器影像,反演地表反射率出现系统性偏移,裸地反射率年际差超过 0.04,而且偏移方向与定标系数更新方向一致,不是随机噪声。

原因:星上定标器会逐渐衰减,传感器的增益不是恒定的,所以遥感数据集会定期重新定标,发布新的系数表。如果拿旧的系数算新影像,等于把传感器的物理衰减直接混入地表信号。国产卫星定标系数的更新频率通常以季度或年度为单位。

解决:从包或数据集的元数据里提取定标系数的生效日期,按影像成像时间匹配对应版本。处理时间序列影像时,不要把系数当成常数,每次都检查目标影像的日期落在哪个系数有效期内。归档时把系数文件按日期命名,不要只存最后一个版本。这个习惯能省掉后续很多重算的时间和尴尬。

6. 最后一道工序:用参考传感器交叉比对验证定标参数

参数包好不好用,跑通流程只算完成了 80%,剩下 20% 是验证。我现在处理新拿到的定标系数,习惯性找一个同期过境的参考传感器做交叉比对。Landsat-8/9 OLI 光谱分辨率高,定标稳定,常被当作参考基准。做法是选一景晴空影像,找一块大面积均匀目标区,清洁水体或裸地都可以,分别用各自定标参数计算 TOA 反射率,再做 SRF 光谱匹配,看差值是否在合理范围内。

一个最小验证脚本是取两景影像同名点的 TOA 反射率,用第 4.3 节的波段等效函数把参考传感器模拟成目标传感器波段,然后计算差值:

def validate_toa(rho_target, rho_ref_simulated, tolerance=0.02): diff = rho_target - rho_ref_simulated print(f"目标波段反射率: {rho_target:.4f}") print(f"参考模拟反射率: {rho_ref_simulated:.4f}") print(f"差值: {diff:+.4f}") if abs(diff) < tolerance: print("通过:光谱匹配后差值在合理范围") else: print("注意:差值超出预期,检查定标系数有效期和 SRF 归一化")

如果差值是随机正负波动,多半是 BRDF 或气溶胶差异;如果所有波段系统性偏亮或偏暗,就要回到定标系数的单位和公式版本上。我吃过一次亏,拿到包后没看公式版本,直接用线性模型算了一整景影像,发现水体反射率 0.11,比实测高了 0.03,排查半天才发现是二次项缺失。从那以后,我每次解压后先写三行脚本把 README 里的公式、单位、有效期打印出来贴在处理脚本头部,当作参数包的使用声明。这个习惯让我少走了很多弯路,也希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询