☰
遥感影像预处理实战:归一化与异常值去除的完整流程
2026/10/15 5:34:14 网站建设 项目流程

1. 影像预处理到底在解决什么问题

遥感影像拿到手,第一件事不是急着跑分类或者做时序分析,而是先看看数据本身干不干净。我见过太多人直接把原始影像丢进模型里,结果跑出来的结果惨不忍睹,回头排查半天才发现是异常值在作祟。影像归一化和异常值去除这两个步骤,说白了就是给数据“洗澡”和“理发”——把量纲统一了,把毛刺磨平了,后面的分析才有意义。

归一化的核心目的是消除不同传感器、不同时相之间的量纲差异。比如Landsat 8的SR数据反射率范围是0到10000,MODIS的反射率是0到32767,哨兵2号的L2A数据又是0到10000但带有偏移量。你不做归一化,把这些数据混在一起分析,就好比拿厘米和英寸直接相加,结果肯定是错的。异常值去除则是处理那些云边缘、阴影、传感器故障导致的极端像素值,这些值如果不处理,会严重拉偏统计结果。

这套流程适合谁呢?如果你在做土地利用分类、植被指数时序分析、水体提取、城市扩张监测这类工作,影像预处理是绕不过去的基本功。新手可以把它当作入门必修课,老手也可以看看有没有自己平时忽略的细节。我下面会从设计思路、核心原理、实操步骤到踩坑经验,完整走一遍这个流程。

2. 归一化的本质与实现路径拆解

2.1 为什么不能跳过归一化直接分析

很多人觉得归一化就是简单除个系数,没什么技术含量。但实际项目中,归一化做得好不好,直接影响后续分析的精度。我举个实际例子:假设你在做某区域的NDVI时序分析,用了Landsat 8和Landsat 9两颗卫星的数据。这两颗卫星的OLI传感器虽然设计相似,但辐射定标系数有细微差异。如果不做归一化,同一块农田在Landsat 8上算出来的NDVI是0.75,在Landsat 9上可能是0.73,这个差异在时序曲线上就会表现为一个虚假的波动,你可能误判为作物受到了胁迫。

归一化的数学本质是把数据映射到一个标准区间,通常是[0, 1]或者[-1, 1]。对于遥感反射率数据,最常见的做法是除以缩放因子。Landsat Collection 2的SR数据缩放因子是0.0000275,偏移量是-0.2;哨兵2号L2A的缩放因子是0.0001。这些参数不是随便定的,而是根据传感器的辐射分辨率计算出来的。

注意:不同数据集的缩放因子和偏移量完全不同,用错一个参数,整幅影像的反射率就全错了。建议在代码里把参数写成常量,不要硬编码在计算式里。

2.2 归一化的三种主流方案对比

在实际操作中,归一化有几种不同的实现路径,各有适用场景。我整理了一个对比表格,方便你根据项目需求选择。

方案计算公式适用场景优点缺点
缩放因子法(DN × scale) + offset标准SR产品物理意义明确,可逆需要知道准确的缩放参数
最大最小值法(DN - min) / (max - min)单幅影像快速可视化简单直观受异常值影响大,不可跨影像比较
均值标准差法(DN - mean) / std机器学习输入分布标准化,利于模型收敛破坏物理量纲,不适合定量遥感

缩放因子法是我最推荐的方式,因为它保留了数据的物理意义。你算出来的反射率就是真实反射率,可以直接和地面实测数据对比。最大最小值法适合快速出图看看效果,但如果你要做多时相分析,每幅影像的min和max都不一样,归一化后的值就没法比较了。均值标准差法在深度学习领域很常见,但它把数据变成了标准正态分布,你就没法再说“这块地的反射率是0.3”了。

2.3 在GEE中实现归一化的关键细节

在Google Earth Engine里做归一化,表面上看就是一行map函数的事,但有几个细节特别容易翻车。第一个是波段名称的匹配问题。Landsat 8的SR产品波段名是SR_B1到SR_B7,Landsat 5是SR_B1到SR_B7但对应的是TM传感器,波段波长范围不一样。你如果直接按波段名批量处理,可能会把不同物理意义的波段混在一起。

第二个细节是偏移量的处理。Landsat Collection 2引入了一个-0.2的偏移量,目的是让反射率可以为负值,从而更准确地表示暗目标(比如水体)的反射率。如果你忘了加这个偏移量,水体的反射率会被截断在0附近,导致水体指数计算出现偏差。

// Landsat 8 Collection 2 SR归一化示例 var normalizeLandsat8 = function(image) { var opticalBands = image.select('SR_B.').multiply(0.0000275).add(-0.2); var thermalBands = image.select('ST_B.*').multiply(0.00341802).add(149.0); return image.addBands(opticalBands, null, true) .addBands(thermalBands, null, true); };

