ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

使用Python实现遥感影像绝对辐射定标全流程

使用Python实现遥感影像绝对辐射定标全流程 简介面向遥感与Python初学者的绝对辐射定标实战资源包以GDAL库为核心讲解如何将遥感图像原始灰度值转换为具有物理意义的辐射值如地表反射率或亮度温度适用于环境监测、气候变化、资源调查等领域的图像预处理教学与科研入门。包内共9个文件包括1个Python主脚本、1份JSON定标参数文件、5个XML工程配置文档及若干项目辅助文件整个压缩包仅16KB结构清晰便于直接阅读。主脚本演示了从打开遥感图像、解析JSON定标参数、逐波段读取像元灰度值、计算辐射校正值到写出新图像的完整流程JSON中提供了增益、暗电流等典型定标系数XML文件则记录了工程配置和依赖信息方便在IDE中还原项目环境也能帮助读者理清不同文件在定标工作中的分工。目前已有902人学习或下载特别适合需要亲手实践辐射定标步骤、理解GDAL栅格处理逻辑并快速迁移到自身实验中的入门者。1. 绝对辐射定标从 DN 到辐亮度这一步不做后面全是白算做遥感应用的人迟早会在某个项目里碰一次壁下载回来的影像打开后是一片灰蒙蒙的数字直方图挤在 0 到几千的区间里直接拿去算 NDVI出来的结果不是大片零值就是异常高值。原因几乎都指向同一件事——没有做绝对辐射定标。绝对辐射定标解决的是把传感器记录的 DN 值无量纲量化值转换成有物理意义的辐亮度或反射率的问题它是任何定量遥感任务真正意义上的起点。本文面向正在做地表分类、植被反演或者深度影像预处理的一线开发者和研究生用 Python 把从 DN 到反射率的全流程拆开讲包括元数据读取、公式选择、参数陷阱和验证方法。看完你应该能独立处理 Landsat、哨兵2这类常用的中分辨率影像并且知道怎么确认自己的结果没算错。很多教程喜欢直接甩一句“辐射定标就是把 DN 转成反射率”然后贴一段代码。但真正上手你就会发现卡住你的永远不是那三行公式而是元数据里的参数到底怎么取、反射率和辐亮度该选哪个、以及公式里的太阳角度到底用 sin 还是 cos。这篇文章不回避这些绊脚石我们一步步把它们踩平。2. 辐射定标的数学基础两条公式、三个物理量先别写代码2.1 辐亮度和反射率定标到底在换算什么卫星传感器记录的是地物反射或发射的电磁波能量经过大气、传感器光学系统、光电转换后的一个数字量化值这个值就是 DNDigital Number。它和真实的物理量之间有一个近似线性的响应关系把这个线性关系还原出来的过程就是辐射定标。定标后你会得到两种最常见的物理量辐亮度Radiance和反射率Reflectance。辐亮度的单位是 W·m⁻²·sr⁻¹·μm⁻¹代表传感器入瞳处单位面积、单位立体角、单位波长间隔内接收到的辐射能量反射率是地物反射能量与入射能量的比值无量纲取值范围理论上在 0 到 1 之间。这两个物理量服务于不同场景。如果你要做地表温度反演、火焰探测或者能量平衡分析你需要的是辐亮度因为它直接对应传感器接收到的绝对能量如果你要做植被指数、土地覆盖分类或者机器学习样本制作你需要的是反射率因为它消除了太阳辐照度和太阳高度的影响让不同时相、不同季节的影像可以相互比较。记住这个选择逻辑后面就不会在输出文件里存错变量。反射率又分表观反射率Top of Atmosphere Reflectance也叫大气顶部反射率和地表反射率Surface Reflectance。绝对辐射定标直接得到的是表观反射率因为它只做了传感器响应修正没有去掉大气的吸收和散射影响。大气校正则是把表观反射率进一步变成地表反射率。很多人把这两个步骤混为一谈用没做大气校正的影像直接和地面实测光谱比误差随气溶胶和水汽含量波动最高可以相差 20% 以上。2.2 增益-偏置法与查表法两种产品两套代码辐射定标最常见的实现方式是增益-偏置法公式是 L gain × DN offset其中 gain 就是定标系数里的增益项offset 是偏置项。不同卫星产品对这两个参数的叫法不同Landsat 8/9 的 MTL 文件里写作 RADIANCE_MULT_BAND_x 和 RADIANCE_ADD_BAND_x哨兵2号的 Level-1C 产品则在 XML 里直接给出了一个反射率缩放因子用户只需要把 DN 除以 10000 就能得到表观反射率。这就是为什么网上抄来的代码换个数据源就完全跑不通——参数名字变了公式结构也变了。还有一种做法是查表法适用于那些传感器响应不完全线性、或者官方没有公开增益偏置系数的数据源。查表法一般用在航空高光谱或者一些商业卫星上通过一个查找表LUT把 DN 映射到辐亮度。对于日常处理 Landsat 和 Sentinel-2增益-偏置法就够了不需要自己建 LUT。需要注意的是Landsat 从 Collection 1 升级到 Collection 2 之后定标系数、几何精度、元数据格式都有变化如果你的代码还是按 C1 时代的脚本跑 C2 数据结果会明显偏暗或者偏亮这个坑后面会专门展开。2.3 为什么先做一个 mini 辐射定标实验再上批量我曾经接过一个模拟项目X需要一次性处理同一个区域跨三年的 80 景影像。当时图省事直接套了一年前写的脚本批量跑结果第三年的数据在 NDVI 直方图上整体偏移了 0.05 左右最后排查了整整两天才发现是当年的存档数据里混入了两个不同版本的产品定标系数来源不一致。血的教训是批量处理之前一定要先拿一小块影像手工验证公式、参数和单位。mini 实验的做法很简单选一景影像裁剪出 100×100 像元的区域用 Excel 或者手写计算器把其中 3 个像元从 DN 手工换算到反射率然后再用 Python 脚本跑一遍对比结果。这样做的成本只有十分钟却能帮你提前暴露 90% 的公式写错、参数取错、单位弄反的问题。别嫌这一步多余它才是整个定标流程里性价比最高的一环。3. 定标前的数据准备从元数据里把参数抠出来3.1 常见影像的元数据文件长什么样元数据是辐射定标的参数来源拿到影像后第一件事不是打开栅格而是打开和它放在一起的文本或 XML 文件。以最常见的 Landsat 8/9 OLI 产品为例每景影像解压后都会有一个以 _MTL.txt 结尾的文本文件里面以 KEY VALUE 的形式记录了成像时间、太阳高度角、太阳方位角、辐亮度增益/偏置、反射率增益/偏置等信息。哨兵2号 Level-1C 产品则是一个 .SAFE 文件夹里面嵌套着 MTD_MSIL1C.xml反射率缩放因子在 XML 的 BOA 或 TOA 相关节点里。不同产品需要提取的参数不完全一样但核心就是下面这张表。建议把它打印出来贴在工位上每次换数据源时对照检查。参数Landsat 8/9 MTL 字段名Sentinel-2 L1C XML 字段名用途辐亮度增益RADIANCE_MULT_BAND_x无官方不推荐转辐亮度DN 转辐亮度辐亮度偏置RADIANCE_ADD_BAND_x无DN 转辐亮度反射率增益REFLECTANCE_MULT_BAND_x无DN 转表观反射率反射率偏置REFLECTANCE_ADD_BAND_x无DN 转表观反射率太阳高度角SUN_ELEVATIONSun_zenith_angle或由天顶角换算反射率公式修正项日地距离EARTH_SUN_DISTANCE无官方产品已内置反射率公式修正项通用缩放因子无Quantification value通常为 10000DN 转反射率3.2 用正则表达式从 MTL 里提取定标系数MTL 文件是纯文本格式最稳妥的提取方式是用正则表达式按字段名抓取。我一般习惯写一个独立的解析函数返回一个字典这样后面不管做单景还是批量代码都能复用。下面这段代码针对 Landsat 8/9 的 MTL 文件核心思路是把所有 GROUP 块忽略掉直接按字段名匹配。import re from pathlib import Path def parse_mtl(mtl_path): 从 Landsat MTL 文本中解析定标相关参数返回字典 text Path(mtl_path).read_text(encodingutf-8, errorsignore) params {} # 匹配形如 KEY VALUE 的行只取我们需要的关键字段 # 注意MTL 里存在 GROUP 嵌套但字段名全局唯一直接全局匹配即可 keys [ SPACECRAFT_ID, SENSOR_ID, SUN_ELEVATION, EARTH_SUN_DISTANCE, RADIANCE_MULT_BAND_4, RADIANCE_ADD_BAND_4, REFLECTANCE_MULT_BAND_4, REFLECTANCE_ADD_BAND_4 ] for key in keys: pattern rf^{key}\s*\s*([^\s]) # 行首精确匹配避免读到其他值 m re.search(pattern, text, re.MULTILINE) if m: params[key] m.group(1) # 数值字段统一转成 float方便后面运算 for key in params: try: params[key] float(params[key]) except ValueError: pass # 非数值字段保留字符串比如 SPACECRAFT_ID return params mtl parse_mtl(LC08_L2SP_123032_20241010_20241010_02_T1_MTL.txt) print(mtl[REFLECTANCE_MULT_BAND_4], mtl[SUN_ELEVATION])这段代码里最需要注意的是正则的写法。^SUN_ELEVATION限制了匹配必须从行首开始防止误匹配到其他包含相同子串的字段REFLECTANCE_MULT_BAND_4这类带波段号的字段如果后续要处理全波段可以用 for 循环批量生成 key。提取后统一转 float 是一个容易忽略的细节——如果你拿着字符串去计算Python 会直接抛 TypeError而如果你手动转了一半留了一半计算结果会变得非常奇怪且难以排查。3.3 数据组织与检查清单元数据提取之后不要急着写计算代码先用清单检查一遍数据完整性。我通常的做法是把所有待处理的影像按“产品版本号/获取日期/景号”三级目录组织定标脚本只接收目录路径这样做的好处是当 80 景影像处理到第 63 景报错时你能立刻知道是哪一天、哪一个产品出的问题。检查清单包括影像波段数和元数据波段数是否一致MTL 里是否存在 NaN 或 0 值太阳高度角是否落在合理范围内正午成像一般 40° 到 70°如果出现 0° 或 90° 基本可以断定元数据解析错了日地距离是否在 0.983 到 1.017 这个范围之间。还要检查一个容易被忽略的项元数据版本。Landsat C2 产品的 MTL 里会有 COLLECTION_NUMBER 字段如果你发现同一批次数据里混了 C1 和 C2立刻停手先把数据按版本分拣再继续。版本混用造成的误差不会直接报错而是让你在后续所有定量结果里都背着一个隐蔽的系统误差这种错误最难发现最好在入口处就杜绝。4. 用 Python 实现绝对辐射定标从 DN 到表观反射率的完整流程4.1 用 rasterio 打开影像并构造输出数据集完成元数据解析后进入真正的定标计算环节。我推荐用 rasterio 做这件事它是对 GDAL 的 Python 封装读写 GeoTIFF 很方便而且直接支持 float32 输出。环境安装建议用 conda 一次性装好避免源码编译 GDAL 的折腾conda create -n radiance python3.11 conda activate radiance conda install -c conda-forge rasterio gdal numpy打开影像时有一个关键点先确认像素数据类型。大多数原始 DN 影像都是 uint16而定标公式里的增益和偏置是 float直接把 uint16 数组和 float 标量相乘时numpy 的自动类型提升会生成 float64这在内存上直接翻倍。对于一景 Landsat 全幅影像约 8000×8000 像元、7 个波段float64 会占掉将近 3.5GB 内存很容易把笔记本拖垮。正确的做法是读取时就用 rasterio 设置 dtype 为 float32import rasterio from rasterio.enums import Resampling def load_band_as_float32(band_path): 读取单波段 DN 影像强制转换为 float32 返回 with rasterio.open(band_path) as src: # 关键参数out_dtypefloat32 在读取阶段完成类型转换 # 比读取后再 astype 省一笔中间内存开销 return src.read(1, out_dtypefloat32) # 示例读取红波段Landsat 8 第 4 波段 red_dn load_band_as_float32( LC08_L2SP_123032_20241010_..._SR_B4.TIF ) print(读取完成dtype , red_dn.dtype, shape , red_dn.shape)from pathlib import Path def check_metadata_complete(mtl_dict, required_keys): 确认元数据里的关键字段没有缺失缺失时提前暴露错误 missing [k for k in required_keys if k not in mtl_dict] if missing: raise ValueError(f元数据缺少字段: {missing}) return True required [RADIANCE_MULT_BAND_4, RADIANCE_ADD_BAND_4, REFLECTANCE_MULT_BAND_4, REFLECTANCE_ADD_BAND_4, SUN_ELEVATION, EARTH_SUN_DISTANCE] check_metadata_complete(mtl, required)注意 rasterio 的out_dtype参数它会在解码时直接把原始数据转换为目标类型比read().astype(np.float32)少一次完整数组的拷贝。另外如果你用 gdal_translate 或 gdal.Warp 做过预处理一定要确认输出没有把 DN 裁成 8-bit否则定标系数的有效位数就全浪费了。4.2 DN 转辐亮度再转表观反射率的核心代码现在给出完整链路。我们要在 float32 的 DN 数组上完成两步操作先转辐亮度再转表观反射率。辐亮度到反射率的转换需要太阳高度角和日地距离Landsat C2 的元数据直接提供了这两个值其中日地距离 EARTH_SUN_DISTANCE 可以进一步抽离成修正项。import numpy as np def dn_to_radiance(dn, mult, add): DN 转辐亮度L gain * DN offset 输入 dn 必须是 float32 数组mult/add 是元数据里的浮点定标系数 return dn * mult add def radiance_to_reflectance(radiance, sun_elevation, earth_sun_distance, esun): 辐亮度转表观反射率 reflectance pi * radiance * d^2 / (ESUN * cos(theta_z)) 其中 theta_z 90° - sun_elevation太阳天顶角 # 太阳高度角转天顶角并统一为弧度制 theta_z np.deg2rad(90.0 - sun_elevation) # 核心公式注意 d^2 是日地距离平方 refl np.pi * radiance * (earth_sun_distance ** 2) refl refl / (esun * np.cos(theta_z)) return refl # 示例Landsat 8 红波段常用 ESUN 值W/m²/μm # 严格来说不同波段不同需按波段查表 ESUN_BAND4 2067.0 radiance dn_to_radiance( red_dn, mtl[RADIANCE_MULT_BAND_4], mtl[RADIANCE_ADD_BAND_4] ) reflectance radiance_to_reflectance( radiance, mtl[SUN_ELEVATION], mtl[EARTH_SUN_DISTANCE], ESUN_BAND4 ) print(辐亮度范围: %.3f ~ %.3f % (radiance.min(), radiance.max())) print(反射率范围: %.4f ~ %.4f % (reflectance.min(), reflectance.max()))参数说明sun_elevation是太阳高度角单位为度元数据中直接给出earth_sun_distance单位是天文单位取平方是因为辐照度与距离成反比esun是波段平均太阳辐照度这个值不在 MTL 里需要从数据发布方提供的定标手册中按波段查表。如果你不想自己查 ESUNLandsat C2 产品还提供了一条更简洁的官方路线直接使用元数据里的 REFLECTANCE_MULT_BAND_x 和 REFLECTANCE_ADD_BAND_x公式变为 ρ (Mρ × DN Aρ) / sin(太阳高度角)这一步已经把所有系数的换算打包好了def dn_to_reflectance_fast(dn, refl_mult, refl_add, sun_elevation): Landsat 官方简化公式直接从 DN 得到表观反射率 # 官方公式分母用 sin(sun_elevation)等价于 cos(天顶角) return (dn * refl_mult refl_add) / np.sin(np.deg2rad(sun_elevation)) reflectance_fast dn_to_reflectance_fast( red_dn, mtl[REFLECTANCE_MULT_BAND_4], mtl[REFLECTANCE_ADD_BAND_4], mtl[SUN_ELEVATION] )两条路径的结果理论上应该在浮点精度内一致。如果差异超过 1%说明你的 ESUN 取值或日地距离有误这时候以官方简化公式的结果为准去反推排查。4.3 批量处理多景影像并输出 GeoTIFF单景做完之后批量处理的核心就只剩下循环和文件命名管理。这里有一个我自己定下的规则输出文件名里必须带上“处理时间”和“产品版本”比如LC08_20241010_refl_f32.tif这样三个月后回看数据谁处理的、用的什么版本一目了然。批量脚本的组织方式如下import numpy as np import rasterio from pathlib import Path from datetime import datetime def calibrate_scene(dn_dir, mtl_path, output_dir, band_list): 处理单个场景的一组波段输出定标后的多波段反射率 GeoTIFF mtl parse_mtl(mtl_path) # 用第一个波段建立输出文件的投影和变换信息 first_band sorted(band_list)[0] with rasterio.open(dn_dir / f{first_band}.TIF) as src: profile src.profile.copy() profile.update(dtypefloat32, countlen(band_list), driverGTiff, compresslzw) # 注意压缩用 LZW浮点数据用 DEFLATE 会有精度损失风险 transform src.transform out_path output_dir / f{Path(dn_dir).stem}_refl_{datetime.now():%Y%m%d}.tif with rasterio.open(out_path, w, **profile) as dst: for i, band in enumerate(band_list, start1): dn load_band_as_float32(dn_dir / f{band}.TIF) # 这里实际处理时可以从 mtl 按波段号取系数 refl dn_to_reflectance_fast( dn, mtl[fREFLECTANCE_MULT_{band}], mtl[fREFLECTANCE_ADD_{band}], mtl[SUN_ELEVATION] ) dst.write(refl.astype(np.float32), i) # 顺手把处理参数写入侧car文件方便复查 with open(out_path.with_suffix(.log), w, encodingutf-8) as f: f.write(fmtl: {mtl_path}\n) f.write(fsun_elevation: {mtl[SUN_ELEVATION]}\n) return out_path批量处理中有三个参数需要特别注意。第一个是压缩算法浮点反射率数据用 LZW 是无损的而默认的 DEFLATE 虽然压缩率更高但处理不当会引入额外的计算开销而且一些旧的 GIS 软件读取 DEFLATE 压缩的 float32 GeoTIFF 时会卡死。第二个是输出波段顺序强烈建议和输入保持一致不要自作主张调整顺序否则后续做波段组合时容易张冠李戴。第三个是 profile 更新时一定要把 count 改成实际波段数否则写入超过 count 的波段会直接抛异常。4.4 定标结果正确性的快速自检定标跑完不等于工作结束你必须用几个硬指标验证输出没有系统性错误。最直接的验证是检查反射率的取值分布表观反射率理论范围应该在 -0.01 到 1.5 之间负值通常来自传感器暗电流偏置校正过度超过 1 的值通常来自云、雪或镜面反射。如果整个影像的反射率均值超过了 0.5几乎可以断定公式写错了。其次选一个你熟悉的参考地物做单点验证水体在近红外的反射率一般低于 0.05裸土在红波段一般 0.2 到 0.4 之间植被在近红外一般 0.3 到 0.7 之间。如果你的结果和这些常识量级不符先回头检查系数不要急着往下游走。def quick_stat_report(refl_array, label): 输出定标结果的核心统计量用于快速判断量级是否正确 valid refl_array[np.isfinite(refl_array)] print(f[{label}]) print(f 最小值: {valid.min():.4f}) print(f 最大值: {valid.max():.4f}) print(f 均值: {valid.mean():.4f}) print(f 中位数: {np.median(valid):.4f}) # 反射率影像的正常中位数一般落在 0.05~0.30 if np.median(valid) 0.5: print( !! 警告中位数异常偏高请检查定标系数) quick_stat_report(reflectance_fast, Red Band TOA Reflectance)统计量之外我还会在影像上随机取 5 个像元用代码打印 DN、mult、add、sun_elevation 和最终反射率做成一条完整的手工推算链路。这一步看似啰嗦但当你需要向同行解释结果可信度时这一条日志就是最有说服力的证据。5. 绝对辐射定标避坑指南五类高频翻车现场与排查方法5.1 版本混用C 系列产品参数拿旧版脚本算结果偏了一个数量级现象同样一景影像网上的参考脚本跑出来反射率最大值在 0.8 左右自己跑出来只有 0.05整体暗得发黑而且所有波段都成比例偏小。原因Landsat Collection 2 的定标系数与 Collection 1 不兼容尤其是反射率增益项C2 的 REFLECTANCE_MULT_BAND_x 是 C1 按比例缩放后的版本直接用 C1 脚本处理 C2 数据等于把系数错配到了一个完全不同的量级。解决确认 mtl 文件中 COLLECTION_NUMBER 字段处理前用脚本统一校验版本元数据解析函数里加一个断言遇到非目标版本直接抛错别给批量处理留混用的机会。5.2 太阳高度角与天顶角转换写反NDVI 全片飘移现象定标后反射率整体被放大或缩小植被区域的 NDVI 比其他来源的同类产品整体偏高约 0.05而且正午影像和早晚影像的偏差不一致。原因反射率公式里的角度修正项到底是 sin(高度角) 还是 cos(天顶角)很多人记混了。实际上太阳高度角 30° 时 sin 值是 0.5cos(天顶角 60°) 也是 0.5两者数学上等价但如果你先算天顶角又用了 sin分母就严重高估导致结果偏小。解决统一按“反射率公式分母 cos(太阳天顶角)”记忆代码中显式写np.cos(np.deg2rad(90 - sun_elevation))不要用 sin 或 cos 的其他英文缩写。5.3 忘掉数据类型转换uint16 乘浮点系数后整景翻车现象脚本跑完没有报错但输出影像出现大量斑块状异常值直方图呈阶梯状低值区域全部变成 0。原因uint16 数组乘以 float 标量时numpy 会把结果提升为 float64但如果某个中间步骤用了 in-place 运算或写回了 uint16 数组小数部分就被直接截断增益偏置的作用被吞掉严重时整幅影像的暗像元全部归零。解决定标前在读取阶段强制转 float32定标后也用 float32 存储在代码里对最终数组执行一次np.isfinite检查把非有限值直接暴露出来不要让它带着进下游。5.4 山区影像强行用固定太阳角度定标现象一景山区影像定标后阴坡的反射率整体比阳坡低很多同样地物在南北坡的光谱曲线差异远超合理范围拿去分类时把同一种植被分成了两类。原因反射率公式里的太阳角度修正只考虑了太阳位置没有考虑地形坡度坡向。卫星接收到的辐射强度在一个坡面上和在一个水平面上完全不同固定角度修正会把地形阴影误当成地物光谱特征。解决如果研究区地形起伏超过 300 米建议先做地形校正如简单的余弦校正或更严格的 C 校正再做辐射定标如果只是平坦区域这个坑可以跳过。5.5 云和饱和像元没处理验证时被“异常元”误导现象验证反射率范围时发现最大值达到了 5 甚至更高一开始以为是定标系数错折腾半天发现是云顶反射和传感器饱和像元混在里面。原因云在可见光波段的反射率可以超过 0.8加上镜面反射效应某些像元值超出传感器的线性响应范围定标公式在饱和区域失效。解决在定标流程里加一步质量掩膜用 QA 波段Landsat C2 提供了 QA_PIXEL 波段把云、云影、冰雪像元标记为 0再做后续统计如果手头没有 QA 波段至少也要在统计反射率时剔除大于 1.0 的值。6. 定标结果的验证与进阶把辐射定标做成预处理流水线的一环6.1 第一眼验证拉一条光谱剖面和官方 L2 产品对比定标有没有做对最好的参照物是官方发布的地表反射率产品。Landsat C2 的 L2 产品直接提供了 SR_B 系列地表反射率虽然它多了大气校正这一步但你可以拿一景同一区域的 L1 影像定标后再做一次大气校正两者对比。更快的办法是做一个“光谱剖面验证”在影像上用经纬度取同一个像元把你的定标结果和官方 L2 产品的值放到同一坐标系里比较。如果两者差异在 0.02 以内说明你的定标链路的传感器部分是正确的差异主要来自大气校正与否。这里放一段提取单像元光谱的代码def extract_pixel_spectrum(stack_path, lon, lat, band_names): 从定标后的多波段影像中提取单像元光谱曲线 import rasterio with rasterio.open(stack_path) as src: # rasterio 的 index 直接用投影坐标转行列号 row, col src.index(lon, lat) spectrum src.read( range(1, len(band_names) 1), window((row-1, row), (col-1, col)) ).flatten() return dict(zip(band_names, spectrum)) # 用法示例取某一块水体像元看近红外是否低于 0.05 spectrum extract_pixel_spectrum( LC08_20241010_refl_f32.tif, 116.352, 39.873, [B, G, R, NIR] ) print(spectrum)6.2 把定标写成预处理流水线的一个环节单独跑通一次定标并不难难的是让定标在深度学习和时间序列分析里稳定复现。我现在的习惯是把定标函数封装成独立的 Python 模块只暴露一个接口输入目录和输出目录内部处理元数据解析、系数校验、批量定标、质量掩膜并把统计日志写入 JSON 文件。下游不管是做 NDVI 还是做样本切片都只读定标后的反射率 GeoTIFF。这样做的好处是换了一个研究区换了一批发行的卫星影像只需要改元数据解析那一层计算逻辑不用动这也是我踩过前面这些坑之后养成的习惯。6.3 定标之后大气校正的一步之遥如果表观反射率已经满足大部分应用需求定标工作在这里可以收尾。但如果你要反演叶面积指数、生物量或者做跨传感器的地表参数对比建议再走一步大气校正。主流做法是用辐射传输模型正查表比如经典的路径辐射和透过率表或者直接用成熟的校正工具处理。需要记住的是绝对辐射定标是大气的输入不是它的替代。很多人以为做完定标就能直接和地面实测光谱对比实际上那是大气校正的职责。从第一次写定标脚本到现在我自己最深的感触是千万别把遥感影像当作一张普通图片去处理定标是让数据具备物理意义的最低门槛。回到开头那 80 景影像的教训我现在每批数据交付前都会人工抽查 3 个地物点的反射率并且把 MTL 里的产品版本号、太阳角度、处理时间写成日志宁可检查多花十分钟也绝不交付一个带着隐蔽系统误差的成果。希望这篇文章能帮你少走几趟弯路把绝对辐射定标这一步走得稳稳当当。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进