这段代码里,multiply(0.0000275).add(-0.2)就是标准的缩放因子加偏移量操作。热红外波段用的是另一套参数,因为热红外的物理量是亮温,单位是开尔文,缩放因子和偏移量完全不同。我见过有人把光学波段的参数套到热红外上,结果亮温算出来是负几百开尔文,那就闹笑话了。

3. 异常值去除的底层逻辑与实操方法

3.1 异常值从哪来,长什么样

异常值的来源主要有四类:云和云阴影、传感器故障、地形阴影、以及数据处理过程中的错误。云和云阴影是最常见的,一朵薄云就能让反射率飙升到0.8以上,而云阴影又会让反射率骤降到接近0。传感器故障比较少见,但一旦出现就是整条扫描线的数据异常,表现为规则的条纹。地形阴影在山区很普遍,阳坡和阴坡的反射率差异可能达到3倍以上。

这些异常值在直方图上的表现很典型:正常反射率分布是一个近似正态的钟形曲线,异常值会在两端形成孤立的小峰。如果你不做处理,后续的均值、标准差、回归系数都会被这些极端值拉偏。我做过一个测试,在一幅有5%云覆盖的影像上,不剔除异常值算出来的NDVI均值是0.42,剔除后是0.38,差了将近10%。

3.2 基于位运算的云掩膜方案

在GEE里处理Landsat数据,最标准的做法是用QA_PIXEL波段做位运算。QA_PIXEL是一个16位的整数,每一位代表一种状态。比如第3位是云,第4位是云阴影,第5位是雪。你需要用位运算提取这些标志位,然后生成掩膜。

// Landsat 8 QA_PIXEL云掩膜 var maskLandsat8 = function(image) { var qa = image.select('QA_PIXEL'); var cloudBit = 1 << 3; var shadowBit = 1 << 4; var snowBit = 1 << 5; var mask = qa.bitwiseAnd(cloudBit).eq(0) .and(qa.bitwiseAnd(shadowBit).eq(0)) .and(qa.bitwiseAnd(snowBit).eq(0)); return image.updateMask(mask); };

这段代码的逻辑是:如果某一位是1,说明该像素被标记为对应的状态,我们就把它掩掉。1 << 3表示把1左移3位,得到二进制1000,也就是十进制的8。bitwiseAnd做按位与运算,如果结果等于0,说明这一位是0,像素是干净的。

提示:不同传感器的QA波段位定义不同。Landsat 8的云位是第3位,Landsat 5的云位是第4位,哨兵2号的QA60波段又是另一套逻辑。用之前一定要查清楚文档,不要凭记忆写。

3.3 基于统计阈值的异常值剔除

QA波段不是万能的,有些异常值它标记不出来,比如薄云边缘的过渡像素、传感器噪声、以及某些地形阴影。这时候就需要用统计方法做二次过滤。最常用的是百分位截断法:计算整个影像或者整个时间序列的某个百分位(比如2%和98%),把超出这个范围的像素值截断或者掩掉。

// 基于百分位数的异常值剔除 var removeOutliers = function(image) { var ndvi = image.select('NDVI'); var percentiles = ndvi.reduceRegion({ reducer: ee.Reducer.percentile([2, 98]), geometry: image.geometry(), scale: 30, maxPixels: 1e13 }); var lower = ee.Number(percentiles.get('NDVI_p2')); var upper = ee.Number(percentiles.get('NDVI_p98')); var mask = ndvi.gte(lower).and(ndvi.lte(upper)); return image.updateMask(mask); };

这个方法的关键在于百分位的选择。2%和98%是比较保守的,适合大多数场景。如果你做的是水体提取,可能需要用1%和99%,因为水体的NDVI分布很集中,稍微放宽一点就会把陆地混进来。如果你做的是稀疏植被监测,可能需要用5%和95%,因为稀疏植被的NDVI本身就很低,太严格的截断会把有效信号也去掉。

3.4 时序维度的异常值检测

单幅影像的异常值好处理,时序上的异常值更隐蔽。比如某一年因为云覆盖严重,整幅影像的NDVI都偏低,你在做年际比较时就会误判为植被退化。这时候需要用时序滤波的方法,比如Savitzky-Golay滤波或者谐波分析,来识别和修复异常。

在GEE里做时序异常检测,我常用的思路是:先计算每个像素的时序均值和标准差,然后把偏离均值超过2倍标准差的时相标记为异常。这个方法假设NDVI的时序变化是平稳的,对于季节性明显的区域,需要先做去季节化处理。

// 时序异常检测 var detectTemporalOutliers = function(collection) { var mean = collection.mean(); var std = collection.reduce(ee.Reducer.stdDev()); var threshold = 2; var lower = mean.subtract(std.multiply(threshold)); var upper = mean.add(std.multiply(threshold)); return collection.map(function(image) { var mask = image.gte(lower).and(image.lte(upper)); return image.updateMask(mask); }); };

这个方法的局限在于,它假设异常是双向的(既可能偏高也可能偏低)。但实际中,云污染通常导致反射率偏高,云阴影导致偏低,两者的物理机制不同,可能需要分别处理。我一般会先做云掩膜,再做时序异常检测,这样能减少误判。

4. 完整实操流程与参数调优记录

4.1 数据准备与区域筛选

假设我要分析某农业区2015到2023年的NDVI时序变化。第一步是筛选数据。Landsat 8从2013年开始有数据,Landsat 9从2021年开始,所以2015到2023年主要用Landsat 8,2021年之后可以融合Landsat 9。区域筛选要注意去掉影像边缘的无效区域,以及云覆盖超过80%的影像。

var region = ee.Geometry.Rectangle([116.0, 36.0, 117.0, 37.0]); var landsat8 = ee.ImageCollection('LANDSAT/LC08/C02/T1_L2') .filterBounds(region) .filterDate('2015-01-01', '2023-12-31') .filter(ee.Filter.lt('CLOUD_COVER', 80));

CLOUD_COVER是元数据里的云覆盖百分比,但这个值是基于整幅影像的,你的研究区可能只占影像的一小部分。所以即使CLOUD_COVER是80%,你的研究区可能完全无云。我一般会把这个阈值设得宽松一些,比如80%到90%,然后用QA波段做精细掩膜。

4.2 归一化与云掩膜的串联处理

数据筛选完之后,把归一化和云掩膜串成一个map函数。顺序很重要:先归一化,再云掩膜。因为QA波段是基于原始DN值的,如果你先做了归一化,QA波段的值也被缩放了,位运算就会出错。

var preprocess = function(image) { // 第一步:归一化 var optical = image.select('SR_B.').multiply(0.0000275).add(-0.2); var thermal = image.select('ST_B.*').multiply(0.00341802).add(149.0); var normalized = image.addBands(optical, null, true) .addBands(thermal, null, true); // 第二步:云掩膜 var qa = normalized.select('QA_PIXEL'); var mask = qa.bitwiseAnd(1 << 3).eq(0) .and(qa.bitwiseAnd(1 << 4).eq(0)); // 第三步:计算NDVI var ndvi = normalized.normalizedDifference(['SR_B5', 'SR_B4']) .rename('NDVI'); return normalized.addBands(ndvi).updateMask(mask); }; var processed = landsat8.map(preprocess);

这里有个细节:addBands的第三个参数设为true,表示替换原有的波段。如果不设这个参数,归一化后的波段会以新名字追加,原来的DN值波段还在,后面容易搞混。我建议养成替换的习惯,保持波段列表干净。

4.3 异常值去除的参数调优

云掩膜之后,还有大约5%到10%的异常像素残留。这时候用百分位截断做二次过滤。百分位的选择需要根据你的研究区调整。我一般会先跑一遍不做截断的统计,看看NDVI的分布范围,再决定截断阈值。

// 先看看NDVI的分布 var ndviStats = processed.select('NDVI').reduceRegion({ reducer: ee.Reducer.percentile([1, 5, 95, 99]), geometry: region, scale: 30, maxPixels: 1e13 }); print('NDVI percentiles:', ndviStats);

如果1%和99%的值在-0.2到0.9之间,说明数据比较干净,用2%和98%截断就够了。如果1%的值低于-0.5,说明有云阴影残留,需要更严格的截断。我遇到过一种情况:研究区有大量水体,水体的NDVI是负值,如果用2%截断,会把浅水区的有效信号也去掉。这时候需要先做水体掩膜,再对陆地部分做异常值剔除。

4.4 时序合成与结果导出

预处理完之后,通常需要合成到月尺度或者年尺度。月合成用中位数比均值更稳健,因为中位数对残余异常值不敏感。合成的时候要注意,如果某个月的有效像素太少(比如少于30%),这个月的合成结果就不可靠,应该标记为无效。

var monthlyComposite = processed .filterDate('2020-01-01', '2020-12-31') .map(function(image) { return image.set('month', image.date().get('month')); }) .filter(ee.Filter.eq('month', 6)) .median();

导出的时候,如果研究区很大,直接导出可能会超时。我一般会先导出到Drive,再用本地脚本做后续分析。导出分辨率设为30米,和原始Landsat一致。如果要做区域统计,可以先用reduceRegions在GEE里算好,只导出统计结果,数据量小很多。

5. 常见问题排查与避坑经验

5.1 归一化后反射率出现负值正常吗

正常。Landsat Collection 2的偏移量-0.2就是为了让暗目标的反射率可以为负。水体的反射率在近红外波段接近0,加上偏移量后可能是-0.01到-0.02。这不是错误,而是传感器噪声和大气校正残差的表现。如果你做的是植被分析,这些负值会被NDVI计算自动处理掉;如果你做的是水体分析,需要把负值截断到0。

5.2 云掩膜后影像出现大量空洞怎么办

这是云掩膜的常见副作用。如果研究区云覆盖频繁,单幅影像掩膜后可能只剩30%的有效像素。解决方案有两个:一是用多时相合成,把一个月内所有影像的中位数作为该月的代表,这样即使单幅有云,合成后也能填补空洞;二是用时序插值,比如用前后两个月的均值来填补缺失月份。我一般优先用多时相合成,因为插值会引入虚假的平滑信号。

5.3 百分位截断把有效信号去掉了怎么排查

先检查百分位的计算范围。如果你用的是整幅影像的百分位,而研究区只占影像的一小部分,那百分位可能被影像其他区域的特征主导。比如影像大部分是水体,NDVI集中在-0.1到0.1,你的农田NDVI是0.6,用整幅影像的98%百分位可能是0.2,直接把农田截掉了。解决方法是用reduceRegion限定在研究区范围内计算百分位。

5.4 时序异常检测误判了真实变化怎么办

时序异常检测的假设是“正常变化是平稳的”,但真实的植被变化可能很剧烈,比如森林砍伐、火灾、洪水。这些真实变化会被误判为异常。解决方法是在时序检测之前,先用已知的干扰事件(比如火灾记录)做掩膜,或者用更宽松的阈值(比如3倍标准差)。我一般会先用2倍标准差跑一遍,看看被标记的像素是不是集中在已知干扰区域,如果是,就调整阈值。

5.5 不同传感器数据融合时的归一化陷阱

Landsat 8和Sentinel-2的波段设置不同,Landsat 8没有红边波段,Sentinel-2有。如果你要做两者的NDVI时序融合,需要确保使用的波段是对应的。Landsat 8的NIR是B5,Red是B4;Sentinel-2的NIR是B8,Red是B4。波段名一样但物理意义不同,这是最容易翻车的地方。我建议在融合之前,先把两个数据集重命名成统一的波段名,比如都叫‘NIR’和‘Red’,然后再做计算。

问题现象可能原因排查方法解决方案
反射率全为负偏移量用错检查缩放参数确认使用对应传感器的参数
掩膜后无有效像素云覆盖过高查看QA波段统计放宽云覆盖阈值或改用多时相合成
NDVI异常偏高云边缘残留检查QA掩膜位增加云置信度阈值或做百分位截断
时序曲线断裂异常值剔除过度查看被掩膜像素比例放宽百分位阈值或分区域处理
不同影像反射率不一致归一化参数不统一对比同一地物反射率统一使用缩放因子法

6. 我个人的实操体会与进阶建议

这套流程我用了好几年,踩过的坑基本都写在上面了。最后分享几个文档里不会写但很实用的经验。第一个是关于计算效率的:GEE的reduceRegion操作很耗时,如果你要对几百幅影像做百分位统计,最好先合并成一个ImageCollection,用reduce而不是逐幅map。我试过逐幅计算,100幅影像跑了20分钟,改成集合操作后只要2分钟。

第二个经验是关于掩膜顺序的。很多人习惯先算NDVI再掩膜,但NDVI计算涉及两个波段,如果其中一个波段有异常值,NDVI也会异常。正确的顺序是先对每个波段做异常值检测,生成一个综合掩膜,再应用到NDVI上。这样能避免异常值通过波段运算传播。

第三个经验是关于结果验证的。预处理完之后,一定要抽样检查。我一般会随机选20个点,把预处理前后的反射率值打印出来对比。如果某个点的反射率从0.3变成了0.8,那肯定是哪里出错了。这个步骤花不了几分钟,但能帮你发现很多隐蔽的问题。

如果你要做深度学习,归一化后的数据还需要做进一步的标准化。我通常会把反射率截断到[0, 1]区间,然后减去0.5再除以0.5,映射到[-1, 1]。这个范围对大多数神经网络更友好,梯度不会消失也不会爆炸。但记住,这个操作是不可逆的,如果你后面要做定量反演,需要保留原始反射率数据。

最后说一个关于数据版本的问题。GEE上的数据集会更新,Collection 1和Collection 2的预处理参数完全不同。Collection 1的Landsat 8 SR缩放因子是0.0001,没有偏移量;Collection 2是0.0000275加-0.2。如果你在网上抄了一段代码,一定要确认它对应的是哪个版本。我见过有人混用两个版本的参数,结果反射率算出来差了将近一倍。

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

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

立即咨询